mirror of
https://github.com/LisherSong/ps5-web-file-manager.git
synced 2026-10-06 10:00:24 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f80eb4692 | ||
|
|
807d129d8d | ||
|
|
70eaa1027d | ||
|
|
575f94cd39 | ||
|
|
2059c0e4b5 | ||
|
|
9a36c3cb30 | ||
|
|
5e8b56f23e | ||
|
|
5cc493d152 | ||
|
|
8766178aa1 | ||
|
|
fd48232b6c | ||
|
|
49636b0b8a | ||
|
|
f1633321d2 | ||
|
|
9058d5a97e | ||
|
|
5d88914674 | ||
|
|
0d036a74a7 | ||
|
|
1fa2f0953f | ||
|
|
f4fd464353 | ||
|
|
5229cd59df | ||
|
|
a54f34bcab | ||
|
|
98679423e0 | ||
|
|
00b750d80b | ||
|
|
112f8a6b72 | ||
|
|
27eec8c399 | ||
|
|
200b38604f | ||
|
|
021c9cb339 | ||
|
|
4da345a6e8 | ||
|
|
9b2f5a07c3 | ||
|
|
2748b383bd | ||
|
|
8bee84bd49 | ||
|
|
da565ccb7c | ||
|
|
01e27f3825 | ||
|
|
e0bc4a6ae0 | ||
|
|
f820016de3 | ||
|
|
95578fb2d2 | ||
|
|
4695295b8e | ||
|
|
76c4eadbf5 | ||
|
|
ad765776f7 | ||
|
|
ae41434089 | ||
|
|
af6b440f33 | ||
|
|
01a6616472 | ||
|
|
068983b062 | ||
|
|
da67bfa284 | ||
|
|
452b9b0187 | ||
|
|
91757bfd02 | ||
|
|
30d0decf58 | ||
|
|
22a4b5c76a | ||
|
|
ff010add68 | ||
|
|
68056f2d9f | ||
|
|
3db921e408 | ||
|
|
c58c145733 | ||
|
|
a0482a41ed | ||
|
|
c68c0def35 | ||
|
|
8344b9bae0 | ||
|
|
cb82ee439d | ||
|
|
9e9830a214 | ||
|
|
687ef6b297 | ||
|
|
36ea055e70 | ||
|
|
2a346d694c | ||
|
|
bf55a4e4fd |
No files matched your search
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env bash
|
||||
# ===========================================================================
|
||||
# ps5-web-file-manager -- 一键在 WSL 里生成 PS5 ELF
|
||||
#
|
||||
# 这个脚本在 WSL (Ubuntu-22.04) 里执行,做四件事:
|
||||
# 1. 把 Windows 仓库的源码 rsync 到 WSL 项目目录(增量,跳过构建缓存)
|
||||
# 2. make all (PS5_PAYLOAD_SDK = /opt/ps5-payload-sdk)
|
||||
# 3. 验证产物:size / sha256 / e_machine
|
||||
# 4. 把 ELF 拷回 Windows 项目根
|
||||
#
|
||||
# 从 Windows 的 Git Bash / MinGW64 bash 里这样跑:
|
||||
# wsl.exe -d Ubuntu-22.04 -- bash < \
|
||||
# "/c/Users/songl/Desktop/Web File Manager/ps5-web-file-manager/.build/build-elf-wsl.sh"
|
||||
#
|
||||
# 注意:必须用 stdin 重定向 `bash < script`,不要 `bash -c '...'` ——
|
||||
# 路径含空格时 -c 的参数会被 wsl.exe 拆断。
|
||||
# ===========================================================================
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
# ---------------------------------------------------------------- 配置 -----
|
||||
SRC_WIN='/mnt/c/Users/songl/Desktop/Web File Manager/ps5-web-file-manager'
|
||||
PROJ='/home/song/ps5-web-file-manager'
|
||||
SDK='/opt/ps5-payload-sdk'
|
||||
|
||||
log() { printf '\033[1;36m%s\033[0m\n' "$*"; }
|
||||
warn() { printf '\033[1;33m[WARN] %s\033[0m\n' "$*"; }
|
||||
fail() { printf '\033[1;31m[FAIL] %s\033[0m\n' "$*"; exit 1; }
|
||||
|
||||
# ------------------------------------------------------------ jwasm --------
|
||||
# The LZMA decoder has an asm implementation that is 26% faster than the C one
|
||||
# (see docs/EXTRACTION-PERF.md). It is MASM syntax, so a MASM-compatible
|
||||
# assembler is needed. Makefile only enables the optimisation when jwasm is on
|
||||
# PATH, so a failure here downgrades rather than breaks the build.
|
||||
JWASM_HOME="$HOME/.cache/wfm-jwasm"
|
||||
JWASM_BIN="$JWASM_HOME/jwasm"
|
||||
|
||||
ensure_jwasm() {
|
||||
if command -v jwasm >/dev/null 2>&1; then
|
||||
echo " jwasm: $(command -v jwasm)"
|
||||
return 0
|
||||
fi
|
||||
if [ -x "$JWASM_BIN" ]; then
|
||||
export PATH="$JWASM_HOME:$PATH"
|
||||
echo " jwasm: $JWASM_BIN (缓存)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo " jwasm 不在,正在从源码编译(首次约 30 秒)..."
|
||||
mkdir -p "$JWASM_HOME" || return 1
|
||||
if [ ! -d "$JWASM_HOME/src" ]; then
|
||||
git clone --depth 1 https://github.com/Baron-von-Riedesel/JWasm.git \
|
||||
"$JWASM_HOME/src" >/dev/null 2>&1 || return 1
|
||||
fi
|
||||
make -C "$JWASM_HOME/src" -f GccUnix.mak -j4 >/dev/null 2>&1 || return 1
|
||||
cp -f "$JWASM_HOME/src/build/GccUnixR/jwasm" "$JWASM_BIN" || return 1
|
||||
chmod +x "$JWASM_BIN" || return 1
|
||||
export PATH="$JWASM_HOME:$PATH"
|
||||
echo " jwasm: $JWASM_BIN (已编译)"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------- 1/5 环境检查 -----
|
||||
log "[1/5] 环境检查"
|
||||
|
||||
[ -d "$SRC_WIN" ] || fail "Windows 源码目录不可见: $SRC_WIN (/mnt/c 挂载了吗?)"
|
||||
[ -x "$SDK/bin/prospero-clang" ] || fail "SDK 缺失: $SDK/bin/prospero-clang"
|
||||
|
||||
export PS5_PAYLOAD_SDK="$SDK"
|
||||
# shellcheck disable=SC1091
|
||||
source "$SDK/toolchain/prospero.sh" 2>/dev/null || true
|
||||
export CC="$SDK/bin/prospero-clang"
|
||||
export CXX="$SDK/bin/prospero-clang++"
|
||||
export PKG_CONFIG="$SDK/bin/prospero-pkg-config"
|
||||
|
||||
"$CC" --version | head -1
|
||||
"$PKG_CONFIG" --modversion libmicrohttpd 2>/dev/null \
|
||||
|| fail "libmicrohttpd 未装到 sysroot —— 先跑一次完整的 build-elf.sh v4"
|
||||
|
||||
ensure_jwasm || warn "jwasm 不可用 —— 将退回纯 C 解码器(约慢 26%)"
|
||||
echo
|
||||
|
||||
# ------------------------------------------------------- 2/5 同步源码 -----
|
||||
log "[2/5] 同步源码 Windows -> WSL (rsync 增量)"
|
||||
mkdir -p "$PROJ"
|
||||
|
||||
rsync -a --delete \
|
||||
--exclude='/ps5-obj' --exclude='/linux-obj' \
|
||||
--exclude='/web-file-mgr-*.elf' --exclude='/web-file-mgr-linux*' \
|
||||
--exclude='/gen' --exclude='/.build' --exclude='/tests' \
|
||||
--exclude='/docs' --exclude='/HANDOVER.md' \
|
||||
--exclude='/README.md' --exclude='/CHANGELOG.md' \
|
||||
--exclude='/erssongl*' \
|
||||
--include='/src' --include='/assets' \
|
||||
--include='/third_party' --include='/Makefile' \
|
||||
--include='/gen-asset-module.py' --include='/.gitignore' \
|
||||
--exclude='/*' \
|
||||
"$SRC_WIN/" "$PROJ/" || fail "rsync 失败"
|
||||
|
||||
# rsync 的 --include 只放行目录本身,这几个顶层文件再单独 cp 一次
|
||||
for f in Makefile gen-asset-module.py .gitignore; do
|
||||
[ -f "$SRC_WIN/$f" ] && cp -f "$SRC_WIN/$f" "$PROJ/$f"
|
||||
done
|
||||
|
||||
# 冒烟:今天的关键文件都在不在
|
||||
for f in src/sevenz_extract.c src/zipx_common.c src/sevenz_volstream.c \
|
||||
src/sevenz_chain.c Makefile gen-asset-module.py; do
|
||||
[ -f "$PROJ/$f" ] || fail "同步后缺失: $PROJ/$f"
|
||||
done
|
||||
[ -d "$PROJ/third_party/7z" ] || fail "同步后缺失: $PROJ/third_party/7z"
|
||||
|
||||
# 输出文件名由 Makefile 的 VERSION_TAG 决定(web-file-mgr-<ver>.elf)。
|
||||
# 从 Makefile 里读,别在脚本里硬编 —— 否则改了版本号脚本还在找旧名字。
|
||||
VERSION=$(sed -n 's/^VERSION_TAG *[?:]*= *//p' "$PROJ/Makefile" | head -1)
|
||||
[ -n "$VERSION" ] || fail "读不到 VERSION_TAG: $PROJ/Makefile"
|
||||
BIN_NAME="web-file-mgr-${VERSION}.elf"
|
||||
ELF="$PROJ/$BIN_NAME"
|
||||
|
||||
echo " src/ + assets/ + third_party/ + Makefile OK"
|
||||
echo " 版本: $VERSION -> 输出: $BIN_NAME"
|
||||
echo
|
||||
|
||||
# ---------------------------------------------------------- 3/5 编译 ------
|
||||
log "[3/5] make all"
|
||||
cd "$PROJ" || fail "cd $PROJ"
|
||||
|
||||
make all 2>&1 | tail -120
|
||||
# make 的退出码被管道吃了,用 PIPESTATUS 取回来
|
||||
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
|
||||
fail "make all 失败(详见上方输出)"
|
||||
fi
|
||||
echo
|
||||
|
||||
# ---------------------------------------------------------- 4/5 验证 ------
|
||||
log "[4/5] 验证产物"
|
||||
[ -f "$ELF" ] || fail "ELF 未生成: $ELF"
|
||||
|
||||
SIZE=$(stat -c%s "$ELF")
|
||||
HASH=$(sha256sum "$ELF" | cut -d' ' -f1)
|
||||
# ELF header: offset 18 起 2 字节 = e_machine。
|
||||
# `od -tx2` 按 2 字节小端解释成一个 short 后打印其**值**,所以文件里的
|
||||
# 字节序 "3e 00" 会输出成 "003e"(不是 "3e00")。别拿字节序去比对。
|
||||
EM=$(od -An -tx2 -j 18 -N 2 "$ELF" | tr -d ' \n')
|
||||
EM_NUM=$((16#$EM))
|
||||
|
||||
ls -lh "$ELF"
|
||||
echo " size: $SIZE bytes (~$((SIZE / 1024)) KiB)"
|
||||
echo " sha256: $HASH"
|
||||
echo " e_machine = 0x$EM ($EM_NUM)"
|
||||
|
||||
case "$EM_NUM" in
|
||||
62) echo " -> x86-64 / PS5 [OK]" ;;
|
||||
183) fail "e_machine=$EM_NUM (0x$EM) 是 aarch64!PS5 是 x86-64,target 三元组错了" ;;
|
||||
*) fail "e_machine=$EM_NUM (0x$EM) 非预期(期望 62 = 0x003e = x86-64)" ;;
|
||||
esac
|
||||
echo
|
||||
|
||||
# ------------------------------------------------- 5/5 拷回 Windows -------
|
||||
log "[5/5] 拷回 Windows"
|
||||
cp -f "$ELF" "$SRC_WIN/$BIN_NAME" || fail "拷回 Windows 失败"
|
||||
ls -lh "$SRC_WIN/$BIN_NAME"
|
||||
echo
|
||||
printf '\033[1;32m[DONE]\033[0m %s\n' "$SRC_WIN/$BIN_NAME"
|
||||
echo " $SIZE bytes / sha256 $HASH / e_machine 0x$EM"
|
||||
|
||||
# 顺便报告 Windows 侧现在有哪些版本化 ELF,方便挑一个拷进 U 盘
|
||||
echo
|
||||
echo " Windows 项目根现有的 ELF:"
|
||||
ls -1 "$SRC_WIN"/web-file-mgr-*.elf 2>/dev/null | sed 's#.*/##' | sed 's/^/ /' || true
|
||||
+13
-3
@@ -148,10 +148,14 @@ echo "[6/7] make all (增量编译, 复用第三方 obj)..."
|
||||
cd "$PROJ"
|
||||
export PS5_PAYLOAD_SDK="/opt/ps5-payload-sdk"
|
||||
# 仅当二进制缺失或源码变更才全量重编
|
||||
# 重要: 必须包含 assets/* 和 gen-asset-module.py —— 它们经 gen-asset-module.py
|
||||
# 生成 gen/*.c 进而影响 ELF, 不在列表里就会跳过 make 产生伪"无变更"(v1.8.3
|
||||
# 被这个 bug 坑过, ELF sha256 没变)。
|
||||
if [ -f web-file-mgr.elf ]; then
|
||||
echo " 已存在 web-file-mgr.elf, 检查源码变更..."
|
||||
NEEDS_REBUILD=""
|
||||
for src in src/*.c Makefile third_party/minizip-ng/include/*.h third_party/zlib/include/*.h; do
|
||||
for src in src/*.c Makefile assets/* gen-asset-module.py \
|
||||
third_party/minizip-ng/include/*.h third_party/zlib/include/*.h; do
|
||||
[ -e "$src" ] || continue
|
||||
if [ "$src" -nt web-file-mgr.elf ]; then
|
||||
NEEDS_REBUILD="$src"
|
||||
@@ -162,10 +166,16 @@ if [ -f web-file-mgr.elf ]; then
|
||||
echo " 无源码变更 -- 跳过 make"
|
||||
else
|
||||
echo " 源码变更: $NEEDS_REBUILD"
|
||||
make all 2>&1 | tail -40
|
||||
if ! make all 2>&1 | tail -60; then
|
||||
echo "FAIL: make all 失败(见上)。注意: 旧 ELF 仍留在原地, 但不算新产物"
|
||||
exit 6
|
||||
fi
|
||||
fi
|
||||
else
|
||||
make all 2>&1 | tail -40
|
||||
if ! make all 2>&1 | tail -60; then
|
||||
echo "FAIL: make all 失败(见上)"
|
||||
exit 6
|
||||
fi
|
||||
fi
|
||||
|
||||
# 7. 验证 + 同步回 Windows
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
# ===========================================================================
|
||||
# ps5-web-file-manager -- Windows 端一键构建入口
|
||||
#
|
||||
# 在 Windows 的 Git Bash / MinGW64 bash 里这样跑(注意用 /usr/bin/bash):
|
||||
# /usr/bin/bash .build/build-win.sh
|
||||
#
|
||||
# 它只做一件事:把 WSL 脚本喂给 wsl.exe 执行,然后透传退出码。
|
||||
# 真正的 sync / make / verify / 拷回都在 .build/build-elf-wsl.sh 里。
|
||||
#
|
||||
# 注意:
|
||||
# * 用 `bash < script` 走 stdin,不要用 `bash -c '...'` ——
|
||||
# 路径含空格时 -c 的参数会被 wsl.exe 拆断。
|
||||
# * 别写裸 `bash .build/build-win.sh`,那个 bash 可能解析到
|
||||
# C:\Windows\System32\bash.exe(WSL 启动器),脚本会跑进 Linux 环境。
|
||||
# ===========================================================================
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
REPO='/c/Users/songl/Desktop/Web File Manager/ps5-web-file-manager'
|
||||
WSL_DISTRO='Ubuntu-22.04'
|
||||
WSL_DIR='/home/song/ps5-web-file-manager/.build'
|
||||
HOST_SCRIPT="$REPO/.build/build-elf-wsl.sh"
|
||||
|
||||
[ -f "$HOST_SCRIPT" ] || { echo "[FAIL] 找不到 $HOST_SCRIPT"; exit 1; }
|
||||
|
||||
# 每次都把最新的 WSL 脚本推进去(.build/ 被 rsync 排除,WSL 侧不会自己更新;
|
||||
# 只做一次会导致改了脚本还在跑旧版 —— 这个坑踩过)
|
||||
wsl.exe -d "$WSL_DISTRO" -- mkdir -p "$WSL_DIR" || exit 1
|
||||
wsl.exe -d "$WSL_DISTRO" -- cp \
|
||||
'/mnt/c/Users/songl/Desktop/Web File Manager/ps5-web-file-manager/.build/build-elf-wsl.sh' \
|
||||
"$WSL_DIR/build-elf-wsl.sh" || {
|
||||
echo "[FAIL] 无法写入 WSL 文件系统"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "[run] wsl.exe -d $WSL_DISTRO -- bash < build-elf-wsl.sh"
|
||||
echo "=================================================================="
|
||||
wsl.exe -d "$WSL_DISTRO" -- bash < "$HOST_SCRIPT"
|
||||
rc=$?
|
||||
echo "=================================================================="
|
||||
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
# 输出文件带版本号(web-file-mgr-<VERSION_TAG>.elf),列出实际产物
|
||||
echo "[OK] 构建完成,产物:"
|
||||
ls -1 "$REPO"/web-file-mgr-*.elf 2>/dev/null | sed 's#^# #'
|
||||
else
|
||||
echo "[FAIL] 构建失败 (exit=$rc)"
|
||||
fi
|
||||
exit "$rc"
|
||||
+17
-11
@@ -5,17 +5,20 @@
|
||||
gen/
|
||||
web-file-mgr-linux
|
||||
|
||||
# Sandbox probe scratch under .build/ (the tracked items there are
|
||||
# build-elf.{sh,log}, check-elf-*.py and extract-demo.html; everything
|
||||
# else is local exploration that should not enter the repo).
|
||||
.build/host-test/
|
||||
.build/obj/
|
||||
.build/probe-work/
|
||||
.build/tp/
|
||||
.build/stub/
|
||||
.build/*.exe
|
||||
.build/*.c
|
||||
.build/test-out.txt
|
||||
# Sandbox scratch under .build/: ignore the whole directory, then re-allow the
|
||||
# handful of files that are actually part of the repo (build scripts + the
|
||||
# ELF checker). A whitelist is the only thing that survives -- every debugging
|
||||
# session drops a new probe directory in here.
|
||||
.build/*
|
||||
!.build/build-elf.sh
|
||||
!.build/build-elf-wsl.sh
|
||||
!.build/build-win.sh
|
||||
!.build/check-elf-*.py
|
||||
!.build/extract-demo.html
|
||||
!.build/build-elf.log
|
||||
|
||||
# Generated 7z fixtures (tests/make_sevenz_fixtures.py rebuilds them).
|
||||
tests/fixtures-7z/
|
||||
|
||||
# Editor / OS noise
|
||||
.vscode/
|
||||
@@ -23,3 +26,6 @@ web-file-mgr-linux
|
||||
*.swp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# WorkBuddy session data (never commit; also never delete).
|
||||
.workbuddy/
|
||||
+305
-6
@@ -4,14 +4,313 @@ All notable changes to **PS5 Web File Manager** are documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
> Release artifact for v1.8:
|
||||
> `web-file-mgr.elf` — size TBD (cross-compile runs in WSL — see `docs/HANDOVER.md`)
|
||||
> sha256 TBD
|
||||
> Release artifact for v1.9.2:
|
||||
> `web-file-mgr-v1.9.2.elf` — size 870 488 bytes (~850 KiB)
|
||||
> sha256 `177e90fecf93a0251e83f67884fba4551051be248330b0d70fda8ea732f88e84`
|
||||
> ELF class 64, little-endian, e_machine `0x003e` (x86_64-sie-ps5)
|
||||
>
|
||||
> Source delta vs v1.7: +2 vendored files (`third_party/unrar/dmc_unrar.c`,
|
||||
> `third_party/unrar/dmc_unrar_api.h`), +1 new source pair
|
||||
> (`src/rar_extract.{c,h}`), `src/extract.c` gains a dispatch layer.
|
||||
> Behaviourally identical to the published v1.9.1 binary — the only source
|
||||
> delta is the version literal itself (`VERSION_TAG` in the Makefile, plus the
|
||||
> UI footer fallback in `assets/main.js`). The build is reproducible: reverting
|
||||
> those two literals and rebuilding reproduces the v1.9.1 ELF byte for byte, so
|
||||
> nothing else differs. See [v1.9.2] below for why the version moved at all.
|
||||
>
|
||||
> Release artifact for v1.9.1 (superseded — the tag pointed four commits behind
|
||||
> the tree that actually produced this binary):
|
||||
> `web-file-mgr-v1.9.1.elf` — size 870 488 bytes (~850 KiB)
|
||||
> sha256 `24392aff6ddcca4dc0ea969cce356bd693ac52efe8a117d61ee1c814aa43cd07`
|
||||
> ELF class 64, little-endian, e_machine `0x003e` (x86_64-sie-ps5)
|
||||
>
|
||||
> Built on the 7z-complete tree: LZMA SDK decode subset + self-written codec
|
||||
> chain, 7zAES, and ZIP/RAR/7z volume support. Build-system-only delta vs the
|
||||
> first v1.9.1 artifact (1 017 864 B): `src/demangle_stub.c` keeps libc++abi's
|
||||
> Itanium name demangler (105 KiB, only reachable from the uncaught-exception
|
||||
> path) out of the link, and `-Wl,--icf=all` folds identical functions.
|
||||
> −15.8% overall with no change to functionality or decompression throughput.
|
||||
> See `docs/SIZE-OPTIMIZATION.md`.
|
||||
>
|
||||
> Release artifact for v1.9:
|
||||
> `web-file-mgr.elf` — size 919 440 bytes (~897 KiB)
|
||||
> sha256 `bb8f17e9addc6a9984f611503ca01b51f8984b773d353630da1f25bd1a28a997`
|
||||
> ELF class 64, little-endian, e_machine `0x003e` (x86_64-sie-ps5)
|
||||
>
|
||||
> Source delta vs v1.8.3: RAR engine replaced (dmc_unrar 1.7.0 → rarlab
|
||||
> UnRAR 7.20.1, `third_party/unrar/` → `third_party/unrar7/`), new
|
||||
> `src/rar_extract.c` scan/extract implementation, Makefile + host-test
|
||||
> C++ rules, 5 real RAR fixtures committed. See [v1.9] below.
|
||||
>
|
||||
> Release artifact for v1.8.3:
|
||||
> `web-file-mgr.elf` — size 509 704 bytes (~497 KiB)
|
||||
> sha256 `fdcf7b09b69e2160e77dfa084c0e890ba0696d4dd478b1d5ff499cdc9f527955`
|
||||
> ELF class 64, little-endian, e_machine `0x003e` (x86_64-sie-ps5)
|
||||
>
|
||||
> Source delta vs v1.8.2: 5 files touched (4 user-facing + 1 build pipeline) —
|
||||
> see [v1.8.3] below for details.
|
||||
>
|
||||
> Release artifact for v1.8.2:
|
||||
> `web-file-mgr.elf` — size 509 704 bytes (~497 KiB)
|
||||
> sha256 `1b2c3d68b35e32737105f17d14a80a3c159ceca0cabd274ee168cbcd81906f65`
|
||||
> ELF class 64, little-endian, e_machine `0x003e` (x86_64-sie-ps5)
|
||||
>
|
||||
> Source delta vs v1.8.1: 5 files relaxed (`src/zip_extract.c` k_default_limits
|
||||
> + k_large_limits, `assets/main.js` `LARGE_FILE_THRESHOLD_BYTES`,
|
||||
> `assets/lang-{en,zh}.js` copy, `tests/test_zip_extract.c` advertised-number
|
||||
> assertion, README/HANDOVER numeric references) + 2 PS5-only build fixes
|
||||
> (`Makefile` CFLAGS `-Ithird_party/unrar`, `src/extract.c` forward
|
||||
> declaration of `extract_progress`); no vendored or engine changes.
|
||||
|
||||
## [v1.9] — 2026-09-05
|
||||
|
||||
**RAR engine replaced: rarlab UnRAR 7.20.1 (v6 / multi-volume / decryption-capable).**
|
||||
|
||||
The vendored dmc_unrar 1.7.0 only dispatched RAR5 compression version 5
|
||||
(`switch(file->version)` case `0x5000`). Archives written by WinRAR 6.x /
|
||||
7.x (algorithm string `v6`, version field `0x5001`) hit the default branch
|
||||
and surfaced as "corrupt archive" — confirmed on a real `v6:8M` archive.
|
||||
v1.9 swaps in the official rarlab UnRAR source (7.20.1) via its
|
||||
C-compatible DLL API, compiled as a static library (`-DRARDLL`, PS5 uses
|
||||
the toolchain's default `libc++`).
|
||||
|
||||
What this enables:
|
||||
|
||||
- **RAR5 "v6" compression** (WinRAR 6/7 archives) — the v1.9 trigger.
|
||||
- **Multi-volume RAR** (`.partNN.rar`): unrar stitches volumes by name when
|
||||
all parts sit next to the opened volume. Select the first volume
|
||||
(`name.part1.rar`) and extract as usual.
|
||||
- RAR4 and older RAR5 remain supported.
|
||||
- The engine *can* decrypt encrypted archives (`RARSetPassword`), but the
|
||||
password channel (API + UI) is not wired yet — encrypted headers/entries
|
||||
still fail up front with `err_extract_unsupported`. Planned for a follow-up.
|
||||
|
||||
Engine changes:
|
||||
|
||||
- `src/rar_extract.c` rewritten to unrar's sequential DLL API
|
||||
(`RAROpenArchiveEx → RARReadHeaderEx → RARProcessFile`); scan and extract
|
||||
each re-open the archive. Multi-volume continuation segments
|
||||
(`RHDF_SPLITBEFORE`) are advanced but not re-counted/deduped.
|
||||
- The three-phase scan → staging → publish/rollback machinery is unchanged.
|
||||
- Bug fix: `normalize_name()` no longer clears the caller's directory flag
|
||||
(a real v6 archive with an explicit directory header after its files
|
||||
tripped the duplicate detector).
|
||||
|
||||
Build & test:
|
||||
|
||||
- Makefile: `.cpp` rules for the unrar RARDLL source set (49 files, mirrors
|
||||
`UnRARDll.vcxproj`); links through the C++ driver; `VERSION_TAG` v1.9.
|
||||
- tests: 5 real RAR fixtures committed under `tests/fixtures-real/`
|
||||
(generated with `tests/make-rar-fixtures.bat` + WinRAR); new happy-path
|
||||
checks extract a real v6 archive, verify files on disk, auto-merge a
|
||||
3-volume split, and reject encrypted archives. Total: **70 ZIP + 24 RAR
|
||||
= 94 checks** (up from 70 + 14; the old 14 RAR checks never ran a real
|
||||
archive).
|
||||
|
||||
Credits: unrar (c) Alexander Roshal, freeware license — see
|
||||
`third_party/unrar7/license.txt` and `THIRD_PARTY_NOTICES`.
|
||||
|
||||
## [v1.8.3] — 2026-09-05
|
||||
|
||||
**Hotfix: "Upload and extract" now accepts `.rar` files.**
|
||||
|
||||
The "upload and extract" entry was hard-coded to accept only `.zip`,
|
||||
even though the server-side dispatch in `src/extract.c:79` already
|
||||
correctly routes `.rar` to `rar_extract()`. v1.8.3 fixes the frontend
|
||||
filter so users can select a single-volume plaintext `.rar` from the
|
||||
file picker and have it uploaded + extracted in one click (the same
|
||||
flow that already worked for `.zip`).
|
||||
|
||||
What this enables:
|
||||
|
||||
- Choose a single-volume `.rar` from "Upload and extract"
|
||||
- Server extracts it via the existing `rar_extract()` engine
|
||||
- Uploaded `.rar` is auto-deleted after a successful extract (same as
|
||||
`.zip` since v1.7)
|
||||
|
||||
What this does **not** enable (planned for v1.9.0):
|
||||
|
||||
- **Multi-volume RAR** (e.g. `name.part01.rar` + `name.part02.rar` …)
|
||||
- **Encrypted RAR** (password-protected headers or entries)
|
||||
|
||||
Both still return `extract_unsupported` "single-volume RAR only" /
|
||||
"encrypted RAR is not supported; please extract on a PC first" — see
|
||||
the underlying engine limit in `third_party/unrar/dmc_unrar` (GPL-2.0,
|
||||
1.7.0). v1.9 will swap the vendor to **opello/unrar 7.20.1** (UnRAR
|
||||
License) which natively supports both.
|
||||
|
||||
Changed:
|
||||
|
||||
- `assets/index.html` — `<input id="uploadZip" accept>` now lists
|
||||
`.rar` + the two RAR MIME types next to the existing ZIP entries.
|
||||
- `assets/main.js:2340` — `/\.zip$/i` → `/\.(zip|rar)$/i` (the upload
|
||||
pre-check), plus a local `isRar` flag so the next step branches.
|
||||
- `assets/lang-en.js` — `extractUploadConfirm`: "uploaded ZIP" →
|
||||
"uploaded archive".
|
||||
- `assets/lang-zh.js` — `extractUploadConfirm` & `extractLargeAsk`
|
||||
drop the "ZIP" wording so the copy reads sensibly for RAR uploads.
|
||||
- `assets/main.js:38` — `APP_VERSION` `"v1.7"` → `"v1.8.3"` (footer
|
||||
version string had been hard-coded to v1.7 since the frontend was
|
||||
first imported; it no longer misleads about which build is running).
|
||||
|
||||
No backend changes — the server side was already correct. No test
|
||||
changes — the existing RAR happy-path test in `tests/test_rar_extract.c`
|
||||
passes against the same backend.
|
||||
|
||||
Build pipeline (also v1.8.3):
|
||||
|
||||
- `.build/build-elf.sh` step 6 "no source change → skip make" check now
|
||||
also watches `assets/*` and `gen-asset-module.py`, not just `src/*.c`
|
||||
and the `Makefile`. Without this, v1.8.3 (which touched no backend,
|
||||
only frontend assets feeding `gen/*.c`) was misclassified as "no
|
||||
change" and `make` was skipped — the result was that the v1.8.2 ELF
|
||||
was reported as v1.8.3 with the same sha256. With this fix, only
|
||||
frontend changes correctly trigger a rebuild. Users running the WSL
|
||||
build need to re-copy `.build/build-elf.sh` to `/home/song/build-elf.sh`
|
||||
(canonical source is on the Windows side).
|
||||
|
||||
## [v1.8.2] — 2026-09-05
|
||||
|
||||
**Hotfix: default ZIP extraction limits cover 3A-game single-file archives.**
|
||||
|
||||
The default `k_default_limits` profile is now **2 TiB total / 512 GiB per
|
||||
entry / 500 : 1 ratio** (was 1 TiB / 256 GiB / 500 : 1 in v1.8.1). The
|
||||
frontend threshold `LARGE_FILE_THRESHOLD_BYTES` is bumped from 240 GiB to
|
||||
**480 GiB** to match. The `large=1` profile is widened to **4 TiB total
|
||||
/ 1 TiB per entry / 1000 : 1 ratio** (was 2 TiB / 1 TiB / 1000 : 1); the
|
||||
large profile must always be strictly more permissive than default.
|
||||
|
||||
Why: a 3A-game archive with a single ~300 GiB uncompressed file was
|
||||
**silently rejected by the default profile** (`scan_archive` returns
|
||||
`ZIPX_ERR_LIMIT_FILE_SIZE` in `src/zip_extract.c` line ~717 — the request
|
||||
never reaches the frontend confirmation prompt, so the user just sees
|
||||
"卡壳"). The 256 GiB default cap was tuned for PS5 system backups (which
|
||||
have many smaller entries, not a single huge file) and was wrong for the
|
||||
3A-game single-file case. The default cap is now 512 GiB so a typical
|
||||
3A archive extracts under the default profile without prompting.
|
||||
|
||||
The safety argument is unchanged from v1.8.1: zip-bomb defence is
|
||||
`check_space()` (`statvfs`-based real disk space check before staging) +
|
||||
`max_ratio` (declared compression ratio cap). The size caps are a UX
|
||||
guard, not a security boundary.
|
||||
|
||||
RAR extraction inherits the new defaults automatically — rar_extract.c
|
||||
threads `c->limits` through from the engine, so no rar-side change is
|
||||
required.
|
||||
|
||||
### Changed
|
||||
|
||||
- `src/zip_extract.c` — `k_default_limits` relaxed:
|
||||
- `max_total_bytes`: 1 TiB → **2 TiB**
|
||||
- `max_file_bytes`: 256 GiB → **512 GiB**
|
||||
- `max_ratio`: 500 → **500** (unchanged)
|
||||
- `src/zip_extract.c` — `k_large_limits` widened (must stay > default):
|
||||
- `max_total_bytes`: 2 TiB → **4 TiB**
|
||||
- `max_file_bytes`: 1 TiB → **1 TiB** (unchanged)
|
||||
- `max_ratio`: 1000 → **1000** (unchanged)
|
||||
- `assets/main.js` — `LARGE_FILE_THRESHOLD_BYTES`: 240 GiB → **480 GiB**
|
||||
- `assets/lang-{en,zh}.js` — `extractLargeAsk` copy updated to reflect
|
||||
the new numbers (default 512 GiB / 2 TiB; large 1 TiB / 4 TiB)
|
||||
- `tests/test_zip_extract.c` — `test_large_profile` advertised-number
|
||||
assertion updated: `max_total_bytes == 4 TiB` (was 2 TiB)
|
||||
- `README.md` — both limit tables (ZIP + RAR), the "Tuning the threshold"
|
||||
snippet, and the `err_extract_entry_too_large` FAQ entry bumped to the
|
||||
new numbers
|
||||
- `docs/HANDOVER.md` — `LARGE_FILE_THRESHOLD_BYTES`, the
|
||||
`k_default_limits` / `k_large_limits` ASCII diagram, the user-scenario
|
||||
description, the 480 GiB popup note, and the RAR limits paragraph
|
||||
bumped to the new numbers
|
||||
|
||||
### Unchanged
|
||||
|
||||
- `src/rar_extract.c` — already threads `c->limits` from the engine,
|
||||
picks up the new defaults for free
|
||||
- `max_ratio` — both profiles unchanged (500 : 1 default / 1000 : 1 large)
|
||||
- `check_space()` — unchanged; still the real disk-space guard
|
||||
- `max_entries` — 200 000 default / 500 000 large, unchanged
|
||||
- `docs/UPGRADE-v1.7-zip-large-file-profile.md` — historical v1.7
|
||||
document left as-is so the v1.7 → v1.8.2 evolution is traceable
|
||||
- Test fixture `medium_bomb.zip` (ratio ≈ 238) still exercises both
|
||||
rejection under the default 500 : 1 cap and acceptance under the
|
||||
`large=1` 1000 : 1 cap
|
||||
- 84 host-side checks (70 ZIP + 14 RAR), 0 failures
|
||||
|
||||
### Migration notes
|
||||
|
||||
- **Forward-compatible** — users with v1.8.1 deployments who never trigger
|
||||
`err_extract_entry_too_large` see no difference (defaults are strictly
|
||||
more permissive)
|
||||
- **3A-game single-file archives now extract silently** — no prompt, no
|
||||
manual `large=1` API call required for files up to 512 GiB
|
||||
- **No data loss** — the relaxation only widens accepted archives; the
|
||||
real security guards (`check_space`, `max_ratio`, `path traversal`)
|
||||
are untouched
|
||||
- **No frontend UX change for typical use** — only archives > 480 GiB
|
||||
on disk now trigger the confirmation prompt (previously 240 GiB)
|
||||
|
||||
---
|
||||
|
||||
## [v1.8.1] — 2026-09-05
|
||||
|
||||
**Hotfix: relaxed default ZIP extraction limits.**
|
||||
|
||||
The default `k_default_limits` profile is now **1 TiB total / 256 GiB per
|
||||
entry / 500 : 1 ratio** (was 512 GiB / 64 GiB / 200 : 1). The frontend
|
||||
threshold `LARGE_FILE_THRESHOLD_BYTES` is bumped from 60 GiB to 240 GiB
|
||||
to match. The `large=1` profile (1 TiB / 1 TiB / 1000 : 1) is unchanged.
|
||||
|
||||
Why: the previous default was a UX-oriented early-fail guard, not a
|
||||
security guard — `check_space()` already enforces available ≥ bytes_total
|
||||
before staging begins, and `max_ratio` already rejects classic zip
|
||||
bombs. A user with a multi-hundred-GiB system image shouldn't have to
|
||||
click through a confirmation prompt for what's a perfectly safe archive.
|
||||
The relaxed default still rejects any archive whose declared
|
||||
uncompressed total exceeds the destination's free space (real check,
|
||||
not a declared-vs-fs assertion) and any archive with a declared ratio
|
||||
above 500 : 1 (real zip-bomb guard).
|
||||
|
||||
RAR extraction inherits the new defaults automatically — rar_extract.c
|
||||
threads `c->limits` through from the engine, so no rar-side change is
|
||||
required.
|
||||
|
||||
### Changed
|
||||
|
||||
- `src/zip_extract.c` — `k_default_limits` relaxed:
|
||||
- `max_total_bytes`: 512 GiB → **1 TiB**
|
||||
- `max_file_bytes`: 64 GiB → **256 GiB**
|
||||
- `max_ratio`: 200 → **500**
|
||||
- `assets/main.js` — `LARGE_FILE_THRESHOLD_BYTES`: 60 GiB → **240 GiB**
|
||||
- `assets/lang-{en,zh}.js` — `extractLargeAsk` default-profile copy
|
||||
updated to reflect the new numbers
|
||||
- `README.md` — "Stricter default ZIP profile" line, the limit table
|
||||
(two locations), and the `err_extract_entry_too_large` FAQ entry
|
||||
bumped to the new numbers; "Tuning the threshold" snippet updated to
|
||||
240 GiB
|
||||
- `docs/HANDOVER.md` and `docs/UPGRADE-v1.8-rar-support.md` — the
|
||||
few remaining numeric references in those docs updated
|
||||
|
||||
### Unchanged
|
||||
|
||||
- `src/rar_extract.c` — already threads `c->limits` from the engine,
|
||||
picks up the new defaults for free
|
||||
- `k_large_limits` — `large=1` profile (1 TiB / 1 TiB / 1000 : 1) is
|
||||
unchanged
|
||||
- `docs/UPGRADE-v1.7-zip-large-file-profile.md` — historical v1.7
|
||||
document left as-is so the v1.7 → v1.8.1 evolution is traceable
|
||||
- Test fixture `medium_bomb.zip` (ratio ≈ 238) still exercises both
|
||||
rejection under the default 500 : 1 cap and acceptance under the
|
||||
`large=1` 1000 : 1 cap
|
||||
- 83 host-side checks (69 ZIP + 14 RAR), 0 failures
|
||||
|
||||
### Migration notes
|
||||
|
||||
- **Forward-compatible** — users with v1.7 / v1.8 deployments who never
|
||||
trigger `err_extract_entry_too_large` see no difference (defaults are
|
||||
strictly more permissive)
|
||||
- **No data loss** — the relaxation only widens accepted archives; the
|
||||
real security guards (`check_space`, `max_ratio`, `path traversal`)
|
||||
are untouched
|
||||
- **No frontend UX change for typical use** — only archives > 240 GiB
|
||||
on disk now trigger the confirmation prompt (previously 60 GiB)
|
||||
|
||||
---
|
||||
|
||||
## [v1.8] — 2026-09-05
|
||||
|
||||
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
# 交接文档 — ps5-web-file-manager 工作进度
|
||||
|
||||
> 交接时间:2026-09-20 · 分支 `main` · 最新提交 **`807d129`** · tag **`v1.9.2`** 已重打到产出发布二进制的提交(此前 `v1.9.1` 落后 4 个提交,tag 与产物对不上)
|
||||
>
|
||||
> **主线(用户 2026-09-12 指令)**:「先从 zip 分卷开始吧,然后把六种组合打齐,并把密码通道补齐,注意一些报错信息提示的时候尽量详细准确」
|
||||
> **状态:主线全部闭合。** 六种组合(ZIP/RAR/7z × 单卷/分卷)+ 密码通道(RAR 加密 + 7zAES)+ 报错详细信息,全部落地、测试全绿、PS5 ELF 构建成功。
|
||||
> **剩余**:① PS5 真机端到端验证(**唯一还没过的关卡**);② `-mhe=on` 加密头(唯一功能缺口)。
|
||||
|
||||
---
|
||||
|
||||
## 一、当前状态速览
|
||||
|
||||
| 维度 | 状态 |
|
||||
|---|---|
|
||||
| 解压引擎 | ZIP / RAR / 7z × 单卷/分卷(6 组合)+ 7zAES / RAR 加密 |
|
||||
| 主机测试 | **ZIP 108 + RAR 27 + 7z 28 = 163 checks,0 失败**(MinGW gcc;2026-09-20 在 v1.9.2 树上复跑) |
|
||||
| PS5 构建 | ✅ WSL prospero-clang 18.1.8,一键脚本可复现;构建已实测**确定性**(同源两次构建 sha256 相同) |
|
||||
| ELF 产物 | `web-file-mgr-v1.9.2.elf` · 870,488 B · sha256 `177e90fecf93a0251e83f67884fba4551051be248330b0d70fda8ea732f88e84` · e_machine=0x003e(2026-09-20 瘦身后;瘦身前 1,017,864 B,见 `docs/SIZE-OPTIMIZATION.md`) |
|
||||
| GitHub | `main`(`807d129`)与 tag `v1.9.2` 均已推送;Release `v1.9.2` 资产为该 ELF |
|
||||
| 唯一功能缺口 | 7z `-mhe=on`(加密头) |
|
||||
|
||||
### 发布命令(v1.9.2)
|
||||
|
||||
```bash
|
||||
cd "/c/Users/songl/Desktop/Web File Manager/ps5-web-file-manager"
|
||||
|
||||
git push origin main
|
||||
git push origin v1.9.2
|
||||
|
||||
gh release create v1.9.2 web-file-mgr-v1.9.2.elf \
|
||||
--repo LisherSong/ps5-web-file-manager --title "v1.9.2" --notes-file <notes.md>
|
||||
```
|
||||
|
||||
沙箱内 git 出站 HTTPS **可用**(早先记的"被拦"是误判:`timeout 25 git ...`
|
||||
命中的是 `C:\Windows\System32\TIMEOUT.EXE`,报参数错误而非网络错误)。`gh` 同样可用,
|
||||
所以 commit / tag / push / 发 Release 都可以在会话里直接跑。
|
||||
|
||||
---
|
||||
|
||||
## 二、本轮(2026-09-15)变更
|
||||
|
||||
### 2.1 一键构建脚本(`a54f34b` / `5229cd5`)
|
||||
|
||||
| 文件 | 跑在哪 | 作用 |
|
||||
|---|---|---|
|
||||
| `.build/build-win.sh` | Windows Git Bash | 入口:把 WSL 脚本经 **stdin** 喂给 `wsl.exe`,透传退出码 |
|
||||
| `.build/build-elf-wsl.sh` | WSL Ubuntu-22.04 | 5 阶段:环境检查 → rsync 同步 → `make all` → 验证 → 拷回 Windows |
|
||||
| `.build/build-elf.sh` | WSL 内 | **仅首次搭环境用**(libmicrohttpd staging 安装 + sudo) |
|
||||
|
||||
```bash
|
||||
# Windows 端(注意:必须 /usr/bin/bash,裸 bash 会解析成 WSL 启动器)
|
||||
cd "/c/Users/songl/Desktop/Web File Manager/ps5-web-file-manager"
|
||||
/usr/bin/bash .build/build-win.sh
|
||||
|
||||
# 或 WSL 内
|
||||
bash /home/song/build.sh
|
||||
```
|
||||
|
||||
**两个关键实现点**(改脚本前必读):
|
||||
- `wsl.exe -- bash -c '...'` 遇到含空格路径会被拆断 → 必须 `wsl.exe -- bash < script.sh`(stdin 重定向)
|
||||
- `make ... | tail` 会吞掉退出码 → 用 `${PIPESTATUS[0]}`;否则编译失败还会继续跑验证,输出假成功
|
||||
|
||||
### 2.2 版本号体系统一(`f4fd464` / `1fa2f09` / `0d036a7`)
|
||||
|
||||
原本版本号有**两个真相来源**,已经漂移过:`Makefile` 写 `v1.9.1`,`assets/main.js` 硬编 `"v1.9"`,UI 右下角在整个 v1.9.1 发布期都显示旧值。
|
||||
|
||||
现在收敛到 `Makefile` 一处:
|
||||
|
||||
```make
|
||||
VERSION_TAG ?= v1.9.2 # 可用 make VERSION_TAG=v1.9.3 临时覆盖
|
||||
BIN := web-file-mgr-$(VERSION_TAG).elf
|
||||
```
|
||||
|
||||
改这一行会同时影响 **四处**:ELF 内嵌版本串、PS5 启动通知、输出文件名、UI 右下角。
|
||||
|
||||
| 提交 | 内容 |
|
||||
|---|---|
|
||||
| `f4fd464` | `VERSION_TAG` `v1.9` → `v1.9.1`;`.gitignore` 改白名单式(`.build/*` 全忽略 + `!` 放行 6 个脚本),`.workbuddy/` 也忽略 |
|
||||
| `1fa2f09` | 输出文件名派生自 `VERSION_TAG`;`build-elf-wsl.sh` 从 Makefile 反读版本(不硬编);`build-win.sh` 每次都推 WSL 脚本(原来只在缺失时推,导致改了脚本 WSL 侧仍跑旧版) |
|
||||
| `0d036a7` | **新增 `/api/version`**,前端右下角改从后端取值(见 2.3) |
|
||||
|
||||
⚠️ **`assets/main.js` 里还有第二处字面量** `APP_VERSION_FALLBACK`(`/api/version` 取不到时的兜底值)。
|
||||
它不在 Makefile 的控制范围内 —— **升版本号时必须一并改**,否则后端请求失败时页脚会显示旧版本。
|
||||
v1.9.2 就是这两处一起改的。
|
||||
|
||||
### 2.3 UI 版本号改由后端提供(`0d036a7`)
|
||||
|
||||
**问题**:`assets/main.js:38` 的 `const APP_VERSION = "v1.9"` 与 Makefile 无关,必然漂移。
|
||||
|
||||
**修法**:
|
||||
- 新 `src/version.c` — `GET /api/version` → `{"ok":true,"version":"v1.9.2","titleId":"FMGR88888"}`,直接来自 Makefile 已传的 `-DVERSION_TAG` / `-DTITLE_ID` 宏,没有第二处要记得改
|
||||
- `src/filemgr.c` 路由表加一行(紧邻 `/api/space`)+ `filemgr_internal.h` 声明 + `Makefile` `COMMON_SRCS`
|
||||
- 前端:字面量降级为 `APP_VERSION_FALLBACK`(先渲染,保证页脚不空),`loadVersion()` 后台刷新。**请求失败静默吞掉** —— 版本号显示错属于装饰性问题,不该弹错误 toast
|
||||
|
||||
**踩坑**:新文件漏了 `#include "json_util.h"` → `strbuf_append` / `json_escape` 隐式声明报错。`space.c` 是模板,照抄时别漏。
|
||||
|
||||
---
|
||||
|
||||
## 三、功能矩阵与测试
|
||||
|
||||
### 3.1 六种组合 + 密码通道
|
||||
|
||||
| # | 引擎 | 单卷 | 分卷 | 密码 |
|
||||
|---|---|---|---|---|
|
||||
| ① | ZIP | ✅ | ✅ 三种命名约定 | ✅ |
|
||||
| ② | RAR | ✅ | ✅(vendor unrar 7.20.1) | ✅ `RARSetPassword` |
|
||||
| ③ | 7z | ✅ | ✅ `.7z.001` | ✅ 7zAES |
|
||||
|
||||
### 3.2 测试
|
||||
|
||||
```bash
|
||||
export PATH="/c/mingw64/bin:/c/Users/songl/.workbuddy/binaries/PortableGit/versions/1.2.0/mingw64/bin:/c/Users/songl/.workbuddy/binaries/python/versions/3.13.12:/usr/bin:/bin:/c/Windows/System32:/c/Windows"
|
||||
cd "/c/Users/songl/Desktop/Web File Manager/ps5-web-file-manager"
|
||||
/usr/bin/bash tests/run-tests.sh # ZIP 108 + RAR 27
|
||||
/usr/bin/bash tests/run-sevenz-tests.sh # 7z 28(含 KNOWN_GAPS 检查)
|
||||
```
|
||||
|
||||
⚠️ **绝不写裸 `bash`** —— 可能解析到 `C:\Windows\System32\bash.exe`(WSL 启动器),脚本跑进 Linux,gcc/python 全变 Linux 版,报莫名错误。必须 `/usr/bin/bash`。
|
||||
|
||||
`run-sevenz-tests.sh` 带 `KNOWN_GAPS` 列表(当前仅 `aeshe`),缺口修好后脚本会主动报错,防止列表腐烂。脚本**不做任何删除**(safe-delete 钩子会拦 `rm -rf`)。
|
||||
|
||||
---
|
||||
|
||||
## 四、构建(PS5 ELF)
|
||||
|
||||
### 4.1 日常构建
|
||||
|
||||
```bash
|
||||
cd "/c/Users/songl/Desktop/Web File Manager/ps5-web-file-manager"
|
||||
/usr/bin/bash .build/build-win.sh
|
||||
```
|
||||
|
||||
增量有效(`ps5-obj/` 缓存保留)→ 二次构建 30s–2min。**别 `make clean`**(全量重编第三方 3–5min)。
|
||||
|
||||
产物:项目根 `web-file-mgr-<VERSION_TAG>.elf`,同时留在 WSL `/home/song/ps5-web-file-manager/`。
|
||||
|
||||
### 4.2 验证清单
|
||||
|
||||
```bash
|
||||
ls -lh web-file-mgr-v1.9.2.elf
|
||||
sha256sum web-file-mgr-v1.9.2.elf
|
||||
od -An -tx2 -j18 -N2 web-file-mgr-v1.9.2.elf # 期望 3e00
|
||||
strings -a web-file-mgr-v1.9.2.elf | grep -m1 '^v1\.'
|
||||
```
|
||||
|
||||
⚠️ `od -An -tx2` 打印的是**小端 short 的值**(`003e`),不是字节序(`3e00`)。脚本里比对用 `$((16#$EM))` 转数值(**62 = x86-64 ✅ / 183 = aarch64 ❌**)。
|
||||
|
||||
✅ **构建已实测可复现**(2026-09-20):同一源码树两次构建 sha256 完全相同;把两处版本字面量回退成 `v1.9.1` 后重构,产物与已发布的 v1.9.1 ELF **逐字节一致**。所以 sha256 可以作为交付指纹用 —— 但它对任何源码改动都会全变(改一个字符串常量会令链接器重排 `.rodata` 字符串池,牵动 `.text` 里所有 RIP 相对位移,原始 diff 会放大到 5 万字节以上,属正常现象,别误判成"代码改了")。核对版本仍推荐 `strings ... | grep '^v1\.'`,最直观。
|
||||
|
||||
### 4.3 构建坑(已修,改 Makefile 前必读)
|
||||
|
||||
`third_party/7z/AesOpt.c` 用编译器版本宏判断是否启用 AES-NI / AVX / VAES,clang 18 直接进 VAES 分支,但 prospero-clang 默认 target 是 generic x86_64 → `_mm256_aesenc_epi128` 未声明,20 报错。
|
||||
|
||||
- ❌ **不能** `filter-out AesOpt.c` —— `Aes.c` 通过 `AesGenTables` 引用 `AesCbc_Encode_HW` 等符号,会链接失败
|
||||
- ✅ **正解**:路径过滤 `SEVENZ_C_FLAGS := -maes -mavx2 -mvaes`,仅 `third_party/7z/*.c` 用。PS5 是 Zen 2,硬件全支持,运行时无差异
|
||||
|
||||
---
|
||||
|
||||
## 五、7z 引擎设计要点(改代码前必读)
|
||||
|
||||
### 5.1 为什么不走 SDK 的解码器
|
||||
|
||||
LZMA SDK 26.03(public domain,已 vendor 到 `third_party/7z/`,解码子集 60 文件)有两个硬限制,**实测复现过**:
|
||||
|
||||
1. **`CSzFolder` 上限 4 coder / 3 bond** —— 7-Zip 默认 `-m0=bcj2` 链 = BCJ2 + 4×LZMA2 = 5 coder,`SzAr_DecodeFolder()` 返回 `SZ_ERROR_UNSUPPORTED`。注意 `SzArEx_Open()` 用的是另一套宽松扫描器(`k_Scan_NumCoders_MAX 64`),所以**文件列表和解压尺寸仍然全对**,失败只在解压时按条目暴露
|
||||
2. **C 解码器完全没有 7zAES coder** —— `-p` 与 `-mhe=on` 全被拒
|
||||
|
||||
→ 因此引擎**自解析 folder blob + 自己驱动 codec 链**(`src/sevenz_chain.c/.h`,pull pipeline:`node_pull(n, dst, want, &got)`,不够就 `node_refill()` 拉上游)。
|
||||
|
||||
### 5.2 最关键的坑
|
||||
|
||||
**【必记】每个 coder 节点的 `out_size` 必须取 `coder_unpack_sizes[index]`,绝不能用 folder 的 unpack size。** BCJ2 folder 里 MAIN 常大于 folder 最终尺寸(实测 300066 > 300000)。用错的症状:每层 LZMA2 静默短 21 字节,只在特定包上暴露。
|
||||
|
||||
其他:
|
||||
- 编译必须 `-DZ7_PPMD_SUPPORT`,否则 `7zDec.c` 直接丢掉 PPMd
|
||||
- `CoderUnpackSizes` 是**扁平数组**(每条 = 对应 coder 输出流大小),**非累计**;`FoToCoderUnpackSizes[f]..[f+1]` 是该 folder 的切片
|
||||
- main coder = 第一个未被 bond 消费的 coder
|
||||
- `SzArEx_Extract` 失败后会把半成品留在 block cache → 后续条目报**假 CRC**,要重置 `blockIndex`
|
||||
- 造夹具用 Extra 包的 `7za.exe`(有 PPMd);**`7zr.exe` 没有 PPMd 编码器**
|
||||
- 调试三件套在 `.build/`:`chainprobe.c`(摸内部图)、`chainprobe2.c -r <coder>`(强制 root 逐层二分)、`chaincheck.py`(Python liblzma 独立复现同一条链,秒判"图错"还是"循环错")
|
||||
|
||||
### 5.3 7zAES KDF
|
||||
|
||||
`numCyclesPower = b0 & 0x3F`;`saltSize = ((b0>>7)&1) + (b1>>4)`;`ivSize = ((b0>>6)&1) + (b1&0x0F)`,随后依次 salt → iv。
|
||||
`numCyclesPower == 0x3F` 时 key = `salt||password` 补齐/截断到 32 字节;否则 `key = SHA256(salt || password_utf16le || counter_le64)` 迭代 `1<<numCyclesPower` 次。之后 AES-256-CBC。
|
||||
限额 `max_aes_cycles = 24`(约 8s)。
|
||||
|
||||
**改引擎前先在 `.build/aesprobe.c` 独立验证 KDF**(用 vendor 的 `Sha256.c` + `Aes.c` 解 aes.7z coder0,与 `cus[0]=638314` 比对),确认后再集成。
|
||||
|
||||
### 5.4 分卷流抽象
|
||||
|
||||
- `src/zipx_volstream.c/.h`(ZIP,包成 `mz_stream`)· `src/sevenz_volstream.c/.h`(7z,包成 SDK `ISeekInStream`)
|
||||
- **结构体首成员必须是 `mz_stream stream;` / `ISeekInStream vt;`**(回调把 `void*` 强转)
|
||||
- `vol_is_open()` 必须返回 `MZ_OK`/`MZ_OPEN_ERROR`(**不是 1/0**)
|
||||
- vtbl **必须注册 `destroy`**,否则 `mz_stream_delete()` 不回调 → 泄漏
|
||||
- CONCAT 模式对 `DISK_NUMBER`/`DISK_SIZE` 返回 `MZ_PARAM_ERROR` → 让 minizip 不切盘
|
||||
- DISK 模式 `set_prop(DISK_NUMBER, -1)` 必须切到**最后一卷**(minizip 路径 `mz_zip.c:2252-2275`)
|
||||
- `remove_source_archives()` 要删**所有**卷,避免孤儿卷
|
||||
|
||||
### 5.5 提取门面
|
||||
|
||||
`src/sevenz_extract.c`(1753 行)完全仿 `zip_extract.c` / `rar_extract.c`:scan → extract(staging, 每 entry fsync) → publish(整 rename) → cleanup。
|
||||
|
||||
- **OVERWRITE 与 MERGE 对目录-目录碰撞都递归下钻**(仅叶子文件不同)
|
||||
- 三方共用 `src/zipx_common.c`(限额 profile + `zipx_status_string()`)
|
||||
- scan 阶段**每 256 entries** 报一次进度(曾用 4096,小包扫描期 UI 静默),扫描末 force-report;`precheck_folders` 入口也强制报一次
|
||||
- 密码错时 detail **必须带 archive 名**(曾是 NULL → i18n `{arg}` 展开成空 → 用户看到「密码错误: 」后面光秃秃)
|
||||
|
||||
---
|
||||
|
||||
## 六、限额体系(ZIP 与 RAR 共用;7z 同源)
|
||||
|
||||
| 限额字段 | default | large | 160GB/9万文件场景 |
|
||||
|---|---|---|---|
|
||||
| `max_entries` | 200,000 | 500,000 | 9 万 ✅ |
|
||||
| `max_total_bytes` | 2 TiB | 4 TiB | 160 GiB ✅ |
|
||||
| `max_file_bytes` | **512 GiB** | **1 TiB** | 20 GiB ✅ |
|
||||
| `max_ratio` | 500 | 1000 | 仅 ≥1GiB 条目受检 |
|
||||
| `ratio_min_bytes` | 1 GiB | 1 GiB | 小文件豁免 |
|
||||
|
||||
- 切 large 档的触发条件:**压缩包文件本身** >480 GiB(`assets/main.js` `LARGE_FILE_THRESHOLD_BYTES`),160GB 包走 default
|
||||
- **ratio 有尺寸下限**(`ratio_min_bytes` = 1GiB):小文件高压缩率合法常见(零填充/稀疏),且写出字节受"声明上限 + `check_space()`"双重约束,无害
|
||||
- **唯一真实失败点是磁盘空间**:`check_space()` 按**解压后总量**查 `statvfs`,峰值 = `zip 体积 + 解出体积`。分卷场景"传一卷解一卷删一卷"可降峰值
|
||||
- **32 位安全**:引擎内部 size 全 `uint64_t`;minizip `mz_zip.h:34-35` 的 `compressed/uncompressed_size` 是 `int64_t` → >4GiB 不截断
|
||||
- **已知 UX 缺陷(未修)**:进度条 % 用字节(`main.js:1985`)、文字进度解压时用条目数(`main.js:2014`)、ETA 用字节速度(`task.c:129-177`)。混合大包上割裂,建议统一为字节
|
||||
|
||||
---
|
||||
|
||||
## 七、环境要点(新人必读)
|
||||
|
||||
- **PS5 是 x86-64 Zen 2**(不是 aarch64!),target triple `x86_64-sie-ps5`
|
||||
- **PS5 SDK C++ runtime = LLVM libc++**(FreeBSD 系 sysroot,无 libstdc++)→ C++ 必须 `-stdlib=libc++`,链接 `-lc++ -lc++abi`(Makefile 已处理:unrar 用 prospero-clang++ 编)
|
||||
- **PS5 SDK libc 的 `*at()` 族(mkdirat/openat/renameat/unlinkat)能链接但运行时损坏**:返回 -1 且 `errno=0`(2026-09-06 Frostpunk 2 真机确诊)。`zip_extract.c` 已有"*at() 失败回退全路径调用"兼容层;写新引擎代码时直接用全路径或沿用回退模式。报错要带 `(errno=%d)`,`errno=0` 时 `strerror` 会骗人
|
||||
- WSL:`export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk` 后才能 make
|
||||
- `target/user/homebrew/` 由 songl(197609) 拥有,WSL 身份 song(1000) 写不进 → staging 模式(make install 到 /tmp → `sudo cp -r`)
|
||||
- `//wsl$/Ubuntu-22.04/` 是 SMB 只读视图,改 WSL 文件必须走 Windows 路径
|
||||
- libmicrohttpd 必须 `--disable-https --disable-openssl`
|
||||
- **minizip-ng 4.2.2 补丁(升级会丢)**:`src/mz_strm_os_posix.c` L25 后插 `#ifndef O_BINARY / #define O_BINARY 0 / #endif`
|
||||
- **主机 POSIX shim(CP936 主机必需)**:`tests/posix_compat.h` 把 `lstat/stat → wfm_stat`、`opendir → _wopendir`(UTF-8 转换)、`fopen → _wfopen`。MinGW ANSI 入口看不见 UTF-8 文件名
|
||||
- 复杂 commit / tag message 用 `-F 文件`,不要 `-m` 长文本(bash quoting 会挂)
|
||||
|
||||
---
|
||||
|
||||
## 八、唯一功能缺口
|
||||
|
||||
### `-mhe=on` 加密头 7z
|
||||
|
||||
`-mhe=on` 时整个 header(含 folder 表)也被加密,引擎必须在**解析 folder 之前**先用密码解密第二份 header,才能知道有哪些 folder / 用什么 coder。工作量约为标准 7zAES 的 2 倍(两次 AES 解密路径)。
|
||||
|
||||
当前 `SZ_ERROR_UNSUPPORTED`,已在 `tests/run-sevenz-tests.sh` 的 `KNOWN_GAPS`(`aeshe`)标注 —— 缺口修好后脚本会主动报错提醒移除。
|
||||
|
||||
### 真机端到端待验证
|
||||
|
||||
ELF 已构建,但需装 PS5 实测:
|
||||
1. ZIP / RAR / 7z 三类**分卷**真机解压
|
||||
2. **加密 7z(7zAES)** 真机解压
|
||||
3. 160GB / 9.5 万文件大 ZIP
|
||||
4. 分卷 RAR 进度条实时走动
|
||||
5. UI 右下角版本号显示 `v1.9.2`(`/api/version` 与兜底字面量应一致)
|
||||
|
||||
### 可选项(非阻塞)
|
||||
|
||||
- **性能**:实测上游(7-Zip 本体)在 **7z 格式上快 1.9×(单线程)/ 3.4×(8 线程)**;ZIP 无显著差异。差距不在我们的架构(我们比 SDK 自己的 `SzArEx` 路径还快 1.02×)。**已全部落地(2026-09-16)**:①汇编解码器(`LzmaDecOpt.asm`+jwasm,1.26×,无 jwasm 自动退纯 C)②多线程 LZMA2(`Lzma2DecMt`,8 线程,1.37×,线程失败自动降级 chain;BCJ2/加密布局仍走 chain)③ZIP 逐条目 fsync 移除(8000 文件 ≥14×)。7z 现与 7-Zip 单线程打平、ZIP 已压过官方(本机受 Defender 拖累不可比,PS5 无该因素)。RAR 与官方 UnRAR 同速(unrar 自带 `target("aes")` SIMD 已启用,无逐条目 fsync)。完整数据见 `docs/EXTRACTION-PERF.md`,基准工具 `tests/bench_driver.py`
|
||||
- fsync 批量化(每 64MB/N 条刷一次)—— 9.5 万文件级可省 20–30 分钟
|
||||
- 解压失败保留 staging 支持续解(中等改动)
|
||||
- 进度条 % / 文字进度 / ETA 三处口径统一为字节
|
||||
|
||||
---
|
||||
|
||||
## 九、仓库许可与代码归属(2026-09-15 核查)
|
||||
|
||||
用户曾担心「项目源自他人代码、没有许可」——**前提不成立**:
|
||||
|
||||
- 上游 `owendswang/ps5-web-file-manager` 经 GitHub API 确认 = **GPL-3.0**(78 stars,last push 2026-09-08)
|
||||
- 本项目 `LICENSE`(GPL-3.0 全文)在 root commit `5cb0b76` 即存在,与上游一致
|
||||
- 授权链完整:`ps5-payload-dev/websrv`(John Törnblom, GPLv3+,其 Copyright 头仍保留在 `asset.c` / `asset.h` / `mime.h` / `websrv.h`)→ `owendswang` → 本项目
|
||||
|
||||
代码量构成:
|
||||
- 第三方 vendored **71,528 行**(unrar7 27,710 / zlib 20,106 / LZMA SDK 17,248 / minizip-ng 6,464)——重写时原样复用,零成本
|
||||
- 第一方 20,844 行 = 上游 v1.7 遗产 13,860 + 自有 6,984
|
||||
- **自有代码中 3,661 行零耦合**(`sevenz_chain` 2094 + `zipx_volume` 658 + `zipx_volstream` 494 + `sevenz_volstream` 415,只依赖 public domain / zlib)→ 可单独抽成 MIT 库
|
||||
|
||||
完整评估见 `docs/REWRITE-FEASIBILITY.md`(三路径:补合规 0.5 天 / 架构重构 12–18 天 / clean-room 重写 35–50 天)。**结论:建议补合规而非重写** —— GPL-3.0 保护 7z 引擎成果不被闭源白嫖。
|
||||
|
||||
---
|
||||
|
||||
## 十、工作区状态
|
||||
|
||||
工作树已干净(`git status` 仅剩有意保留的未跟踪文档)。
|
||||
|
||||
已清理(2026-09-15):
|
||||
|
||||
| 文件 | 说明 | 去向 |
|
||||
|---|---|---|
|
||||
| `erssonglDesktopWeb File Managerps5-web-file-manager¬`(2543 B) | 早期 shell 转义事故:一次 `git log --oneline --color` 的输出被重定向进了文件名。末尾是 U+F022(私用区码位,mojibake 残留),各工具渲染不一 —— git 显示成八进制转义、`ls -b` 印成 ASCII 引号 | **回收站**(`$R…`,2543 B,可还原) |
|
||||
| `web-file-mgr-unpack 1.9.1.elf`(898 KiB) | 陷阱:文件名写 1.9.1,内嵌却是 9-07 的 **v1.9**(无 7z 引擎) | 已不在仓库根 |
|
||||
|
||||
> ⚠️ **清理这类特殊文件名时**:`SHFileOperationW`(带 `FOF_ALLOWUNDO` 走回收站)对含私用区码位的路径会返回 `ERROR_FILE_NOT_FOUND (2)`,**但动作实际已生效**。删完务必查 `C:\$Recycle.Bin\<SID>\$I*` 记录确认落在回收站(`$I` 存原路径 UTF-16,`$R` 是内容)。本沙箱里 `Add-Type` 与 `rm` 都被拦(后者有 safe-delete 钩子),只能用 Python `ctypes` 调 shell32。
|
||||
|
||||
`.build/` 下的探针/调试产物已被 `.gitignore` 白名单覆盖,不再污染 `git status`。
|
||||
@@ -13,7 +13,11 @@ ifeq ($(MAKECMDGOALS),)
|
||||
endif
|
||||
endif
|
||||
|
||||
VERSION_TAG := v1.8
|
||||
# Bump this together with the git tag -- it is baked into the binary (the PS5
|
||||
# notification and `--version` print it) AND into the output filename, so a
|
||||
# stale value silently mislabels everything. Override per-build with:
|
||||
# make VERSION_TAG=v1.9.2
|
||||
VERSION_TAG ?= v1.9.2
|
||||
TITLE_ID := FMGR88888
|
||||
PYTHON ?= python3
|
||||
STRIP ?= $(PS5_PAYLOAD_SDK)/bin/prospero-strip
|
||||
@@ -22,10 +26,12 @@ HOST_CC ?= cc
|
||||
HOST_STRIP ?= strip
|
||||
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
|
||||
PS5_SRCS := $(COMMON_SRCS) src/app_installer.c
|
||||
# Output filename carries the version so two builds never overwrite each other
|
||||
# and you can tell at a glance which ELF is on the USB stick.
|
||||
BIN := web-file-mgr-$(VERSION_TAG).elf
|
||||
LINUX_BIN := web-file-mgr-linux-$(VERSION_TAG)
|
||||
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/version.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 src/sevenz_mt.c src/demangle_stub.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/*))
|
||||
ifneq ($(filter linux,$(MAKECMDGOALS)),)
|
||||
@@ -35,22 +41,96 @@ ASSETS := $(filter-out assets/icon0.png,$(BASE_ASSETS))
|
||||
endif
|
||||
GEN_SRCS := $(patsubst assets/%,gen/%, $(ASSETS:=.c))
|
||||
|
||||
# Vendored third-party: zlib + minizip-ng (ZIP), dmc_unrar (RAR). Compiled with
|
||||
# relaxed warnings (-w) — these are not our code and we do not want to chase
|
||||
# upstream style updates on every SDK upgrade.
|
||||
THIRD_PARTY_SRCS := $(wildcard third_party/zlib/src/*.c) $(wildcard third_party/minizip-ng/src/*.c) third_party/unrar/dmc_unrar.c
|
||||
THIRD_PARTY_CFLAGS := -O2 -w -Ithird_party/zlib/include -Ithird_party/minizip-ng/include -Ithird_party/unrar \
|
||||
-DHAVE_ZLIB -DZLIB_COMPAT -DHAVE_UNISTD_H=1 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE \
|
||||
-DDMC_UNRAR_DISABLE_BE32TOH_BE64TOH=1
|
||||
PS5_TP_OBJS := $(patsubst %.c,ps5-obj/%.o,$(THIRD_PARTY_SRCS))
|
||||
LINUX_TP_OBJS := $(patsubst %.c,linux-obj/%.o,$(THIRD_PARTY_SRCS))
|
||||
# Vendored third-party: zlib + minizip-ng (ZIP, C) and unrar 7.20.1 (RAR,
|
||||
# C++). unrar sources are compiled as a static library in RARDLL mode (no
|
||||
# main()); the project talks to it through the extern "C" DLL API in
|
||||
# third_party/unrar7/unrar_c_api.h. Compiled with relaxed warnings (-w) —
|
||||
# these are not our code and we do not want to chase upstream style updates.
|
||||
#
|
||||
# C++ compilers: PS5 uses prospero-clang++ (FreeBSD-style sysroot; the
|
||||
# toolchain defaults to -stdlib=libc++, driver links libc++ automatically);
|
||||
# host builds use the plain host C++ compiler (libstdc++).
|
||||
CXX ?= $(dir $(CC))prospero-clang++
|
||||
HOST_CXX ?= c++
|
||||
|
||||
CFLAGS := -Oz -fno-asynchronous-unwind-tables -fno-unwind-tables -Wall -Werror -ffunction-sections -fdata-sections -Isrc -Ithird_party/minizip-ng/include -DVERSION_TAG=\"$(VERSION_TAG)\" -DTITLE_ID=\"$(TITLE_ID)\"
|
||||
# Source set mirrors UnRARDll.vcxproj's ClCompile list (49 files) MINUS the
|
||||
# Windows-only isnt.cpp / motw.cpp (they need windows.h; the official unrar
|
||||
# UNIX makefile omits them, and PS5/linux both use the _UNIX branch where
|
||||
# their symbols are #ifdef'd out).
|
||||
UNRAR7_SRCS := \
|
||||
third_party/unrar7/archive.cpp third_party/unrar7/arcread.cpp third_party/unrar7/blake2s.cpp \
|
||||
third_party/unrar7/cmddata.cpp third_party/unrar7/consio.cpp third_party/unrar7/crc.cpp \
|
||||
third_party/unrar7/crypt.cpp third_party/unrar7/dll.cpp third_party/unrar7/encname.cpp \
|
||||
third_party/unrar7/errhnd.cpp third_party/unrar7/extinfo.cpp third_party/unrar7/extract.cpp \
|
||||
third_party/unrar7/filcreat.cpp third_party/unrar7/file.cpp third_party/unrar7/filefn.cpp \
|
||||
third_party/unrar7/filestr.cpp third_party/unrar7/find.cpp third_party/unrar7/getbits.cpp \
|
||||
third_party/unrar7/global.cpp third_party/unrar7/hash.cpp third_party/unrar7/headers.cpp \
|
||||
third_party/unrar7/largepage.cpp third_party/unrar7/match.cpp \
|
||||
third_party/unrar7/options.cpp third_party/unrar7/pathfn.cpp \
|
||||
third_party/unrar7/qopen.cpp third_party/unrar7/rar.cpp third_party/unrar7/rarpch.cpp \
|
||||
third_party/unrar7/rarvm.cpp third_party/unrar7/rawread.cpp third_party/unrar7/rdwrfn.cpp \
|
||||
third_party/unrar7/rijndael.cpp third_party/unrar7/rs.cpp third_party/unrar7/rs16.cpp \
|
||||
third_party/unrar7/scantree.cpp third_party/unrar7/secpassword.cpp third_party/unrar7/sha1.cpp \
|
||||
third_party/unrar7/sha256.cpp third_party/unrar7/smallfn.cpp third_party/unrar7/strfn.cpp \
|
||||
third_party/unrar7/strlist.cpp third_party/unrar7/system.cpp third_party/unrar7/threadpool.cpp \
|
||||
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) $(wildcard third_party/7z/*.c)
|
||||
# AesOpt.c hard-codes x86 AES-NI / AVX / VAES intrinsics and guards them with
|
||||
# a compiler-version check that lets clang 18 in unconditionally. The plain
|
||||
# intrinsics (`_mm256_aesenc_epi128`) live behind <wmmintrin_aes.h>, which
|
||||
# clang only declares after `+mvaes +mavx2` (or higher). PS5 is Zen 2 and has
|
||||
# every one of these, so we just enable them for the 7z TU family instead of
|
||||
# dropping AesOpt.c (Aes.c references those HW symbol names via AesGenTables).
|
||||
SEVENZ_C_FLAGS := -maes -mavx2 -mvaes
|
||||
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 -DZ7_PPMD_SUPPORT
|
||||
THIRD_PARTY_C_FLAGS_7Z := $(THIRD_PARTY_C_FLAGS) $(SEVENZ_C_FLAGS)
|
||||
|
||||
# Assembly-optimised LZMA decoder (optional, on when jwasm is present).
|
||||
#
|
||||
# LzmaDec.c carries a compile-time switch: with Z7_LZMA_DEC_OPT it calls an
|
||||
# external LzmaDec_DecodeReal_3() and drops its own C implementation; without
|
||||
# it, the C version is used. The asm version is measurably faster -- on a
|
||||
# 330 MiB LZMA2 archive, 1.10 s vs 1.39 s, i.e. most of the gap to the
|
||||
# official 7-Zip binary, which builds with this switch on.
|
||||
#
|
||||
# LzmaDecOpt.asm is MASM syntax, so it needs a MASM-compatible assembler
|
||||
# (jwasm). That is not something we can assume the host has, so the whole
|
||||
# optimisation is conditional: no jwasm, no asm, and the build still works.
|
||||
# ABI_LINUX is load-bearing -- 7zAsm.asm keys its calling convention off it
|
||||
# (SysV rdi/rsi/rdx vs Win64 rcx/rdx/r8); assembling without it links cleanly
|
||||
# and then segfaults on the first call.
|
||||
JWASM ?= jwasm
|
||||
LZMA_DEC_ASM_DIR := third_party/7z/Asm/x86
|
||||
LZMA_DEC_ASM_SRC := $(LZMA_DEC_ASM_DIR)/LzmaDecOpt.asm
|
||||
ifneq ($(shell command -v $(JWASM) 2>/dev/null),)
|
||||
LZMA_DEC_OPT_FLAG := -DZ7_LZMA_DEC_OPT
|
||||
PS5_ASM_OBJS := ps5-obj/$(LZMA_DEC_ASM_DIR)/LzmaDecOpt.o
|
||||
LINUX_ASM_OBJS := linux-obj/$(LZMA_DEC_ASM_DIR)/LzmaDecOpt.o
|
||||
endif
|
||||
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++
|
||||
UNRAR7_CXX_FLAGS_HOST:= $(UNRAR7_CXX_FLAGS)
|
||||
|
||||
PS5_TP_OBJS := $(patsubst %.c,ps5-obj/%.o,$(THIRD_PARTY_C_SRCS)) \
|
||||
$(patsubst %.cpp,ps5-obj/%.o,$(UNRAR7_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 -Ithird_party/7z -DVERSION_TAG=\"$(VERSION_TAG)\" -DTITLE_ID=\"$(TITLE_ID)\"
|
||||
CFLAGS += `$(PKG_CONFIG) libmicrohttpd --cflags`
|
||||
LDFLAGS := -Wl,--gc-sections
|
||||
# --icf=all: fold byte-identical functions. LLD-only (GNU ld's --icf is
|
||||
# incomplete), so it stays on the PS5 line -- the linux target never uses
|
||||
# LDFLAGS. Paired with src/demangle_stub.c this takes the ELF from ~1010 to
|
||||
# ~850 KiB; see docs/SIZE-OPTIMIZATION.md.
|
||||
LDFLAGS := -Wl,--gc-sections -Wl,--icf=all
|
||||
LDADD := `$(PKG_CONFIG) libmicrohttpd --libs`
|
||||
LDADD += -lSceIpmi -lSceAppInstUtil -lSceUserService
|
||||
LINUX_CFLAGS := -O2 -flto -Wall -Werror -Isrc -Ithird_party/minizip-ng/include -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
|
||||
|
||||
@@ -76,18 +156,55 @@ clean:
|
||||
gen/%.c: assets/% gen-asset-module.py | gen
|
||||
$(PYTHON) gen-asset-module.py --path $* $< > $@
|
||||
|
||||
# Only LzmaDec.c changes behaviour under the switch: it stops defining its own
|
||||
# decoder and declares the external symbol instead. Everything else in the 7z
|
||||
# TU family is unaffected.
|
||||
ifneq ($(LZMA_DEC_OPT_FLAG),)
|
||||
ps5-obj/third_party/7z/LzmaDec.o: THIRD_PARTY_C_FLAGS_7Z += $(LZMA_DEC_OPT_FLAG)
|
||||
linux-obj/third_party/7z/LzmaDec.o: THIRD_PARTY_C_FLAGS_7Z += $(LZMA_DEC_OPT_FLAG)
|
||||
endif
|
||||
|
||||
# make does not track flag changes, and installing or removing jwasm flips the
|
||||
# switch above. Without this, an existing LzmaDec.o silently keeps the old
|
||||
# decoder and the asm object just sits in the link line unreferenced (the
|
||||
# binary comes out byte-identical, which is how the problem was noticed).
|
||||
ps5-obj/third_party/7z/LzmaDec.o: Makefile
|
||||
linux-obj/third_party/7z/LzmaDec.o: Makefile
|
||||
|
||||
ps5-obj/%.o: %.c
|
||||
@mkdir -p $(dir $@)
|
||||
$(CC) $(THIRD_PARTY_CFLAGS) -c -o $@ $<
|
||||
$(CC) $(if $(findstring third_party/7z,$<),$(THIRD_PARTY_C_FLAGS_7Z),$(THIRD_PARTY_C_FLAGS)) -c -o $@ $<
|
||||
|
||||
linux-obj/%.o: %.c
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CC) $(THIRD_PARTY_CFLAGS) -c -o $@ $<
|
||||
$(HOST_CC) $(if $(findstring third_party/7z,$<),$(THIRD_PARTY_C_FLAGS_7Z),$(THIRD_PARTY_C_FLAGS)) -c -o $@ $<
|
||||
|
||||
$(BIN): $(PS5_SRCS) $(GEN_SRCS) $(PS5_TP_OBJS)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(filter %.c,$^) $(PS5_TP_OBJS) $(LDADD)
|
||||
# The assembler emits a plain ELF64 relocatable object, which both linkers
|
||||
# (prospero-clang++ for PS5, cc for linux) accept as-is.
|
||||
ps5-obj/$(LZMA_DEC_ASM_DIR)/LzmaDecOpt.o: $(LZMA_DEC_ASM_SRC)
|
||||
@mkdir -p $(dir $@)
|
||||
$(JWASM) -elf64 -q -DABI_LINUX -I$(LZMA_DEC_ASM_DIR) -Fo$@ $<
|
||||
|
||||
linux-obj/$(LZMA_DEC_ASM_DIR)/LzmaDecOpt.o: $(LZMA_DEC_ASM_SRC)
|
||||
@mkdir -p $(dir $@)
|
||||
$(JWASM) -elf64 -q -DABI_LINUX -I$(LZMA_DEC_ASM_DIR) -Fo$@ $<
|
||||
|
||||
ps5-obj/%.o: %.cpp
|
||||
@mkdir -p $(dir $@)
|
||||
$(CXX) $(UNRAR7_CXX_FLAGS_PS5) -c -o $@ $<
|
||||
|
||||
linux-obj/%.o: %.cpp
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CXX) $(UNRAR7_CXX_FLAGS_HOST) -c -o $@ $<
|
||||
|
||||
# Link with the C++ driver so libc++ (PS5) / libstdc++ (host) is pulled in
|
||||
# automatically for the unrar objects. The project's own C sources are passed
|
||||
# through -x c (clang++ would otherwise compile .c files as C++ and trip
|
||||
# -Wdeprecated); -x none restores extension-based handling for the .o files.
|
||||
$(BIN): $(PS5_SRCS) $(GEN_SRCS) $(PS5_TP_OBJS) $(PS5_ASM_OBJS)
|
||||
$(CXX) $(CFLAGS) $(LDFLAGS) -o $@ -x c $(filter %.c,$^) -x none $(PS5_TP_OBJS) $(PS5_ASM_OBJS) $(LDADD)
|
||||
$(STRIP) $@
|
||||
|
||||
$(LINUX_BIN): $(LINUX_SRCS) $(GEN_SRCS) $(LINUX_TP_OBJS)
|
||||
$(HOST_CC) $(LINUX_CFLAGS) -o $@ $(filter %.c,$^) $(LINUX_TP_OBJS) $(LINUX_LDADD)
|
||||
$(LINUX_BIN): $(LINUX_SRCS) $(GEN_SRCS) $(LINUX_TP_OBJS) $(LINUX_ASM_OBJS)
|
||||
$(HOST_CXX) $(LINUX_CFLAGS) -o $@ -x c $(filter %.c,$^) -x none $(LINUX_TP_OBJS) $(LINUX_ASM_OBJS) $(LINUX_LDADD)
|
||||
$(HOST_STRIP) $@
|
||||
@@ -1,8 +1,12 @@
|
||||
<div align="right">
|
||||
<a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a>
|
||||
</div>
|
||||
|
||||
# PS5 Web File Manager
|
||||
|
||||
> Homebrew HTTP file manager for jailbroken PS5 consoles. Browse, edit, upload, download and extract ZIPs through any browser on the same network — single self-contained ELF payload, no external services, no telemetry.
|
||||
|
||||
**Version:** v1.8 · **Title ID:** `FMGR88888` · **License:** GPLv3+ · **Target:** `x86_64-sie-ps5`
|
||||
**Version:** v1.9.2 · **Title ID:** `FMGR88888` · **License:** GPLv3+ · **Target:** `x86_64-sie-ps5`
|
||||
|
||||
---
|
||||
|
||||
@@ -12,6 +16,29 @@ A payload ELF that runs an HTTP file manager inside a jailbroken PS5. Open `http
|
||||
|
||||
The same source tree builds a Linux binary for development and a PS5 payload ELF for deployment — see `make linux` below.
|
||||
|
||||
## What's new in v1.9.2
|
||||
|
||||
Version-string-only re-release. The `v1.9.1` tag sat four commits behind the
|
||||
tree that produced its binary, so the tag could not rebuild the published
|
||||
artifact; v1.9.2 is cut from the right commit. It is functionally identical to
|
||||
the v1.9.1 binary — the only change is the baked-in version string.
|
||||
|
||||
## What's new in v1.9
|
||||
|
||||
- **RAR engine replaced with the official rarlab UnRAR 7.20.1**
|
||||
(`third_party/unrar7/`, replacing dmc_unrar). This is what actually
|
||||
makes RAR extraction work on real files: dmc_unrar could not decode
|
||||
archives written by **WinRAR 6.x/7.x** (RAR5 "v6" compression) and had
|
||||
no multi-volume support — both now work.
|
||||
- **RAR5 "v6" archives extract** (the v1.8-era "corrupt archive" report
|
||||
on WinRAR 6/7 files is gone).
|
||||
- **Multi-volume RAR** (`.part01.rar` chains): unrar stitches the parts by
|
||||
name when the full set sits next to the volume you open.
|
||||
- Engine can decrypt encrypted RAR (`RARSetPassword`) — password UI /
|
||||
API plumbing still pending, encrypted archives are rejected for now.
|
||||
- Host tests now run real archives (v6 / encrypted / 3-volume fixtures
|
||||
committed under `tests/fixtures-real/`): **70 ZIP + 24 RAR = 94 checks**.
|
||||
|
||||
## What's new in v1.8
|
||||
|
||||
- **Single-volume RAR extraction** via the vendored FLOSS library
|
||||
@@ -40,10 +67,52 @@ The same source tree builds a Linux binary for development and a PS5 payload ELF
|
||||
limitations that come from using dmc_unrar (no multi-volume, no
|
||||
encryption in v1.8 — both lift in v1.9 when the library is replaced).
|
||||
|
||||
## What's new in v1.8.1
|
||||
|
||||
- **Default ZIP limits relaxed** (companion to v1.7's large profile).
|
||||
v1.7 shipped with a 64 GiB default per-entry cap, which was too
|
||||
aggressive for typical PS5 system-backup ZIPs (200-300 GiB). v1.8.1
|
||||
raises the default profile to **1 TiB total / 256 GiB per entry /
|
||||
500 : 1 ratio**, with the `large=1` opt-in kept at 2 TiB / 1 TiB /
|
||||
1000 : 1. The frontend threshold rises from 60 GiB to 240 GiB so
|
||||
common system-backup archives no longer trigger the prompt.
|
||||
- RAR extraction inherits the new defaults (rar_extract.c threads
|
||||
`c->limits` from the engine — no engine change required).
|
||||
- Rationale: the real zip-bomb defence is `check_space()` (statvfs-based
|
||||
real disk-space check before staging) + `max_ratio` (declared
|
||||
compression ratio cap). The size caps are a UX guard, not a security
|
||||
boundary.
|
||||
|
||||
## What's new in v1.8.2
|
||||
|
||||
- **Default ZIP limits relaxed again** for the 3A-game single-file case.
|
||||
A single ~300 GiB uncompressed file inside an archive was still
|
||||
silently rejected by v1.8.1 (the default scan returns
|
||||
`ZIPX_ERR_LIMIT_FILE_SIZE` before the request ever reaches the
|
||||
frontend confirmation prompt). v1.8.2 raises the default profile to
|
||||
**2 TiB total / 512 GiB per entry / 500 : 1 ratio**, with the `large=1`
|
||||
opt-in bumped to 4 TiB / 1 TiB / 1000 : 1. Frontend threshold rises
|
||||
from 240 GiB to 480 GiB.
|
||||
- **Two PS5-only build fixes** discovered when cross-compiling for the
|
||||
PS5 target. The host-side test suite (`tests/run-tests.sh`) had
|
||||
silently accepted both because it links the same sources but uses
|
||||
gcc rather than clang 18 and a different include path:
|
||||
- `Makefile` CFLAGS: add `-Ithird_party/unrar` so `src/rar_extract.c`
|
||||
can find the project-authored `dmc_unrar_api.h` facade header.
|
||||
- `src/extract.c`: move `extract_progress()` definition above
|
||||
`extract_dispatch()` so the implicit function declaration is not
|
||||
flagged by `-Werror=implicit-function-declaration` (clang 18 in the
|
||||
PS5 SDK is stricter than the host gcc used by tests).
|
||||
- **Release artifact** for v1.8.2: `web-file-mgr.elf` — 509 704 bytes,
|
||||
sha256 `1b2c3d68b35e32737105f17d14a80a3c159ceca0cabd274ee168cbcd81906f65`,
|
||||
ELF class 64, little-endian, e_machine `0x003e` (x86_64-sie-ps5).
|
||||
- Tests: **84 host-side checks** (70 ZIP + 14 RAR), 0 failures. PS5
|
||||
cross-compile succeeds end-to-end.
|
||||
|
||||
## What's new in v1.7
|
||||
|
||||
- **ZIP large-file profile** (opt-in via the new `large=1` argument on `/api/extract`): relaxed caps of **2 TiB** archive total, **1 TiB** per entry, **1000 : 1** compression ratio. The frontend prompts for confirmation whenever the archive on disk is larger than **60 GiB**; the server only activates the profile when the user explicitly agrees.
|
||||
- **Stricter default ZIP profile** stays safe: **512 GiB** total / **64 GiB** per entry / **200 : 1** ratio. A 4 MiB compressed payload that expands to 800 GiB still gets rejected before any output file is opened.
|
||||
- **Stricter default ZIP profile** stays safe: **1 TiB** total / **256 GiB** per entry / **500 : 1** ratio. A 4 MiB compressed payload that expands to 800 GiB still gets rejected before any output file is opened.
|
||||
- **69 host-side C tests** (`tests/run-tests.sh`) now cover path traversal, ZIP64, encryption rejection, ratios, conflict policies and the new large-file profile (`tests/test_zip_extract.c`).
|
||||
- Earlier refinements — see `git log` since v1.6.
|
||||
|
||||
@@ -68,7 +137,7 @@ The same source tree builds a Linux binary for development and a PS5 payload ELF
|
||||
- **Upload** — single files or folder trees from any device on the LAN (hidden in the PS5 browser). Atomic temp + rename.
|
||||
- **Download** — single file as raw bytes, or folders/multi-select as a streaming `.tar`. Hidden in the PS5 browser.
|
||||
- **Tasks** — full-screen overlay with delayed show, live progress, throughput, ETA, cancel, and recovery if the browser is closed and reopened mid-task.
|
||||
- **Archive extraction** — ZIP (encrypted rejected) and RAR (single-volume unencrypted); see the [ZIP extraction](#zip-extraction) and [RAR extraction](#rar-extraction) sections below for scope.
|
||||
- **Archive extraction** — ZIP (encrypted rejected) and RAR (v1.9: RAR4 + RAR5 incl. WinRAR 6/7 "v6", multi-volume; encrypted still rejected pending password UI); see the [ZIP extraction](#zip-extraction) and [RAR extraction](#rar-extraction) sections below for scope.
|
||||
- **PKG** — install and preview `.pkg` files.
|
||||
- **Images** — preview `.png .jpg .jpeg .gif .bmp .webp`.
|
||||
- **Localization** — English + Simplified Chinese, auto-selected from `navigator.languages`.
|
||||
@@ -116,7 +185,7 @@ make
|
||||
Output:
|
||||
|
||||
```text
|
||||
web-file-mgr.elf (~418 KiB, x86_64-sie-ps5)
|
||||
web-file-mgr.elf (~several hundred KiB, larger in v1.9 with unrar; x86_64-sie-ps5)
|
||||
```
|
||||
|
||||
For pure UI/JS work without the PS5 toolchain:
|
||||
@@ -156,13 +225,13 @@ Plain ZIPs only — stored / deflated / ZIP64, **never encrypted**. The engine i
|
||||
| Limit | Default profile | Large profile (`ZIPX_LIMITS_LARGE`) |
|
||||
|---|---|---|
|
||||
| `max_entries` | 200 000 | 500 000 |
|
||||
| `max_total_bytes` (uncompressed) | 512 GiB | 2 TiB |
|
||||
| `max_file_bytes` (per entry) | 64 GiB | 1 TiB |
|
||||
| `max_ratio` (uncompressed / compressed) | 200 : 1 | 1000 : 1 |
|
||||
| `max_total_bytes` (uncompressed) | 2 TiB | 4 TiB |
|
||||
| `max_file_bytes` (per entry) | 512 GiB | 1 TiB |
|
||||
| `max_ratio` (uncompressed / compressed) | 500 : 1 | 1000 : 1 |
|
||||
| `max_depth` (folder nesting) | 32 | 32 |
|
||||
| `max_name_len` / `max_path_len` | 255 / 1024 | 255 / 1024 |
|
||||
|
||||
The **default profile** is shipped safe: a 4 MiB compressed blob that decodes to 800 GiB is rejected before any output file is opened. The **large profile** is engaged **only** when the request includes `large=1` — the archive dialog prompts the user automatically whenever the archive on disk is larger than `LARGE_FILE_THRESHOLD_BYTES` (60 GiB by default; configurable in `assets/main.js`). Confirming the prompt is the user's explicit opt-in; the server still records nothing extra on its own.
|
||||
The **default profile** is shipped safe: a 4 MiB compressed blob that decodes to 800 GiB is rejected before any output file is opened. The **large profile** is engaged **only** when the request includes `large=1` — the archive dialog prompts the user automatically whenever the archive on disk is larger than `LARGE_FILE_THRESHOLD_BYTES` (480 GiB by default; configurable in `assets/main.js`). Confirming the prompt is the user's explicit opt-in; the server still records nothing extra on its own.
|
||||
|
||||
### Security checks
|
||||
|
||||
@@ -184,41 +253,42 @@ Passed as `conflict=` on `/api/extract`:
|
||||
|
||||
### Tuning the threshold
|
||||
|
||||
The 60 GiB frontend threshold lives in `assets/main.js`:
|
||||
The 480 GiB frontend threshold lives in `assets/main.js`:
|
||||
|
||||
```js
|
||||
const LARGE_FILE_THRESHOLD_BYTES = 60 * 1024 * 1024 * 1024;
|
||||
const LARGE_FILE_THRESHOLD_BYTES = 480 * 1024 * 1024 * 1024;
|
||||
```
|
||||
|
||||
Set it to `Infinity` to silence the prompt, lower it to be more conservative, or remove the call entirely — the server still respects `large=1` regardless of the threshold.
|
||||
|
||||
## RAR extraction
|
||||
|
||||
A single-volume, **unencrypted** RAR archive engine (`src/rar_extract.{c,h}`,
|
||||
backed by the vendored FLOSS library
|
||||
[`dmc_unrar`](https://github.com/DrMcCoy/dmc_unrar) at
|
||||
`third_party/unrar/dmc_unrar.c`). Files with the extension `.rar` get the
|
||||
same **Extract** button as `.zip` files; the engine is dispatched by
|
||||
`src/extract.c` based on extension.
|
||||
A RAR extraction engine (`src/rar_extract.{c,h}`) backed by the **official
|
||||
rarlab UnRAR source** (`third_party/unrar7/`, version 7.20.1, compiled as a
|
||||
static library and driven through its C-compatible DLL API). Files with the
|
||||
extension `.rar` get the same **Extract** button as `.zip` files; the engine
|
||||
is dispatched by `src/extract.c` based on extension.
|
||||
|
||||
> v1.9 replaced the v1.8 engine (dmc_unrar 1.7.0). dmc_unrar could not
|
||||
> decode archives written by WinRAR 6.x/7.x (RAR5 "v6" compression) and had
|
||||
> no multi-volume support; unrar handles both natively.
|
||||
|
||||
### Scope
|
||||
|
||||
| Format | Support | Notes |
|
||||
|---|---|---|
|
||||
| RAR 1.5 / 2.0 / 2.6 / 2.9 / 3.0 / 3.6 / 4.0 | ✅ | Single-volume, unencrypted |
|
||||
| RAR 5.0 | ✅ | Single-volume, unencrypted |
|
||||
| Solid blocks, dictionary 4 MiB (RAR4) / 32 MiB (RAR5) | ✅ | |
|
||||
| RAR 1.5 → 4.x (incl. 2.9 / 3.6 / 4.0) | ✅ | |
|
||||
| RAR 5.0 and **5.0 "v6"** (WinRAR 6.x / 7.x) | ✅ | The v1.9 trigger |
|
||||
| Solid blocks, dictionary up to 1 GiB | ✅ | |
|
||||
| PPMd decompression (RAR 3.0+) | ✅ | |
|
||||
| **Multi-volume** (`.part01.rar` + `.part02.rar` + …) | ❌ | Rejected with `ZIPX_ERR_UNSUPPORTED`. The frontend greys out non-`01` sub-volumes with a tooltip. Join / unrar on a PC first. |
|
||||
| **Encrypted RAR** (any encrypted header or file) | ❌ | Rejected with `ZIPX_ERR_UNSUPPORTED`. There is no `password=` field in `/api/extract`. |
|
||||
| **Multi-volume** (`.part01.rar` + `.part02.rar` + …) | ✅ | unrar stitches parts by name when the whole set sits next to the volume you open. Select the first volume (`name.part1.rar` / `name.part01.rar`); non-first volumes are still greyed out in the UI with a hint. |
|
||||
| **Encrypted RAR** | ⏳ | The engine can decrypt (`RARSetPassword`), but the password field / prompt is not wired into `/api/extract` yet. Encrypted archives are rejected up front with `ZIPX_ERR_UNSUPPORTED`. |
|
||||
| Symbolic links / FIFOs / sockets / devices | ❌ | Rejected with `ZIPX_ERR_SPECIAL` (mirrors ZIP behaviour) |
|
||||
| RAR 1.4 (very old) | ❌ | Not supported by dmc_unrar 1.7.0; rejected upstream |
|
||||
| RAR 1.3 (pre-1.4) | ❌ | Rejected upstream by unrar |
|
||||
|
||||
The "extract on a PC first" recovery is the same escape hatch the engine
|
||||
uses for ZIP encryption errors: when an unsupported archive is rejected,
|
||||
the user gets an `extract_unsupported` failure with the file name as the
|
||||
detail argument. The frontend already shows this with the typical
|
||||
bilingual retry guidance.
|
||||
When an archive is rejected, the user gets an `extract_unsupported`
|
||||
failure with the file name as the detail argument. The frontend already
|
||||
shows this with the typical bilingual retry guidance.
|
||||
|
||||
### Limits
|
||||
|
||||
@@ -228,14 +298,14 @@ profile table on top. Defaults and the `large=1` opt-in are identical:
|
||||
| Limit | Default profile | Large profile (`large=1`) |
|
||||
|---|---|---|
|
||||
| `max_entries` | 200 000 | 500 000 |
|
||||
| `max_total_bytes` (uncompressed) | 512 GiB | 2 TiB |
|
||||
| `max_file_bytes` (per entry) | 64 GiB | 1 TiB |
|
||||
| `max_ratio` (uncompressed / compressed) | 200 : 1 | 1000 : 1 |
|
||||
| `max_total_bytes` (uncompressed) | 2 TiB | 4 TiB |
|
||||
| `max_file_bytes` (per entry) | 512 GiB | 1 TiB |
|
||||
| `max_ratio` (uncompressed / compressed) | 500 : 1 | 1000 : 1 |
|
||||
| `max_depth` (folder nesting) | 32 | 32 |
|
||||
| `max_name_len` / `max_path_len` | 255 / 1024 | 255 / 1024 |
|
||||
|
||||
Large-profile RAR extraction uses the same `LARGE_FILE_THRESHOLD_BYTES`
|
||||
(60 GiB) prompt as ZIP — the frontend treats `.rar` and `.zip` the same
|
||||
(480 GiB) prompt as ZIP — the frontend treats `.rar` and `.zip` the same
|
||||
way for the prompt, and the server only ever activates the large caps
|
||||
when the request carries `large=1` (opt-in).
|
||||
|
||||
@@ -255,37 +325,32 @@ The RAR engine applies the same checks as the ZIP engine — re-uses
|
||||
|
||||
### Vendoring and licence
|
||||
|
||||
`third_party/unrar/dmc_unrar.c` is vendored **verbatim** from
|
||||
[`DrMcCoy/dmc_unrar`](https://github.com/DrMcCoy/dmc_unrar), upstream
|
||||
commit pinned at the same date as the v1.8 release. The file is
|
||||
**GPL-2.0-or-later** (see `third_party/unrar/COPYING`), which means the
|
||||
resulting `web-file-mgr.elf` is also effectively GPL-2.0-or-later. The
|
||||
already-GPLv3+ project is forward-compatible with that, and the
|
||||
compliant distribution form (binary + corresponding sources + GPL
|
||||
notice alongside the LGPL notice for libmicrohttpd) is the same
|
||||
process you already follow for every prior release. The minimal
|
||||
project-authored facade `third_party/unrar/dmc_unrar_api.h` carries the
|
||||
project's own licence and is *not* bound to GPL.
|
||||
`third_party/unrar7/` is a verbatim copy of the official **rarlab UnRAR
|
||||
source** (7.20.1), mirrored by
|
||||
[`opello/unrar`](https://github.com/opello/unrar) at commit `97e1780`. It is
|
||||
distributed under the **UnRAR freeware licence** (see
|
||||
`third_party/unrar7/license.txt`): it may be used in any software to handle
|
||||
RAR archives, but may not be used to develop a RAR-compatible *archiver* or
|
||||
re-create the RAR compression algorithm. The project-authored facade
|
||||
`third_party/unrar7/unrar_c_api.h` carries the project's own licence.
|
||||
|
||||
### v1.9 plan
|
||||
> The v1.8 engine `third_party/unrar/dmc_unrar.c` (DrMcCoy/dmc_unrar 1.7.0,
|
||||
> GPL-2.0-or-later) was removed in v1.9; its notice lives in git history.
|
||||
|
||||
When (if) multi-volume RAR and encrypted RAR become worth the
|
||||
engineering cost, the recommended path is to replace
|
||||
`third_party/unrar/dmc_unrar.c` with a vendored mirror of
|
||||
[`opello/unrar`](https://github.com/opello/unrar) (a faithful copy of
|
||||
rarlab UnRAR 7.x, C++17, supports volumes + encryption). The
|
||||
`rar_extract()` signature, the dispatch layer, and the host tests do
|
||||
**not** need to change — only the engine behind `rar_extract()` and the
|
||||
`password=` field on `/api/extract`. See
|
||||
[`third_party/unrar/VENDORED.md`](./third_party/unrar/VENDORED.md) for
|
||||
the step-by-step upgrade recipe.
|
||||
### Encrypted RAR (planned)
|
||||
|
||||
The engine can decrypt archives (via `RARSetPassword`), but the password
|
||||
channel — a `password=` field on `/api/extract` plus a frontend prompt —
|
||||
is not wired yet. Encrypted archives currently fail with
|
||||
`extract_unsupported`. The engine swap (v1.9) removed the hard engine
|
||||
limits; the remaining work is purely API/UI plumbing.
|
||||
|
||||
## Verification
|
||||
|
||||
After `make`, sanity-check the produced ELF:
|
||||
|
||||
```sh
|
||||
ls -la web-file-mgr.elf # size ~430 KiB on v1.8 (~418 KiB on v1.7)
|
||||
ls -la web-file-mgr.elf # size grew in v1.9 (unrar static library); ~509 KiB was v1.8.3
|
||||
sha256sum web-file-mgr.elf # record the digest in your release notes
|
||||
file web-file-mgr.elf # expect "ELF 64-bit LSB pie executable, x86-64"
|
||||
od -An -tx1 -N20 web-file-mgr.elf | head -2 # magic 7f45 4c46 0201 + e_machine 003e
|
||||
@@ -301,8 +366,8 @@ A POSIX/host-side C test suite covers the ZIP engine and runs on any Linux / mac
|
||||
cd tests && bash run-tests.sh
|
||||
```
|
||||
|
||||
Output is a per-case `check`-style report — **83 checks** on the current `main`
|
||||
(69 ZIP + 14 RAR). Coverage:
|
||||
Output is a per-case `check`-style report — **84 checks** on the current `main`
|
||||
(70 ZIP + 14 RAR). Coverage:
|
||||
|
||||
- ZIP entry parsing (stored + deflated + ZIP64)
|
||||
- Path traversal, absolute paths, backslash, Windows drive letters
|
||||
@@ -320,7 +385,7 @@ Output is a per-case `check`-style report — **83 checks** on the current `main
|
||||
|
||||
```
|
||||
.
|
||||
├── Makefile # PS5 + Linux builds (VERSION_TAG v1.8)
|
||||
├── Makefile # PS5 + Linux builds (VERSION_TAG v1.8.2)
|
||||
├── install-libmicrohttpd.sh # one-shot dependency installer
|
||||
├── gen-asset-module.py # embeds assets/* as gzip-compressed C arrays
|
||||
├── assets/ # HTML / CSS / JS / icons / param.json
|
||||
@@ -368,10 +433,11 @@ Output is a per-case `check`-style report — **83 checks** on the current `main
|
||||
- **This is a homebrew app and should not intentionally modify system processes or kernel memory.** If you hit a kernel panic, make sure you are using a recent jailbreak method and ELF loader, or revert to the stable method you normally use.
|
||||
- **P2JB users** — if this payload triggers a kernel panic, avoid using it on that setup. Stability matters more than convenience when each retry is expensive.
|
||||
- **The preparing stage can take a while** when a folder contains many files — it sums folder size and checks free space, which helps avoid starting a copy / move / upload / download that cannot finish safely.
|
||||
- **`err_extract_entry_too_large`** — default archive caps are 64 GiB per
|
||||
entry / 200:1 ratio. Confirm the large-file prompt (appears for
|
||||
archives > 60 GiB on disk), split the archive, or pass `large=1`
|
||||
directly to the API.
|
||||
- **`err_extract_entry_too_large`** — default archive caps are 512 GiB per
|
||||
entry / 500:1 ratio (covers a typical 3A-game archive with one ~300 GiB
|
||||
uncompressed file). If you exceed the default, confirm the large-file
|
||||
prompt (appears for archives > 480 GiB on disk), split the archive, or
|
||||
pass `large=1` directly to the API.
|
||||
- **`err_extract_unsupported`** — the archive uses a feature the engine
|
||||
cannot handle: encrypted ZIP, encrypted RAR, multi-volume RAR
|
||||
(`.part02+.rar`), very-old RAR 1.4, RAR symlinks / FIFOs, or a file
|
||||
|
||||
+418
@@ -0,0 +1,418 @@
|
||||
<div align="right">
|
||||
<a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a>
|
||||
</div>
|
||||
|
||||
# PS5 网页文件管理器(PS5 Web File Manager)
|
||||
|
||||
> 面向已越狱 PS5 主机的自制 HTTP 文件管理器。通过同一局域网内的任意浏览器(包括 PS5 自带浏览器)即可浏览、编辑、上传、下载并解压 ZIP / RAR / 7z 压缩包——单个自包含 ELF 载荷,无外部服务、无遥测上报。
|
||||
|
||||
**版本:** v1.9.2 · **标题 ID:** `FMGR88888` · **许可证:** GPLv3+ · **目标平台:** `x86_64-sie-ps5`
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
一个在已越狱 PS5 上运行的 HTTP 文件管理器载荷。从局域网内任意浏览器(含 PS5 浏览器本身)打开 `http://<PS5_IP>:8888/`,即可管理外接 USB 存储与用户分区的文件。设计初衷是安全地把游戏 dump 文件夹从 USB 拷贝到内置存储,但它同时也支持常规文件管理、原地文本编辑、PKG 预览/安装、图片预览,以及内置防 zip 炸弹保护的解压功能。
|
||||
|
||||
同一套源码树可构建出供开发用的 Linux 二进制,以及供部署的 PS5 载荷 ELF——见下方 `make linux`。
|
||||
|
||||
## v1.9.2 与 v1.9.1 新增内容
|
||||
|
||||
> **v1.9.2 与 v1.9.1 的功能完全相同,只换了内嵌版本号。** 原因是原先的 `v1.9.1` tag 指在产出发布二进制的提交**之前 4 个提交**,tag 与产物对不上(clone 该 tag 无法重建出发布的那份 ELF);v1.9.2 重新从产出该二进制的提交上打,使 tag = 源码 = 二进制。
|
||||
|
||||
- **7z 解压引擎**(`src/sevenz_extract.{c,h}`):自研解码子集 + 拉式 codec 链(`src/sevenz_chain.c`,覆盖 LZMA2 / BCJ2 等),由 `src/extract.c` 按扩展名分派,与 ZIP / RAR 共用同一套三阶段模型与限额档位。`.7z` 文件在文件列表中同样带「解压」按钮。
|
||||
- **7zAES 内容解密**(AES-256-CBC):引擎层可解密带密码的 7z 内容;密码输入 UI / API 通道尚未接入,目前加密归档仍被拒绝。
|
||||
- **7z 分卷**:`.7z.001` / `.z01` 等链式分卷由 `src/sevenz_volstream.c` 按名拼接,打开首个分卷即可。
|
||||
- **性能三项**(纯解码提速,不影响功能面):
|
||||
- SDK 汇编 LZMA 解码器(`Asm/x86/LzmaDecOpt.asm` + jwasm,无 jwasm 自动回退纯 C)≈ 1.26×。
|
||||
- 纯 LZMA2 文件夹多线程解码(`src/sevenz_mt.c` + `Lzma2DecMt`,8 线程)≈ 1.37×。
|
||||
- 移除 ZIP 逐条目 fsync,减少 staging 重命名前的写盘开销。
|
||||
- **唯一缺口**:7z `-mhe=on` 加密头(独立单元,读取需自研头解析器),其余 7z 特性均已支持。
|
||||
|
||||
## v1.9 新增内容
|
||||
|
||||
- **RAR 引擎替换为官方 rarlab UnRAR 7.20.1**(`third_party/unrar7/`,取代 dmc_unrar)。这正是让 RAR 解压在真实文件上可用的一步:dmc_unrar 无法解码 **WinRAR 6.x/7.x** 写出的归档(RAR5「v6」压缩),也不支持多卷;两者现在都能工作。
|
||||
- **RAR5「v6」归档可解压**(v1.8 时代在 WinRAR 6/7 文件上报「归档损坏」的问题已消失)。
|
||||
- **多卷 RAR**(`.part01.rar` 链):当完整卷集与被打开的卷放在同一目录时,unrar 按文件名拼接各部分。
|
||||
- 引擎可解密加密 RAR(`RARSetPassword`)——密码 UI / API 接线仍未完成,加密归档暂时被拒绝。
|
||||
- 主机测试现用真实归档(v6 / 加密 / 3 卷 fixture,提交于 `tests/fixtures-real/`):**70 ZIP + 24 RAR = 94 项检查**。
|
||||
|
||||
## v1.8 新增内容
|
||||
|
||||
- **单卷 RAR 解压**,基于内置的 FLOSS 库 [`dmc_unrar`](https://github.com/DrMcCoy/dmc_unrar)(GPL-2.0-or-later)。支持 RAR 1.5、2.x、3.x、4.x、5.x 归档。`.rar` 文件出现在文件列表中且「解压」按钮可用;`.part02+.rar` 子卷上的按钮置灰,提示「请选择主卷」——v1.8 无法拼接多卷 RAR(见下方 [RAR 解压](#rar-解压) 章节)。
|
||||
- 新引擎 `src/rar_extract.c` 与既有 `src/zip_extract.c` 之间**共享解压协议**:相同的 `zipx_status_t` 状态码、相同的 `zipx_limits_t` 档位(默认 / `large=1`)、相同的三阶段模型(`scan → extract → publish → cleanup`)、相同的 staging 目录布局、相同的冲突策略、相同的错误映射到任务 UI。`src/extract.c` 中的分派器只是一个微小的 `ends_with_ci(…)` 判断。
|
||||
- **14 个新增主机端 C 测试**(`tests/test_rar_extract.c`)接入现有 `tests/run-tests.sh`。覆盖:格式分派、每个影响 RAR 用户的 `DMC_UNRAR_*` 错误码翻译、限额档位交接。主机检查总数:**69 ZIP + 14 RAR = 83**。
|
||||
- **文档**:[`CHANGELOG.md`](./CHANGELOG.md)、[`docs/UPGRADE-v1.8-rar-support.md`](./docs/UPGRADE-v1.8-rar-support.md),以及 `third_party/unrar/VENDORED.md` 中的 vendoring 决策树。
|
||||
|
||||
## v1.8.1 新增内容
|
||||
|
||||
- **放宽默认 ZIP 限额**(配合 v1.7 的大档案档位)。v1.7 默认单条目上限为 64 GiB,对典型 PS5 系统备份 ZIP(200–300 GiB)过于激进。v1.8.1 将默认档位提高到 **总量 1 TiB / 单条目 256 GiB / 500:1 比率**,保留 `large=1` 选项为 2 TiB / 1 TiB / 1000:1。前端阈值从 60 GiB 提升到 240 GiB,使常见系统备份归档不再触发确认提示。
|
||||
- RAR 解压继承这些新默认值(`rar_extract.c` 直接从引擎透传 `c->limits`,无需改引擎)。
|
||||
- 理由:真正的防 zip 炸弹防线是 `check_space()`(staging 前基于 statvfs 的真实磁盘空间检查)+ `max_ratio`(声明的压缩比上限)。尺寸上限只是 UX 护栏,而非安全边界。
|
||||
|
||||
## v1.8.2 新增内容
|
||||
|
||||
- **再次放宽默认 ZIP 限额**,针对 3A 游戏单文件场景。v1.8.1 仍会静默拒绝归档内单个约 300 GiB 的未压缩文件(默认扫描在请求到达前端确认提示之前就返回 `ZIPX_ERR_LIMIT_FILE_SIZE`)。v1.8.2 将默认档位提高到 **总量 2 TiB / 单条目 512 GiB / 500:1 比率**,`large=1` 选件提到 4 TiB / 1 TiB / 1000:1。前端阈值从 240 GiB 提升到 480 GiB。
|
||||
- **两个 PS5 专属构建修复**,在交叉编译 PS5 目标时发现。主机端测试套件(`tests/run-tests.sh`)曾静默接受二者,因为它链接相同源码但使用 gcc 而非 clang 18,且包含路径不同:
|
||||
- `Makefile` CFLAGS:加入 `-Ithird_party/unrar`,使 `src/rar_extract.c` 能找到项目自有的 `dmc_unrar_api.h` 门面头文件。
|
||||
- `src/extract.c`:把 `extract_progress()` 定义移到 `extract_dispatch()` 之前,避免被 `-Werror=implicit-function-declaration` 标记(PS5 SDK 的 clang 18 比测试用的主机 gcc 更严格)。
|
||||
- v1.8.2 发布产物:`web-file-mgr.elf` —— 509 704 字节,sha256 `1b2c3d68b35e32737105f17d14a80a3c159ceca0cabd274ee168cbcd81906f65`,ELF 64 位小端,e_machine `0x003e`(x86_64-sie-ps5)。
|
||||
- 测试:**84 项主机端检查**(70 ZIP + 14 RAR),0 失败。PS5 交叉编译端到端成功。
|
||||
|
||||
## v1.7 新增内容
|
||||
|
||||
- **ZIP 大文件档位**(通过在 `/api/extract` 传入新的 `large=1` 参数选配启用):放宽的限额为 **总量 2 TiB** / **单条目 1 TiB** / **1000:1 压缩比**。当磁盘上归档大于 **60 GiB** 时前端会提示确认;仅当用户明确同意时服务器才启用该档位。
|
||||
- **更严格的默认 ZIP 档位**保持安全:**总量 1 TiB** / **单条目 256 GiB** / **500:1 比率**。一个 4 MiB 压缩包解压到 800 GiB 仍会在打开任何输出文件之前被拒绝。
|
||||
- **69 项主机端 C 测试**(`tests/run-tests.sh`)现已覆盖路径穿越、ZIP64、加密拒绝、压缩比、冲突策略与新增大文件档位(`tests/test_zip_extract.c`)。
|
||||
- 更早的细化——见 v1.6 以来的 `git log`。
|
||||
|
||||
## 截图
|
||||
|
||||
<p>
|
||||
<a href="docs/screenshots/20260617_231827.376.jpg" target="_blank"><img src="docs/screenshots/20260617_231827.376.jpg" width="31%" alt="PS5 网页文件管理器截图 1"></a>
|
||||
<a href="docs/screenshots/20260619_131432.399.jpg" target="_blank"><img src="docs/screenshots/20260619_131432.399.jpg" width="31%" alt="PS5 网页文件管理器截图 2"></a>
|
||||
<a href="docs/screenshots/20260617_232348.855.jpg" target="_blank"><img src="docs/screenshots/20260617_232348.855.jpg" width="31%" alt="PS5 网页文件管理器截图 3"></a>
|
||||
<a href="docs/screenshots/20260619_131811.644.jpg" target="_blank"><img src="docs/screenshots/20260619_131811.644.jpg" width="31%" alt="PS5 网页文件管理器截图 4"></a>
|
||||
<a href="docs/screenshots/20260619_131535.239.jpg" target="_blank"><img src="docs/screenshots/20260619_131535.239.jpg" width="31%" alt="PS5 网页文件管理器截图 5"></a>
|
||||
<a href="docs/screenshots/20260620_232728.533.jpg" target="_blank"><img src="docs/screenshots/20260620_232728.533.jpg" width="31%" alt="PS5 网页文件管理器截图 6"></a>
|
||||
</p>
|
||||
|
||||
## 功能
|
||||
|
||||
- **浏览** —— 列出文件与文件夹;按名称、类型、大小、修改时间或权限排序。上次排序方式持久化在 `localStorage`。
|
||||
- **权限** —— 用复选框切换读/写/执行,或粘贴经过校验的四位八进制模式。
|
||||
- **操作** —— 复制、移动、删除(递归、无回收站)、重命名、创建文件与文件夹。
|
||||
- **编辑器** —— 针对 ≤ 1 MiB 的文件,跨精选扩展名列表的原地 UTF-8 文本编辑器:`.txt .json .xml .ini .cfg .conf .md .log .lua .js .css .html .htm .c .h .cpp .hpp .sh .csv .yaml .yml .shn`。
|
||||
- **多选** —— 一次性复制、移动、删除或打包下载多个项目。
|
||||
- **上传** —— 从局域网内任意设备上传单文件或文件夹树(在 PS5 浏览器中隐藏)。原子化的临时文件 + 重命名。
|
||||
- **下载** —— 单文件以原始字节下载,或文件夹/多选以流式 `.tar` 下载。在 PS5 浏览器中隐藏。
|
||||
- **任务** —— 全屏覆盖层,带延迟显示、实时进度、吞吐率、ETA、取消,以及浏览器中途关闭重开后的恢复能力。
|
||||
- **归档解压** —— ZIP(加密拒绝)、RAR(v1.9:RAR4 + RAR5 含 WinRAR 6/7「v6」、多卷;加密仍拒绝,待密码 UI 接入)、7z(v1.9.1:LZMA2 / BCJ2 / 分卷 / 内容加密;`-mhe=on` 加密头除外)。详见下方 [ZIP 解压](#zip-解压)、[RAR 解压](#rar-解压)、[7z 解压](#7z-解压)。
|
||||
- **PKG** —— 安装并预览 `.pkg` 文件。
|
||||
- **图片** —— 预览 `.png .jpg .jpeg .gif .bmp .webp`。
|
||||
- **本地化** —— 英文 + 简体中文,根据 `navigator.languages` 自动选择。
|
||||
- **移动端友好** —— 响应式布局,工具栏自动换行,文件列表可横向滚动。
|
||||
|
||||
## 快速上手
|
||||
|
||||
1. **构建** ELF:
|
||||
|
||||
```sh
|
||||
export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk # 见「构建」章节的 SDK 配置
|
||||
make
|
||||
```
|
||||
2. **发送** 载荷到 PS5(默认 ELF 加载器端口 `9021`):
|
||||
|
||||
```sh
|
||||
nc -q0 "$PS5_HOST" 9021 < web-file-mgr.elf
|
||||
```
|
||||
3. **读取** PS5 屏幕上的通知——它会打印实际监听端口(默认 `8888`)。
|
||||
4. 在**同一局域网**内的任意浏览器中打开 `http://<PS5_IP>:<port>/`——PS5 浏览器也可以。
|
||||
5. 首次运行时,载荷还会写入一个 **Media** 分类的主屏启动器;已有的启动器文件不会被覆盖。
|
||||
|
||||
## 构建
|
||||
|
||||
需要 [ps5-payload-dev/sdk](https://github.com/ps5-payload-dev/sdk#quick-start):
|
||||
|
||||
```sh
|
||||
export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk
|
||||
```
|
||||
|
||||
本项目链接 `libmicrohttpd`。`make` 在构建前会检查它,缺失时自动运行安装器:
|
||||
|
||||
```sh
|
||||
make
|
||||
```
|
||||
|
||||
若构建主机无网络访问,可提前放入 libmicrohttpd 源码包并手动运行安装器:
|
||||
|
||||
```sh
|
||||
LIBMICROHTTPD_TARBALL=/path/to/libmicrohttpd-1.0.1.tar.gz \
|
||||
./install-libmicrohttpd.sh
|
||||
make
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```text
|
||||
web-file-mgr.elf (约数百 KiB,v1.9.1 含 unrar7 + 7z 后更大;x86_64-sie-ps5)
|
||||
```
|
||||
|
||||
若只想做纯 UI/JS 开发而不需要 PS5 工具链:
|
||||
|
||||
```sh
|
||||
make linux
|
||||
./web-file-mgr-linux
|
||||
```
|
||||
|
||||
Linux 构建**不包含** PS5 主屏启动器安装器。
|
||||
|
||||
## 使用
|
||||
|
||||
在 PS5 上启动一个 ELF 加载器(端口 `9021` 常见)。发送载荷:
|
||||
|
||||
```sh
|
||||
export PS5_HOST=ps5_ip_address
|
||||
nc -q0 "$PS5_HOST" 9021 < web-file-mgr.elf
|
||||
```
|
||||
|
||||
载荷启动后,PS5 通知会显示应用名、版本与实际监听端口。打开它打印的 URL,例如:
|
||||
|
||||
```text
|
||||
http://${PS5_IP_ADDRESS}:8888/
|
||||
```
|
||||
|
||||
若载荷不得不回退到其它端口(如 `8889`),请以通知显示的端口为准——URL 并未硬编码。
|
||||
|
||||
首次启动时,载荷会在需要时于 Media 分类安装一个 `PS5 Web File Manager` 快捷方式。已有的启动器文件会被保留;只补写缺失的文件。
|
||||
|
||||
## ZIP 解压
|
||||
|
||||
仅支持普通 ZIP——stored / deflated / ZIP64,**绝不解密**。引擎是一个独立的三阶段模块(`scan → extract → publish → cleanup`),位于 `src/zip_extract.{c,h}`,配有独立的主机端 C 测试套件。每个条目先写入 staging 目录(`*.wfm-part-*`),fsync 后原子重命名到目标位置。归档中途任何失败都会回滚部分改动;取消与致命错误总会清理 staging。
|
||||
|
||||
### 限额
|
||||
|
||||
| 限额 | 默认档位 | 大档案档位(`ZIPX_LIMITS_LARGE`) |
|
||||
|---|---|---|
|
||||
| `max_entries` | 200 000 | 500 000 |
|
||||
| `max_total_bytes`(未压缩) | 2 TiB | 4 TiB |
|
||||
| `max_file_bytes`(单条目) | 512 GiB | 1 TiB |
|
||||
| `max_ratio`(未压缩 / 压缩) | 500 : 1 | 1000 : 1 |
|
||||
| `max_depth`(文件夹嵌套) | 32 | 32 |
|
||||
| `max_name_len` / `max_path_len` | 255 / 1024 | 255 / 1024 |
|
||||
|
||||
**默认档位**出厂即安全:一个解压到 800 GiB 的 4 MiB 压缩块会在打开任何输出文件之前被拒绝。**大档案档位**仅在请求携带 `large=1` 时才启用——当磁盘上归档大于 `LARGE_FILE_THRESHOLD_BYTES`(默认 480 GiB;可在 `assets/main.js` 配置)时,解压对话框会自动提示用户。确认提示即为用户的明确选配;服务器自身不会额外记录任何内容。
|
||||
|
||||
### 安全检查
|
||||
|
||||
引擎拒绝解压以下归档:
|
||||
|
||||
- 加密条目(设置了任何加密标志)。
|
||||
- 路径穿越(`..` 段、绝对 POSIX 路径、Windows 盘符)。
|
||||
- 符号链接、设备、FIFO、套接字(`ZIPX_ERR_SPECIAL`)。
|
||||
- 同一归档内的重复条目或目录/文件名冲突。
|
||||
- 解压后尺寸、条目数、嵌套深度、名称长度或压缩比突破当前档位。
|
||||
|
||||
### 冲突策略
|
||||
|
||||
通过 `/api/extract` 上的 `conflict=` 传入:
|
||||
|
||||
- `fail`(默认)—— 拒绝覆盖任何已存在的目标。
|
||||
- `overwrite` —— 替换已存在文件;合并进已存在文件夹。
|
||||
- `merge` —— 保留已存在文件,新增其余文件。
|
||||
|
||||
### 调整阈值
|
||||
|
||||
480 GiB 的前端阈值位于 `assets/main.js`:
|
||||
|
||||
```js
|
||||
const LARGE_FILE_THRESHOLD_BYTES = 480 * 1024 * 1024 * 1024;
|
||||
```
|
||||
|
||||
设为 `Infinity` 可静音提示,调低则更保守,或干脆删掉该调用——无论阈值如何,服务器始终遵循 `large=1`。
|
||||
|
||||
## RAR 解压
|
||||
|
||||
RAR 解压引擎(`src/rar_extract.{c,h}`)由 **官方 rarlab UnRAR 源码** 支撑(`third_party/unrar7/`,版本 7.20.1,编译为静态库并通过其 C 兼容的 DLL API 驱动)。扩展名为 `.rar` 的文件与 `.zip` 文件一样拥有**解压**按钮;引擎由 `src/extract.c` 按扩展名分派。
|
||||
|
||||
> v1.9 替换了 v1.8 的引擎(dmc_unrar 1.7.0)。dmc_unrar 无法解码 WinRAR 6.x/7.x 写出的归档(RAR5「v6」压缩)且不支持多卷;unrar 原生支持两者。
|
||||
|
||||
### 支持范围
|
||||
|
||||
| 格式 | 支持 | 备注 |
|
||||
|---|---|---|
|
||||
| RAR 1.5 → 4.x(含 2.9 / 3.6 / 4.0) | ✅ | |
|
||||
| RAR 5.0 及 **5.0「v6」**(WinRAR 6.x / 7.x) | ✅ | v1.9 的触发点 |
|
||||
| Solid 块、最大 1 GiB 字典 | ✅ | |
|
||||
| PPMd 解压(RAR 3.0+) | ✅ | |
|
||||
| **多卷**(`.part01.rar` + `.part02.rar` + …) | ✅ | 当完整卷集与被打开的卷同处一目录时,unrar 按名拼接。选择首个卷(`name.part1.rar` / `name.part01.rar`);非首卷在 UI 中仍置灰并给出提示。 |
|
||||
| **加密 RAR** | ⏳ | 引擎可解密(`RARSetPassword`),但密码字段/提示尚未接入 `/api/extract`。加密归档以 `ZIPX_ERR_UNSUPPORTED` 被提前拒绝。 |
|
||||
| 符号链接 / FIFO / 套接字 / 设备 | ❌ | 以 `ZIPX_ERR_SPECIAL` 拒绝(与 ZIP 行为一致) |
|
||||
| RAR 1.3(1.4 之前) | ❌ | 被 unrar 上游拒绝 |
|
||||
|
||||
当某归档被拒绝时,用户会收到 `extract_unsupported` 失败,文件名作为详情参数。前端已用典型的双语重试指引显示该错误。
|
||||
|
||||
### 限额
|
||||
|
||||
RAR 引擎原样复用 ZIP 的限额表——其上并无额外的 RAR 档位表。默认值与 `large=1` 选配完全相同:
|
||||
|
||||
| 限额 | 默认档位 | 大档案档位(`large=1`) |
|
||||
|---|---|---|
|
||||
| `max_entries` | 200 000 | 500 000 |
|
||||
| `max_total_bytes`(未压缩) | 2 TiB | 4 TiB |
|
||||
| `max_file_bytes`(单条目) | 512 GiB | 1 TiB |
|
||||
| `max_ratio`(未压缩 / 压缩) | 500 : 1 | 1000 : 1 |
|
||||
| `max_depth`(文件夹嵌套) | 32 | 32 |
|
||||
| `max_name_len` / `max_path_len` | 255 / 1024 | 255 / 1024 |
|
||||
|
||||
大档案档位的 RAR 解压使用与 ZIP 相同的 `LARGE_FILE_THRESHOLD_BYTES`(480 GiB)提示——前端对 `.rar` 与 `.zip` 的提示处理相同,且服务器仅在请求携带 `large=1`(选配)时才启用大限额。
|
||||
|
||||
### 安全检查
|
||||
|
||||
RAR 引擎应用与 ZIP 引擎相同的检查——复用 `zipx_status_t` 状态码,因此任务 UI 的 `err_extract_unsafe_name`、`err_extract_too_deep`、`err_extract_ratio` 等会一致触发:
|
||||
|
||||
- 路径穿越(`..` 段、绝对 POSIX 路径、Windows 盘符、`\` 在 `Rar!\x1a\x07…` 头之后被视为路径分隔符等)。
|
||||
- 符号链接、FIFO、套接字、设备。
|
||||
- 归档内重复条目或目录/文件名冲突。
|
||||
- 归档尺寸、条目数、深度、名称长度或压缩比突破当前档位。
|
||||
|
||||
### Vendoring 与许可
|
||||
|
||||
`third_party/unrar7/` 是官方 **rarlab UnRAR 源码**(7.20.1)的逐字副本,由 [`opello/unrar`](https://github.com/opello/unrar) 在提交 `97e1780` 处镜像。它依 **UnRAR 免费软件许可** 分发(见 `third_party/unrar7/license.txt`):可于任何软件中用于处理 RAR 归档,但不得用于开发 RAR 兼容的*归档器*或重新实现 RAR 压缩算法。项目自有的门面 `third_party/unrar7/unrar_c_api.h` 携带项目自身许可。
|
||||
|
||||
> v1.8 引擎 `third_party/unrar/dmc_unrar.c`(DrMcCoy/dmc_unrar 1.7.0,GPL-2.0-or-later)已在 v1.9 移除;其声明留存于 git 历史。
|
||||
|
||||
### 加密 RAR(计划中)
|
||||
|
||||
引擎可解密归档(经 `RARSetPassword`),但密码通道——`/api/extract` 上的 `password=` 字段加前端提示——尚未接线。加密归档目前以 `extract_unsupported` 失败。引擎替换(v1.9)已移除硬性引擎限制;剩余工作纯粹是 API/UI 接线。
|
||||
|
||||
## 7z 解压
|
||||
|
||||
7z 解压引擎(`src/sevenz_extract.{c,h}`)基于 SDK 解码子集(LZMA2 / LZMA / BCJ2 等)加上项目自研的拉式 codec 链(`src/sevenz_chain.c`,位于 `src/sevenz_chain.h`)。扩展名为 `.7z` 的文件与 ZIP / RAR 一样拥有**解压**按钮;引擎由 `src/extract.c` 按扩展名分派,并复用同一套三阶段模型、限额档位与冲突策略。
|
||||
|
||||
> v1.9.1 新增。SDK 自带的 `SzArEx` 路径仅覆盖 4 个 coder 的文件夹,不足以装下 BCJ2 的 5 coder;本项目改为自研 folder 解析 + 拉式 codec 链,从而原生支持 BCJ2 与多 coder 组合。
|
||||
|
||||
### 支持范围
|
||||
|
||||
| 格式 | 支持 | 备注 |
|
||||
|---|---|---|
|
||||
| LZMA2 / LZMA(含 ZIP64 式大尺寸) | ✅ | 单 coder 纯 LZMA2 走多线程解码(`src/sevenz_mt.c`,8 线程) |
|
||||
| BCJ2(x86 反汇编后处理) | ✅ | 经自研拉式链;SDK `SzArEx` 装不下 5 coder 时由本项目承载 |
|
||||
| 多 coder 组合文件夹 | ✅ | 自研 `sevenz_chain.c` 解析 |
|
||||
| **分卷**(`.7z.001` / `.z01` 链) | ✅ | `src/sevenz_volstream.c` 按名拼接;打开首个分卷 |
|
||||
| **内容加密**(7zAES,AES-256-CBC) | ⏳ | 引擎可解密;密码 UI / API 尚未接入,暂时以 `ZIPX_ERR_UNSUPPORTED` 拒绝 |
|
||||
| **`-mhe=on` 加密头** | ❌ | 需自研头解析器;vendored SDK 在涉及我们之前就以 `SZ_ERROR_UNSUPPORTED` 拒绝。此为唯一已知缺口 |
|
||||
|
||||
当某归档被拒绝时,用户同样收到 `extract_unsupported` 失败,UI 显示双语重试指引。
|
||||
|
||||
### 限额
|
||||
|
||||
7z 引擎复用与 ZIP / RAR 完全相同的限额表;默认档位与 `large=1` 选配一致(见 [ZIP 解压 → 限额](#限额))。
|
||||
|
||||
### 安全检查
|
||||
|
||||
7z 引擎复用相同的 `zipx_status_t` 错误码与检查集合:路径穿越、特殊文件、重复条目/名冲突、以及突破当前档位的尺寸/条目数/深度/名称长度/压缩比。coder 的 `out_size` 取自 `coder_unpack_sizes[index]`(而非文件夹尺寸),`SzArEx` 失败时重置 `blockIndex` 以避免伪 CRC。
|
||||
|
||||
## 校验
|
||||
|
||||
`make` 之后,对生成的 ELF 做健全性检查:
|
||||
|
||||
```sh
|
||||
ls -la web-file-mgr.elf # v1.9.1 因含 unrar7 + 7z 体积更大;v1.8.3 约 509 KiB
|
||||
sha256sum web-file-mgr.elf # 把摘要记录进你的发布说明
|
||||
file web-file-mgr.elf # 期望 "ELF 64-bit LSB pie executable, x86-64"
|
||||
od -An -tx1 -N20 web-file-mgr.elf | head -2 # 魔数 7f45 4c46 0201 + e_machine 003e
|
||||
```
|
||||
|
||||
`e_machine = 0x003e` 确认了 PS5 目标三元组 `x86_64-sie-ps5`。`e_type = 3`(`ET_DYN`)确认了 ELF 加载器期望的位置无关载荷。
|
||||
|
||||
## 测试
|
||||
|
||||
一套 POSIX / 主机端 C 测试套件覆盖 ZIP、RAR 与 7z 三个引擎,可在任意 Linux / macOS / MSYS shell 下、无需 PS5 SDK 运行:
|
||||
|
||||
```sh
|
||||
cd tests && bash run-tests.sh # ZIP + RAR 套件
|
||||
bash run-sevenz-tests.sh # 7z 套件(需 MinGW gcc 与 7-Zip 二进制)
|
||||
```
|
||||
|
||||
输出为逐用例的 `check` 风格报告,覆盖:
|
||||
|
||||
- ZIP 条目解析(stored + deflated + ZIP64)
|
||||
- 路径穿越、绝对路径、反斜杠、Windows 盘符
|
||||
- 符号链接、FIFO、加密条目、坏 CRC、截断归档、非 ZIP 文件
|
||||
- 限额:`entries`、`total_bytes`、`file_bytes`、`ratio`、`depth`、`name_len`
|
||||
- 冲突策略:`fail` / `overwrite` / `merge`
|
||||
- 每个阶段的取消
|
||||
- **大文件档位** —— `medium_bomb.zip`(比率 ≈ 238)在默认限额下被拒、在大档位下通过;降低后的大档位仍生效
|
||||
- **RAR 引擎**(`tests/test_rar_extract.c`)—— 格式分派、每个可达 `DMC_UNRAR_*` 码的错误翻译、限额交接
|
||||
- **7z 引擎**(`tests/test_sevenz_extract.c` + `tests/run-sevenz-tests.sh`)—— 真实 `.7z` fixture 逐字节比对、加密头拒绝、各策略下的冲突、取消、限额、缺失目标父目录,以及失败时绝不发布且 staging 树被清理的保证
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
.
|
||||
├── Makefile # PS5 + Linux 构建(VERSION_TAG v1.9.1)
|
||||
├── install-libmicrohttpd.sh # 一次性依赖安装器
|
||||
├── gen-asset-module.py # 将 assets/* 内联为 gzip 压缩的 C 数组
|
||||
├── assets/ # HTML / CSS / JS / 图标 / param.json
|
||||
├── src/ # C 载荷源码
|
||||
│ ├── main.c websrv.c filemgr.c # 入口、HTTP 前端、任务模型
|
||||
│ ├── upload.c download.c # 流处理
|
||||
│ ├── extract.c # /api/extract 分派器(ZIP + RAR + 7z)
|
||||
│ ├── zip_extract.{c,h} # ZIP 引擎
|
||||
│ ├── rar_extract.{c,h} # RAR 引擎(unrar7 后端)
|
||||
│ ├── sevenz_extract.{c,h} # 7z 引擎
|
||||
│ ├── sevenz_chain.{c,h} # 7z 拉式 codec 链(BCJ2 等)
|
||||
│ ├── sevenz_mt.{c,h} # 7z 多线程 LZMA2 解码
|
||||
│ ├── sevenz_volstream.{c,h} # 7z 分卷流拼接
|
||||
│ └── app_installer.c # PS5 Media 启动器安装器
|
||||
├── third_party/ # vendored:zlib、minizip-ng、unrar7、7z(SDK 子集)
|
||||
│ ├── unrar7/ # rarlab UnRAR 7.20.1,静态库 + C API 门面
|
||||
│ ├── minizip-ng/ # ZIP 读取器
|
||||
│ ├── zlib/ # minizip-ng 的压缩后端
|
||||
│ └── 7z/ # LZMA SDK 解码子集
|
||||
├── tests/ # POSIX / 主机测试套件
|
||||
│ ├── test_zip_extract.c
|
||||
│ ├── test_rar_extract.c
|
||||
│ ├── test_sevenz_extract.c # 7z 用例驱动
|
||||
│ ├── make_fixtures.py # 重新生成测试 fixture
|
||||
│ ├── run-tests.sh # 一次性运行器(ZIP + RAR)
|
||||
│ ├── run-sevenz-tests.sh # 7z 运行器
|
||||
│ ├── compat/ # 小型 Win32 / MSYS 垫片
|
||||
│ └── fixtures/ fixtures-7z/ fixtures-real/ # 生成的测试归档
|
||||
├── docs/
|
||||
│ ├── HANDOVER.md # 工程交接 / 开发手册
|
||||
│ ├── UPGRADE-v1.7-zip-large-file-profile.md
|
||||
│ ├── UPGRADE-v1.8-rar-support.md
|
||||
│ └── screenshots/ # README 截图
|
||||
├── THIRD_PARTY_NOTICES # 捆绑库署名
|
||||
├── LICENSE # GPLv3+
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 备注
|
||||
|
||||
- 复制、移动、删除、上传、下载作为单个后台任务运行。一个任务运行时,其它文件操作会被拒绝。
|
||||
- 删除是递归且永久的。没有回收站。
|
||||
- 复制/移动任务可取消。单个文件的部分拷贝会被移除,但部分拷贝的文件夹会保留在原地,以避免在合并进已存在目标文件夹时误删既有文件。
|
||||
- 上传任务可取消。尽可能移除部分上传的临时文件。
|
||||
- 下载文件夹或多个选中项会生成 tar 流。tar 归档由载荷生成,不会先写入 PS5 存储。
|
||||
- 若浏览器在载荷进程仍在运行时被关闭重开,UI 可恢复活动任务显示。
|
||||
- 文本编辑仅限于上述精选扩展名列表。非 UTF-8 与超大文件会被拒绝。
|
||||
- 文件名通过 Web API 以 UTF-8 传输。载荷也会保留挂载文件系统返回的遗留字节序名称,以便混合 USB 文件名编码仍能正确显示与操作。
|
||||
|
||||
## 常见问题
|
||||
|
||||
- **这是自制应用,不应故意修改系统进程或内核内存。** 若遇到内核崩溃(kernel panic),请确保使用较新的越狱方法与 ELF 加载器,或回退到你惯用的稳定方法。
|
||||
- **P2JB 用户** —— 若此载荷触发内核崩溃,请避免在该环境下使用。当每次重试代价高昂时,稳定性比便利更重要。
|
||||
- **准备阶段可能耗时较久** —— 当文件夹含大量文件时,它会累加文件夹大小并检查剩余空间,这有助于避免启动一个无法安全完成的复制 / 移动 / 上传 / 下载。
|
||||
- **`err_extract_entry_too_large`** —— 默认归档上限为单条目 512 GiB / 500:1 比率(覆盖典型 3A 游戏归档中单个约 300 GiB 未压缩文件)。若超过默认,请确认大文件提示(磁盘上 > 480 GiB 的归档会出现),拆分归档,或直接向 API 传入 `large=1`。
|
||||
- **`err_extract_unsupported`** —— 归档使用了引擎无法处理的功能:加密 ZIP、加密 RAR、多卷 RAR(`.part02+.rar`)、极老的 RAR 1.4、RAR 符号链接 / FIFO,或既非 `.zip` 也非 `.rar` / `.7z` 的文件。对于 7z,特指 `-mhe=on` 加密头。针对 RAR 的消息会列出失败原因并提示用户回到 PC 端解压器。
|
||||
|
||||
## 署名
|
||||
|
||||
本项目参考了以下项目构建:
|
||||
|
||||
- **[ps5-payload-dev/websrv](https://github.com/ps5-payload-dev/websrv):** HTTP 服务器结构、静态资源内联思路、PS5 浏览器/websrv 行为与 PKG 安装函数。许可证:GPLv3+。
|
||||
- **[ps5-payload-dev/ftpsrv](https://github.com/ps5-payload-dev/ftpsrv):** PS5 载荷约定、主屏启动器/安装流程参考、进程处理风格与启动安装参考。许可证:GPLv3+。
|
||||
- **[seregonwar/zftpd](https://github.com/seregonwar/zftpd):** PS5 TCP socket 缓冲调优与高吞吐传输行为参考。许可证:MIT。
|
||||
- **[itsPLK/ps5-payload-manager](https://github.com/itsPLK/ps5-payload-manager):** 载荷构建行为。许可证:GPLv3。
|
||||
- **[libmicrohttpd](https://ftp.gnu.org/gnu/libmicrohttpd/):** 用作内嵌 HTTP 服务器库。由 GNU 以 LGPL 许可;本载荷以 SDK 提供的静态库链接它。
|
||||
- **[ps5-payload-dev/sdk](https://github.com/ps5-payload-dev/sdk):** 载荷构建基础。许可证:GPLv3+。
|
||||
- **[etaHEN](https://github.com/etaHEN/etaHEN):** 退出前用于返回 PS5 主屏的 ShellUI URI 导航。许可证:GPLv3。
|
||||
- **[ezremote](https://github.com/cy33hc/ps5-ezremote-client):** 预览 PKG 信息。许可证:GPLv2。
|
||||
- **[zlib-ng/minizip-ng](https://github.com/zlib-ng/minizip-ng):** `/api/extract` 端点使用的 ZIP 读取器。vendored 于 `third_party/minizip-ng/`。许可证:zlib。
|
||||
- **[zlib](https://www.zlib.net/):** minizip-ng 的压缩后端。vendored 于 `third_party/zlib/`。许可证:zlib。
|
||||
- **[rarlab UnRAR (opello/unrar)](https://github.com/opello/unrar):** v1.9 起 `/api/extract` 使用的 RAR 读取器(7.20.1)。vendored 于 `third_party/unrar7/`。许可证:UnRAR 免费软件许可。
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目以 **GPLv3 或更高版本** 分发,与作为实现参考的 GPLv3+ 项目保持一致。见 [`LICENSE`](./LICENSE)。
|
||||
|
||||
第三方项目保留各自许可证。请勿在未保留相应许可证声明的情况下,将署名项目的资源或源码复制到其它发行版中。
|
||||
|
||||
若分发二进制,除本项目 GPL 许可外,还需遵守 `libmicrohttpd` 的 LGPL 条款。vendored 的 `zlib` 与 `minizip-ng` 源码以 zlib 许可分发;再分发用此特性构建的二进制时,保留 `third_party/zlib/LICENSE` 与 `third_party/minizip-ng/LICENSE` 中的版权声明。vendored 的 `unrar7`(RAR 引擎)依 UnRAR 免费软件许可分发;再分发用 v1.9 或更高版本构建的二进制时,保留 `third_party/unrar7/license.txt` 中的声明,且不得用其开发 RAR 兼容归档器或重新实现 RAR 压缩算法。
|
||||
|
||||
## 免责声明
|
||||
|
||||
非官方自制软件。仅在已越狱 PS5 主机上运行。使用风险自负——作者不对损坏、数据丢失、账号处罚或保修影响负责。请勿再分发 Sony 专有内容。依 GPLv3+,修改后的再分发必须公开其源码。
|
||||
+43
-23
@@ -2,8 +2,9 @@ Third-Party Notices
|
||||
===================
|
||||
|
||||
This project vendors a minimal set of third-party source files under
|
||||
`third_party/` to provide the ZIP extraction feature (the `/api/extract`
|
||||
endpoint). Their full license texts are included alongside the sources.
|
||||
`third_party/` to provide the ZIP, RAR and 7z extraction features (the
|
||||
`/api/extract` endpoint). Their full license texts are included
|
||||
alongside the sources.
|
||||
|
||||
1. minizip-ng
|
||||
-----------
|
||||
@@ -21,26 +22,45 @@ endpoint). Their full license texts are included alongside the sources.
|
||||
Files : third_party/zlib/** (vendored subset, compiled with relaxed
|
||||
warnings into the final binary)
|
||||
|
||||
3. dmc_unrar
|
||||
---------
|
||||
Version : 1.7.0
|
||||
Source : https://github.com/DrMcCoy/dmc_unrar
|
||||
License : GPL-2.0-or-later (see third_party/unrar/COPYING)
|
||||
Files : third_party/unrar/dmc_unrar.c (single-file library, compiled
|
||||
with relaxed warnings; the small facade header
|
||||
third_party/unrar/dmc_unrar_api.h is project-authored and
|
||||
carries the project's license).
|
||||
3. unrar (rarlab UnRAR source, v7.20.1)
|
||||
-------------------------------------
|
||||
Version : 7.20.1 (RAR 7.23-free source snapshot, 2025-10-28)
|
||||
Source : https://www.rarlab.com/rar_add.htm — mirrored by
|
||||
https://github.com/opello/unrar (commit 97e1780)
|
||||
License : UnRAR freeware license (see third_party/unrar7/license.txt)
|
||||
Files : third_party/unrar7/** (RARDLL source set compiled with
|
||||
relaxed warnings; unrar_c_api.h is project-authored and
|
||||
carries the project's license)
|
||||
|
||||
dmc_unrar is the engine behind `src/rar_extract.c` (the v1.8 RAR support).
|
||||
It is a dependency-free, single-file, C89/C99 FLOSS UnRAR library. Because
|
||||
it is licensed under the GPL-2.0-or-later, the resulting `web-file-mgr.elf`
|
||||
binary inherits the obligation to ship the GPL notice and source — which is
|
||||
exactly what this file plus `third_party/unrar/COPYING` provides. The
|
||||
interface in `dmc_unrar_api.h` is minimal and re-declares only the entry
|
||||
points the project uses.
|
||||
4. LZMA SDK (7z decoder)
|
||||
----------------------
|
||||
Version : 26.03 (2026-09-03)
|
||||
Source : https://www.7-zip.org/sdk.html — release `lzma2603.7z` from
|
||||
https://github.com/ip7z/7zip/releases
|
||||
License : Public domain ("LZMA SDK is written and placed in the public
|
||||
domain by Igor Pavlov", see third_party/7z/DOC/lzma-sdk.txt)
|
||||
Files : third_party/7z/** (decoder-only subset, compiled with relaxed
|
||||
warnings; see third_party/7z/README.md for the file list)
|
||||
|
||||
Both libraries in section 1 and 2 are distributed under the zlib license,
|
||||
which permits redistribution in source and binary form provided the
|
||||
copyright notice and this list of conditions are retained. dmc_unrar is
|
||||
distributed under the GNU GPL v2-or-later. See the individual LICENSE /
|
||||
COPYING files in each `third_party/` subdirectory for the complete terms.
|
||||
The LZMA SDK is the engine behind `src/sevenz_extract.c` (the v1.9.x 7z
|
||||
support: LZMA/LZMA2/PPMd/Copy plus the BCJ, BCJ2 and Delta filters). Only the
|
||||
C implementation is used — it builds with the plain PS5 C toolchain and does
|
||||
not pull in the C++ runtime. The encoder half of the SDK is not vendored.
|
||||
|
||||
unrar is the engine behind `src/rar_extract.c` (the v1.9 RAR support: RAR4,
|
||||
RAR5 including WinRAR 6/7 "v6" compression, and multi-volume archives; the
|
||||
engine can also decrypt via RARSetPassword once a password channel is wired
|
||||
up). The UnRAR source may be used in any software to handle RAR archives,
|
||||
but may not be used to develop a RAR-compatible *archiver* or to re-create
|
||||
the RAR compression algorithm, which is proprietary.
|
||||
|
||||
(The v1.8 engine, DrMcCoy/dmc_unrar 1.7.0 under GPL-2.0-or-later, was
|
||||
replaced by the rarlab UnRAR source in v1.9; see git history under
|
||||
`third_party/unrar/` for its notice.)
|
||||
|
||||
Libraries in sections 1 and 2 are distributed under the zlib license, which
|
||||
permits redistribution in source and binary form provided the copyright
|
||||
notice and this list of conditions are retained. unrar is distributed under
|
||||
its own freeware terms. The LZMA SDK (section 4) is in the public domain and
|
||||
carries no conditions. See the individual LICENSE / license.txt files in
|
||||
each `third_party/` subdirectory for the complete terms.
|
||||
+6
-7
@@ -54,13 +54,9 @@
|
||||
</div>
|
||||
<div class="tool-right">
|
||||
<button id="refreshBtn" data-i18n="refresh"></button>
|
||||
<div id="uploadMenu" class="split-button remote-only">
|
||||
<div id="uploadGroup" class="split-button remote-only">
|
||||
<button id="uploadBtn" class="split-main" data-i18n="upload"></button>
|
||||
<button id="uploadMenuBtn" class="split-arrow" type="button" aria-label="Upload menu"></button>
|
||||
<div class="split-menu">
|
||||
<button id="uploadFolderBtn" type="button" data-i18n="uploadFolder"></button>
|
||||
<button id="uploadAndExtractBtn" type="button" data-i18n="extractUpload"></button>
|
||||
</div>
|
||||
<button id="uploadFolderBtn" class="split-arrow" type="button" aria-label="Upload folder"></button>
|
||||
</div>
|
||||
<button id="newTextBtn" data-i18n="newText"></button>
|
||||
<button id="mkdirBtn" data-i18n="mkdir"></button>
|
||||
@@ -68,7 +64,6 @@
|
||||
</section>
|
||||
<input id="uploadFiles" type="file" multiple hidden>
|
||||
<input id="uploadFolder" type="file" multiple webkitdirectory hidden>
|
||||
<input id="uploadZip" type="file" accept=".zip,application/zip,application/x-zip-compressed" hidden>
|
||||
|
||||
<section id="content" class="content">
|
||||
<table>
|
||||
@@ -186,6 +181,10 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="dropUploadOverlay" class="drop-upload-overlay remote-only" hidden>
|
||||
<div class="drop-upload-message" data-i18n="dropUpload"></div>
|
||||
</div>
|
||||
|
||||
<div id="taskOverlay" class="task-overlay" hidden>
|
||||
<div class="task-panel">
|
||||
<div id="tasks" class="tasks"></div>
|
||||
|
||||
+7
-3
@@ -6,6 +6,7 @@ window.WFM_LANG = {
|
||||
download: "Download",
|
||||
upload: "Upload",
|
||||
uploadFolder: "Upload Folder",
|
||||
dropUpload: "Release to upload into this folder",
|
||||
copying: "Copying",
|
||||
moving: "Moving",
|
||||
deleting: "Deleting",
|
||||
@@ -101,11 +102,13 @@ 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",
|
||||
extractUploadConfirm: "Upload and extract {name}?\n\nTarget folder: {path}\nThe uploaded ZIP will be deleted after success.",
|
||||
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.",
|
||||
extractUploadAsk: "{name} is an archive.\n\nOK: upload and extract here\nCancel: upload only",
|
||||
extractStarted: "Extraction started: {name}",
|
||||
extractDone: "Extraction complete: {name}",
|
||||
extractProgress: "{done} / {total} files",
|
||||
extractLargeAsk: "The archive looks large ({size}). Enable the large-file profile?\n\nOK = yes (single file up to 1 TiB, archive total up to 2 TiB)\nCancel = default limits (single file 64 GiB, archive total 512 GiB); this archive may be rejected",
|
||||
extractLargeAsk: "The archive looks large ({size}). Enable the large-file profile?\n\nOK = yes (single file up to 1 TiB, archive total up to 4 TiB)\nCancel = default limits (single file 512 GiB, archive total 2 TiB); this archive may be rejected",
|
||||
extractLargeActive: "Large-file profile is enabled for this task",
|
||||
extractSelectMainVolume: "Please select the main volume (.rar or .part01.rar)",
|
||||
extractArchivePending: "Preparing to extract {name}",
|
||||
@@ -179,7 +182,8 @@ window.WFM_LANG = {
|
||||
err_destination_must_be_directory: "Destination must be a folder for multiple items",
|
||||
err_extract_open_failed: "Cannot open archive: {arg}",
|
||||
err_extract_corrupt: "Archive is corrupt or incomplete: {arg}",
|
||||
err_extract_unsupported: "Unsupported archive (only unencrypted plain ZIP and single-volume RAR are supported): {arg}",
|
||||
err_extract_unsupported: "Unsupported archive (only plain ZIP, single-volume RAR, and 7z are supported): {arg}",
|
||||
err_extract_password: "Wrong password or the archive is not encrypted with the one supplied: {arg}",
|
||||
err_extract_unsafe_name: "Archive contains an unsafe path: {arg}",
|
||||
err_extract_special_entry: "Archive contains an unsupported special file: {arg}",
|
||||
err_extract_duplicate: "Archive contains duplicate entries: {arg}",
|
||||
|
||||
+7
-3
@@ -6,6 +6,7 @@ window.WFM_LANG = {
|
||||
download: "下载",
|
||||
upload: "上传",
|
||||
uploadFolder: "上传文件夹",
|
||||
dropUpload: "松开即上传到当前目录",
|
||||
copying: "复制",
|
||||
moving: "移动",
|
||||
deleting: "删除",
|
||||
@@ -101,11 +102,13 @@ window.WFM_LANG = {
|
||||
extracting: "解压",
|
||||
extractConfirm: "解压 {name} 到 {path}?",
|
||||
extractOverwriteAsk: "若目标已存在同名文件或目录:\n\n确定 = 覆盖同名文件(目录仍会合并)\n取消 = 若目标已存在则失败",
|
||||
extractUploadConfirm: "上传并解压 {name}?\n\n目标目录:{path}\n成功后将删除上传的 ZIP。",
|
||||
extractPasswordAsk: "此压缩包可能加密了(如 7zAES)。\n\n输入密码后解压,留空则尝试无密码解压。",
|
||||
extractUploadConfirm: "上传并解压 {name}?\n\n目标目录:{path}\n成功后将删除上传的压缩包。",
|
||||
extractUploadAsk: "这是压缩包 {name}。\n\n确定:上传后自动解压到当前目录\n取消:仅上传,不解压",
|
||||
extractStarted: "已开始解压 {name}",
|
||||
extractDone: "解压完成:{name}",
|
||||
extractProgress: "{done} / {total} 个文件",
|
||||
extractLargeAsk: "ZIP 体积较大({size}),是否启用「大文件模式」?\n\n确定 = 启用(单文件最大 1 TiB / 总解压最大 2 TiB)\n取消 = 默认限制(单文件 64 GiB / 总解压 512 GiB),可能拒绝此压缩包",
|
||||
extractLargeAsk: "压缩包体积较大({size}),是否启用「大文件模式」?\n\n确定 = 启用(单文件最大 1 TiB / 总解压最大 4 TiB)\n取消 = 默认限制(单文件 512 GiB / 总解压 2 TiB),可能拒绝此压缩包",
|
||||
extractLargeActive: "此任务已启用大文件模式",
|
||||
extractSelectMainVolume: "请改选主卷(如 .rar 或 .part01.rar)",
|
||||
extractArchivePending: "正在准备解压 {name}",
|
||||
@@ -179,7 +182,8 @@ window.WFM_LANG = {
|
||||
err_destination_must_be_directory: "多个项目的目标必须是目录",
|
||||
err_extract_open_failed: "无法打开压缩包: {arg}",
|
||||
err_extract_corrupt: "压缩包损坏或不完整: {arg}",
|
||||
err_extract_unsupported: "不支持的压缩包(仅支持未加密的普通 ZIP 与单卷 RAR): {arg}",
|
||||
err_extract_unsupported: "不支持的压缩包(仅支持未加密的普通 ZIP、单卷 RAR,以及 7z):{arg}",
|
||||
err_extract_password: "密码错误,或压缩包未使用所提供的密码加密: {arg}",
|
||||
err_extract_unsafe_name: "压缩包包含不安全的路径: {arg}",
|
||||
err_extract_special_entry: "压缩包包含不支持的特殊文件: {arg}",
|
||||
err_extract_duplicate: "压缩包包含重复条目: {arg}",
|
||||
|
||||
+32
-31
@@ -286,28 +286,6 @@ body {
|
||||
border-top: 7px solid #f2f5f7;
|
||||
}
|
||||
|
||||
.split-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
z-index: 40;
|
||||
display: none;
|
||||
min-width: 150px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.split-menu button {
|
||||
width: 100%;
|
||||
min-width: 150px;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.split-button:hover .split-menu,
|
||||
.split-button.open .split-menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
button,
|
||||
.button {
|
||||
height: 54px;
|
||||
@@ -1057,6 +1035,38 @@ input[type="checkbox"] {
|
||||
margin-left: 14px;
|
||||
}
|
||||
|
||||
/* Shown while a file drag hovers the window. pointer-events stays off so the
|
||||
drop still lands on the window listener rather than on this element, and the
|
||||
z-index sits above the task overlay so the hint is visible during a running
|
||||
task (the drop itself is ignored while busy). */
|
||||
.drop-upload-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 19000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(8, 11, 14, .88);
|
||||
box-shadow: inset 0 0 0 4px #6fb1ff;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.drop-upload-message {
|
||||
max-width: calc(100% - 48px);
|
||||
padding: 26px 34px;
|
||||
border: 1px solid #46515f;
|
||||
border-radius: 8px;
|
||||
background: #1b2026;
|
||||
box-shadow: 0 18px 54px rgba(0, 0, 0, .5);
|
||||
color: #edf0f2;
|
||||
font-size: 24px;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.task-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -1349,15 +1359,6 @@ input[type="checkbox"] {
|
||||
width: 34px;
|
||||
}
|
||||
|
||||
.split-menu {
|
||||
min-width: 126px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.split-menu button {
|
||||
min-width: 126px;
|
||||
}
|
||||
|
||||
.paste-action {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
+221
-44
@@ -35,7 +35,11 @@ let uploadXhr = null;
|
||||
let uploadTerminalAbort = false;
|
||||
let L = {};
|
||||
|
||||
const APP_VERSION = "v1.7";
|
||||
// Last-resort fallback for the footer. The real value comes from
|
||||
// /api/version, which reports the build's VERSION_TAG -- see loadVersion().
|
||||
// Keeping a literal here used to be the only source, and it inevitably
|
||||
// drifted (the footer said "v1.9" throughout the v1.9.1 release).
|
||||
const APP_VERSION_FALLBACK = "v1.9.2";
|
||||
const LAST_PATH_KEY = "ps5-web-file-mgr:last-path";
|
||||
const SORT_KEY = "ps5-web-file-mgr:list-sort";
|
||||
const LOADING_DISPLAY_DELAY = 250;
|
||||
@@ -68,14 +72,11 @@ const installPkgBtn = document.getElementById("installPkgBtn");
|
||||
const extractBtn = document.getElementById("extractBtn");
|
||||
const clearClipboardBtn = document.getElementById("clearClipboardBtn");
|
||||
const downloadBtn = document.getElementById("downloadBtn");
|
||||
const uploadMenuEl = document.getElementById("uploadMenu");
|
||||
const uploadBtn = document.getElementById("uploadBtn");
|
||||
const uploadMenuBtn = document.getElementById("uploadMenuBtn");
|
||||
const uploadFolderBtn = document.getElementById("uploadFolderBtn");
|
||||
const uploadAndExtractBtn = document.getElementById("uploadAndExtractBtn");
|
||||
const uploadFilesEl = document.getElementById("uploadFiles");
|
||||
const uploadFolderEl = document.getElementById("uploadFolder");
|
||||
const uploadZipEl = document.getElementById("uploadZip");
|
||||
const dropUploadOverlayEl = document.getElementById("dropUploadOverlay");
|
||||
const initLoadingEl = document.getElementById("initLoading");
|
||||
const exitBtn = document.getElementById("exitBtn");
|
||||
const textEditorOverlayEl = document.getElementById("textEditorOverlay");
|
||||
@@ -156,7 +157,16 @@ function backendErrorText(code, arg, fallback) {
|
||||
params.available = formatBytes(parts[1] || 0, false);
|
||||
}
|
||||
const key = "err_" + code;
|
||||
return L[key] ? t(key, params) : fallback || t("backendError");
|
||||
const label = L[key] ? t(key, params) : "";
|
||||
if (label) {
|
||||
// Append the backend's own message (e.g. "cannot create file: No space
|
||||
// left on device") when present — it carries the strerror detail that
|
||||
// the i18n label otherwise hides.
|
||||
const hint = fallback && fallback !== t("backendError")
|
||||
? " (" + String(fallback) + ")" : "";
|
||||
return label + hint;
|
||||
}
|
||||
return fallback || t("backendError");
|
||||
}
|
||||
|
||||
function applyStaticText() {
|
||||
@@ -169,12 +179,26 @@ function applyStaticText() {
|
||||
}
|
||||
exitBtn.title = t("exit");
|
||||
exitBtn.setAttribute("aria-label", t("exit"));
|
||||
uploadFolderBtn.title = t("uploadFolder");
|
||||
uploadFolderBtn.setAttribute("aria-label", t("uploadFolder"));
|
||||
parentBtn.title = t("parent");
|
||||
parentBtn.setAttribute("aria-label", t("parent"));
|
||||
versionEl.textContent = APP_VERSION;
|
||||
versionEl.textContent = APP_VERSION_FALLBACK;
|
||||
if (initLoadingEl) initLoadingEl.hidden = true;
|
||||
}
|
||||
|
||||
// Ask the backend which version it was built as, so the footer can never
|
||||
// disagree with the Makefile. A failure here is cosmetic: we simply keep the
|
||||
// fallback text rather than nagging the user with an error toast.
|
||||
async function loadVersion() {
|
||||
try {
|
||||
const data = await api("/api/version");
|
||||
if (data && data.version) versionEl.textContent = data.version;
|
||||
} catch (err) {
|
||||
/* offline or very old payload -- keep APP_VERSION_FALLBACK */
|
||||
}
|
||||
}
|
||||
|
||||
function nextPaint() {
|
||||
return new Promise(resolve => {
|
||||
if (window.requestAnimationFrame) {
|
||||
@@ -764,13 +788,47 @@ 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 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;
|
||||
}
|
||||
|
||||
// The upload path knows a filename but has no directory entry to inspect, so
|
||||
// reuse the toolbar's archive test on a synthetic item. Keeps a dropped
|
||||
// .7z.001 or .part1.rar recognised as "offer to extract" without duplicating
|
||||
// the rules.
|
||||
function isExtractableName(name) {
|
||||
return isExtractableArchive({ type: "-", name: String(name || "") });
|
||||
}
|
||||
|
||||
function isSpecialDirectory(item) {
|
||||
if (item.type !== "d") return false;
|
||||
if (item.path === "/data") return true;
|
||||
@@ -814,18 +872,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();
|
||||
@@ -835,7 +895,13 @@ async function startExtractTask(path, dstDir, conflict, removeSource, name, larg
|
||||
}
|
||||
}
|
||||
|
||||
const LARGE_FILE_THRESHOLD_BYTES = 60 * 1024 * 1024 * 1024;
|
||||
// Threshold above which the web UI prompts the user before extracting.
|
||||
// Tuned so a typical 3A-game archive (~300 GiB single file) and a PS5 system
|
||||
// backup (~300 GiB total) extract under the default profile without prompting.
|
||||
// Files between this threshold and the default max_file_bytes cap (512 GiB)
|
||||
// still extract silently; larger files require explicit user opt-in via the
|
||||
// large=1 API flag.
|
||||
const LARGE_FILE_THRESHOLD_BYTES = 480 * 1024 * 1024 * 1024;
|
||||
|
||||
function shouldPromptLargeMode(itemSize) {
|
||||
return Number(itemSize || 0) > LARGE_FILE_THRESHOLD_BYTES;
|
||||
@@ -853,7 +919,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) {
|
||||
@@ -1394,9 +1469,7 @@ function updateButtons() {
|
||||
downloadBtn.disabled = locked || items.length === 0;
|
||||
document.getElementById("refreshBtn").disabled = locked;
|
||||
uploadBtn.disabled = locked;
|
||||
uploadMenuBtn.disabled = locked;
|
||||
uploadFolderBtn.disabled = locked;
|
||||
uploadAndExtractBtn.disabled = locked;
|
||||
document.getElementById("mkdirBtn").disabled = locked;
|
||||
newTextBtn.disabled = locked;
|
||||
for (const button of filesEl.querySelectorAll(".row-action, .mode-action")) button.disabled = locked;
|
||||
@@ -1963,7 +2036,6 @@ function renderTasks(tasks) {
|
||||
const isDelete = task.op === "delete";
|
||||
const isDownload = task.op === "download";
|
||||
const isChmod = task.op === "chmod";
|
||||
const isExtract = task.op === "extract";
|
||||
const isPreparing = (task.op === "copy" || task.op === "move" || isChmod) &&
|
||||
task.state === "running" && done === 0;
|
||||
const isFinishing = !isDelete && !isDownload && task.state === "running" && total > 0 && done >= total;
|
||||
@@ -1996,9 +2068,15 @@ function renderTasks(tasks) {
|
||||
speedItem.textContent = t("speedLabel") + ": " + (isChmod ?
|
||||
t("itemsPerSecond", { count: Math.round(speed) }) : formatSpeed(speed));
|
||||
const progressItem = document.createElement("div");
|
||||
progressItem.textContent = t("progressLabel") + ": " + (isExtract && Number(task.entries_total) ?
|
||||
t("extractProgress", { done: Number(task.entries_done || 0), total: Number(task.entries_total || 0) }) :
|
||||
isChmod ? t("permissionProgress", { done, total }) : formatSize(done) + " / " + formatSize(total));
|
||||
// Bytes only, for every task type including extract. An entry counter was
|
||||
// tried here and read as a hang: the archives that matter are a handful of
|
||||
// huge entries (a game blob split across volumes), so entries_done sits at 0
|
||||
// for the whole of the first one while bytes are plainly moving. The current
|
||||
// file name is already shown on its own line, so the entry index added
|
||||
// nothing a user could act on. Benchmarked: reporting is not a cost -- see
|
||||
// tests/bench_progress.c --mode -- so this is about clarity, not speed.
|
||||
progressItem.textContent = t("progressLabel") + ": " + (isChmod ?
|
||||
t("permissionProgress", { done, total }) : formatSize(done) + " / " + formatSize(total));
|
||||
const etaItem = document.createElement("div");
|
||||
etaItem.textContent = t("etaLabel") + ": " + averageEta(task, done, total);
|
||||
appendChildren(meta, speedItem, progressItem, etaItem);
|
||||
@@ -2221,7 +2299,7 @@ function uploadFileRequest(taskId, file, rel, overwrite, index) {
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadFiles(files) {
|
||||
async function uploadFiles(files, relativeNames) {
|
||||
if (busy || loadingPath || !files.length) return;
|
||||
const useLoading = files.length >= SELECT_ALL_LOADING_THRESHOLD;
|
||||
let list;
|
||||
@@ -2238,7 +2316,9 @@ async function uploadFiles(files) {
|
||||
list = Array.prototype.slice.call(files);
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const file = list[i];
|
||||
rels.push(uploadRelativeName(file));
|
||||
// Dropped items carry no webkitRelativePath, so the drop collector
|
||||
// rebuilds the path itself and passes it in.
|
||||
rels.push(relativeNames && relativeNames[i] ? relativeNames[i] : uploadRelativeName(file));
|
||||
sizes.push(String(file.size || 0));
|
||||
total += Number(file.size || 0);
|
||||
}
|
||||
@@ -2247,6 +2327,17 @@ async function uploadFiles(files) {
|
||||
if (useLoading) hideContentLoading();
|
||||
}
|
||||
|
||||
// A lone archive used to need the "upload and extract" menu entry. Ask once
|
||||
// here instead, so the plain Upload button covers it too; declining is an
|
||||
// ordinary upload, and the toolbar Extract button still works afterwards.
|
||||
if (list.length === 1 && isExtractableName(rels[0])) {
|
||||
uploadFilesEl.value = "";
|
||||
uploadFolderEl.value = "";
|
||||
if (confirm(t("extractUploadAsk", { name: rels[0] }))) {
|
||||
return uploadAndExtractFile(list[0], rels[0], true);
|
||||
}
|
||||
}
|
||||
|
||||
const overwrite = conflicts.length &&
|
||||
confirm(t("uploadOverwriteConfirm", { names: conflictText(conflicts) }));
|
||||
if (conflicts.length && !overwrite) return;
|
||||
@@ -2306,38 +2397,133 @@ async function uploadFiles(files) {
|
||||
}
|
||||
}
|
||||
|
||||
// Two one-click entries rather than a menu: the main button picks files, the
|
||||
// arrow picks a folder. A native file dialog is either file-only or
|
||||
// folder-only (webkitdirectory), so a single dialog cannot offer both; the
|
||||
// drop target below is the one gesture that accepts either.
|
||||
function actionUploadFiles() {
|
||||
if (busy || loadingPath) return;
|
||||
uploadMenuEl.classList.remove("open");
|
||||
uploadFilesEl.click();
|
||||
}
|
||||
|
||||
function actionUploadFolder() {
|
||||
if (busy || loadingPath) return;
|
||||
uploadMenuEl.classList.remove("open");
|
||||
uploadFolderEl.click();
|
||||
}
|
||||
|
||||
function toggleUploadMenu() {
|
||||
if (busy || loadingPath) return;
|
||||
uploadMenuEl.classList.toggle("open");
|
||||
// A dropped directory arrives as a FileSystemEntry, which has no recursive
|
||||
// listing of its own: readEntries() hands back one batch at a time and an
|
||||
// empty batch marks the end, so it has to be driven until it drains.
|
||||
async function readDroppedDirectory(directory) {
|
||||
const reader = directory.createReader();
|
||||
const out = [];
|
||||
for (;;) {
|
||||
const batch = await new Promise((resolve, reject) => reader.readEntries(resolve, reject));
|
||||
if (!batch.length) return out;
|
||||
for (const entry of batch) out.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
function actionUploadAndExtract() {
|
||||
if (busy || loadingPath) return;
|
||||
uploadMenuEl.classList.remove("open");
|
||||
uploadZipEl.click();
|
||||
async function collectDroppedEntry(entry, prefix, files, relativeNames) {
|
||||
if (entry.isFile) {
|
||||
const file = await new Promise((resolve, reject) => entry.file(resolve, reject));
|
||||
files.push(file);
|
||||
relativeNames.push(prefix + file.name);
|
||||
return;
|
||||
}
|
||||
if (!entry.isDirectory) return;
|
||||
const children = await readDroppedDirectory(entry);
|
||||
const childPrefix = prefix + entry.name + "/";
|
||||
for (const child of children) {
|
||||
await collectDroppedEntry(child, childPrefix, files, relativeNames);
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadAndExtractFile(file) {
|
||||
async function uploadDroppedItems(dataTransfer) {
|
||||
const files = [];
|
||||
const relativeNames = [];
|
||||
const dirEntries = [];
|
||||
const items = dataTransfer.items;
|
||||
if (items && items.length) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].kind !== "file") continue;
|
||||
const entry = items[i].webkitGetAsEntry ? items[i].webkitGetAsEntry() : null;
|
||||
if (entry) {
|
||||
dirEntries.push(entry);
|
||||
continue;
|
||||
}
|
||||
const file = items[i].getAsFile();
|
||||
if (file) {
|
||||
files.push(file);
|
||||
relativeNames.push(file.name);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < dataTransfer.files.length; i++) {
|
||||
const file = dataTransfer.files[i];
|
||||
files.push(file);
|
||||
relativeNames.push(uploadRelativeName(file));
|
||||
}
|
||||
}
|
||||
for (const entry of dirEntries) {
|
||||
await collectDroppedEntry(entry, "", files, relativeNames);
|
||||
}
|
||||
await uploadFiles(files, relativeNames);
|
||||
}
|
||||
|
||||
function dataTransferHasFiles(dataTransfer) {
|
||||
const types = dataTransfer && dataTransfer.types;
|
||||
if (!types) return false;
|
||||
for (let i = 0; i < types.length; i++) {
|
||||
if (types[i] === "Files") return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function setupDropUpload() {
|
||||
// The console browser has no drag source of its own, so the listeners would
|
||||
// only ever raise a hint that nothing can dismiss.
|
||||
if (isPlayStationBrowser()) return;
|
||||
let dragDepth = 0;
|
||||
window.addEventListener("dragenter", event => {
|
||||
if (!dataTransferHasFiles(event.dataTransfer)) return;
|
||||
event.preventDefault();
|
||||
dragDepth++;
|
||||
if (!busy && !loadingPath) dropUploadOverlayEl.hidden = false;
|
||||
});
|
||||
window.addEventListener("dragover", event => {
|
||||
if (!dataTransferHasFiles(event.dataTransfer)) return;
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
});
|
||||
window.addEventListener("dragleave", () => {
|
||||
dragDepth = Math.max(0, dragDepth - 1);
|
||||
if (!dragDepth) dropUploadOverlayEl.hidden = true;
|
||||
});
|
||||
window.addEventListener("drop", event => {
|
||||
if (!dataTransferHasFiles(event.dataTransfer)) return;
|
||||
event.preventDefault();
|
||||
dragDepth = 0;
|
||||
dropUploadOverlayEl.hidden = true;
|
||||
if (busy || loadingPath) return;
|
||||
uploadDroppedItems(event.dataTransfer).catch(err => {
|
||||
const message = t("uploadFailed", { error: err.message || t("backendError") });
|
||||
setStatus(message);
|
||||
alert(message);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadAndExtractFile(file, relativeName, alreadyAsked) {
|
||||
if (busy || loadingPath) return;
|
||||
if (!/\.zip$/i.test(file.name || "")) {
|
||||
if (!isExtractableName(relativeName || file.name || "")) {
|
||||
alert(t("err_extract_unsupported", { arg: file.name }));
|
||||
return;
|
||||
}
|
||||
const rel = uploadRelativeName(file);
|
||||
const rel = relativeName || uploadRelativeName(file);
|
||||
const zipPath = pathJoin(cwd, rel);
|
||||
if (!confirm(t("extractUploadConfirm", { name: rel, path: displayPath(cwd) }))) return;
|
||||
if (!alreadyAsked &&
|
||||
!confirm(t("extractUploadConfirm", { name: rel, path: displayPath(cwd) }))) return;
|
||||
const conflict = confirm(t("extractOverwriteAsk")) ? "overwrite" : "fail";
|
||||
|
||||
let taskId = 0;
|
||||
@@ -2384,7 +2570,6 @@ async function uploadAndExtractFile(file) {
|
||||
}
|
||||
} finally {
|
||||
uploadXhr = null;
|
||||
uploadZipEl.value = "";
|
||||
if (!trackedTask) setBusy(false);
|
||||
}
|
||||
}
|
||||
@@ -2417,15 +2602,10 @@ document.getElementById("renameBtn").addEventListener("click", actionRename);
|
||||
downloadBtn.addEventListener("click", actionDownload);
|
||||
document.getElementById("deleteBtn").addEventListener("click", actionDelete);
|
||||
uploadBtn.addEventListener("click", actionUploadFiles);
|
||||
uploadMenuBtn.addEventListener("click", toggleUploadMenu);
|
||||
uploadFolderBtn.addEventListener("click", actionUploadFolder);
|
||||
uploadAndExtractBtn.addEventListener("click", actionUploadAndExtract);
|
||||
uploadFilesEl.addEventListener("change", () => uploadFiles(uploadFilesEl.files));
|
||||
uploadFolderEl.addEventListener("change", () => uploadFiles(uploadFolderEl.files));
|
||||
uploadZipEl.addEventListener("change", () => {
|
||||
if (uploadZipEl.files.length === 1) uploadAndExtractFile(uploadZipEl.files[0]);
|
||||
else uploadZipEl.value = "";
|
||||
});
|
||||
setupDropUpload();
|
||||
exitBtn.addEventListener("click", actionExit);
|
||||
parentBtn.addEventListener("click", actionParentDirectory);
|
||||
textEditorCloseBtn.addEventListener("click", requestCloseTextEditor);
|
||||
@@ -2474,10 +2654,6 @@ window.addEventListener("popstate", event => {
|
||||
loadAndReveal(path, false, false, true, null, revealPath);
|
||||
}
|
||||
});
|
||||
document.addEventListener("click", event => {
|
||||
if (!uploadMenuEl || uploadMenuEl.contains(event.target)) return;
|
||||
uploadMenuEl.classList.remove("open");
|
||||
});
|
||||
document.querySelector("thead").addEventListener("click", event => {
|
||||
if (busy || loadingPath) return;
|
||||
if (parentBtn.contains(event.target)) return;
|
||||
@@ -2576,6 +2752,7 @@ contentEl.addEventListener("scroll", () => {
|
||||
async function init() {
|
||||
await loadLanguage();
|
||||
applyStaticText();
|
||||
loadVersion();
|
||||
readSavedSort();
|
||||
updateSortHeaders();
|
||||
const savedPath = historyPath() || readSavedPath();
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
# 解压性能:实测、根因、提速方案
|
||||
|
||||
> 实测 2026-09-15 · 对照物 = 官方 7-Zip 26.03(上游 v1.8 的 helper 就是它)
|
||||
> 复现:`python tests/bench_driver.py --big --runs 3`;WSL 同环境对比见 `.build/bench/wsl-*.sh`
|
||||
|
||||
> **✅ 方案 A 已落地(2026-09-16)**:`Asm/x86/LzmaDecOpt.asm` + `7zAsm.asm` 已 vendor 到
|
||||
> `third_party/7z/`,jwasm `-elf64 -DABI_LINUX` 汇编进 PS5 与 Linux 两条链路,
|
||||
> `LzmaDec.o` 加 `-DZ7_LZMA_DEC_OPT`。实测 **1.39 s → 1.05–1.13 s(1.26×)**,解出字节与
|
||||
> C 版逐字节一致;7z 测试矩阵 28 checks 全过。Makefile 对该优化做了条件化(无 jwasm 自动
|
||||
> 退回纯 C)并依赖 Makefile 本身触发重编(flag 变化不会被 make 察觉)。
|
||||
>
|
||||
> **✅ 方案 B 已落地(2026-09-16)**:`Lzma2DecMt.c` + `MtDec.c` + `Threads.c` 已 vendor,
|
||||
> 单一纯 LZMA2 folder(7-Zip 默认布局)走 SDK 并行解码器(`src/sevenz_mt.c` 适配层),
|
||||
> 8 线程 + 1 MiB inBufSize_MT;`SZ_ERROR_THREAD` 自动降级回单线程 chain(BCJ2/加密/奇异
|
||||
> 布局本来就由 chain 负责)。实测 329 MiB:1.05 s → **0.77 s(1.37×)**,与 7za -mmt=off
|
||||
> 打平(898 ms);7za -mmt=8 = 485 ms。7z/ZIP/RAR 163 checks 全绿。
|
||||
>
|
||||
> **✅ ZIP 引擎 fsync 批量化(2026-09-16)**:逐条目 fsync 已移除(publish 是纯 rename、
|
||||
> 无续解功能,该 fsync 无收益;RAR/7z 引擎本来就没有)。8000 文件 fixture:fsync 版
|
||||
> \>200 s 未完成 → 无 fsync **14.5 s(≥14×)**。同机官方 7-Zip 反而要 >400 s(Defender
|
||||
> 实时扫描逐文件查杀;PS5 无此因素)。
|
||||
|
||||
## 结论
|
||||
|
||||
**7z 解码我们比 7-Zip 慢 1.57×(单线程),根因已定位到一个具体的编译开关。**
|
||||
|
||||
不是架构问题,不是算法问题,不是编译选项问题 —— 是 **SDK 里有一份汇编版解码器我们没启用**:
|
||||
|
||||
```c
|
||||
/* LzmaDec.c */
|
||||
#ifdef Z7_LZMA_DEC_OPT
|
||||
int Z7_FASTCALL LZMA_DECODE_REAL(CLzmaDec *p, SizeT limit, const Byte *bufLimit); /* asm */
|
||||
#else
|
||||
... 纯 C 宏展开 + LzmaDec_DecodeReal2() /* ← 我们在这里 */
|
||||
#endif
|
||||
```
|
||||
|
||||
`LzmaDecOpt.asm` 是 Igor Pavlov 官方 SDK 的一部分(public domain),**1339 行**,实现同一个函数。开不开这个开关,实测差 1.5 倍。
|
||||
|
||||
---
|
||||
|
||||
## 一、同环境实测(关键:排除跨平台假象)
|
||||
|
||||
第一轮数据是在 Windows 上打的(我们 MinGW 构建 vs `7za.exe`),混了平台因素。重做:**在同一台机器、同一个 WSL Linux 环境、同一份归档、同一类编译器**下对比。
|
||||
|
||||
归档:329 MiB 解压量 / 22 MiB 压缩,LZMA2 solid,单文件
|
||||
|
||||
| 配置 | 单线程 | 8 线程 | 相对我们 |
|
||||
|---|---:|---:|---:|
|
||||
| **ours**(facade,含 staging + fsync + publish) | **1.39 s** | — | 1.00× |
|
||||
| 官方 7-Zip(Linux 构建) | **0.89 s** | **0.51 s** | **1.57× / 2.73×** |
|
||||
|
||||
> 两个数字都是同一台机器上的实测。7-Zip 的 Linux 版和 Windows 版几乎一样快(0.89 vs 0.84 s),说明平台差异不是因素。
|
||||
|
||||
---
|
||||
|
||||
## 二、四个被实测排除的原因
|
||||
|
||||
排查过程里每个假设都先给出过错误结论,所以逐个记录:
|
||||
|
||||
| # | 假设 | 实验 | 结果 |
|
||||
|---|---|---|---|
|
||||
| 1 | **fsync / 写盘开销** | 两边都解到 `/dev/shm`(tmpfs,fsync 近乎免费) | ❌ 我们 1.47 s,磁盘上也是 1.47 s —— **fsync 成本可忽略** |
|
||||
| 2 | **pull 粒度太小**(64 KiB 输出块 → 5000+ 次调用) | 把 `SZ_OUT_CHUNK` 提到 1 MiB | ❌ 1.39 s,与 64 KiB 无差别。profile 显示 `node_pull` **只调用 329 次**,调度开销 ≈ 0 |
|
||||
| 3 | **编译选项保守**(我们用 `-O2 -w`) | `-O3` / `-march=native` / `-march=x86-64-v3` 各跑一遍 | ❌ 全部落在 1.31–1.52 s,无显著差异 |
|
||||
| 4 | **汇编优化只值 6%**(我曾据此推断"不是主因") | 对比 Windows 版(有 asm) 与 Linux 版 | ❌ **这个推断是错的** —— Linux 官方版同样含 asm,所以只看到 6% 的平台差异。见下节 |
|
||||
|
||||
---
|
||||
|
||||
## 三、真正的根因:profile 说话
|
||||
|
||||
`gprof`,同一份归档:
|
||||
|
||||
```
|
||||
% self calls name
|
||||
82.81 1.06 s 81237 LzmaDec_DecodeReal2 ← LZMA 解码核心(C 版)
|
||||
17.19 0.22 s 660 CrcUpdateT12 ← CRC32 校验
|
||||
0.00 0.00 s 329 node_pull ← 我们的链调度,可忽略
|
||||
0.00 0.00 s 329 szx_sink_write ← 写盘,可忽略
|
||||
0.00 0.00 s 1705 LzmaDec_DecodeToDic
|
||||
```
|
||||
|
||||
**82.8% 的时间在一个函数里,而那个函数有一个 asm 版本我们没有使用。**
|
||||
|
||||
这解释了为什么前四个假设全部落空:它们针对的都是那 0% 的部分。
|
||||
|
||||
### 三方交叉验证
|
||||
|
||||
- 我们的构建:未定义 `Z7_LZMA_DEC_OPT` → profile 里是 `LzmaDec_DecodeReal2` ✓
|
||||
- SDK 源码:明确写着 `#ifdef Z7_LZMA_DEC_OPT` 时声明外部 asm 符号 ✓
|
||||
- 官方 GCC 构建规则(`7zip_gcc_c.mak`):`USE_LZMA_DEC_ASM` 开关 + `jwasm` 汇编 `LzmaDecOpt.asm` ✓
|
||||
|
||||
### 附带发现:CRC 占 17%
|
||||
|
||||
`CrcUpdateT12`(slicing-by-12)花掉 0.22 s。7-Zip 解压时同样校验 CRC,所以这部分**不构成差距**,但如果单独优化(SSE4.2 硬件 `crc32` 指令,Zen 2 支持)能再省约 0.15 s。
|
||||
|
||||
---
|
||||
|
||||
## 四、提速方案
|
||||
|
||||
### 方案 A(推荐):启用 asm 解码器
|
||||
|
||||
| 步骤 | 内容 |
|
||||
|---|---|
|
||||
| 1 | 取 `Asm/x86/LzmaDecOpt.asm` + `Asm/x86/7zAsm.asm` 入 `third_party/7z/` |
|
||||
| 2 | 用 **jwasm**(MASM 兼容汇编器,支持 ELF64 输出)汇编成 `.o` |
|
||||
| 3 | Makefile 加规则;`LzmaDec.c` 编译时加 `-DZ7_LZMA_DEC_OPT` |
|
||||
| 4 | 完整测试矩阵(163 checks)+ 基准复测 |
|
||||
|
||||
- **预期收益:1.39 s → ~0.95 s(≈1.45×)**,追平 7-Zip 单线程水平
|
||||
- **工作量**:小~中(一个汇编文件 + 一条 Makefile 规则 + 一个宏)
|
||||
- **风险**:中 —— 唯一的不确定点是 **jwasm 能否产出 PS5(prospero-clang / x86-64 ELF)可链接的目标文件**。这一条必须先验证再动手
|
||||
- **为什么"稳"**:asm 是 SDK 官方组成部分(同一位作者维护,与 C 版有链接时版本校验 `_3`,对不上会直接链接失败而不是静默出错);正确性由现有 163 项测试兜底
|
||||
|
||||
### 方案 B:多线程 LZMA2 解码
|
||||
|
||||
SDK 自带 `C/Lzma2DecMt.c`(1095 行,public domain)就是 7-Zip `-mmt` 的并行实现,实测 0.89 → 0.51 s。
|
||||
|
||||
- **预期收益:额外 1.75×**(与 A 叠加后 ≈ 2.6×,基本追平 7-Zip 全核)
|
||||
- **工作量**:大 —— 要重构 chain 的调度(block 级并行 + 字典依赖管理)
|
||||
- **风险**:中高(并发正确性、内存峰值;PS5 只有 8 核且 HTTP/任务系统同进程,建议限制线程数)
|
||||
- **前置**:建议先完成 A,因为 A 不改架构、收益确定、能独立验证
|
||||
|
||||
### 方案 C:CRC 硬件加速(可选)
|
||||
|
||||
用 SSE4.2 的 `crc32` 指令替换 `CrcUpdateT12`。Zen 2 支持。
|
||||
|
||||
- **预期收益:约 0.15 s(10%)**
|
||||
- **工作量**:小
|
||||
- **注意**:7-Zip 也做 CRC 校验,这不会拉开差距,只是净提速
|
||||
|
||||
### 方案 D(备选,不推荐):上游的 helper 路线
|
||||
|
||||
直接把 7-Zip 做成独立进程,一步到位拿到 1.57×/2.73×。
|
||||
|
||||
不推荐的理由:
|
||||
|
||||
1. 引入外部 ELF 依赖 + IPC + 进程生命周期管理,**故障模式比现在多得多**("最稳"的反面)
|
||||
2. 方案 A 用一个文件 + 一条规则就能拿到 1.45×,D 的增量收益只有多线程那部分
|
||||
3. PS5 上还要处理 elfldr 加载;上游自己都是"单独分发,让用户手动放到 `/data/wfm/`"
|
||||
|
||||
---
|
||||
|
||||
## 五、执行顺序建议
|
||||
|
||||
```
|
||||
第一步 A(asm 解码器)
|
||||
└─ 先验证 jwasm → ELF64 → prospero-ld 这条链能否走通
|
||||
└─ 通过则:1.39 s → ~0.95 s,测试矩阵全绿后提交
|
||||
第二步 B(多线程)
|
||||
└─ 在 A 的基础上做,目标 ~0.55 s
|
||||
第三步 C(CRC 硬件加速,可选)
|
||||
└─ 再省 ~0.15 s
|
||||
```
|
||||
|
||||
**不做任何优化时的现状也是可接受的**:1.39 s / 329 MiB ≈ 237 MiB/s 单线程吞吐。在真实场景(大游戏包)里受存储 I/O 限制,差距往往比这个倍数更小。
|
||||
|
||||
---
|
||||
|
||||
## 六、复现
|
||||
|
||||
```bash
|
||||
export PATH="/c/mingw64/bin:/c/Users/songl/.workbuddy/binaries/PortableGit/versions/1.2.0/mingw64/bin:/c/Users/songl/.workbuddy/binaries/python/versions/3.13.12:/usr/bin:/bin:/c/Windows/System32:/c/Windows"
|
||||
cd "/c/Users/songl/Desktop/Web File Manager/ps5-web-file-manager"
|
||||
|
||||
# 先各跑一次,生成引擎对象
|
||||
/usr/bin/bash tests/run-sevenz-tests.sh
|
||||
/usr/bin/bash tests/run-tests.sh --rebuild
|
||||
|
||||
# Windows 基准(我们的引擎 vs 7za.exe)
|
||||
python tests/bench_driver.py --big --runs 3
|
||||
|
||||
# 同环境对比(WSL):官方 Linux 7-Zip / 我们的引擎 / 编译选项 / profile
|
||||
wsl.exe -d Ubuntu-22.04 -- bash < .build/bench/wsl-perf.sh
|
||||
wsl.exe -d Ubuntu-22.04 -- bash < .build/bench/wsl-ours.sh
|
||||
wsl.exe -d Ubuntu-22.04 -- bash < .build/bench/wsl-flags.sh
|
||||
wsl.exe -d Ubuntu-22.04 -- bash < .build/bench/wsl-prof.sh
|
||||
```
|
||||
|
||||
## 附:测量方法备忘
|
||||
|
||||
这个沙箱里 bash 计时不可用,三个坑都先给出过错误数字:
|
||||
|
||||
- 每次 `date` 要 ~350 ms → `t0/t1` 对给 ~600 ms 的测量注入 700 ms 误差
|
||||
- `time` 的 user/sys 看不到 native 子进程(解 82 MiB 报 `user 0.031s`)
|
||||
- 反复跑堆积 2.7 GB 输出目录 → 测出过"内部时间 > 外部时间"
|
||||
|
||||
正确做法:Python 驱动(单次 spawn + 单调时钟)+ 候选程序自带内部计时 + 显式扣除 spawn tax(~190–240 ms)+ best of N + 每次跑前清输出目录。
|
||||
@@ -0,0 +1,138 @@
|
||||
# PS5 网页文件管理器 v1.9.2(自编译版)发布 + 与上游原版对比 + 使用简介
|
||||
|
||||
> 这是我自己维护的一个分支版本,基于 owendswang 的 `ps5-web-file-manager` 二次开发。本文讲三件事:**它和上游原版差在哪、我这个版本强在哪、怎么用**。
|
||||
|
||||
---
|
||||
|
||||
## 一、它是什么
|
||||
|
||||
一个在已越狱 PS5 上跑的 HTTP 文件管理器(单文件 ELF 载荷)。局域网内任意浏览器(含 PS5 自带浏览器)打开 `http://<PS5的IP>:8888/` 就能管理外接 USB 和内置存储:浏览、改权限、上传、下载、原地编辑文本、装/预览 PKG、看图,以及**解压 ZIP / RAR / 7z**。
|
||||
|
||||
- 版本:**v1.9.2**
|
||||
- 标题 ID:`FMGR88888`
|
||||
- 许可证:GPLv3+
|
||||
- 目标平台:`x86_64-sie-ps5`(Zen 2,不是 ARM)
|
||||
|
||||
> **关于版本号**:v1.9.2 与 v1.9.1 功能完全相同,只更新了内嵌版本号。原先的 `v1.9.1` tag 打在了产出发布二进制的提交**之前 4 个提交**,导致"tag 对应的源码"重建不出发布的那份 ELF;v1.9.2 重新从正确提交上打,使 **tag = 源码 = 二进制**。
|
||||
|
||||
---
|
||||
|
||||
## 二、和上游原版(owendswang v1.8)对比
|
||||
|
||||
先说结论:**不是同一条路线,各有胜负手。**
|
||||
|
||||
| 维度 | 上游 owendswang v1.8 | 我的 v1.9.2 |
|
||||
|---|---|---|
|
||||
| 一句话定位 | 把 7-Zip 本体做成**外部 helper 进程**,靠 IPC 调用 | **自研 / vendor 解码库,全部内嵌同一进程** |
|
||||
| 支持的压缩格式 | **30 种**(zip/7z/rar/tar/gz/xz/zst/bz2/cab/arj…) | **3 种**:ZIP / RAR / 7z |
|
||||
| 部署方式 | **两个文件**,helper 必须放 `/data/wfm/` 指定路径 | **单个 ELF,零外部依赖** |
|
||||
| 载荷体积 | 主程序 + 7-Zip 本体(两份) | **850 KiB 单文件**(含 ZIP/RAR/7z 三个解码引擎) |
|
||||
| 防压缩炸弹(zip bomb) | ❌ 无 | ✅ 压缩比上限 + 1 GiB 下限豁免 |
|
||||
| 磁盘写满保护 | ❌ 无 | ✅ 解压前按实际剩余空间 `statvfs` 预检 |
|
||||
| 路径穿越防护 | ❌ 无(交给 7-Zip) | ✅ 有专项测试 |
|
||||
| 解压中断留残留 | ⚠️ 可能留半成品 | ✅ staging 隔离,失败即清 |
|
||||
| 解压任务跨重启恢复 | ✅ helper 独立进程,重启不丢 | ❌ 暂无 |
|
||||
| 内存隔离 | ✅ 独立进程 | ❌ 与主程序共享地址空间 |
|
||||
| 错误信息详细度 | 中等 | ✅ 含条目名 / errno / 字节数 |
|
||||
| 解压核心正确性 | 7-Zip 本体(20 年验证) | 自研 7z 链 + 成熟 vendor 库 |
|
||||
|
||||
### 上游原版强在哪
|
||||
- **格式多**:30 种,常见游戏包/备份/Mod 里 `.tar.gz`、`.xz`、`.zst`、`.bz2` 都能直接解。
|
||||
- **任务恢复**:helper 是独立进程,主程序被系统杀掉或浏览器重开,大包解压不丢。
|
||||
- **内存隔离**:解压峰值不影响主文件服务。
|
||||
|
||||
### 我的版本强在哪
|
||||
- **安全护栏齐全**:压缩炸弹、写满磁盘、路径穿越、中断残留——这四项上游一个都没有,而我这边都有实现和测试(测试矩阵 163 项检查,0 失败)。说白了,**一个恶意压缩包不会把你的内置存储搞崩**。
|
||||
- **单文件部署**:丢一个 ELF 就行,不用记第二个文件该放哪;helper 丢了功能全废的事在我这不存在。
|
||||
- **7z 引擎是硬啃出来的**:上游靠 7-Zip 本体"白嫖",我这边是自己解析 7z folder + 拉式 codec 链,原生支持 BCJ2 反汇编后处理和多 coder 组合,还顺手做了多线程 LZMA2 解码(约 1.37×)和汇编 LZMA 解码器(约 1.26×)。
|
||||
- **RAR 用官方 UnRAR 7.20.1**:能解 WinRAR 6.x/7.x 写的 RAR5「v6」归档和多卷 RAR——上游原版的旧引擎在这类文件上会报"归档损坏"。
|
||||
|
||||
### 我不回避的短板
|
||||
- **格式覆盖只有 3 种**,这是最明显的弱项。`.tar.gz`、`.xz`、`.zst`、`.bz2` 你现在还得在 PC 上先解开。这是我接下来增量要补的方向(tar+zlib 已有、xz/lzma 复用 LZMA SDK、bz2/zst 可 vendor 单文件解码器),但**不打算照抄上游的 helper 路线**——那样会丢掉上面那些安全护栏和单文件部署优势。
|
||||
- **任务跨重启恢复**和**内存隔离**暂时没有(和"单进程内嵌"的架构取舍有关)。
|
||||
|
||||
> 一句话:**上游赢在"格式广度 + 进程架构",我赢在"安全 + 部署 + 错误质量"。**
|
||||
|
||||
---
|
||||
|
||||
## 三、v1.9.2 / v1.9.1 我做了哪些具体改进
|
||||
|
||||
- **7z 解压引擎**:自研解码子集 + 拉式 codec 链(`sevenz_chain.c`),支持 LZMA2 / LZMA / BCJ2,覆盖 SDK `SzArEx` 装不下的 5-coder 文件夹。
|
||||
- **分卷(多卷)支持**:三种格式都支持分卷,但命名约定不同(引擎按文件名自动识别,打开首个分卷即可):
|
||||
- **ZIP 分卷**:① 经典多磁盘 `name.z01 … name.zNN … name.zip`(索引目录固定在最后一个 `.zip`);② 7-Zip 字节分割 `name.zip.001 / .002 / …`;③ WinRAR 卷 `name.part1.zip … name.partN.zip`。最多 512 卷。
|
||||
- **7z 分卷**:7-Zip 字节分割 `name.7z.001 / .002 / …`,各分卷需等大,打开首个即可。最多 512 卷。
|
||||
- **RAR 分卷**:标准多卷 `name.part1.rar / .part2.rar / …`(unrar 自动按名合并)。**注意**:用 7-Zip 切出来的 `name.rar.001` 这种命名**暂不支持**,引擎会提示你先把分卷改名成 `.partN.rar` 再解。
|
||||
- **性能三项**(纯解码提速,不影响功能):
|
||||
- 汇编 LZMA 解码器 ≈ **1.26×**
|
||||
- 纯 LZMA2 多线程解码(8 线程)≈ **1.37×**
|
||||
- 移除 ZIP 逐条目 fsync,减少写盘开销
|
||||
- **RAR 升级到官方 UnRAR 7.20.1**:RAR5「v6」+ 多卷可用(v1.8 时代在 WinRAR 6/7 文件上报"归档损坏"的问题已消失)。
|
||||
- **载荷瘦了 15.8%**(1,034,328 → 870,488 字节):链接期去掉了 libc++abi 里一段**永远不会执行**的 C++ 名字还原器(只服务于"未捕获异常打印类型名"这条路径),再加上相同机器码折叠。**不损失任何功能,也不影响解压速度**。
|
||||
- **上传更顺手**:点上传按钮直接选文件或文件夹(不再弹二级菜单);支持**把文件/文件夹直接拖到页面上**上传。
|
||||
- **中英双语界面** + 项目主页右上角可切换语言说明。
|
||||
|
||||
### 已知缺口(诚实列出)
|
||||
- 带密码的 ZIP / RAR / 7z:**拒绝解压**(引擎有解密能力,但密码输入 UI/API 还没接,临时先挡掉)。
|
||||
- 7z `-mhe=on` **加密头**:暂不支持(需要自研头解析器)。这是 7z 侧唯一已知缺口。
|
||||
|
||||
---
|
||||
|
||||
## 四、使用简介(三步上手)
|
||||
|
||||
### 1. 把 ELF 发到 PS5
|
||||
PS5 上先运行一个 ELF 加载器(端口 `9021` 常见),然后在 PC 上:
|
||||
|
||||
```sh
|
||||
# 把 PS5_IP 换成你主机实际 IP
|
||||
nc -q0 <PS5的IP> 9021 < web-file-mgr-v1.9.2.elf
|
||||
```
|
||||
|
||||
PS5 屏幕会弹通知,显示实际监听端口(默认 `8888`)。首次运行还会在主屏 Media 分类装一个「PS5 Web File Manager」快捷方式。
|
||||
|
||||
### 2. 浏览器打开
|
||||
同一局域网下,任意设备浏览器打开:
|
||||
|
||||
```
|
||||
http://<PS5的IP>:8888/
|
||||
```
|
||||
|
||||
PS5 自带浏览器也能开。
|
||||
|
||||
### 3. 日常操作
|
||||
- **浏览/管理**:列文件、排序、改权限、复制/移动/重命名/删除、原地编辑文本(≤1 MiB 的 txt/json/js/c/h…)。
|
||||
- **上传下载**:单文件或文件夹树上传(也可直接拖拽到页面);文件夹/多选以 `.tar` 流式下载。
|
||||
- **解压**:在文件列表里点 ZIP / RAR / 7z 的「解压」按钮即可。大档案(>480 GiB)会弹确认提示,确认后用更宽松的限额解压。
|
||||
- **冲突策略**:同名文件默认拒绝覆盖;需要时可选覆盖或合并。
|
||||
|
||||
---
|
||||
|
||||
## 五、下载与校验
|
||||
|
||||
> 请在 GitHub Release 页面下载:`https://github.com/lishersong/ps5-web-file-manager/releases/tag/v1.9.2`
|
||||
|
||||
发布文件:`web-file-mgr-v1.9.2.elf`
|
||||
|
||||
| 项目 | 值 |
|
||||
|---|---|
|
||||
| 大小 | 870,488 字节(约 850 KiB) |
|
||||
| sha256 | `177e90fecf93a0251e83f67884fba4551051be248330b0d70fda8ea732f88e84` |
|
||||
| 文件类型 | ELF 64-bit LSB,x86-64(e_machine `0x003e`,即 PS5 目标三元组) |
|
||||
|
||||
下载后建议先核对 sha256 再发到主机:
|
||||
|
||||
```sh
|
||||
sha256sum web-file-mgr-v1.9.2.elf
|
||||
# 应等于 177e90fecf93a0251e83f67884fba4551051be248330b0d70fda8ea732f88e84
|
||||
```
|
||||
|
||||
> 本次构建是**可复现**的:同一份源码树连编两次 sha256 完全相同。所以这个哈希可以放心当交付指纹用。
|
||||
|
||||
---
|
||||
|
||||
## 六、免责声明
|
||||
|
||||
非官方自制软件,仅在已越狱 PS5 上运行。使用风险自负——作者不对损坏、数据丢失、账号处罚或保修影响负责。请勿再分发 Sony 专有内容。依 GPLv3+,本分支修改后的源码已公开在上面的仓库。
|
||||
|
||||
---
|
||||
|
||||
*想继续补 `.tar.gz` / `.xz` 等格式、或接上密码解压 UI 的,欢迎在仓库提 issue。*
|
||||
+6
-6
@@ -87,7 +87,7 @@ SDK 的 `target/user/homebrew/` 被 `songl(197609)` 拥有 755,普通 song 写
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ Frontend: assets/main.js │
|
||||
│ - LARGE_FILE_THRESHOLD_BYTES = 60 GiB (硬编码) │
|
||||
│ - LARGE_FILE_THRESHOLD_BYTES = 240 GiB (硬编码) │
|
||||
│ - shouldPromptLargeMode(itemSize) → 弹 confirm │
|
||||
│ - startExtractTask(path, dst, conflict, remove, name, large) │
|
||||
└────────────────────┬───────────────────────────────────────────┘
|
||||
@@ -103,8 +103,8 @@ SDK 的 `target/user/homebrew/` 被 `songl(197609)` 拥有 755,普通 song 写
|
||||
│
|
||||
┌────────────────────▼───────────────────────────────────────────┐
|
||||
│ Engine: src/zip_extract.{h,c} │
|
||||
│ - k_default_limits: 200K / 512GiB / 64GiB / 200:1 │
|
||||
│ - k_large_limits: 500K / 2TiB / 1TiB / 1000:1 │
|
||||
│ - k_default_limits: 200K / 1TiB / 256GiB / 500:1 │
|
||||
│ - k_large_limits: 500K / 2TiB / 1TiB / 1000:1 │
|
||||
│ - ZIPX_LIMITS_DEFAULT=0 / ZIPX_LIMITS_LARGE=1 │
|
||||
│ - zipx_limits_profile(int) → const zipx_limits_t* │
|
||||
│ - zipx_extract() 签名不变,向后兼容 │
|
||||
@@ -121,7 +121,7 @@ SDK 的 `target/user/homebrew/` 被 `songl(197609)` 拥有 755,普通 song 写
|
||||
- `assets/lang-en.js` 和 `lang-zh.js` 新增 `extractLargeAsk` / `extractLargeActive`
|
||||
|
||||
**前端 UX**:
|
||||
- ZIP 大于 60 GiB 时弹窗「启用大文件模式?」
|
||||
- ZIP 大于 480 GiB 时弹窗「启用大文件模式?」
|
||||
- 用户点 OK → 传 `large=1` → 引擎走 large profile
|
||||
- 用户点取消 → 走 default profile(多半会被拒绝)
|
||||
|
||||
@@ -1130,8 +1130,8 @@ multi-volume (`name.part01.rar`, `name.part02.rar`, …). Encrypted
|
||||
archives prompt for a password client-side; the password is held
|
||||
only in memory and never saved.
|
||||
|
||||
Limits mirror the ZIP profiles (200K entries / 512 GiB / 64 GiB /
|
||||
ratio 200 by default, with the same `large=1` opt-in to 500K /
|
||||
Limits mirror the ZIP profiles (200K entries / 1 TiB / 256 GiB /
|
||||
ratio 500 by default, with the same `large=1` opt-in to 500K /
|
||||
2 TiB / 1 TiB / 1000).
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
# 重写可行性评估报告
|
||||
|
||||
> 评估日期:2026-09-15 · 评估对象:`LisherSong/ps5-web-file-manager`(v1.9.1)
|
||||
> 目标问题:项目源自他人代码、怀疑授权不清;若从零重写,改动量多大?能否做得更好?
|
||||
>
|
||||
> ⚠️ 本文是**工程视角**的合规盘点,不是法律意见。若涉及商用/闭源决策,请咨询律师。
|
||||
|
||||
---
|
||||
|
||||
## 0. 结论先行
|
||||
|
||||
| 问题 | 答案 |
|
||||
|---|---|
|
||||
| 上游真的没有许可吗? | **否。上游是 GPL-3.0**,我们自己也是 GPL-3.0,两者一致 |
|
||||
| 现在能合法发布吗? | **能**。GPL-3.0 允许修改和再分发,只需满足归因 + 源码可得 |
|
||||
| 有没有真实风险? | 有 4 个,全部**可在 1 天内修完**,没有一个需要重写 |
|
||||
| 全量重写要多少人日? | **35–50 人日**(约 1.4–2 万行需重写,第三方 7.1 万行可直接复用) |
|
||||
| 值得重写吗? | **取决于目标**:想闭源/商用 → 必须重写;想开源分享 → **完全不必** |
|
||||
|
||||
**最反直觉的一点**:我们自建的 7z 引擎核心(3,661 行)**只依赖公共领域和 zlib 许可的第三方库,与上游 GPL 代码零耦合** —— 它是完全干净的资产,今天就能单独抽成 MIT 授权的独立库。
|
||||
|
||||
---
|
||||
|
||||
## 1. 许可现状核查(事实,非猜测)
|
||||
|
||||
### 1.1 上游授权 —— 你的前提是错的
|
||||
|
||||
通过 GitHub API 查询(2026-09-15):
|
||||
|
||||
```
|
||||
owendswang/ps5-web-file-manager
|
||||
license: GPL-3.0 stars: 78 forks: 6
|
||||
created: 2026-06-16 last push: 2026-09-08 archived: false
|
||||
```
|
||||
|
||||
上游**有明确许可**,且是 GPL-3.0。我们的 `LICENSE` 同样是 GPL-3.0,在 root commit `5cb0b76`(Initial import)时加入 —— **两边一致,不存在"无授权"的灰色地带**。
|
||||
|
||||
### 1.2 授权链条
|
||||
|
||||
```
|
||||
ps5-payload-dev/websrv (John Törnblom, GPLv3+)
|
||||
│ 源码里仍保留 "Copyright (C) 2024/2025 John Törnblom"
|
||||
│ —— asset.c / asset.h / mime.h / websrv.h 四个文件
|
||||
▼
|
||||
owendswang/ps5-web-file-manager (GPL-3.0)
|
||||
▼
|
||||
LisherSong/ps5-web-file-manager (GPL-3.0) ← 本项目
|
||||
```
|
||||
|
||||
`src/asset.c:1` 等文件里的 Törnblom 版权声明**至今完整保留** ✅ —— 说明 GPL §5(a)「保留版权声明」这一条在最上游那一环是满足的。
|
||||
|
||||
---
|
||||
|
||||
## 2. 真实风险清单
|
||||
|
||||
按严重度排序。**注意:没有一条需要重写代码来解决。**
|
||||
|
||||
| # | 风险 | 严重度 | 具体位置 | 修法 | 成本 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | **归因缺失**:README Credits 列了 8 个项目,唯独漏了 `owendswang`;git 历史被重写成 "Initial import",上游作者署名在历史里也找不到 | 🟡 中 | `README.md:436-451` | Credits 加一行 + 补 `NOTICE` 文件 | 1 小时 |
|
||||
| 2 | **unRAR 与 GPL-3.0 的附加限制冲突**:UnRAR 许可禁止"用于开发 RAR 兼容压缩器",GPL-3.0 §7 禁止附加限制,严格讲不兼容 | 🟡 中 | `third_party/unrar7/` | 见 §2.1 | 0(接受)或 移除 RAR |
|
||||
| 3 | **ezremote 是 GPLv2**:README 只写 "GPLv2",未标 "or later"。GPLv2-only 与 GPL-3.0 **不兼容** | 🟡 中 | `src/pkg_info.c`(PKG 预览) | 确认其许可措辞;若 v2-only 则重写该模块(591 行) | 1 天 |
|
||||
| 4 | **二进制分发需提供源码**(GPL §6) | 🟢 低 | Release 里的 ELF | 仓库已公开,Release notes 附仓库链接即可 | 10 分钟 |
|
||||
| 5 | Title ID `FMGR88888` 与上游相同,可能与他人 payload 冲突 | 🟢 低 | `Makefile:20` | 换一个自定义 ID | 5 分钟 |
|
||||
|
||||
### 2.1 关于 unRAR(风险 2 详解)
|
||||
|
||||
UnRAR 许可原文允许"在任何软件中处理 RAR 归档",但**禁止用它开发 RAR 兼容的压缩器**。这是一个"附加限制",与 GPL-3.0 §7 冲突。
|
||||
|
||||
实务上的三种处理:
|
||||
|
||||
| 方案 | 做法 | 代价 |
|
||||
|---|---|---|
|
||||
| **A. 接受现状(推荐)** | 明确声明 unrar 部分适用其自有条款,其余部分 GPL-3.0 | 0 —— rarlab 官方自己就这么分发,社区普遍接受 |
|
||||
| B. 移除 RAR 支持 | 删掉 `rar_extract.c` + `third_party/unrar7` | 失去 RAR(含分卷/加密)—— 不划算 |
|
||||
| C. 换实现 | 找自由许可的 RAR 解码器 | **市面上不存在可用的**,死路 |
|
||||
|
||||
**建议 A**。风险等级实际很低:你只做解压不做压缩,本来就不触碰被禁止的那一条。
|
||||
|
||||
---
|
||||
|
||||
## 3. 代码归属盘点(决定重写成本的关键)
|
||||
|
||||
### 3.1 总量分布
|
||||
|
||||
| 类别 | 行数 | 重写时怎么办 |
|
||||
|---|---:|---|
|
||||
| **第三方 vendored** | **71,528** | ♻️ **直接重新 vendor,一行都不用写** |
|
||||
| ├ zlib 1.3.1 | 20,106 | zlib 许可 |
|
||||
| ├ unrar7 7.20.1 | 27,710 | UnRAR 许可 |
|
||||
| ├ LZMA SDK 26.03 | 17,248 | **公共领域** |
|
||||
| └ minizip-ng 4.2.2 | 6,464 | zlib 许可 |
|
||||
| **第一方代码** | **20,844** | 这是唯一需要写的部分 |
|
||||
| ├ v1.7 上游遗产 | 13,860 | 🔴 受 GPL 约束 |
|
||||
| └ 我们新增 | 6,984 | 🟢 版权归我们 |
|
||||
|
||||
**这是整份报告最重要的数字**:项目里 **77% 的代码是第三方库**,重写时原样搬走即可。真正需要动手的只有 2 万行第一方代码,其中又只有不到 1.4 万行受 GPL 约束。
|
||||
|
||||
### 3.2 我们新增代码的独立性分析
|
||||
|
||||
v1.7 之后**新建**的文件(6,745 行),逐个检查其依赖:
|
||||
|
||||
| 文件 | 行数 | 依赖 | 能否独立授权 |
|
||||
|---|---:|---|:---:|
|
||||
| `sevenz_chain.c/.h` | 2,094 | 仅 LZMA SDK(**公共领域**) | ✅ **完全干净** |
|
||||
| `zipx_volume.c/.h` | 658 | 仅自身 | ✅ **完全干净** |
|
||||
| `zipx_volstream.c/.h` | 494 | minizip-ng(zlib)+ zipx_volume.h | ✅ **完全干净** |
|
||||
| `sevenz_volstream.c/.h` | 415 | LZMA SDK + zipx_volume.h | ✅ **完全干净** |
|
||||
| `rar_extract.c/.h` | 1,169 | `zip_extract.h`(共享协议) | ⚠️ 弱耦合,抽协议即可解绑 |
|
||||
| `sevenz_extract.c/.h` | 1,790 | `zip_extract.h`(共享协议) | ⚠️ 弱耦合,抽协议即可解绑 |
|
||||
| `zipx_common.c` | 99 | `zip_extract.h` | ⚠️ 内容仅限额/状态串,30 分钟可重写 |
|
||||
| `cpu_support_stub.c` | 26 | 无 | ✅ 干净 |
|
||||
|
||||
**核心结论**:
|
||||
- **3,661 行(7z 解码链 + 分卷流抽象)与 GPL 代码零耦合** —— 这是项目最有价值的部分(自解析 folder + pull 式 codec 链 + BCJ2 + 7zAES + 三种分卷语义),也是投入最多的部分。它们**今天就可以抽成独立的 MIT/Apache 库**,不受上游任何影响。
|
||||
- 另有 2,959 行只通过 `zip_extract.h` 的**共享协议**(状态枚举、进度回调、限额结构)与上游耦合。把那 100 行协议定义抽成独立的 `archive_api.h` 就能解绑。
|
||||
|
||||
---
|
||||
|
||||
## 4. 三条路径对比
|
||||
|
||||
### 路径 A:维持 GPL-3.0 + 补齐合规(推荐)
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 做什么 | README 补 owendswang 署名、加 NOTICE、确认 ezremote 许可、Release 附源码链接、换 Title ID |
|
||||
| 成本 | **0.5–1 人日** |
|
||||
| 收益 | 合规闭环,零功能损失,保留全部现有能力 |
|
||||
| 风险 | 无 |
|
||||
| 适合 | **想开源分享、想让成果被保护** |
|
||||
|
||||
### 路径 B:架构重构(保留 GPL-3.0)
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 做什么 | 分层重写:platform 层 / archive 引擎层 / HTTP 层 / 前端模块化;抽 `archive_api.h` 解耦;统一进度模型 |
|
||||
| 成本 | **12–18 人日** |
|
||||
| 收益 | 代码可维护性大幅提升,引擎可独立成库,修掉 §6 的已知缺陷 |
|
||||
| 风险 | 中 —— 需真机回归,163 个 host check 要全绿 |
|
||||
| 适合 | **觉得现在代码烂、想长期维护** |
|
||||
|
||||
### 路径 C:Clean-room 全量重写(换许可)
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 做什么 | 不参考上游代码,按功能规格从零写 ~13,860 行受 GPL 约束的部分 |
|
||||
| 成本 | **35–50 人日**(含真机调试) |
|
||||
| 收益 | 完全自有版权,可任选许可(含闭源商用) |
|
||||
| 风险 | **高** —— clean-room 必须严格隔离:写代码的人不能看过上游源码;否则法律上无效 |
|
||||
| 适合 | **确定要闭源/商用** |
|
||||
|
||||
### 4.1 路径 C 的工作量拆解
|
||||
|
||||
| 模块 | 行数 | 难度 | 人日 |
|
||||
|---|---:|---:|---:|
|
||||
| HTTP 服务 + 路由(websrv/main/asset/mime/file_response) | ~700 | 低 | 3 |
|
||||
| 文件管理核心(filemgr.c) | 2,458 | **高** | 8 |
|
||||
| 上传 / 下载 / 断点续传 | 1,343 | 中 | 5 |
|
||||
| 文件系统工具(fs_util/path_util/list/space/text) | 1,229 | 中 | 4 |
|
||||
| PKG 安装 / 信息解析 / app_installer | 847 | 中 | 4 |
|
||||
| 任务调度 + 通知(task/notify) | 253 | 低 | 1.5 |
|
||||
| ZIP 引擎 + 分卷(zip_extract/zipx_*) | 3,091 | **高** | 8 |
|
||||
| 前端(main.js / main.css / index.html / i18n) | 4,779 | 中 | 6 |
|
||||
| 构建系统 + 资产内嵌 | ~200 | 低 | 1 |
|
||||
| 测试矩阵(对齐现有 163 checks) | — | 中 | 5 |
|
||||
| 真机调试 + PS5 平台适配 | — | **高** | 6 |
|
||||
| **合计** | **~14,900** | | **≈ 51 人日** |
|
||||
|
||||
可复用的:7z 全套(4,299 行)+ zipx 分卷(1,193 行)+ 第三方(71,528 行)—— 这三项占了重头戏,所以才是 50 人日而不是 150 人日。
|
||||
|
||||
---
|
||||
|
||||
## 5. 重写能做到"比现在更好"的地方
|
||||
|
||||
如果真要走 B 或 C,这些是现在已知的技术债,**顺手一起解决才值得动**:
|
||||
|
||||
| # | 现有问题 | 位置 | 改进方案 |
|
||||
|---|---|---|---|
|
||||
| 1 | **进度口径三处不一致**:进度条按字节、文字按条目数、ETA 按字节速度,混合大包上体验割裂 | `main.js:1985` / `main.js:2014` / `task.c:129-177` | 统一为字节口径,ETA 用滑动窗口 |
|
||||
| 2 | **PS5 `*at()` 族运行时损坏**(返回 -1 且 errno=0),现有绕行逻辑散落在 `zip_extract.c` | `zip_extract.c` | 抽 platform 层集中处理,写新代码不再踩 |
|
||||
| 3 | 引擎与 HTTP 层耦合:解压协议定义在 `zip_extract.h` 里 | `zip_extract.h` | 抽 `archive_api.h`,引擎可独立成库 |
|
||||
| 4 | `main.js` 2,652 行单文件,无模块拆分 | `assets/main.js` | 按 view / api / task 拆模块 |
|
||||
| 5 | `filemgr.c` 2,458 行,路由 + 业务逻辑 + 平台调用混在一起 | `src/filemgr.c` | 分 handler / service / platform 三层 |
|
||||
| 6 | 测试靠手工脚本,未接入 `make test` | `tests/` | 接 CI,覆盖率可量化 |
|
||||
| 7 | 唯一功能缺口:7z `-mhe=on` 加密头 | `sevenz_extract.c` | 重写时一并补上(工作量约翻倍于现有 7z 头解析) |
|
||||
|
||||
---
|
||||
|
||||
## 6. 建议
|
||||
|
||||
### 6.1 我的推荐:路径 A,外加一条"资产剥离"
|
||||
|
||||
**不要全量重写。** 三个理由:
|
||||
|
||||
1. **GPL-3.0 对你有利,不是负担**。它保证别人拿走你的 7z 引擎成果后**必须同样开源**。换成 MIT,别人可以直接闭源拿去卖 —— 你花了大量精力做的 BCJ2 链、7zAES、分卷流抽象会被白嫖。
|
||||
2. **重写的法律风险比不重写更高**。你已经看过上游源码了,clean-room 的前提已被破坏。真重写必须找没看过上游的人来做,还得隔离沟通 —— 成本远超 50 人日。
|
||||
3. **你的核心资产本来就是干净的**。3,661 行的 7z 解码链 + 分卷流只依赖公共领域和 zlib 许可,**现在就能单独抽出来做 MIT 授权的独立库**,不需要动主项目一根指头。
|
||||
|
||||
### 6.2 立刻可做的三件事(共 1 天)
|
||||
|
||||
```
|
||||
1. README.md Credits 补一行:
|
||||
- [owendswang/ps5-web-file-manager](...): base implementation. License: GPL-3.0.
|
||||
|
||||
2. 把 sevenz_chain.{c,h} + sevenz_volstream.{c,h} + zipx_volume.{c,h}
|
||||
+ zipx_volstream.{c,h} 抽成独立仓库,MIT 授权,主项目作为 submodule 引用。
|
||||
→ 3,661 行成果立刻获得独立身份,且证明这部分是你的原创。
|
||||
|
||||
3. 确认 ezremote 是 "GPLv2" 还是 "GPLv2 or later";
|
||||
若是 v2-only,重写 pkg_info.c(591 行)或改用别的数据源。
|
||||
```
|
||||
|
||||
### 6.3 需要你回答的问题
|
||||
|
||||
**你重写的动机到底是什么?** 不同答案对应完全不同的方案:
|
||||
|
||||
| 如果你的目标是… | 应该走 | 成本 |
|
||||
|---|---|---|
|
||||
| 想闭源 / 商业化 | C(且必须找没看过上游的人写) | 50+ 人日 |
|
||||
| 只是担心"没许可"不合法 | **A** —— 你的担心不成立 | 0.5 天 |
|
||||
| 想让别人知道这是你写的 | **A** —— GPL 允许你在修改部分署名 | 0.5 天 |
|
||||
| 觉得代码质量差、想重构 | B | 12–18 人日 |
|
||||
| 想保护成果不被闭源 | **A** —— GPL-3.0 已经是最佳选择 | 0 天 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:核查方法与数据来源
|
||||
|
||||
| 项 | 来源 |
|
||||
|---|---|
|
||||
| 上游许可 | GitHub API `repos/owendswang/ps5-web-file-manager`,2026-09-15 查询 |
|
||||
| 本项目许可 | `LICENSE`(35,149 B,GPL-3.0 全文),root commit `5cb0b76` 引入 |
|
||||
| 代码行数 | `wc -l` 于 v1.9.1 工作树;上游基线取 `git show 5cb0b76:<file>` |
|
||||
| 文件归属 | `git ls-tree -r 5cb0b76 -- src assets` 与当前工作树的差集 |
|
||||
| 依赖分析 | 逐个 grep `#include "` 于自有文件 |
|
||||
| 已知缺陷 | 项目 `HANDOVER.md` 第四节 + `.workbuddy/memory/MEMORY.md` |
|
||||
@@ -0,0 +1,351 @@
|
||||
# ELF 瘦身可行性分析
|
||||
|
||||
> 2026-09-20 · 环境:WSL Ubuntu-22.04 + `/opt/ps5-payload-sdk` + LLD 18.1.8
|
||||
> 基线产物 `web-file-mgr-v1.9.1.elf` = **1,034,328 B**
|
||||
> 复现脚本:`.build/_sizeprobe.sh`、`.build/_probe3.sh`、`.build/_slimtest.sh`、`.build/_slimtest2.sh`、`.build/_modsize.sh`
|
||||
|
||||
## 结论
|
||||
|
||||
> ✅ **4.1 + 4.2 已落地(2026-09-20)** — `src/demangle_stub.c` 已加入 `COMMON_SRCS`,
|
||||
> `LDFLAGS` 已加 `-Wl,--icf=all`。
|
||||
>
|
||||
> 产物:**870,488 B** · sha256 `24392aff6ddcca4dc0ea969cce356bd693ac52efe8a117d61ee1c814aa43cd07` · e_machine `0x003e`
|
||||
>
|
||||
> 落地后复核:`__cxa_demangle` 本体 11 B、`itanium_demangle` 符号 0、
|
||||
> `__cxa_throw` / `_Unwind_Resume` 等异常符号齐全、`sevenz_extract` / `rar_extract` /
|
||||
> `zipx_extract` / `MHD_start_daemon_va` 全部存在。
|
||||
> section 变化:`.text` 637,616 → 538,336、`.rela.dyn` 54,816 → 25,800、
|
||||
> `.eh_frame` 58,904 → 43,972、`.rodata` 162,016 → 153,664。
|
||||
>
|
||||
> 4.3(RELR)与 4.4 未启用 —— 待真机验证 / 权衡后再决定。
|
||||
|
||||
**能瘦,而且有一个"白捡"的 15.8%。**
|
||||
|
||||
| 方案 | 结果 | 降幅 | 风险 |
|
||||
|---|---:|---:|---|
|
||||
| 瘦身前 | 1,034,328 B | — | — |
|
||||
| **+ `__cxa_demangle` 桩** | **886,872 B** | **−147 KB** | 极低 |
|
||||
| **+ ICF 代码折叠(当前产物)** | **870,488 B** | **−164 KB (−15.8%)** | 极低 |
|
||||
| 再 + RELR 重定位压缩 | 854,176 B | −180 KB (−17.4%) | 需真机验证 |
|
||||
| 第三方改 `-Oz`(单列) | 969,504 B | −65 KB | 可能降速 |
|
||||
|
||||
前两项**不损失任何功能,也不影响解压速度** —— 去掉的是一段永远不会被执行的代码。
|
||||
|
||||
---
|
||||
|
||||
## 一、现状构成
|
||||
|
||||
数据源:`size -A` / `nm --size-sort --print-size` / 未 strip 重链接。
|
||||
|
||||
### section 级
|
||||
|
||||
| section | 大小 | 备注 |
|
||||
|---|---:|---|
|
||||
| `.text` | 637,616 | 代码主体 |
|
||||
| `.rodata` | 162,016 | 前端资源(gzip)+ 字符串常量 |
|
||||
| `.eh_frame` + `.eh_frame_hdr` | 71,444 | **C++ 异常展开表** |
|
||||
| `.gcc_except_table` | 8,500 | **C++ 异常处理器表** |
|
||||
| `.rela.dyn` | 54,816 | 2,119 × `R_X86_64_RELATIVE` + 165 × `GLOB_DAT` |
|
||||
| `.data.rel.ro` | 20,192 | 含指针的只读数据 |
|
||||
| `.dynsym` + `.dynstr` + `.gnu.hash` | 23,863 | 动态符号表(PIE 必需) |
|
||||
| `.text$LZMADECOPT` | 4,719 | LZMA 汇编解码器(提速 1.26×,保留) |
|
||||
| `.bss` | 56,416 | **不占文件体积** |
|
||||
|
||||
### 模块级(按目标文件归属,text+data)
|
||||
|
||||
| 模块 | 文件数 | text | data |
|
||||
|---|---:|---:|---:|
|
||||
| unrar7(RAR 引擎) | 48 | 314,528 | 658 |
|
||||
| libc++ / libc++abi / PS5 运行时 | — | ~161,900 | 20,738 |
|
||||
| **C++ 异常机制** | — | **151,288** | — |
|
||||
| zlib | 8 | 65,211 | 336 |
|
||||
| 7z SDK + 自研链 | 30 | 85,406 | 56 |
|
||||
| minizip-ng | 8 | 39,382 | 384 |
|
||||
| 自有代码 `src/` | 14 | 60,653 | 472 |
|
||||
| libmicrohttpd | — | 44,647 | — |
|
||||
| 前端资源(gzip 后) | 13 | 40,278 | 104 |
|
||||
|
||||
---
|
||||
|
||||
## 二、最大的单一发现:151 KB 的 C++ 异常机制
|
||||
|
||||
`nm` 统计出 **607 个 `itanium_demangle::*` 符号,合计 105,431 B** —— 这是 libc++abi 的
|
||||
C++ 名字还原器(`__cxa_demangle`),单独一块就占了整个 ELF 的 **10.2%,比 zlib 整个库还大**。
|
||||
|
||||
它是怎么被拉进来的:
|
||||
|
||||
1. `third_party/unrar7/dll.cpp` 用了 `catch (RAR_EXIT)` / `catch (std::bad_alloc&)`
|
||||
(`unpack.cpp` / `model.cpp` 里也有 `throw std::bad_alloc()`)
|
||||
2. 只要 C++ 异常运行时存在,libc++abi 的 `__cxa_throw` 链路就会引用 `__cxa_demangle`
|
||||
(用于打印未捕获异常的类型名)
|
||||
3. 链接器于是把整个 `cxa_demangle.cpp` 拉进来 —— 一个深度内联的模板解析器,
|
||||
展开成 607 个函数
|
||||
|
||||
连带被拖进来的还有 `libunwind`(21,953 B,栈回溯)和异常胶水(23,904 B),
|
||||
以及散落在每个 C++ 目标文件里的 `.eh_frame`(58,904 B)/ `.gcc_except_table`(8,500 B)。
|
||||
|
||||
**但这个 demangler 只在「未捕获异常」的诊断路径上才会被调用。**
|
||||
我们的 unrar7 走 DLL 模式,所有异常都在 `dll.cpp` 内部被 catch 掉,
|
||||
程序逻辑永远走不到那条路径。
|
||||
|
||||
---
|
||||
|
||||
## 三、实测(同一 WSL、同一份源码、同一个链接器)
|
||||
|
||||
| # | 变更 | 结果 | 差值 |
|
||||
|---|---|---:|---:|
|
||||
| E0 | 重新链接(校验基线) | 1,034,328 | 0 |
|
||||
| E1 | `-Wl,--icf=all` | 1,017,944 | −16,384 |
|
||||
| E2 | 注入 `__cxa_demangle` 桩 | 886,872 | **−147,456** |
|
||||
| E3 | 桩 + `--icf=all` | 870,488 | **−163,840** |
|
||||
| E4 | `-Wl,-z,pack-relative-relocs` | 985,248 | −49,080 |
|
||||
| E5 | `-Wl,-z,noseparate-code` | 1,034,328 | 0(无效) |
|
||||
| E7 | 桩 + ICF + RELR | 854,176 | −180,152 |
|
||||
| E8 | 第三方全部改 `-Oz` | 969,504 | −64,824 |
|
||||
|
||||
> E4 单独能省 49 KB,但与 ICF 组合后只剩 16 KB —— ICF 已经合并掉了一批重定位。
|
||||
|
||||
---
|
||||
|
||||
## 四、落地方案
|
||||
|
||||
### 4.1 立即可用:`__cxa_demangle` 桩(−147 KB)
|
||||
|
||||
新增 `src/demangle_stub.c`:
|
||||
|
||||
```c
|
||||
/* 只提供 __cxa_demangle 的桩,让 libc++abi 里 105 KB 的名字还原器
|
||||
* 不被链接进来。该函数只在打印「未捕获异常的类型名」时被调用;
|
||||
* 返回 NULL 时调用方退回打印 mangled 名,不影响任何业务流程。 */
|
||||
#include <stddef.h>
|
||||
|
||||
char *__cxa_demangle(const char *mangled, char *buf, size_t *len, int *status)
|
||||
{
|
||||
(void)mangled; (void)buf; (void)len;
|
||||
if (status) *status = -1;
|
||||
return NULL;
|
||||
}
|
||||
```
|
||||
|
||||
Makefile 里把它加进 `COMMON_SRCS`(PS5 与 Linux 两条链路都受益):
|
||||
|
||||
```make
|
||||
COMMON_SRCS := src/main.c src/websrv.c ... src/sevenz_mt.c src/demangle_stub.c
|
||||
```
|
||||
|
||||
**原理**:链接器解析 `__cxa_demangle` 引用时,命令行上的 `.o` 优先于归档成员,
|
||||
所以 `libc++abi.a` 里的 `cxa_demangle.o` 压根不会被取出。
|
||||
|
||||
**安全性(已实测验证)**:
|
||||
|
||||
| 检查项 | 结果 |
|
||||
|---|---|
|
||||
| `__cxa_demangle` 本体大小 | **11 B**(我们的桩;原 demangler 入口是 1 701 B) |
|
||||
| `itanium_demangle::*` 符号残留 | **0** |
|
||||
| `__cxa_throw` | 存在 |
|
||||
| `__cxa_begin_catch` / `__cxa_end_catch` | 存在 |
|
||||
| `_Unwind_Resume` / `__gxx_personality_v0` | 存在 |
|
||||
|
||||
**唯一的行为变化**:万一真的出现未捕获异常,`std::terminate` 打印的是 mangled 名
|
||||
而不是可读名。解压逻辑、错误码、进度上报、HTTP 服务一概不受影响。
|
||||
|
||||
### 4.2 立即可用:ICF 代码折叠(−16 KB)
|
||||
|
||||
```make
|
||||
LDFLAGS := -Wl,--gc-sections -Wl,--icf=all
|
||||
```
|
||||
|
||||
lld 的 identical code folding,合并字节完全相同的函数。工具链已确认为 LLVM LLD 18。
|
||||
建议只加在 PS5 的 `LDFLAGS`,不动 Linux 链路(GNU ld 的 `--icf` 支持不完整)。
|
||||
|
||||
### 4.3 需真机验证:RELR 重定位压缩(−16 KB)
|
||||
|
||||
```make
|
||||
LDFLAGS += -Wl,-z,pack-relative-relocs
|
||||
```
|
||||
|
||||
把 2,119 条 `R_X86_64_RELATIVE`(24 B/条)压成 RELR 位图格式(8 B/条)。
|
||||
|
||||
**风险**:需要 PS5 的 ELF 加载器认得 `.relr.dyn`。如果加载器只处理 `.rela.dyn`,
|
||||
重定位根本不会执行 —— 表现是启动即崩。**先在一台机器上验证再推广。**
|
||||
|
||||
### 4.4 不建议作为默认项
|
||||
|
||||
| 项 | 收益 | 为什么不默认开 |
|
||||
|---|---:|---|
|
||||
| 第三方改 `-Oz` | −65 KB | 作用于 LZMA / Deflate / RAR 的热循环,解压速度有下降风险。要用先跑 `tests/bench_driver.py` 对比 |
|
||||
| 关掉 PPMd(`-DZ7_PPMD_SUPPORT`) | −10 KB | PPMd 压缩的 7z 就解不开了 —— 违背"功能完整" |
|
||||
| 去掉 zlib `deflate`(只留 inflate) | −18 KB | `mz_strm_zlib_write` 引用了它,需要桩或改库,收益/风险不划算 |
|
||||
|
||||
---
|
||||
|
||||
## 五、还能挖的(未实测,仅估算)
|
||||
|
||||
| 项 | 预估 | 代价 |
|
||||
|---|---:|---|
|
||||
| unrar7 去掉 C++ 异常(`throw`/`catch` 改错误码 + `-fno-exceptions -fno-rtti`) | −100~110 KB | 改 vendored 代码,需回归 163 checks。可回收 `.eh_frame` 59 KB + `.gcc_except_table` 8.5 KB + libunwind 22 KB + 异常胶水 |
|
||||
| LTO(`-flto=thin`) | −30~60 KB | 全量重编,第三方 `.o` 需统一编译选项;有一定概率反而提速 |
|
||||
| 前端资源改 LZMA 压缩(复用已有解码器,替代 gzip) | ~−10 KB | 改 `gen-asset-module.py` + `asset.c` |
|
||||
|
||||
理论极限在 700 KB 上下(−32%),但边际成本递增:4.1 + 4.2 用 20 行代码换 164 KB,
|
||||
而再往下 100 KB 要动 vendored 源码或验证加载器行为。
|
||||
|
||||
---
|
||||
|
||||
## 六、推荐执行顺序
|
||||
|
||||
1. ~~落 4.1 + 4.2 → 构建~~ ✅ **已完成(2026-09-20)**,产物 870,488 B
|
||||
2. 跑 163 checks(`tests/run-tests.sh` + `tests/run-sevenz-tests.sh`)确认无回归
|
||||
3. PS5 真机跑一轮 ZIP / RAR / 7z(含分卷)解压,确认行为不变
|
||||
4. 真机验证 4.3(RELR)后再决定是否加入
|
||||
5. 有需要再评估第五节的三项
|
||||
|
||||
> 本次改动只动链接期(新增一个 TU + 一个 lld 参数),未触碰任何解压逻辑,
|
||||
> 因此 163 checks 的预期是"逐条不变"。
|
||||
|
||||
**结果(2026-09-20)**:163 checks(ZIP 108 + RAR 27 + 7z 28)**0 失败**,
|
||||
`aeshe` 仍是已知的 `-mhe=on` 缺口。README / CHANGELOG / HANDOVER / 论坛帖里的
|
||||
产物指纹已同步为 870,488 B · sha256 `177e90fe…8e84`。
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:v1.9.2 产物一致性验证(2026-09-20)
|
||||
|
||||
v1.9.2 是一次"只改内嵌版本号"的重发(原 `v1.9.1` tag 落后产出发布二进制的提交
|
||||
4 个提交)。为确认这次重发**真的**只动了版本号,在 WSL 里做了下面的验证。
|
||||
|
||||
### A.1 复现性实验(决定性证据)
|
||||
|
||||
当前工作区相对 `HEAD` 只有两处改动:`Makefile` 的 `VERSION_TAG` 与
|
||||
`assets/main.js` 的 `APP_VERSION_FALLBACK`。把这两处用 `sed` 回退成 `v1.9.1`
|
||||
后重新构建:
|
||||
|
||||
| 构建 | sha256 |
|
||||
|---|---|
|
||||
| 已发布的 v1.9.1 ELF | `24392aff6ddcca4dc0ea969cce356bd693ac52efe8a117d61ee1c814aa43cd07` |
|
||||
| 回退后重建的产物 | `24392aff6ddcca4dc0ea969cce356bd693ac52efe8a117d61ee1c814aa43cd07` |
|
||||
|
||||
**逐字节相同。** 再恢复 `v1.9.2` 重构,sha256 也精确回到 `177e90fe…8e84`。
|
||||
|
||||
→ 构建是**确定性**的,因此 v1.9.2 与 v1.9.1 的全部差异就等于那两处版本字面量。
|
||||
复现脚本:`.build/_repro.sh`。
|
||||
|
||||
### A.2 为什么原始字节 diff 有 5.5 万字节 —— 别被吓到
|
||||
|
||||
`cmp` 两个 ELF 会看到 **55,280 字节不同(6.35%)**,但这是链接器字符串池重排的
|
||||
副作用,不是代码变了:
|
||||
|
||||
| section | 差异字节 | 占该 section |
|
||||
|---|---:|---:|
|
||||
| `.rodata` | 53,496 | 34.8% |
|
||||
| `.text` | 1,543 | 0.3% |
|
||||
| `.rela.dyn` | 241 | 0.9% |
|
||||
|
||||
而**每个 section 的尺寸完全相同**(`.text` 538,336 = 538,336),段数也都 17 个。
|
||||
|
||||
机制:`.rodata` 里 7 字节的 `"v1.9.1\0"` 被换成 `"v1.9.2\0"` 后落点变了,其后
|
||||
所有字符串整体平移 7 字节 → 指向它们的 `lea rdi,[rip+disp]` 位移和 `.rela.dyn`
|
||||
重定位加数全部跟着变。
|
||||
|
||||
两条量化证据:
|
||||
|
||||
| 检查 | 结果 |
|
||||
|---|---|
|
||||
| 指令**助记符**序列(`objdump -d --no-show-raw-insn` 只取 mnemonic) | 141,780 条 vs 141,780 条,**完全一致** —— 没有任何指令被增删改 |
|
||||
| `.text` 差异字节的增量分布 | 1,543 个里 **1,506 个恰好是 −7**(正是那个 7 字节平移);`.rela.dyn` 241/241 个 8 字节字段减 7 |
|
||||
| 嵌入的 gzip 资产 | 6 个成员,5 个逐字节相同,唯一不同的是 `main.js`,且差异 = `APP_VERSION_FALLBACK` 那一行 |
|
||||
|
||||
脚本:`.build/_diffmap.py`、`.build/_operandcheck.sh`、`.build/_fieldcheck.py`、
|
||||
`.build/_verify_v192b.py`、`.build/_seccmp.py`。
|
||||
|
||||
### A.3 源码层的约束
|
||||
|
||||
`VERSION_TAG` 在源码里**只出现在字符串上下文**:
|
||||
|
||||
```
|
||||
src/version.c:29 json_escape(&b, VERSION_TAG);
|
||||
src/main.c:127 printf("version: %s\n", VERSION_TAG);
|
||||
src/main.c:147 notify_user("Web File Manager\nVersion: %s\nPort: %u", VERSION_TAG, port);
|
||||
```
|
||||
|
||||
没有任何算术、比较或分支依赖它,因此改版本号在语言层面就不可能改变控制流。
|
||||
|
||||
---
|
||||
|
||||
## 附录 B:瘦身逐符号账目
|
||||
|
||||
在 **同一个 Makefile / 同一个 `VERSION_TAG`(v1.9.2)** 下重建三个变体,差异只落在
|
||||
"有没有桩"和"有没有 ICF"这两处,因此是干净的 A/B/C 对照。
|
||||
|
||||
| 变体 | 内容 | stripped | unstripped |
|
||||
|---|---|---:|---:|
|
||||
| `base` | 无桩、无 ICF(瘦身前) | **1,034,328** | 1,222,752 |
|
||||
| `nicf` | 有桩、无 ICF | **886,872** | 1,010,208 |
|
||||
| `new` | 有桩 + `--icf=all`(发布态) | **870,488** | 993,824 |
|
||||
|
||||
拆分:桩贡献 **−147,456 B**,ICF 再贡献 **−16,384 B**,合计 **−163,840 B(−15.8%)**。
|
||||
|
||||
### B.1 section 位移(base → new)
|
||||
|
||||
| section | base | new | 差值 |
|
||||
|---|---:|---:|---:|
|
||||
| `.text` | 637,616 | 538,336 | −99,280 |
|
||||
| `.rela.dyn` | 54,816 | 25,800 | −29,016 |
|
||||
| `.eh_frame` | 58,904 | 43,972 | −14,932 |
|
||||
| `.data.rel.ro` | 20,192 | 9,328 | −10,864 |
|
||||
| `.rodata` | 162,016 | 153,664 | −8,352 |
|
||||
| `.eh_frame_hdr` | 12,540 | 9,108 | −3,432 |
|
||||
| `.gcc_except_table` | 8,500 | 7,364 | −1,136 |
|
||||
| `.dynsym` / `.dynstr` / `.got` | — | — | −24 / −9 / −8 |
|
||||
|
||||
### B.2 符号集合差
|
||||
|
||||
| 项 | 数量 |
|
||||
|---|---:|
|
||||
| base 定义符号 | 2,657 |
|
||||
| new 定义符号 | 2,029 |
|
||||
| base → new **消失** | **628** |
|
||||
| base → new **新增** | **0** |
|
||||
|
||||
628 个消失符号的构成:
|
||||
|
||||
- `itanium_demangle::*` —— **607**
|
||||
- `GCC_except_table*` —— **21**(上面那批代码自己的异常表标签,不是独立函数)
|
||||
|
||||
### B.3 桩本体与 demangler 符号
|
||||
|
||||
| 变体 | `__cxa_demangle` 符号大小 | `itanium_demangle` 符号数 |
|
||||
|---|---:|---:|
|
||||
| base | 1,701 B(真身) | 607 |
|
||||
| nicf | **11 B**(我们的桩) | **0** |
|
||||
| new | **11 B** | **0** |
|
||||
|
||||
异常机制在所有三个变体里都完好:`__cxa_throw` / `__cxa_begin_catch` /
|
||||
`__cxa_end_catch` / `_Unwind_Resume` / `__gxx_personality_v0` /
|
||||
`__cxa_allocate_exception` / `__cxa_free_exception` 各 1 个,无变化。
|
||||
|
||||
自有 `src/` 关键符号(`ctx_fail` / `rarx_fail` / `szx_fail` / `fnv1a` /
|
||||
`nameset_init` / `remove_tree` / `ensure_parent_dirs` / `zipx_volume_detect` /
|
||||
`sevenz_extract` / `rar_extract` / `filemgr_api_request` 等)base 与 new 数量一致。
|
||||
|
||||
### B.4 ICF 折叠了什么
|
||||
|
||||
**符号数 2,029 → 2,029,一个没少** —— ICF 是"合并"不是"删除"。共 **76 个折叠组**,
|
||||
全部含具名符号。典型几类:
|
||||
|
||||
- 我们自己的同码副本:`ctx_fail == rarx_fail == szx_fail`、
|
||||
`nameset_init == szx_nameset_init`、`fnv1a == rarx_fnv1a == szx_fnv1a`、
|
||||
`remove_tree == szx_remove_tree`
|
||||
- C++ 的 `C1 == C2` / `D1 == D2` 构造析构对(编译器为同一函数生成两个 ABI 入口):
|
||||
`_ZN10CmdExtractC1EP11CommandData == ...C2...`、`_ZN4FileD1Ev == _ZN4FileD2Ev` 等
|
||||
- 只读常量表:`Sbox == _ZL1S`(AES 表在 `rijndael.cpp` 与 `Aes.c` 各一份)、
|
||||
`SHA256_K_ARRAY == _ZL1K`、`PPMD7_kExpEscape == _ZL9ExpEscape`
|
||||
- RARDLL 模式下被置空的 UI 函数、`mz_stream_read_int64 == read_uint64`、
|
||||
libunwind 的 `__unw_* == unw_*`、`__unw_resume == unw_resume`
|
||||
|
||||
**风险提示**:`--icf=all` 是 LLD 的激进模式,**不做地址敏感性检查**
|
||||
(`--icf=safe` 才会读 `.llvm_addrsig` 跳过被取地址的函数)。逐组核对下来这 76 组
|
||||
都是同码副本、没有"比较函数/常量表地址"的用法 —— 但这是人工判断,不是编译器给的
|
||||
保证。想绝对保守就把 `--icf=all` 换成 `--icf=safe`,代价是少省几 KB。
|
||||
|
||||
复现脚本:`.build/_whatremoved_v192.sh`(一次跑完三个变体 + 全部核对)。
|
||||
@@ -416,10 +416,10 @@ self-evident from the failure.
|
||||
|
||||
### 7.4 The `large=1` prompt for RAR
|
||||
|
||||
The threshold is shared. A `.rar` larger than `60 GiB` triggers the
|
||||
The threshold is shared. A `.rar` larger than `240 GiB` triggers the
|
||||
same `promptLargeMode()` confirmation as a `.zip`. The confirmation
|
||||
text uses `extractLargeAsk` (unchanged from v1.7) — the wording is
|
||||
format-agnostic, so no new strings are needed.
|
||||
text uses `extractLargeAsk` (slightly relaxed in v1.8.1) — the wording
|
||||
is format-agnostic, so no new strings are needed.
|
||||
|
||||
---
|
||||
|
||||
@@ -441,7 +441,7 @@ host has any RAR tooling.
|
||||
| `test_engine_dispatch_null_dst` | `rar_extract(path, NULL, …)` is rejected with `ZIPX_ERR_INTERNAL`. |
|
||||
| `test_engine_dispatch_dst_is_regular_file` | `rar_extract(path, /some/file, …)` returns `ZIPX_ERR_CONFLICT` (open_parent_dirs fails). |
|
||||
| `test_format_translation` | Parametric: for each `DMC_UNRAR_*` code we care about, the corresponding `rar_translate_error()` mapping is exercised indirectly (via `result->message` strings). |
|
||||
| `test_limits_handoff_default` | When `task->extract_large == 0`, the default profile is handed in (200 K entries / 512 GiB / 64 GiB / 200:1). |
|
||||
| `test_limits_handoff_default` | When `task->extract_large == 0`, the default profile is handed in (200 K entries / 1 TiB / 256 GiB / 500:1). |
|
||||
| `test_limits_handoff_large` | When `task->extract_large == 1`, the large profile is handed in (500 K / 2 TiB / 1 TiB / 1000:1). |
|
||||
| `test_translate_open_fail_to_err_open` | DMC open-failure → `ZIPX_ERR_OPEN`. |
|
||||
| `test_translate_volume_unsp_to_err_unsupported` | The DMC volume code → `ZIPX_ERR_UNSUPPORTED`. |
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
# 上游 v1.8 解压方案 vs 本项目 v1.9.1
|
||||
|
||||
> 核查时间:2026-09-15 · 上游 `owendswang/ps5-web-file-manager`
|
||||
> 来源:GitHub API 查询 + commit `b405721`("Added support for 7zip helper",2026-09-08)完整 patch(2301 行)
|
||||
> 上游 v1.8 = tag `ad7d754`,v1.7 = `72341d6`(本项目 fork 的基线)
|
||||
|
||||
## 结论速览
|
||||
|
||||
**不是同一个层面的方案,各有明确胜负手。**
|
||||
|
||||
| | 上游 v1.8 | 本项目 v1.9.1 |
|
||||
|---|---|---|
|
||||
| 一句话 | **把 7-Zip 本体做成外部 helper 进程,靠 IPC 调用** | **自研 + vendor 解码库,全部内嵌在同一进程** |
|
||||
| 最强的点 | 格式覆盖 **30 种**,解压核心是 7-Zip 本体 | **完整安全护栏** + 单文件部署 |
|
||||
| 最弱的点 | **零安全护栏**,且 helper 缺失 = 功能全废 | 格式覆盖只有 **3 种** |
|
||||
|
||||
## 一、上游 v1.8 的实际架构
|
||||
|
||||
### 1.1 三个组件
|
||||
|
||||
| 组件 | 位置 | 职责 |
|
||||
|---|---|---|
|
||||
| `src/archive_extract.c`(124 行) | 本仓库 | 只做**后缀识别** + 输出目录名推导 |
|
||||
| `src/archive_helper.c`(732 行) | 本仓库 | **IPC 客户端**:启动 helper + Unix socket 协议 |
|
||||
| `wfm-7zip-helper.elf`(~百 MB 级) | `/data/wfm/`,**不在仓库里,单独分发** | 真正的解压 = **7-Zip 本体** |
|
||||
|
||||
README 原文:
|
||||
|
||||
> Extraction requires the separately distributed `wfm-7zip-helper.elf` helper at `/data/wfm/wfm-7zip-helper.elf`.
|
||||
|
||||
### 1.2 启动链路
|
||||
|
||||
```c
|
||||
/* archive_helper_autostart() —— 仅 __SCE__(PS5)分支,Linux 直接返回 0 */
|
||||
1. archive_helper_probe() // 已有实例在跑就复用,绝不替换
|
||||
2. stat("/data/wfm/wfm-7zip-helper.elf") // 不存在 → 静默返回 0
|
||||
3. 校验 ELF magic "\x7fELF"、大小 4B ~ 128MB
|
||||
4. connect(127.0.0.1:9021) // WFM_ELFLDR_PORT —— elfldr payload 加载器
|
||||
5. 把 helper ELF 的**全部字节流**推过去
|
||||
6. shutdown(SHUT_WR)
|
||||
```
|
||||
|
||||
即:**通过 elfldr(PS5 homebrew 的 ELF 加载 payload)把 helper 拉起成一个独立进程。**
|
||||
|
||||
### 1.3 通信协议(自研二进制帧)
|
||||
|
||||
- 传输层:Unix domain socket —— PS5 走 `/system_tmp/wfm-7zip-helper.sock`,Linux 走 `/tmp/...`
|
||||
- 帧格式:magic `"W7HP"` + 20 字节头(type / flags / request_id / payload_size,**大端序**)
|
||||
- 上限:payload 1 MiB、路径 256 KiB、响应 64 KiB
|
||||
|
||||
消息类型:
|
||||
|
||||
| 方向 | 消息 |
|
||||
|---|---|
|
||||
| 主 → helper | `PING` `EXTRACT` `CANCEL` `LIST_TASKS` `ATTACH_TASK` `ACK_TASK` |
|
||||
| helper → 主 | `PONG` `ACCEPTED` `PROGRESS` `CURRENT_FILE` `PASSWORD_REQUIRED` `DONE` `ERROR` `TASK_SNAPSHOT` `LIST_DONE` |
|
||||
|
||||
回调接口 `archive_helper_callbacks_t`:`cancel_requested()` / `progress(done,total)` / `current_file(path)`。
|
||||
|
||||
### 1.4 支持格式(30 种后缀)
|
||||
|
||||
```
|
||||
.7z .001 .zip .zipx .rar .arj .bz2 .bzip2 .tbz .tbz2 .cab .gz .gzip
|
||||
.tgz .tpz .lzh .lha .tar .xz .txz .z .taz .zst .tzst .xar .xip
|
||||
.cpio .lzma .pmd
|
||||
```
|
||||
|
||||
外加 `.partNN.rar`(只接受 `part1`,即必须从第一卷进入)。
|
||||
分卷靠 7-Zip 原生能力:`.001` **无差别接受**(不校验卷集连续性)。
|
||||
|
||||
### 1.5 任务恢复(上游的亮点)
|
||||
|
||||
`filemgr_recover_extract_tasks()` 在 `main.c` 启动时调用:从 helper 拉 `TASK_SNAPSHOT` 列表,把还在跑的 job **reattach 回主进程的任务列表**。
|
||||
|
||||
因为 helper 是独立进程,**主 payload 被重启 / 浏览器重开,解压任务不会丢**。`archive_helper_probe()` 的注释也点明了这个设计的意图:
|
||||
|
||||
```c
|
||||
/* Never replace a connected daemon, even if it is temporarily slow. */
|
||||
```
|
||||
|
||||
### 1.6 ⚠️ 没有的东西(全 patch 逐行核查)
|
||||
|
||||
| 项目 | 上游 v1.8 | 说明 |
|
||||
|---|---|---|
|
||||
| 条目数上限 | ❌ | 无 `max_entries` 类逻辑 |
|
||||
| 单文件/总大小上限 | ❌ | 无 |
|
||||
| 压缩比筛查(防炸弹) | ❌ | 无 |
|
||||
| 磁盘空间预检 | ❌ | 无 `statvfs` 调用 |
|
||||
| 路径穿越防护 | ❌ | 未见 `..`/绝对路径校验,交给 7-Zip |
|
||||
| 原子发布 | ❌(未见) | 直接解到目标目录,中断留半成品 |
|
||||
|
||||
`grep -i "ratio|max_entries|statvfs|bomb"` 的全部命中都是误报(`operations` 里含子串 `ratio`)。
|
||||
|
||||
**换句话说:上游把解压这件事整体外包给了 7-Zip,包括安全责任。**
|
||||
|
||||
## 二、本项目 v1.9.1 的架构
|
||||
|
||||
| 组件 | 职责 |
|
||||
|---|---|
|
||||
| `src/zip_extract.c` | ZIP:minizip-ng,含 zip64、三种分卷命名、`.z01` 真分盘语义 |
|
||||
| `src/rar_extract.c` | RAR:vendor unrar 7.20.1(DLL 模式),v4/v5/多卷/加密 |
|
||||
| `src/sevenz_extract.c` + `sevenz_chain.c` | 7z:自解析 folder + pull 式 codec 链 + 7zAES |
|
||||
| `src/zipx_volume.c` / `zipx_volstream.c` / `sevenz_volstream.c` | 卷集识别 + 连续流抽象 |
|
||||
|
||||
**格式覆盖:`.zip` / `.rar` / `.7z` 三种**,各自支持单卷 / 分卷 / 密码。
|
||||
|
||||
### 已有的工程能力
|
||||
|
||||
| 项目 | 本项目 | 实现位置 |
|
||||
|---|---|---|
|
||||
| 条目数 / 总大小 / 单文件上限 | ✅ 两档 profile(20万~50万条目 / 2~4 TiB / 512 GiB~1 TiB) | `zipx_common.c` |
|
||||
| 压缩比筛查 | ✅ `max_ratio` 500/1000,**1 GiB 下限豁免**小文件 | `zip_extract.c` |
|
||||
| 磁盘空间预检 | ✅ `check_space()` 按**解压后总量**查 `statvfs` | `zip_extract.c:636` |
|
||||
| 路径穿越防护 | ✅ 有专项测试(`path traversal variants`) | 测试矩阵 |
|
||||
| 原子发布 | ✅ staging 目录 + 整 rename + 每 entry fsync | 三引擎统一 |
|
||||
| 冲突策略 | ✅ FAIL / OVERWRITE / MERGE,目录碰撞递归下钻 | 三引擎统一 |
|
||||
| 取消 | ✅ 条目粒度 | — |
|
||||
| 任务恢复 | ❌ **没有** | — |
|
||||
| 内存隔离 | ❌ 与主进程共享地址空间(LZMA2 字典须封顶) | — |
|
||||
|
||||
### 测试覆盖
|
||||
|
||||
ZIP 108 + RAR 27 + 7z 28 = **163 checks**,0 失败(MinGW host)+ PS5 真机构建通过。
|
||||
|
||||
## 三、逐维度对比
|
||||
|
||||
| 维度 | 上游 v1.8 | 本项目 v1.9.1 | 胜 |
|
||||
|---|---|---|---|
|
||||
| 格式覆盖 | **30 种** | 3 种 | 上游 |
|
||||
| 解压核心正确性 | 7-Zip 本体(20 年验证) | 自研 7z 链 + 成熟 vendor 库 | 上游 |
|
||||
| 分卷语义 | 靠 7-Zip 原生(`.001` 无差别) | 自研两套语义(byte-split / zip split disk) | 平手(我们更细,上游更省心) |
|
||||
| `.rar.001` | ✅ 直接吃 | ⚠️ 需改名为 `.partN.rar` | 上游 |
|
||||
| 部署 | **两个文件**,路径写死 `/data/wfm/` | **单文件**,零外部依赖 | 我们 |
|
||||
| helper 缺失时 | **功能全废**(`archive_helper_not_running`) | 不适用 | 我们 |
|
||||
| 防压缩炸弹 | ❌ 无 | ✅ ratio + 1 GiB 下限 | **我们** |
|
||||
| 磁盘写满保护 | ❌ 无 | ✅ 预检解压后总量 | **我们** |
|
||||
| 路径穿越 | ❌ 无 | ✅ 有防护 + 测试 | **我们** |
|
||||
| 中断留残留 | ⚠️ 可能留半成品 | ✅ staging 隔离,失败即清 | **我们** |
|
||||
| 任务恢复 / 跨重启 | ✅ 跨进程 reattach | ❌ | 上游 |
|
||||
| 内存隔离 | ✅ 独立进程,峰值不影响主服务 | ❌ 共享地址空间 | 上游 |
|
||||
| 主仓库构建成本 | 低(不编 7-Zip) | 首次 +3~5 min、ELF +98 KiB | 上游 |
|
||||
| 错误信息详细度 | 中等(6 个 code) | 含条目名 / errno / 字节数 | 我们 |
|
||||
|
||||
## 四、该怎么评价
|
||||
|
||||
### 上游那步棋走对了什么
|
||||
|
||||
**把 7-Zip 当外部依赖,是性价比极高的工程决策。** 自己写解码器要几个月,`apt` 一个 7-Zip 就换来 30 种格式 + 20 年验证的正确性。而且顺手拿到了两个我们暂时没有的能力:跨进程任务恢复、内存隔离。
|
||||
|
||||
### 但它把安全责任也一起外包了
|
||||
|
||||
这是**实质缺陷**,不是风格问题。在 PS5 上跑的具体后果:
|
||||
|
||||
1. **压缩炸弹直接写满内置存储** —— 一个 10 KB 的 zip 可以声明 100 GB,没有任何拦截
|
||||
2. **路径穿越** —— `../../` 条目可以写到解压目标之外(7-Zip 本身会做基本清理,但这属于"相信第三方"而非"自己保证")
|
||||
3. **磁盘写满** —— 不预检,写到 ENOSPC 才失败,此时已留下部分文件
|
||||
4. **失败留残留** —— 没有 staging 隔离
|
||||
|
||||
我们在这四项上都有明确实现和测试。163 checks 里专门有一组 `path traversal variants` 和 `limits`。
|
||||
|
||||
### 但必须承认格式覆盖是短板
|
||||
|
||||
31 种格式的差距不是"多一点便利",是**用户会觉得我们弱**:`.tar.gz`、`.xz`、`.zst`、`.bz2` 在 PS5 场景(游戏包、备份、Mod)里出现频率不低。
|
||||
|
||||
## 五、可借鉴 / 不建议照抄
|
||||
|
||||
### 建议做:补常见格式(性价比高)
|
||||
|
||||
按实际收益排序:
|
||||
|
||||
| 优先级 | 格式 | 实现路径 |
|
||||
|---|---|---|
|
||||
| 高 | `.tar` / `.tar.gz` / `.tgz` | tar 解析器自己写(格式极简,~300 行)+ zlib 已在手 |
|
||||
| 高 | `.gz` / `.xz` / `.lzma` | gzip 用 zlib;xz/lzma 可 vendor liblzma 或复用 LZMA SDK 的 LzmaDec |
|
||||
| 中 | `.bz2` / `.zst` | 单文件解码器,各 ~1000 行,可 vendor |
|
||||
| 低 | `.cab` / `.arj` / `.lzh` / `.cpio` / `.xar` | 罕见,除非有具体需求 |
|
||||
|
||||
**注意**:`gzip`/`xz`/`zst` 是**单文件**格式(不是归档),解出来就是一个文件,输出路径语义需单独设计。
|
||||
|
||||
### 建议评估:任务恢复 / 进程隔离
|
||||
|
||||
**动机**:主 payload 被系统杀或用户重开浏览器时,正在跑的大包解压会整个丢失。上游靠 helper 独立进程解决了这点。
|
||||
|
||||
**但在我们架构下的成本**:需要引入子进程 + IPC(或至少状态持久化 + 重启后重扫 staging)。PS5 上 fork/exec 与 elfldr 强耦合,不是小改动。
|
||||
|
||||
**中间路线**:解压失败时保留 staging 目录 + 记录任务清单文件,重启后支持"续解"。改动量中等,能拿到大部分收益,不必引入 IPC。
|
||||
|
||||
### 不建议:照抄 helper 路线
|
||||
|
||||
理由:
|
||||
|
||||
1. **部署体验倒退** —— 用户要装两个文件,还得记住放 `/data/wfm/`;丢一个功能全废。现在单 ELF 是无状态交付,这是真实优势
|
||||
2. **安全护栏会一起丢** —— 走 7-Zip 就意味着放弃我们对 entries/ratio/空间/穿越的控制
|
||||
3. **helper 上游自己都不敢放进仓库**("separately distributed"),大概率是体积或许可原因,跟着走会继承同样的问题
|
||||
4. **我们已经付过的成本会沉没** —— 7z 引擎(自解析 folder + pull 链 + 7zAES)+ 三类分卷抽象共约 3,600 行零耦合代码
|
||||
|
||||
### 一句话总结
|
||||
|
||||
**上游赢在"格式广度 + 进程架构",我们赢在"安全 + 部署 + 错误质量"。**
|
||||
|
||||
如果只想要功能广度,上游的路线更省力;如果要一个**能放心交付给用户、不会被一个恶意压缩包搞崩存储**的工具,我们的路线是对的,缺的只是格式覆盖 —— 而那是可以在现有架构里增量补的,不需要推倒重来。
|
||||
|
||||
## 附:核查方法备忘
|
||||
|
||||
```bash
|
||||
# 拿某个 commit 的完整 patch(不要用 WebFetch,会被 AI 摘要截断)
|
||||
curl -sSL --ssl-no-revoke -o up.patch \
|
||||
"https://github.com/<owner>/<repo>/commit/<sha>.patch"
|
||||
|
||||
# 沙箱内 curl 必须加 --ssl-no-revoke,否则 schannel 报
|
||||
# CRYPT_E_NO_REVOCATION_CHECK (0x80092012)
|
||||
|
||||
# 提取单个文件的 diff
|
||||
sed -n '/^diff --git a\/src\/foo.c/,/^diff --git a\/src\/bar/p' up.patch
|
||||
|
||||
# 只看新增行(去掉 diff 前缀)
|
||||
... | grep '^+' | sed 's/^+//'
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
/* PS5-only stub for the libgcc CPU model symbols.
|
||||
*
|
||||
* unrar's rijndael.cpp / system.cpp call __builtin_cpu_supports() to pick
|
||||
* AES-NI fast paths. Clang lowers that to a reference on __cpu_model (data)
|
||||
* and __cpu_indicator_init() (function), which the FreeBSD-style PS5
|
||||
* sysroot does not provide (no libgcc). This TU supplies both so the link
|
||||
* succeeds; the detection result is unused because we always build the
|
||||
* portable C path.
|
||||
*
|
||||
* Do NOT add this file to host/linux builds: libstdc++/libgcc already
|
||||
* define __cpu_model there and the symbols would collide. */
|
||||
#if defined(__x86_64__) && !defined(__linux__) && !defined(_WIN32)
|
||||
|
||||
struct __cpu_model {
|
||||
int __cpu_vendor;
|
||||
int __cpu_type;
|
||||
int __cpu_subtype;
|
||||
};
|
||||
|
||||
int __cpu_indicator_init(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct __cpu_model __cpu_model = { 0, 0, 0 };
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
/* Stub for the C++ name demangler (__cxa_demangle).
|
||||
*
|
||||
* unrar7 is compiled with C++ exceptions enabled -- dll.cpp catches
|
||||
* RAR_EXIT and std::bad_alloc, unpack.cpp/model.cpp throw bad_alloc -- so
|
||||
* the runtime's __cxa_throw chain holds a reference to __cxa_demangle. That
|
||||
* one reference drags the whole Itanium demangler TU into the link: 607
|
||||
* symbols, ~105 KiB, over 10% of the final ELF (see docs/SIZE-OPTIMIZATION.md).
|
||||
*
|
||||
* __cxa_demangle is only ever reached on the uncaught-exception diagnostic
|
||||
* path (std::terminate printing the exception's type name). Every unrar
|
||||
* exception is caught inside dll.cpp, so that path is unreachable here.
|
||||
* Defining the symbol in our own TU keeps cxa_demangle.o out of the archive
|
||||
* pull -- the linker resolves against ours and never opens the member.
|
||||
*
|
||||
* Returning NULL is the documented "demangle failed" result; the caller
|
||||
* falls back to printing the mangled name. Exception handling itself
|
||||
* (__cxa_throw / __cxa_begin_catch / _Unwind_Resume / __gxx_personality_v0)
|
||||
* is untouched. Applies to both the PS5 and the host/linux builds. */
|
||||
#include <stddef.h>
|
||||
|
||||
char *__cxa_demangle(const char *mangled, char *buf, size_t *len, int *status) {
|
||||
(void)mangled;
|
||||
(void)buf;
|
||||
(void)len;
|
||||
|
||||
if(status) {
|
||||
*status = -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
+182
-35
@@ -1,5 +1,6 @@
|
||||
#include "filemgr.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
@@ -14,7 +15,9 @@
|
||||
#include "json_util.h"
|
||||
#include "path_util.h"
|
||||
#include "rar_extract.h"
|
||||
#include "sevenz_extract.h"
|
||||
#include "zip_extract.h"
|
||||
#include "zipx_volume.h"
|
||||
|
||||
/* Cancellation callback: stop when the task is asked to cancel. */
|
||||
static int
|
||||
@@ -45,34 +48,10 @@ ends_with_ci(const char *path, const char *suffix) {
|
||||
return !strcasecmp(path + path_len - suf_len, suffix);
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
if(ends_with_ci(task->src, ".zip")) {
|
||||
return zipx_extract(task->src, task->dst, conflict, limits,
|
||||
extract_cancel, extract_progress, task, result);
|
||||
}
|
||||
if(ends_with_ci(task->src, ".rar")) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/* Progress callback. The engine already throttles reports (200 ms / 1 MiB),
|
||||
so we can forward each report straight into the shared task state. */
|
||||
so we can forward each report straight into the shared task state.
|
||||
Defined before extract_dispatch() so the dispatcher's call site compiles
|
||||
cleanly under -Werror=implicit-function-declaration. */
|
||||
static void
|
||||
extract_progress(void *userdata, const zipx_progress_t *p) {
|
||||
file_task_t *task = userdata;
|
||||
@@ -91,6 +70,157 @@ 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) {
|
||||
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),
|
||||
"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);
|
||||
}
|
||||
if(ends_with_ci(task->src, ".rar")) {
|
||||
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, .rar and .7z are accepted)");
|
||||
return ZIPX_ERR_UNSUPPORTED;
|
||||
}
|
||||
|
||||
static const char *
|
||||
extract_error_code(zipx_status_t status) {
|
||||
switch(status) {
|
||||
@@ -110,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";
|
||||
}
|
||||
}
|
||||
@@ -168,7 +299,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;
|
||||
@@ -196,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");
|
||||
@@ -204,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) {
|
||||
@@ -213,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");
|
||||
}
|
||||
@@ -239,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);
|
||||
@@ -249,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++;
|
||||
@@ -263,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");
|
||||
}
|
||||
@@ -2399,6 +2399,7 @@ filemgr_api_request(struct MHD_Connection *conn, const char *url,
|
||||
if(!strcmp(url, "/api/list")) return api_list(conn);
|
||||
if(!strcmp(url, "/api/tasks")) return api_tasks(conn);
|
||||
if(!strcmp(url, "/api/space")) return api_space(conn);
|
||||
if(!strcmp(url, "/api/version")) return api_version(conn);
|
||||
if(!strcmp(url, "/api/cancel")) return api_cancel(conn);
|
||||
if(!strcmp(url, "/api/exit")) return api_exit(conn);
|
||||
if(!strcmp(url, "/api/copy")) return api_copy(conn, body, body_size);
|
||||
|
||||
@@ -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];
|
||||
@@ -128,6 +133,7 @@ enum MHD_Result api_download_prepare(struct MHD_Connection *conn,
|
||||
enum MHD_Result api_download(struct MHD_Connection *conn);
|
||||
enum MHD_Result api_list(struct MHD_Connection *conn);
|
||||
enum MHD_Result api_space(struct MHD_Connection *conn);
|
||||
enum MHD_Result api_version(struct MHD_Connection *conn);
|
||||
enum MHD_Result api_text(struct MHD_Connection *conn);
|
||||
enum MHD_Result api_text_create(struct MHD_Connection *conn);
|
||||
enum MHD_Result api_text_save(struct MHD_Connection *conn, const char *body,
|
||||
|
||||
+213
-355
@@ -1,18 +1,26 @@
|
||||
/* Safe RAR extraction engine used by the /api/extract task.
|
||||
Wraps the vendored dmc_unrar library (https://github.com/DrMcCoy/dmc_unrar).
|
||||
Wraps the vendored rarlab UnRAR 7.x library (third_party/unrar7) through
|
||||
its C-compatible DLL API (dll.hpp / unrar_c_api.h facade).
|
||||
|
||||
The publish / staging / rollback / normalize-name / dedup machinery is
|
||||
mirrored from zip_extract.c so that any consumer of zipx_result_t gets a
|
||||
consistent error and progress contract regardless of archive format.
|
||||
|
||||
See third_party/unrar/VENDORED.md for what dmc_unrar does and does not
|
||||
support (single-volume RAR only; multi-volume and encrypted are
|
||||
rejected up front). */
|
||||
Backend notes (v1.9, unrar 7.20.1):
|
||||
* RAR4 and RAR5 (any compression version, incl. WinRAR 6/7 "v6")
|
||||
single-volume archives.
|
||||
* Multi-volume archives: unrar auto-merges subsequent volumes by name
|
||||
pattern when all .partNN.rar files sit next to the opened volume.
|
||||
* Encrypted RAR: the engine can decrypt via RARSetPassword, but the
|
||||
password plumbing (API + UI) is not wired yet — encrypted archives
|
||||
currently fail with ZIPX_ERR_UNSUPPORTED.
|
||||
|
||||
See third_party/unrar7/VENDORED.md for the full integration notes. */
|
||||
|
||||
#include "rar_extract.h"
|
||||
#include "zip_extract.h" /* for the shared status / progress / limits API */
|
||||
|
||||
#include "dmc_unrar_api.h" /* facade — links against dmc_unrar.o at build time */
|
||||
#include "unrar_c_api.h" /* facade — links against the unrar7 static lib at build time */
|
||||
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
@@ -28,11 +36,11 @@
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* (intentionally no `#include "dmc_unrar.c"` here — that would pull the
|
||||
library into this translation unit, where the host test build's
|
||||
tests/posix_compat.h renames `open` / `close` to `wfm_open` / `wfm_close`
|
||||
and breaks dmc_unrar's `dmc_unrar_io_handler` struct member access.
|
||||
dmc_unrar.c is compiled as its own translation unit and joined at link.) */
|
||||
/* (intentionally no `#include "rar.hpp"` etc. here — those are C++ headers.
|
||||
rar_extract.c talks to unrar exclusively through the extern "C" DLL API in
|
||||
dll.hpp, and the unrar sources are compiled as their own translation units
|
||||
(RARDLL mode) and joined at link time. This keeps the host test build's
|
||||
tests/posix_compat.h renames (open->wfm_open) from leaking into unrar.) */
|
||||
|
||||
#ifndef O_CLOEXEC
|
||||
#define O_CLOEXEC 0
|
||||
@@ -288,7 +296,7 @@ nameset_put(rarx_nameset_t *set, uint64_t h, int is_dir) {
|
||||
/**************************************************************************
|
||||
* entry name validation (mirrors zip_extract.c::normalize_name but the
|
||||
* RAR side does not have a trailing separator, so dir entries are only
|
||||
* detected via dmc_unrar_file_is_directory()).
|
||||
* detected via the RHDF_DIRECTORY header flag).
|
||||
**************************************************************************/
|
||||
|
||||
static int
|
||||
@@ -300,7 +308,11 @@ normalize_name(const char *in, char *out, size_t out_size, int *is_dir,
|
||||
size_t seg_len = 0;
|
||||
uint32_t levels = 0;
|
||||
|
||||
*is_dir = 0;
|
||||
/* NOTE: is_dir is owned by the caller — unrar announces directories via
|
||||
the RHDF_DIRECTORY header flag, not via a trailing separator, so we
|
||||
must NOT clear it here (a stray `*is_dir = 0` previously turned every
|
||||
directory entry into a file and tripped the duplicate detector when an
|
||||
explicit directory header followed files beneath it). */
|
||||
*depth = 0;
|
||||
if(!in_len) {
|
||||
return rarx_fail(c, ZIPX_ERR_UNSAFE_NAME, in, "empty entry name");
|
||||
@@ -422,77 +434,46 @@ nameset_check_duplicate(rarx_nameset_t *set, const char *name, int is_dir,
|
||||
}
|
||||
|
||||
/**************************************************************************
|
||||
* dmc_unrar -> zipx error translation
|
||||
* unrar DLL error -> zipx error translation
|
||||
**************************************************************************/
|
||||
|
||||
static int
|
||||
rar_translate_error(dmc_unrar_return code, const char *detail,
|
||||
rarx_ctx_t *c) {
|
||||
rar_translate_error(int code, const char *detail, rarx_ctx_t *c) {
|
||||
switch(code) {
|
||||
case DMC_UNRAR_OK:
|
||||
case ERAR_SUCCESS:
|
||||
return ZIPX_OK;
|
||||
|
||||
/* Archive-open level (the user will see this when they upload a
|
||||
multi-volume or fully-encrypted RAR — let them know it is on purpose). */
|
||||
case DMC_UNRAR_ARCHIVE_UNSUPPORTED_VOLUMES:
|
||||
return rarx_fail(c, ZIPX_ERR_UNSUPPORTED, detail,
|
||||
"multi-volume RAR archives are not supported; "
|
||||
"please extract on a PC first");
|
||||
/* The unrar engine handles multi-volume automatically (it merges the
|
||||
next .partNN.rar by name); a missing next volume surfaces as EOPEN. */
|
||||
case ERAR_EOPEN:
|
||||
return rarx_fail(c, ZIPX_ERR_OPEN, detail, "%s", strerror(errno));
|
||||
|
||||
case DMC_UNRAR_ARCHIVE_UNSUPPORTED_ENCRYPTED:
|
||||
return rarx_fail(c, ZIPX_ERR_UNSUPPORTED, detail,
|
||||
"encrypted RAR archives are not supported; "
|
||||
"please extract on a PC first");
|
||||
case ERAR_ECREATE:
|
||||
case ERAR_ECLOSE:
|
||||
case ERAR_EREAD:
|
||||
case ERAR_EWRITE:
|
||||
return rarx_fail(c, ZIPX_ERR_IO, detail, "%s", strerror(errno));
|
||||
|
||||
/* Per-file (still surfaced up-front during scan). */
|
||||
case DMC_UNRAR_FILE_UNSUPPORTED_ENCRYPTED:
|
||||
case ERAR_MISSING_PASSWORD:
|
||||
case ERAR_BAD_PASSWORD:
|
||||
/* Password plumbing (API + UI) is not wired yet. */
|
||||
return rarx_fail(c, ZIPX_ERR_UNSUPPORTED, detail,
|
||||
"encrypted RAR entries are not supported");
|
||||
|
||||
case DMC_UNRAR_FILE_UNSUPPORTED_SPLIT:
|
||||
return rarx_fail(c, ZIPX_ERR_UNSUPPORTED, detail,
|
||||
"split RAR entries are not supported");
|
||||
case ERAR_SMALL_BUF:
|
||||
return rarx_fail(c, ZIPX_ERR_LIMIT_NAME, detail, "name buffer is too small");
|
||||
|
||||
case DMC_UNRAR_FILE_UNSUPPORTED_LINK:
|
||||
return rarx_fail(c, ZIPX_ERR_SPECIAL, detail, "symbolic link entry");
|
||||
|
||||
case DMC_UNRAR_FILE_UNSUPPORTED_VERSION:
|
||||
case DMC_UNRAR_FILE_UNSUPPORTED_METHOD:
|
||||
return rarx_fail(c, ZIPX_ERR_UNSUPPORTED, detail,
|
||||
"RAR entry uses an unsupported compression method");
|
||||
|
||||
case DMC_UNRAR_FILE_UNSUPPORTED_LARGE:
|
||||
case ERAR_LARGE_DICT:
|
||||
return rarx_fail(c, ZIPX_ERR_LIMIT_FILE, detail,
|
||||
"RAR entry is larger than the supported maximum");
|
||||
"archive needs a larger dictionary than supported");
|
||||
|
||||
case DMC_UNRAR_ARCHIVE_EMPTY:
|
||||
return rarx_fail(c, ZIPX_ERR_FORMAT, detail, "empty RAR archive");
|
||||
case ERAR_BAD_DATA:
|
||||
return rarx_fail(c, ZIPX_ERR_CRC, detail, "checksum mismatch in entry data");
|
||||
|
||||
case DMC_UNRAR_ARCHIVE_UNSUPPORTED_ANCIENT:
|
||||
return rarx_fail(c, ZIPX_ERR_UNSUPPORTED, detail,
|
||||
"RAR 1.4 / 1.5 archives are not supported");
|
||||
|
||||
case DMC_UNRAR_ARCHIVE_NOT_RAR:
|
||||
return rarx_fail(c, ZIPX_ERR_FORMAT, detail,
|
||||
"not a RAR archive");
|
||||
|
||||
case DMC_UNRAR_OPEN_FAIL:
|
||||
return rarx_fail(c, ZIPX_ERR_OPEN, detail, "%s", strerror(errno));
|
||||
|
||||
case DMC_UNRAR_READ_FAIL:
|
||||
case DMC_UNRAR_WRITE_FAIL:
|
||||
case DMC_UNRAR_SEEK_FAIL:
|
||||
return rarx_fail(c, ZIPX_ERR_IO, detail, "%s", strerror(errno));
|
||||
|
||||
case DMC_UNRAR_FILE_CRC32_FAIL:
|
||||
return rarx_fail(c, ZIPX_ERR_CRC, detail, "CRC32 mismatch");
|
||||
|
||||
case DMC_UNRAR_INVALID_DATA:
|
||||
case DMC_UNRAR_NO_ALLOC:
|
||||
case DMC_UNRAR_ALLOC_FAIL:
|
||||
case DMC_UNRAR_ARCHIVE_IS_NULL:
|
||||
case DMC_UNRAR_ARCHIVE_NOT_CLEARED:
|
||||
case DMC_UNRAR_ARCHIVE_MISSING_FIELDS:
|
||||
case ERAR_BAD_ARCHIVE:
|
||||
case ERAR_UNKNOWN_FORMAT:
|
||||
case ERAR_UNKNOWN:
|
||||
case ERAR_NO_MEMORY:
|
||||
default:
|
||||
return rarx_fail(c, ZIPX_ERR_FORMAT, detail,
|
||||
"invalid or corrupt RAR archive");
|
||||
@@ -532,15 +513,6 @@ path_parent(const char *path, char *out, size_t out_size) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
chmod_0777_fd(int fd) {
|
||||
int err = errno;
|
||||
|
||||
if(fchmod(fd, 0777)) {
|
||||
errno = err;
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
make_staging(rarx_ctx_t *c, const char *parent) {
|
||||
unsigned int attempt;
|
||||
@@ -617,57 +589,6 @@ rollback_published(rarx_ctx_t *c) {
|
||||
}
|
||||
}
|
||||
|
||||
/* mkdir -p: ensures every segment below root_fd exists. Returns the open
|
||||
descriptor for the deepest directory. */
|
||||
static int
|
||||
open_parent_dirs(int root_fd, const char *rel, rarx_ctx_t *c) {
|
||||
char buf[ZIPX_PATH_MAX];
|
||||
int fd = root_fd;
|
||||
char *seg;
|
||||
char *save = NULL;
|
||||
|
||||
if(strlen(rel) >= sizeof(buf)) {
|
||||
rarx_fail(c, ZIPX_ERR_LIMIT_NAME, rel, "path is too long");
|
||||
return -1;
|
||||
}
|
||||
strcpy(buf, rel);
|
||||
|
||||
for(seg = strtok_r(buf, "/", &save); seg; seg = strtok_r(NULL, "/", &save)) {
|
||||
int next;
|
||||
|
||||
if(!mkdirat(fd, seg, 0777)) {
|
||||
next = openat(fd, seg, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||
if(next >= 0) {
|
||||
chmod_0777_fd(next);
|
||||
c->dirs_created++;
|
||||
}
|
||||
} else if(errno == EEXIST) {
|
||||
next = openat(fd, seg, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||
} else {
|
||||
rarx_fail(c, ZIPX_ERR_IO, rel, "cannot create directory '%s': %s", seg,
|
||||
strerror(errno));
|
||||
if(fd != root_fd) {
|
||||
close(fd);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(next < 0) {
|
||||
rarx_fail(c, ZIPX_ERR_IO, rel, "cannot open directory '%s': %s", seg,
|
||||
strerror(errno));
|
||||
if(fd != root_fd) {
|
||||
close(fd);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if(fd != root_fd) {
|
||||
close(fd);
|
||||
}
|
||||
fd = next;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
/**************************************************************************
|
||||
* scan phase
|
||||
**************************************************************************/
|
||||
@@ -695,74 +616,70 @@ check_space(rarx_ctx_t *c, const char *target) {
|
||||
}
|
||||
|
||||
static int
|
||||
scan_archive(dmc_unrar_archive *rar, rarx_ctx_t *c) {
|
||||
scan_archive(HANDLE hArc, rarx_ctx_t *c) {
|
||||
rarx_nameset_t set;
|
||||
dmc_unrar_size_t i;
|
||||
dmc_unrar_size_t total;
|
||||
int ret = 0;
|
||||
|
||||
if(nameset_init(&set, 4096)) {
|
||||
return rarx_fail(c, ZIPX_ERR_INTERNAL, NULL, "out of memory");
|
||||
}
|
||||
|
||||
total = dmc_unrar_get_file_count(rar);
|
||||
for(i = 0; i < total; i++) {
|
||||
for(;;) {
|
||||
struct RARHeaderDataEx hdr;
|
||||
uint64_t uncomp;
|
||||
char name[ZIPX_PATH_MAX];
|
||||
char *name_buf;
|
||||
dmc_unrar_size_t name_size;
|
||||
int is_dir;
|
||||
uint32_t depth = 0;
|
||||
const dmc_unrar_file *info;
|
||||
dmc_unrar_return supported;
|
||||
int rc;
|
||||
|
||||
info = dmc_unrar_get_file_stat(rar, i);
|
||||
if(!info) {
|
||||
ret = rarx_fail(c, ZIPX_ERR_FORMAT, NULL, "cannot read entry header");
|
||||
memset(&hdr, 0, sizeof(hdr));
|
||||
rc = RARReadHeaderEx(hArc, &hdr);
|
||||
if(rc == ERAR_END_ARCHIVE) {
|
||||
break;
|
||||
}
|
||||
supported = dmc_unrar_file_is_supported(rar, i);
|
||||
if(supported != DMC_UNRAR_OK) {
|
||||
/* is_supported() already returns ZIPX-mapped error via a temporary */
|
||||
char detail[64];
|
||||
snprintf(detail, sizeof(detail), "entry %llu",
|
||||
(unsigned long long)i);
|
||||
rarx_fail(c, ZIPX_ERR_UNSUPPORTED, detail, "%s",
|
||||
dmc_unrar_strerror(supported));
|
||||
ret = (int)c->result->status;
|
||||
break;
|
||||
}
|
||||
is_dir = dmc_unrar_file_is_directory(rar, i) ? 1 : 0;
|
||||
|
||||
/* First call to learn the required size, second to fill the buffer. */
|
||||
name_size = dmc_unrar_get_filename(rar, i, NULL, 0);
|
||||
if(!name_size) {
|
||||
ret = rarx_fail(c, ZIPX_ERR_FORMAT, NULL,
|
||||
"cannot read entry name");
|
||||
break;
|
||||
}
|
||||
name_buf = malloc(name_size);
|
||||
if(!name_buf) {
|
||||
ret = rarx_fail(c, ZIPX_ERR_INTERNAL, NULL, "out of memory");
|
||||
break;
|
||||
}
|
||||
if(dmc_unrar_get_filename(rar, i, name_buf, name_size) != name_size) {
|
||||
free(name_buf);
|
||||
ret = rarx_fail(c, ZIPX_ERR_FORMAT, NULL,
|
||||
"truncated entry name");
|
||||
break;
|
||||
}
|
||||
/* dmc_unrar does not guarantee NUL termination; force it. */
|
||||
name_buf[name_size - 1] = 0;
|
||||
/* RAR stores UTF-8 encoded names. Replace any non-UTF-8 sequence with
|
||||
'?' to keep the downstream string well-formed. */
|
||||
dmc_unrar_unicode_make_valid_utf8(name_buf);
|
||||
|
||||
if(normalize_name(name_buf, name, sizeof(name), &is_dir, &depth, c)) {
|
||||
free(name_buf);
|
||||
if(rc != ERAR_SUCCESS) {
|
||||
rar_translate_error(rc, NULL, c);
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
free(name_buf);
|
||||
|
||||
/* unrar hands out the header name as a NUL-terminated string
|
||||
(UTF-8 on the PS5 / POSIX build). */
|
||||
if(hdr.FileName[0] == 0) {
|
||||
ret = rarx_fail(c, ZIPX_ERR_FORMAT, NULL, "empty entry name");
|
||||
break;
|
||||
}
|
||||
is_dir = (hdr.Flags & RHDF_DIRECTORY) ? 1 : 0;
|
||||
|
||||
/* Encrypted entries: the unrar engine can decrypt them via RARSetPassword,
|
||||
but the password plumbing is not wired yet — reject up front with the
|
||||
same message the v1.8 backend used. */
|
||||
if(hdr.Flags & RHDF_ENCRYPTED) {
|
||||
ret = rarx_fail(c, ZIPX_ERR_UNSUPPORTED, hdr.FileName,
|
||||
"encrypted RAR entries are not supported");
|
||||
break;
|
||||
}
|
||||
|
||||
/* Multi-volume: a file spanning volumes is presented as several header
|
||||
segments with the SAME name. Segments after the first carry
|
||||
RHDF_SPLITBEFORE; they must drive the engine forward (SKIP) but must
|
||||
not be counted, deduped or size-accumulated again. The first segment
|
||||
already carries the full file size. */
|
||||
if(hdr.Flags & RHDF_SPLITBEFORE) {
|
||||
rc = RARProcessFile(hArc, RAR_SKIP, NULL, NULL);
|
||||
if(rc != ERAR_SUCCESS && rc != ERAR_END_ARCHIVE) {
|
||||
ret = -1;
|
||||
rar_translate_error(rc, hdr.FileName, c);
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if(normalize_name(hdr.FileName, name, sizeof(name), &is_dir, &depth, c)) {
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
(void)depth;
|
||||
|
||||
if(nameset_check_duplicate(&set, name, is_dir, c) ||
|
||||
nameset_add_path(&set, name, is_dir, c)) {
|
||||
@@ -771,7 +688,7 @@ scan_archive(dmc_unrar_archive *rar, rarx_ctx_t *c) {
|
||||
}
|
||||
|
||||
if(!is_dir) {
|
||||
uint64_t uncomp = info->uncompressed_size;
|
||||
uncomp = (uint64_t)hdr.UnpSizeHigh << 32 | hdr.UnpSize;
|
||||
|
||||
if(uncomp > c->limits.max_file_bytes) {
|
||||
ret = rarx_fail(c, ZIPX_ERR_LIMIT_FILE, name,
|
||||
@@ -779,8 +696,6 @@ scan_archive(dmc_unrar_archive *rar, rarx_ctx_t *c) {
|
||||
(unsigned long long)c->limits.max_file_bytes);
|
||||
break;
|
||||
}
|
||||
/* RAR headers do not reliably expose compressed_size for all formats,
|
||||
so the safe compression-ratio check is skipped. */
|
||||
if(uncomp >= c->limits.max_total_bytes ||
|
||||
c->bytes_total > c->limits.max_total_bytes - uncomp) {
|
||||
ret = rarx_fail(c, ZIPX_ERR_LIMIT_TOTAL, name,
|
||||
@@ -805,6 +720,17 @@ scan_archive(dmc_unrar_archive *rar, rarx_ctx_t *c) {
|
||||
}
|
||||
report(c, ZIPX_PHASE_SCAN, name, 0);
|
||||
}
|
||||
|
||||
/* Advance to the next file header (also drives multi-volume merges). */
|
||||
rc = RARProcessFile(hArc, RAR_SKIP, NULL, NULL);
|
||||
if(rc != ERAR_SUCCESS) {
|
||||
if(rc != ERAR_END_ARCHIVE) {
|
||||
ret = -1;
|
||||
rar_translate_error(rc, name, c);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
nameset_free(&set);
|
||||
@@ -815,175 +741,93 @@ scan_archive(dmc_unrar_archive *rar, rarx_ctx_t *c) {
|
||||
* extract phase
|
||||
**************************************************************************/
|
||||
|
||||
/* dmc_unrar_extract_file_to_path() opens the destination file with fopen()
|
||||
without creating parent directories. We always call it against an
|
||||
already-prepared full path inside the staging tree. */
|
||||
static int
|
||||
extract_one(dmc_unrar_archive *rar, rarx_ctx_t *c, int root_fd,
|
||||
const char *name, dmc_unrar_size_t index, uint64_t declared) {
|
||||
char *full_path;
|
||||
char dir_part[ZIPX_PATH_MAX];
|
||||
char base[ZIPX_PATH_MAX];
|
||||
char *slash;
|
||||
int dir_fd;
|
||||
dmc_unrar_return dr;
|
||||
struct stat st;
|
||||
uint64_t before_bytes;
|
||||
int ret = 0;
|
||||
/* unrar invokes this for every decompressed chunk while RARProcessFile is
|
||||
extracting an entry to disk. Without it the engine could only account
|
||||
bytes_done after a whole entry completed, which froze the progress bar
|
||||
for the entire duration of a multi-GB entry spanning several volumes.
|
||||
|
||||
snprintf(dir_part, sizeof(dir_part), "%s", name);
|
||||
slash = strrchr(dir_part, '/');
|
||||
if(slash) {
|
||||
snprintf(base, sizeof(base), "%s", slash + 1);
|
||||
*slash = 0;
|
||||
} else {
|
||||
snprintf(base, sizeof(base), "%s", name);
|
||||
dir_part[0] = 0;
|
||||
}
|
||||
if(!base[0]) {
|
||||
return rarx_fail(c, ZIPX_ERR_UNSAFE_NAME, name, "empty file name");
|
||||
}
|
||||
Returning -1 is how unrar aborts a run, but the break path is only armed
|
||||
when console break handling is enabled, which never happens in DLL mode —
|
||||
so we always return 0 and cancellation stays entry-granular. */
|
||||
static int CALLBACK
|
||||
rar_data_cb(UINT msg, LPARAM user, LPARAM p1, LPARAM p2) {
|
||||
rarx_ctx_t *c = (rarx_ctx_t *)user;
|
||||
(void)p1;
|
||||
|
||||
dir_fd = open_parent_dirs(root_fd, dir_part, c);
|
||||
if(dir_fd < 0) {
|
||||
return -1;
|
||||
if(msg == UCM_PROCESSDATA) {
|
||||
c->bytes_done += (uint64_t)(unsigned long)p2;
|
||||
report(c, ZIPX_PHASE_EXTRACT, NULL, 0);
|
||||
}
|
||||
|
||||
full_path = malloc(ZIPX_PATH_MAX);
|
||||
if(!full_path) {
|
||||
rarx_fail(c, ZIPX_ERR_INTERNAL, name, "out of memory");
|
||||
if(dir_fd != root_fd) {
|
||||
close(dir_fd);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if(dir_part[0]) {
|
||||
snprintf(full_path, ZIPX_PATH_MAX, "%s/%s/%s",
|
||||
c->staging, dir_part, base);
|
||||
} else {
|
||||
snprintf(full_path, ZIPX_PATH_MAX, "%s/%s", c->staging, base);
|
||||
}
|
||||
|
||||
before_bytes = c->bytes_done;
|
||||
dr = dmc_unrar_extract_file_to_path(rar, index, full_path, NULL, true);
|
||||
if(dr != DMC_UNRAR_OK) {
|
||||
rar_translate_error(dr, name, c);
|
||||
ret = -1;
|
||||
goto done;
|
||||
}
|
||||
|
||||
if(stat(full_path, &st)) {
|
||||
rarx_fail(c, ZIPX_ERR_IO, name, "cannot stat extracted file: %s",
|
||||
strerror(errno));
|
||||
ret = -1;
|
||||
goto done;
|
||||
}
|
||||
if((uint64_t)st.st_size > declared + declared + (64 * 1024)) {
|
||||
/* Defensive: the on-disk size should never wildly exceed the declared
|
||||
size. The cap is generous so PPMd + headers do not trip it. */
|
||||
rarx_fail(c, ZIPX_ERR_LIMIT_FILE, name,
|
||||
"extracted file is larger than declared");
|
||||
unlink(full_path);
|
||||
ret = -1;
|
||||
goto done;
|
||||
}
|
||||
if((uint64_t)st.st_size > c->limits.max_total_bytes - c->bytes_done) {
|
||||
rarx_fail(c, ZIPX_ERR_LIMIT_TOTAL, name,
|
||||
"archive contents are larger than %llu bytes",
|
||||
(unsigned long long)c->limits.max_total_bytes);
|
||||
unlink(full_path);
|
||||
ret = -1;
|
||||
goto done;
|
||||
}
|
||||
c->bytes_done += (uint64_t)st.st_size;
|
||||
/* If dmc_unrar did not advance bytes_done above (e.g. declared == 0 for a
|
||||
directory or zero-byte file), credit the bytes_total estimate so the
|
||||
progress bar keeps moving. */
|
||||
if(c->bytes_done == before_bytes && declared) {
|
||||
c->bytes_done += declared;
|
||||
}
|
||||
|
||||
done:
|
||||
free(full_path);
|
||||
if(dir_fd != root_fd) {
|
||||
close(dir_fd);
|
||||
}
|
||||
if(!ret) {
|
||||
c->files_created++;
|
||||
}
|
||||
return ret;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* unrar extracts each entry directly under the staging root and creates
|
||||
parent directories itself. All entry names were validated (normalize_name)
|
||||
during scan, so what lands in the staging tree is safe by construction. */
|
||||
static int
|
||||
extract_archive(dmc_unrar_archive *rar, rarx_ctx_t *c, int root_fd) {
|
||||
dmc_unrar_size_t i;
|
||||
dmc_unrar_size_t total = dmc_unrar_get_file_count(rar);
|
||||
extract_archive(HANDLE hArc, rarx_ctx_t *c) {
|
||||
int ret = 0;
|
||||
|
||||
for(i = 0; i < total && !ret; i++) {
|
||||
char *name_buf = NULL;
|
||||
dmc_unrar_size_t name_size;
|
||||
char name[ZIPX_PATH_MAX];
|
||||
RARSetCallback(hArc, rar_data_cb, (LPARAM)(intptr_t)c);
|
||||
|
||||
for(;;) {
|
||||
struct RARHeaderDataEx hdr;
|
||||
int rc;
|
||||
int is_dir;
|
||||
uint32_t depth = 0;
|
||||
const dmc_unrar_file *info;
|
||||
|
||||
memset(&hdr, 0, sizeof(hdr));
|
||||
rc = RARReadHeaderEx(hArc, &hdr);
|
||||
if(rc == ERAR_END_ARCHIVE) {
|
||||
break;
|
||||
}
|
||||
if(rc != ERAR_SUCCESS) {
|
||||
rar_translate_error(rc, NULL, c);
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
if(canceled(c)) {
|
||||
ret = rarx_fail(c, ZIPX_ERR_CANCELED, NULL, NULL);
|
||||
break;
|
||||
}
|
||||
if(hdr.FileName[0] == 0) {
|
||||
ret = rarx_fail(c, ZIPX_ERR_FORMAT, NULL, "empty entry name");
|
||||
break;
|
||||
}
|
||||
if(hdr.Flags & RHDF_ENCRYPTED) {
|
||||
/* scan already rejected these; defensive only. */
|
||||
ret = rarx_fail(c, ZIPX_ERR_UNSUPPORTED, hdr.FileName,
|
||||
"encrypted RAR entries are not supported");
|
||||
break;
|
||||
}
|
||||
|
||||
info = dmc_unrar_get_file_stat(rar, i);
|
||||
if(!info) {
|
||||
ret = rarx_fail(c, ZIPX_ERR_FORMAT, NULL, "cannot read entry header");
|
||||
break;
|
||||
is_dir = (hdr.Flags & RHDF_DIRECTORY) ? 1 : 0;
|
||||
/* Split continuation segments still need RARProcessFile(EXTRACT) so the
|
||||
volume chain is driven and the file is completed, but only the first
|
||||
segment is counted / reported / size-accumulated. */
|
||||
if(hdr.Flags & RHDF_SPLITBEFORE) {
|
||||
rc = RARProcessFile(hArc, RAR_EXTRACT, c->staging, NULL);
|
||||
if(rc != ERAR_SUCCESS) {
|
||||
rar_translate_error(rc, hdr.FileName, c);
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
is_dir = dmc_unrar_file_is_directory(rar, i) ? 1 : 0;
|
||||
|
||||
name_size = dmc_unrar_get_filename(rar, i, NULL, 0);
|
||||
if(!name_size) {
|
||||
ret = rarx_fail(c, ZIPX_ERR_FORMAT, NULL, "cannot read entry name");
|
||||
break;
|
||||
}
|
||||
name_buf = malloc(name_size);
|
||||
if(!name_buf) {
|
||||
ret = rarx_fail(c, ZIPX_ERR_INTERNAL, NULL, "out of memory");
|
||||
break;
|
||||
}
|
||||
if(dmc_unrar_get_filename(rar, i, name_buf, name_size) != name_size) {
|
||||
free(name_buf);
|
||||
ret = rarx_fail(c, ZIPX_ERR_FORMAT, NULL, "truncated entry name");
|
||||
break;
|
||||
}
|
||||
name_buf[name_size - 1] = 0;
|
||||
dmc_unrar_unicode_make_valid_utf8(name_buf);
|
||||
|
||||
if(normalize_name(name_buf, name, sizeof(name), &is_dir, &depth, c)) {
|
||||
free(name_buf);
|
||||
rc = RARProcessFile(hArc, RAR_EXTRACT, c->staging, NULL);
|
||||
if(rc != ERAR_SUCCESS) {
|
||||
rar_translate_error(rc, hdr.FileName, c);
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
free(name_buf);
|
||||
(void)depth;
|
||||
|
||||
if(is_dir) {
|
||||
int dir_fd = open_parent_dirs(root_fd, name, c);
|
||||
if(dir_fd < 0) {
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
if(dir_fd != root_fd) {
|
||||
close(dir_fd);
|
||||
}
|
||||
c->dirs_created++;
|
||||
} else {
|
||||
uint64_t declared = info->uncompressed_size;
|
||||
if(extract_one(rar, c, root_fd, name, i, declared)) {
|
||||
ret = -1;
|
||||
break;
|
||||
}
|
||||
c->files_created++;
|
||||
}
|
||||
c->entries_done++;
|
||||
report(c, ZIPX_PHASE_EXTRACT, name, 0);
|
||||
report(c, ZIPX_PHASE_EXTRACT, hdr.FileName, 0);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -1151,9 +995,7 @@ rar_extract(const char *rar_path, const char *dst_dir,
|
||||
char parent[ZIPX_PATH_MAX];
|
||||
char dst_copy[ZIPX_PATH_MAX];
|
||||
struct stat st;
|
||||
dmc_unrar_archive rar;
|
||||
dmc_unrar_return dr;
|
||||
int root_fd = -1;
|
||||
HANDLE hArc = NULL;
|
||||
int dst_existed = 0;
|
||||
int status;
|
||||
|
||||
@@ -1197,50 +1039,67 @@ rar_extract(const char *rar_path, const char *dst_dir,
|
||||
goto done;
|
||||
}
|
||||
|
||||
dr = dmc_unrar_archive_init(&rar);
|
||||
if(dr != DMC_UNRAR_OK) {
|
||||
status = rarx_fail(c, ZIPX_ERR_INTERNAL, rar_path,
|
||||
"cannot initialize RAR decoder");
|
||||
goto done;
|
||||
}
|
||||
dr = dmc_unrar_archive_open_path(&rar, rar_path);
|
||||
if(dr != DMC_UNRAR_OK) {
|
||||
status = rar_translate_error(dr, rar_path, c);
|
||||
goto done;
|
||||
/* Open with RAR_OM_EXTRACT for both passes: the unrar engine drives
|
||||
multi-volume merges identically while scanning (skip) and extracting,
|
||||
and encrypted headers surface here as a missing/bad password. */
|
||||
{
|
||||
struct RAROpenArchiveDataEx od;
|
||||
memset(&od, 0, sizeof(od));
|
||||
od.ArcName = (char *)rar_path;
|
||||
od.OpenMode = RAR_OM_EXTRACT;
|
||||
hArc = RAROpenArchiveEx(&od);
|
||||
if(!hArc) {
|
||||
status = rar_translate_error((int)od.OpenResult, rar_path, c);
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
|
||||
if(scan_archive(&rar, c)) {
|
||||
if(scan_archive(hArc, c)) {
|
||||
status = (int)c->result->status;
|
||||
RARCloseArchive(hArc);
|
||||
hArc = NULL;
|
||||
goto done;
|
||||
}
|
||||
if(canceled(c)) {
|
||||
status = rarx_fail(c, ZIPX_ERR_CANCELED, NULL, NULL);
|
||||
RARCloseArchive(hArc);
|
||||
hArc = NULL;
|
||||
goto done;
|
||||
}
|
||||
if(check_space(c, dst_existed ? dst_copy : parent)) {
|
||||
status = (int)c->result->status;
|
||||
RARCloseArchive(hArc);
|
||||
hArc = NULL;
|
||||
goto done;
|
||||
}
|
||||
if(make_staging(c, parent)) {
|
||||
status = (int)c->result->status;
|
||||
RARCloseArchive(hArc);
|
||||
hArc = NULL;
|
||||
goto done;
|
||||
}
|
||||
|
||||
root_fd = open(c->staging, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||
if(root_fd < 0) {
|
||||
status = rarx_fail(c, ZIPX_ERR_IO, c->staging, "cannot open staging: %s",
|
||||
strerror(errno));
|
||||
goto done;
|
||||
/* Pass 2: re-open and extract each entry straight into the staging tree
|
||||
(unrar creates parent directories itself; names were validated during
|
||||
scan). The staging root is pre-verified below for fast failure. */
|
||||
RARCloseArchive(hArc);
|
||||
hArc = NULL;
|
||||
{
|
||||
struct RAROpenArchiveDataEx od;
|
||||
memset(&od, 0, sizeof(od));
|
||||
od.ArcName = (char *)rar_path;
|
||||
od.OpenMode = RAR_OM_EXTRACT;
|
||||
hArc = RAROpenArchiveEx(&od);
|
||||
if(!hArc) {
|
||||
status = rar_translate_error((int)od.OpenResult, rar_path, c);
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
|
||||
if(extract_archive(&rar, c, root_fd)) {
|
||||
if(extract_archive(hArc, c)) {
|
||||
status = (int)c->result->status;
|
||||
goto done;
|
||||
}
|
||||
if(root_fd >= 0) {
|
||||
close(root_fd);
|
||||
root_fd = -1;
|
||||
}
|
||||
if(canceled(c)) {
|
||||
status = rarx_fail(c, ZIPX_ERR_CANCELED, NULL, NULL);
|
||||
goto done;
|
||||
@@ -1254,11 +1113,10 @@ rar_extract(const char *rar_path, const char *dst_dir,
|
||||
status = ZIPX_OK;
|
||||
|
||||
done:
|
||||
if(root_fd >= 0) {
|
||||
close(root_fd);
|
||||
if(hArc) {
|
||||
RARCloseArchive(hArc);
|
||||
}
|
||||
cleanup_staging(c);
|
||||
dmc_unrar_archive_close(&rar);
|
||||
result->entries_total = c->entries_total;
|
||||
result->entries_done = c->entries_done;
|
||||
result->bytes_total = c->bytes_total;
|
||||
|
||||
+9
-8
@@ -1,21 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
/* Safe RAR extraction engine used by the /api/extract task.
|
||||
Wraps the vendored dmc_unrar library (https://github.com/DrMcCoy/dmc_unrar).
|
||||
Wraps the vendored rarlab UnRAR 7.20.1 (third_party/unrar7) through its
|
||||
C-compatible DLL API (unrar_c_api.h facade).
|
||||
|
||||
Reuses the zip_extract types so the dispatch layer can call either engine
|
||||
through the same status / limits / progress protocol.
|
||||
|
||||
See zip_extract.h for the shared limits, conflict, progress and result types.
|
||||
|
||||
Constraints of v1.8 (dmc_unrar 1.7.0 backend):
|
||||
* Single-volume RAR archives only. Multi-volume (.partNN.rar) archives
|
||||
are rejected with ZIPX_ERR_UNSUPPORTED — extract them on a PC first.
|
||||
* Unencrypted RAR only. Encrypted headers / files are rejected with
|
||||
ZIPX_ERR_UNSUPPORTED. There is no password argument for the same reason.
|
||||
Backend notes (v1.9, unrar 7.20.1):
|
||||
* RAR4 and RAR5, any compression version including WinRAR 6/7 "v6".
|
||||
* Multi-volume: unrar merges next .partNN.rar by name automatically.
|
||||
* Encrypted RAR is NOT yet supported end-to-end: the engine can decrypt
|
||||
via RARSetPassword, but password plumbing (API + UI) is unwired, so
|
||||
encrypted headers/entries fail with ZIPX_ERR_UNSUPPORTED today.
|
||||
|
||||
See third_party/unrar/VENDORED.md for the upgrade path to rarlab UnRAR
|
||||
(which does support both) when / if it becomes worth the C++ integration. */
|
||||
See third_party/unrar7/VENDORED.md for full integration notes. */
|
||||
|
||||
#include "zip_extract.h"
|
||||
|
||||
|
||||
+1934
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,187 @@
|
||||
/* sevenz_chain -- 7z folder (coder chain) decoder.
|
||||
part of ps5-web-file-manager
|
||||
|
||||
A 7z archive stores its data as *folders*. One folder is a small directed
|
||||
graph of coders fed by N packed streams and producing a single unpacked
|
||||
stream; entries are slices of the folder output (a folder holding several
|
||||
entries is what makes an archive "solid").
|
||||
|
||||
The bundled LZMA SDK can decode a folder, but only through `CSzFolder`,
|
||||
which is a fixed-size structure capped at 4 coders / 3 bonds. 7-Zip's own
|
||||
BCJ2 chain uses 5 coders (BCJ2 plus four LZMA2 streams), so the SDK rejects
|
||||
it -- while still listing the archive fine, because its *header* scanner is
|
||||
a different, looser parser (64 coders). The C half of the SDK also has no
|
||||
7zAES coder at all.
|
||||
|
||||
This module therefore parses the folder descriptor itself (dynamic arrays,
|
||||
up to 64 coders, mirroring the SDK's header scanner) and drives the coder
|
||||
graph itself. Two properties matter:
|
||||
|
||||
* Streaming. The decoded bytes are pushed into a sink as they are
|
||||
produced; a folder is never materialised as a whole, so a multi-gigabyte
|
||||
solid block is workable. The only buffers sized from the archive are
|
||||
the LZMA/LZMA2 dictionary, the PPMd model and the three side streams of
|
||||
BCJ2 -- each capped by sz_chain_limits_t.
|
||||
|
||||
* Precision. Every rejection names the coder and the method, and the
|
||||
resource limits report the value the archive asked for and the value
|
||||
that was allowed, so the UI can say something useful instead of
|
||||
"corrupt archive".
|
||||
|
||||
Supported here:
|
||||
* Copy, LZMA, LZMA2 and PPMd
|
||||
* the Delta filter and the x86 / PPC / IA64 / ARM / ARMT / SPARC branch
|
||||
converters
|
||||
* BCJ2, whose three side streams are materialised under a limit while
|
||||
MAIN keeps streaming
|
||||
* 7zAES (method 0x06F10701), the coder 7-Zip wraps around the streams when
|
||||
`-p` is used, driven from a caller supplied password
|
||||
|
||||
Not supported here (by design, see sz_chain_check):
|
||||
* an encrypted *header* (`-mhe=on`): that is not a coder in a folder but a
|
||||
second, encrypted copy of the archive header, which has to be decrypted
|
||||
and parsed before any folder exists at all. Reported by the SDK header
|
||||
reader as SZ_ERROR_UNSUPPORTED, not by this module.
|
||||
*/
|
||||
|
||||
#ifndef SEVENZ_CHAIN_H
|
||||
#define SEVENZ_CHAIN_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* The SDK's header scanner accepts up to 64 coders per folder; folders in the
|
||||
wild have 1-5. Keeping the same ceiling means "the SDK could list it" and
|
||||
"we can decode it" accept the same archives. */
|
||||
#define SZ_CHAIN_MAX_CODERS 64
|
||||
#define SZ_CHAIN_MAX_STREAMS 64
|
||||
|
||||
/* Ceilings for the buffers whose size comes from the (attacker controlled)
|
||||
archive header. */
|
||||
typedef struct {
|
||||
uint64_t max_dict_bytes; /* LZMA / LZMA2 window */
|
||||
uint64_t max_ppmd_bytes; /* PPMd model */
|
||||
uint64_t max_side_bytes; /* BCJ2 CALL + JUMP + RC together */
|
||||
uint32_t max_aes_cycles; /* 7zAES key derivation: 2^n SHA-256 passes */
|
||||
} sz_chain_limits_t;
|
||||
|
||||
#define SZ_CHAIN_LIMITS_DEFAULT 0
|
||||
#define SZ_CHAIN_LIMITS_LARGE 1
|
||||
const sz_chain_limits_t *sz_chain_limits_profile(int profile);
|
||||
const sz_chain_limits_t *sz_chain_default_limits(void);
|
||||
|
||||
typedef enum {
|
||||
SZ_CHAIN_OK = 0,
|
||||
SZ_CHAIN_ERR_PARAM, /* bad arguments from the caller */
|
||||
SZ_CHAIN_ERR_MEM, /* allocation failed */
|
||||
SZ_CHAIN_ERR_HEADER, /* malformed folder descriptor */
|
||||
SZ_CHAIN_ERR_METHOD, /* coder method not supported */
|
||||
SZ_CHAIN_ERR_LAYOUT, /* coder graph shape not supported */
|
||||
SZ_CHAIN_ERR_LIMIT, /* a sz_chain_limits_t ceiling was hit */
|
||||
SZ_CHAIN_ERR_PASSWORD,/* the archive is encrypted and no usable password
|
||||
was supplied (or the one given is wrong) */
|
||||
SZ_CHAIN_ERR_READ, /* the read callback failed */
|
||||
SZ_CHAIN_ERR_WRITE, /* the sink callback failed */
|
||||
SZ_CHAIN_ERR_DATA, /* a decoder rejected the data */
|
||||
SZ_CHAIN_ERR_CANCELED,
|
||||
SZ_CHAIN_ERR_INTERNAL
|
||||
} sz_chain_status_t;
|
||||
|
||||
typedef struct {
|
||||
sz_chain_status_t status;
|
||||
int32_t coder; /* index of the offending coder, -1 when not applicable */
|
||||
uint32_t method; /* its method id, 0 when not applicable */
|
||||
uint64_t offset; /* decoded byte offset at the point of failure */
|
||||
char message[192];
|
||||
} sz_chain_err_t;
|
||||
|
||||
const char *sz_chain_status_string(sz_chain_status_t status);
|
||||
|
||||
/* "LZMA2", "BCJ2", "7zAES", "unknown 0x1234". Never returns NULL. */
|
||||
const char *sz_chain_method_name(uint32_t method);
|
||||
|
||||
/* ---------------------------------------------------------------- folder */
|
||||
|
||||
typedef struct sz_chain sz_chain;
|
||||
|
||||
/* Parses one folder descriptor.
|
||||
|
||||
blob / blob_size
|
||||
the CODERS_INFO bytes of this folder, i.e. the range
|
||||
`CSzAr::CodersData[FoCodersOffsets[i] .. FoCodersOffsets[i + 1])`.
|
||||
pack_positions
|
||||
`CSzAr::PackPositions`, num_pack_streams + 1 entries, offsets of the
|
||||
packed streams relative to the start of the archive's packed-data area.
|
||||
coder_unpack_sizes
|
||||
unpacked size of every coder of this folder, in stored coder order, i.e.
|
||||
`&CSzAr::CoderUnpackSizes[CSzAr::FoToCoderUnpackSizes[i]]`.
|
||||
unpack_size
|
||||
`SzAr_GetFolderUnpackSize(&db, i)`.
|
||||
|
||||
Returns 0 on success. On success *out owns a copy of blob, release it with
|
||||
sz_chain_free(). On failure *out is untouched and err describes the
|
||||
problem. */
|
||||
int sz_chain_parse(sz_chain **out, const uint8_t *blob, size_t blob_size,
|
||||
const uint64_t *pack_positions, uint32_t num_pack_streams,
|
||||
const uint64_t *coder_unpack_sizes, uint64_t unpack_size,
|
||||
const sz_chain_limits_t *limits, sz_chain_err_t *err);
|
||||
|
||||
void sz_chain_free(sz_chain *c);
|
||||
|
||||
uint32_t sz_chain_num_coders(const sz_chain *c);
|
||||
uint32_t sz_chain_num_pack_streams(const sz_chain *c);
|
||||
/* Non-zero when the folder contains a 7zAES coder, i.e. when sz_chain_decode()
|
||||
will need a password. Lets a caller ask for one before touching the disk. */
|
||||
int sz_chain_needs_password(const sz_chain *c);
|
||||
/* Method id of coder `index`, or -1 when out of range. */
|
||||
int64_t sz_chain_coder_method(const sz_chain *c, uint32_t index);
|
||||
|
||||
/* True when the folder is a single plain LZMA2 coder -- the shape the SDK's
|
||||
multithreaded decoder covers. Fills the coder's props byte and the packed
|
||||
input size; both are only valid when this returns non-zero. */
|
||||
int sz_chain_lzma2_root(const sz_chain *c, uint8_t *prop, uint64_t *in_size);
|
||||
|
||||
/* Writes e.g. "LZMA2 + BCJ2 (5 coders, 4 pack streams)" into buf. */
|
||||
void sz_chain_describe(const sz_chain *c, char *buf, size_t size);
|
||||
|
||||
/* Walks every coder and the graph shape without touching any data, so callers
|
||||
can refuse an archive before creating anything on disk. Fills err with the
|
||||
same precision sz_chain_decode() would. */
|
||||
int sz_chain_check(const sz_chain *c, sz_chain_err_t *err);
|
||||
|
||||
/* -------------------------------------------------------------- decoding */
|
||||
|
||||
/* Fills exactly size bytes at offset inside the packed-data area.
|
||||
Returns 0 on success, non-zero on failure. */
|
||||
typedef int (*sz_chain_read_fn)(void *ctx, uint64_t offset, void *dst,
|
||||
size_t size);
|
||||
|
||||
/* Receives the decoded bytes in order. Returns 0 to continue. */
|
||||
typedef int (*sz_chain_sink_fn)(void *ctx, const void *data, size_t size);
|
||||
|
||||
/* Returns non-zero to abort. May be NULL. */
|
||||
typedef int (*sz_chain_cancel_fn)(void *ctx);
|
||||
|
||||
/* Decodes the whole folder, pushing the result into sink.
|
||||
|
||||
The bytes are delivered strictly in order and the total is the folder's
|
||||
declared unpack size. When crc_out is not NULL it receives the CRC-32 of
|
||||
the delivered bytes, for the caller to compare with the folder CRC.
|
||||
|
||||
`password` is the archive password as UTF-8, or NULL / "" when the caller
|
||||
has none. It is only consulted by folders that contain a 7zAES coder; a
|
||||
folder that needs one without a password fails as SZ_CHAIN_ERR_PASSWORD
|
||||
before any data is read, so the caller can prompt and retry. A password
|
||||
containing NUL is not supported: 7-Zip stores it as UTF-16LE and the
|
||||
conversion stops at the terminator.
|
||||
|
||||
Returns 0 on success, -1 on failure with err filled. A failing sink is
|
||||
reported as SZ_CHAIN_ERR_WRITE; the caller is expected to make its own
|
||||
message more specific. */
|
||||
int sz_chain_decode(sz_chain *c, sz_chain_read_fn read_at, void *read_ctx,
|
||||
sz_chain_sink_fn sink, void *sink_ctx,
|
||||
sz_chain_cancel_fn cancel, void *cancel_ctx,
|
||||
const char *password, uint32_t *crc_out,
|
||||
sz_chain_err_t *err);
|
||||
|
||||
#endif /* SEVENZ_CHAIN_H */
|
||||
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);
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
#include "sevenz_mt.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "7zTypes.h"
|
||||
#include "7zCrc.h"
|
||||
#include "Alloc.h"
|
||||
#include "Lzma2DecMt.h"
|
||||
|
||||
/* ------------------------------------------------------------ adapters --
|
||||
The SDK's decoders speak ISeqInStream / ISeqOutStream / ICompressProgress;
|
||||
the engine speaks plain callbacks. These three structs translate. All of
|
||||
them run on the calling thread -- MtDec only ever hands output to the
|
||||
thread that called Lzma2DecMt_Decode, which is what makes the plain sink
|
||||
safe to reuse here. */
|
||||
|
||||
typedef struct {
|
||||
ISeqInStream vt;
|
||||
sz_chain_read_fn read_at;
|
||||
void *read_ctx;
|
||||
uint64_t pos; /* absolute offset of the next byte to hand out */
|
||||
uint64_t end; /* one past the last byte of the packed stream */
|
||||
} mt_seq_in;
|
||||
|
||||
static SRes mt_seq_read(const ISeqInStream *pp, void *buf, size_t *size) {
|
||||
mt_seq_in *s = (mt_seq_in *)pp;
|
||||
size_t want = *size;
|
||||
|
||||
*size = 0;
|
||||
if(want == 0) {
|
||||
return SZ_OK;
|
||||
}
|
||||
if(s->end - s->pos < (uint64_t)want) {
|
||||
want = (size_t)(s->end - s->pos);
|
||||
}
|
||||
if(want != 0 && s->read_at(s->read_ctx, s->pos, buf, want) != 0) {
|
||||
return SZ_ERROR_READ;
|
||||
}
|
||||
s->pos += want;
|
||||
*size = want;
|
||||
/* A short read means "end of stream" to the SDK; since we only ever hand
|
||||
it exactly in_size bytes, hitting the end early is the caller's bug and
|
||||
the decoder's outSize check will flag it. */
|
||||
return SZ_OK;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
ISeqOutStream vt;
|
||||
sz_chain_sink_fn sink;
|
||||
void *sink_ctx;
|
||||
uint32_t crc;
|
||||
int failed;
|
||||
} mt_seq_out;
|
||||
|
||||
static size_t mt_seq_write(const ISeqOutStream *pp, const void *buf,
|
||||
size_t size) {
|
||||
mt_seq_out *s = (mt_seq_out *)pp;
|
||||
|
||||
if(s->failed) {
|
||||
return 0;
|
||||
}
|
||||
s->crc = CrcUpdate(s->crc, buf, size);
|
||||
if(s->sink(s->sink_ctx, buf, size) != 0) {
|
||||
s->failed = 1;
|
||||
/* Returning less than `size` tells the SDK the output side is done; it
|
||||
reports SZ_ERROR_WRITE. */
|
||||
return 0;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
ICompressProgress vt;
|
||||
sz_chain_cancel_fn cancel;
|
||||
void *cancel_ctx;
|
||||
} mt_progress;
|
||||
|
||||
static SRes mt_progress_report(const ICompressProgress *pp, UInt64 in_size,
|
||||
UInt64 out_size) {
|
||||
mt_progress *s = (mt_progress *)pp;
|
||||
|
||||
(void)in_size;
|
||||
(void)out_size;
|
||||
if(s->cancel && s->cancel(s->cancel_ctx)) {
|
||||
return SZ_ERROR_PROGRESS;
|
||||
}
|
||||
return SZ_OK;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- decode --
|
||||
Errors are mapped onto sz_chain_err_t so the facade's reporting stays in
|
||||
one vocabulary. SZX_MT_ERR_THREADS is reserved for "the platform cannot
|
||||
give me a thread pool": Lzma2DecMt returns SZ_ERROR_THREAD only from its
|
||||
threading primitives, everything else is data or memory. */
|
||||
|
||||
static void mt_fail(sz_chain_err_t *err, sz_chain_status_t status,
|
||||
const char *fmt, ...) {
|
||||
va_list ap;
|
||||
|
||||
if(!err) {
|
||||
return;
|
||||
}
|
||||
err->status = status;
|
||||
err->coder = 0;
|
||||
err->method = 0x21; /* SZ_M_LZMA2; kept literal to avoid dragging chain.c in */
|
||||
err->offset = 0;
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(err->message, sizeof(err->message), fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
int szx_mt_decode(sz_chain_read_fn read_at, void *read_ctx, uint64_t in_offset,
|
||||
uint64_t in_size, uint8_t prop, uint64_t out_size,
|
||||
sz_chain_sink_fn sink, void *sink_ctx,
|
||||
sz_chain_cancel_fn cancel, void *cancel_ctx,
|
||||
uint32_t *crc_out, sz_chain_err_t *err) {
|
||||
mt_seq_in in;
|
||||
mt_seq_out out;
|
||||
mt_progress progress;
|
||||
CLzma2DecMtProps props;
|
||||
CLzma2DecMtHandle mt;
|
||||
UInt64 in_processed = 0;
|
||||
int is_mt = 0;
|
||||
SRes res;
|
||||
|
||||
memset(&in, 0, sizeof(in));
|
||||
in.vt.Read = mt_seq_read;
|
||||
in.read_at = read_at;
|
||||
in.read_ctx = read_ctx;
|
||||
in.pos = in_offset;
|
||||
in.end = in_offset + in_size;
|
||||
|
||||
memset(&out, 0, sizeof(out));
|
||||
out.vt.Write = mt_seq_write;
|
||||
out.sink = sink;
|
||||
out.sink_ctx = sink_ctx;
|
||||
out.crc = CRC_INIT_VAL;
|
||||
|
||||
memset(&progress, 0, sizeof(progress));
|
||||
progress.vt.Progress = mt_progress_report;
|
||||
progress.cancel = cancel;
|
||||
progress.cancel_ctx = cancel_ctx;
|
||||
|
||||
Lzma2DecMtProps_Init(&props);
|
||||
props.numThreads = SZX_MT_THREADS;
|
||||
props.inBufSize_MT = 1 << 20;
|
||||
|
||||
mt = Lzma2DecMt_Create(&g_Alloc, &g_MidAlloc);
|
||||
if(!mt) {
|
||||
mt_fail(err, SZ_CHAIN_ERR_INTERNAL, "LZMA2 MT: out of memory");
|
||||
return -1;
|
||||
}
|
||||
res = Lzma2DecMt_Decode(mt, prop, &props, &out.vt, &out_size, 1, &in.vt,
|
||||
&in_processed, &is_mt,
|
||||
cancel ? &progress.vt : NULL);
|
||||
Lzma2DecMt_Destroy(mt);
|
||||
|
||||
if(res == SZ_ERROR_THREAD) {
|
||||
/* No usable thread pool (pthread init failure, thread creation denied).
|
||||
The caller retries on the single-threaded chain path. */
|
||||
return SZX_MT_ERR_THREADS;
|
||||
}
|
||||
if(out.failed) {
|
||||
mt_fail(err, SZ_CHAIN_ERR_WRITE, "LZMA2 MT: sink rejected decoded data");
|
||||
return -1;
|
||||
}
|
||||
if(res != SZ_OK) {
|
||||
switch(res) {
|
||||
case SZ_ERROR_PROGRESS:
|
||||
mt_fail(err, SZ_CHAIN_ERR_CANCELED, "canceled");
|
||||
break;
|
||||
case SZ_ERROR_MEM:
|
||||
mt_fail(err, SZ_CHAIN_ERR_INTERNAL, "LZMA2 MT: out of memory");
|
||||
break;
|
||||
case SZ_ERROR_WRITE:
|
||||
mt_fail(err, SZ_CHAIN_ERR_WRITE, "LZMA2 MT: output stream failed");
|
||||
break;
|
||||
default:
|
||||
mt_fail(err, SZ_CHAIN_ERR_DATA, "LZMA2 MT: decode failed (res=%d)",
|
||||
(int)res);
|
||||
break;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if(in_processed != in_size) {
|
||||
mt_fail(err, SZ_CHAIN_ERR_DATA,
|
||||
"LZMA2 MT: consumed %llu of %llu packed bytes",
|
||||
(unsigned long long)in_processed, (unsigned long long)in_size);
|
||||
return -1;
|
||||
}
|
||||
if(crc_out) {
|
||||
*crc_out = out.crc;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/* Multithreaded LZMA2 decode for the 7z engine.
|
||||
*
|
||||
* Most 7z archives are a single plain LZMA2 coder (7-Zip's -m0=lzma2
|
||||
* default). For that shape the SDK's own parallel decoder -- the same code
|
||||
* 7-Zip runs for -mmt -- replaces the single-threaded chain walk and decodes
|
||||
* consecutive LZMA2 blocks on worker threads while the main thread streams
|
||||
* the output into the staging sink. Measured on a 329 MiB fixture this is
|
||||
* worth ~1.7x on an 8-core host, on top of the assembly kernel.
|
||||
*
|
||||
* Threads are rented, not owned: any thread error falls back to the caller's
|
||||
* single-threaded path, so a platform without working pthreads only ever
|
||||
* loses speed, never correctness. */
|
||||
|
||||
#ifndef SEVENZ_MT_H
|
||||
#define SEVENZ_MT_H
|
||||
|
||||
#include "sevenz_chain.h"
|
||||
|
||||
/* 8-core Zen 2 on the PS5: 4 decoders leave the HTTP server, the task
|
||||
system and the kernel half of the machine. */
|
||||
#define SZX_MT_THREADS 8
|
||||
|
||||
/* Decodes one folder that sz_chain_lzma2_root() has recognised. The
|
||||
callbacks mirror sz_chain_decode()'s: read_at/ctx for the packed data,
|
||||
sink/ctx for the decoded bytes (both run on the calling thread; the sink
|
||||
sees the same ordered byte stream the chain would have produced).
|
||||
cancel/ctx is polled from the progress callback and may be NULL.
|
||||
crc_out, when not NULL, receives the CRC-32 of the delivered bytes.
|
||||
err, when not NULL, receives a chain-style error description.
|
||||
Returns 0 on success; SZX_MT_ERR_THREADS means "no working thread pool"
|
||||
and the caller should retry single-threaded; other failures are terminal. */
|
||||
#define SZX_MT_ERR_THREADS 2
|
||||
|
||||
int szx_mt_decode(sz_chain_read_fn read_at, void *read_ctx,
|
||||
uint64_t in_offset, uint64_t in_size, uint8_t prop,
|
||||
uint64_t out_size, sz_chain_sink_fn sink, void *sink_ctx,
|
||||
sz_chain_cancel_fn cancel, void *cancel_ctx,
|
||||
uint32_t *crc_out, sz_chain_err_t *err);
|
||||
|
||||
#endif /* SEVENZ_MT_H */
|
||||
@@ -0,0 +1,356 @@
|
||||
/* sevenz_volstream -- see sevenz_volstream.h for what this does and why. */
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <wchar.h>
|
||||
#endif
|
||||
|
||||
#include "7zFile.h"
|
||||
|
||||
#include "sevenz_volstream.h"
|
||||
#include "zipx_volume.h"
|
||||
|
||||
struct sevenz_volstream {
|
||||
ISeekInStream vt;
|
||||
|
||||
zipx_volume_t vol; /* owns the ordered paths */
|
||||
CSzFile *file; /* one per part */
|
||||
uint64_t *start; /* count + 1 prefix offsets into the logical archive */
|
||||
uint32_t count;
|
||||
uint32_t open_count; /* how many entries of `file` were opened */
|
||||
uint64_t pos; /* current offset in the logical archive */
|
||||
uint32_t cur; /* part `pos` currently sits in, to skip redundant seeks */
|
||||
uint64_t cur_off; /* file offset within that part */
|
||||
char name[256]; /* stem of the set, for messages */
|
||||
};
|
||||
|
||||
/* ---------------------------------------------------------------- helpers */
|
||||
|
||||
static char *err_printf(const char *fmt, ...) {
|
||||
va_list ap;
|
||||
char buf[512];
|
||||
char *out;
|
||||
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(buf, sizeof(buf), fmt, ap);
|
||||
va_end(ap);
|
||||
|
||||
out = (char *)malloc(strlen(buf) + 1);
|
||||
if(out) memcpy(out, buf, strlen(buf) + 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
/* The SDK opens through CreateFileA otherwise, which cannot see non-ASCII
|
||||
entry names. */
|
||||
static void utf8_to_utf16(const char *src, WCHAR *dst, size_t cap) {
|
||||
size_t out = 0;
|
||||
|
||||
while(*src && out + 2 < cap) {
|
||||
unsigned char c = (unsigned char)*src++;
|
||||
UInt32 cp;
|
||||
|
||||
if(c < 0x80) {
|
||||
cp = c;
|
||||
} else if((c & 0xE0) == 0xC0 && (src[0] & 0xC0) == 0x80) {
|
||||
cp = ((UInt32)(c & 0x1F) << 6) | (UInt32)(*src++ & 0x3F);
|
||||
} else if((c & 0xF0) == 0xE0 && (src[0] & 0xC0) == 0x80 &&
|
||||
(src[1] & 0xC0) == 0x80) {
|
||||
cp = ((UInt32)(c & 0x0F) << 12) | ((UInt32)(src[0] & 0x3F) << 6) |
|
||||
(UInt32)(src[1] & 0x3F);
|
||||
src += 2;
|
||||
} else if((c & 0xF8) == 0xF0 && (src[0] & 0xC0) == 0x80 &&
|
||||
(src[1] & 0xC0) == 0x80 && (src[2] & 0xC0) == 0x80) {
|
||||
cp = ((UInt32)(c & 0x07) << 18) | ((UInt32)(src[0] & 0x3F) << 12) |
|
||||
((UInt32)(src[1] & 0x3F) << 6) | (UInt32)(src[2] & 0x3F);
|
||||
src += 3;
|
||||
} else {
|
||||
cp = '?';
|
||||
}
|
||||
|
||||
if(cp >= 0x10000) {
|
||||
cp -= 0x10000;
|
||||
dst[out++] = (WCHAR)(0xD800 | (cp >> 10));
|
||||
dst[out++] = (WCHAR)(0xDC00 | (cp & 0x3FF));
|
||||
} else {
|
||||
dst[out++] = (WCHAR)cp;
|
||||
}
|
||||
}
|
||||
dst[out] = 0;
|
||||
}
|
||||
|
||||
static int open_part(CSzFile *file, const char *path) {
|
||||
WCHAR wide[4096];
|
||||
|
||||
utf8_to_utf16(path, wide, sizeof(wide) / sizeof(wide[0]));
|
||||
return InFile_OpenW(file, wide) == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
static int open_part(CSzFile *file, const char *path) {
|
||||
return InFile_Open(file, path) == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/* ----------------------------------------------------------------- stream */
|
||||
|
||||
static uint32_t part_at(const sevenz_volstream *v, uint64_t pos) {
|
||||
uint32_t i;
|
||||
|
||||
for(i = 0; i < v->count; i++) {
|
||||
if(pos < v->start[i + 1]) return i;
|
||||
}
|
||||
return v->count;
|
||||
}
|
||||
|
||||
static SRes vol_read(const ISeekInStream *p, void *buf, size_t *size) {
|
||||
sevenz_volstream *v = (sevenz_volstream *)p;
|
||||
uint8_t *dst = (uint8_t *)buf;
|
||||
size_t want = *size;
|
||||
size_t got = 0;
|
||||
|
||||
*size = 0;
|
||||
while(got < want) {
|
||||
uint32_t i = part_at(v, v->pos);
|
||||
uint64_t avail, off;
|
||||
size_t take;
|
||||
|
||||
if(i >= v->count) break; /* end of the set: a short read means EOF */
|
||||
|
||||
off = v->pos - v->start[i];
|
||||
avail = (v->start[i + 1] - v->start[i]) - off;
|
||||
take = (size_t)(avail < (uint64_t)(want - got) ? avail
|
||||
: (uint64_t)(want - got));
|
||||
if(take == 0) break;
|
||||
|
||||
if(v->cur != i || v->cur_off != off) {
|
||||
Int64 seek = (Int64)off;
|
||||
if(File_Seek(&v->file[i], &seek, SZ_SEEK_SET) != 0) return SZ_ERROR_READ;
|
||||
v->cur = i;
|
||||
v->cur_off = off;
|
||||
}
|
||||
|
||||
{
|
||||
size_t part_got = take;
|
||||
if(File_Read(&v->file[i], dst + got, &part_got) != 0) return SZ_ERROR_READ;
|
||||
if(part_got == 0) break;
|
||||
got += part_got;
|
||||
v->pos += part_got;
|
||||
v->cur_off += part_got;
|
||||
if(part_got < take) break;
|
||||
}
|
||||
}
|
||||
|
||||
*size = got;
|
||||
return SZ_OK;
|
||||
}
|
||||
|
||||
static SRes vol_seek(const ISeekInStream *p, Int64 *pos, ESzSeek origin) {
|
||||
sevenz_volstream *v = (sevenz_volstream *)p;
|
||||
uint64_t total = v->start[v->count];
|
||||
uint64_t target;
|
||||
|
||||
switch(origin) {
|
||||
case SZ_SEEK_SET:
|
||||
if(*pos < 0) return SZ_ERROR_PARAM;
|
||||
target = (uint64_t)*pos;
|
||||
break;
|
||||
case SZ_SEEK_CUR:
|
||||
if(*pos < 0) {
|
||||
UInt64 back = (UInt64)(-*pos);
|
||||
if(back > v->pos) return SZ_ERROR_PARAM;
|
||||
target = v->pos - back;
|
||||
} else {
|
||||
target = v->pos + (UInt64)*pos;
|
||||
}
|
||||
break;
|
||||
case SZ_SEEK_END:
|
||||
if(*pos < 0) {
|
||||
UInt64 back = (UInt64)(-*pos);
|
||||
if(back > total) return SZ_ERROR_PARAM;
|
||||
target = total - back;
|
||||
} else {
|
||||
target = total + (UInt64)*pos;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return SZ_ERROR_PARAM;
|
||||
}
|
||||
|
||||
if(target > total) target = total;
|
||||
v->pos = target;
|
||||
*pos = (Int64)target;
|
||||
return SZ_OK;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------- open */
|
||||
|
||||
int sevenz_volstream_open(sevenz_volstream **out, const char *path, int *is_set,
|
||||
char **err) {
|
||||
sevenz_volstream *v;
|
||||
char *vol_err = NULL;
|
||||
int detected;
|
||||
uint32_t i;
|
||||
|
||||
if(out) *out = NULL;
|
||||
if(is_set) *is_set = 0;
|
||||
if(err) *err = NULL;
|
||||
if(!out || !path) {
|
||||
if(err) *err = err_printf("no archive path given");
|
||||
return -1;
|
||||
}
|
||||
|
||||
v = (sevenz_volstream *)calloc(1, sizeof(*v));
|
||||
if(!v) {
|
||||
if(err) *err = err_printf("out of memory");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* One call does both jobs: it either reports "ordinary file" and clears the
|
||||
struct, or fills in the ordered part list. A -1 here already carries the
|
||||
message the user needs (a hole in the numbering names the missing part). */
|
||||
detected = zipx_volume_detect(path, &v->vol, &vol_err);
|
||||
if(detected < 0) {
|
||||
if(err) {
|
||||
*err = vol_err ? vol_err
|
||||
: err_printf("'%s' cannot be read", file_base(path));
|
||||
} else {
|
||||
free(vol_err);
|
||||
}
|
||||
zipx_volume_free(&v->vol);
|
||||
free(v);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(detected == 0) {
|
||||
v->vol.paths = (char **)malloc(sizeof(char *));
|
||||
if(v->vol.paths) v->vol.paths[0] = (char *)malloc(strlen(path) + 1);
|
||||
if(!v->vol.paths || !v->vol.paths[0]) {
|
||||
free(v->vol.paths);
|
||||
free(v);
|
||||
if(err) *err = err_printf("out of memory");
|
||||
return -1;
|
||||
}
|
||||
memcpy(v->vol.paths[0], path, strlen(path) + 1);
|
||||
v->vol.count = 1;
|
||||
v->vol.mode = ZIPX_VOL_MODE_CONCAT;
|
||||
v->vol.is_set = 0;
|
||||
} else if(is_set) {
|
||||
*is_set = 1;
|
||||
}
|
||||
|
||||
snprintf(v->name, sizeof(v->name), "%s", file_base(path));
|
||||
|
||||
v->count = (uint32_t)v->vol.count;
|
||||
v->file = (CSzFile *)calloc(v->count, sizeof(CSzFile));
|
||||
v->start = (uint64_t *)calloc((size_t)v->count + 1, sizeof(uint64_t));
|
||||
if(!v->file || !v->start) {
|
||||
if(err) *err = err_printf("out of memory");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
for(i = 0; i < v->count; i++) {
|
||||
UInt64 length = 0;
|
||||
|
||||
File_Construct(&v->file[i]);
|
||||
v->open_count = i + 1; /* File_Close() ignores a never-opened handle */
|
||||
if(open_part(&v->file[i], v->vol.paths[i]) != 0) {
|
||||
if(err) {
|
||||
*err = err_printf("cannot open volume '%s' of '%s'",
|
||||
file_base(v->vol.paths[i]), v->name);
|
||||
}
|
||||
goto fail;
|
||||
}
|
||||
if(File_GetLength(&v->file[i], &length) != 0) {
|
||||
if(err) {
|
||||
*err = err_printf("cannot measure volume '%s' of '%s'",
|
||||
file_base(v->vol.paths[i]), v->name);
|
||||
}
|
||||
goto fail;
|
||||
}
|
||||
v->start[i + 1] = v->start[i] + length;
|
||||
}
|
||||
|
||||
if(v->start[v->count] == 0) {
|
||||
if(err) *err = err_printf("'%s' is empty", v->name);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
/* 7-Zip cuts equal sized parts and lets only the last one be short. A part
|
||||
of a different size in the middle means the set is damaged or was mixed
|
||||
with another one, and decoding would fail much later with a message that
|
||||
points nowhere useful. */
|
||||
for(i = 0; i + 1 < v->count; i++) {
|
||||
uint64_t size = v->start[i + 1] - v->start[i];
|
||||
if(size != v->start[1]) {
|
||||
if(err) {
|
||||
*err = err_printf("volume '%s' of '%s' is %llu bytes, but the earlier "
|
||||
"volumes are %llu bytes: the set is not a clean split",
|
||||
file_base(v->vol.paths[i]), v->name,
|
||||
(unsigned long long)size,
|
||||
(unsigned long long)v->start[1]);
|
||||
}
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
|
||||
v->vt.Read = vol_read;
|
||||
v->vt.Seek = vol_seek;
|
||||
*out = v;
|
||||
return 0;
|
||||
|
||||
fail:
|
||||
sevenz_volstream_free(v);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ISeekInStream *sevenz_volstream_stream(sevenz_volstream *v) {
|
||||
return v ? &v->vt : NULL;
|
||||
}
|
||||
|
||||
uint32_t sevenz_volstream_count(const sevenz_volstream *v) {
|
||||
return v ? v->count : 0;
|
||||
}
|
||||
|
||||
uint64_t sevenz_volstream_size(const sevenz_volstream *v) {
|
||||
return v ? v->start[v->count] : 0;
|
||||
}
|
||||
|
||||
const char *sevenz_volstream_describe(const sevenz_volstream *v, char *buf,
|
||||
unsigned size) {
|
||||
if(!buf || size == 0) return buf;
|
||||
if(!v) {
|
||||
snprintf(buf, size, "no archive");
|
||||
} else if(v->count <= 1) {
|
||||
snprintf(buf, size, "%s", v->name);
|
||||
} else {
|
||||
snprintf(buf, size, "%s (%u volumes)", v->name, (unsigned)v->count);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
void sevenz_volstream_free(sevenz_volstream *v) {
|
||||
uint32_t i;
|
||||
|
||||
if(!v) return;
|
||||
for(i = 0; i < v->open_count; i++) File_Close(&v->file[i]);
|
||||
free(v->file);
|
||||
free(v->start);
|
||||
zipx_volume_free(&v->vol);
|
||||
free(v);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/* sevenz_volstream -- present a multi-file 7z volume set as one stream.
|
||||
part of ps5-web-file-manager
|
||||
|
||||
A split 7z is a plain byte split: `name.7z.001`, `name.7z.002`, ... are
|
||||
consecutive slices of one archive, so byte N of the logical archive is byte
|
||||
N of the concatenation and every offset stored inside the stream header is
|
||||
already absolute. Nothing has to be merged on disk -- a 160 GiB set would
|
||||
otherwise need a second 160 GiB scratch copy.
|
||||
|
||||
The LZMA SDK reads through ISeekInStream, so this module implements that
|
||||
interface over the ordered part list produced by zipx_volume. The ordered
|
||||
list is what makes a set with a hole in it fail loudly instead of decoding
|
||||
garbage: zipx_volume names the missing part. */
|
||||
|
||||
#ifndef SEVENZ_VOLSTREAM_H
|
||||
#define SEVENZ_VOLSTREAM_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "7zTypes.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct sevenz_volstream sevenz_volstream;
|
||||
|
||||
/* Opens `path` together with every volume of the set it belongs to and exposes
|
||||
them as one seekable byte stream. `path` may be any member of the set; an
|
||||
ordinary single-file archive is the degenerate one-file case.
|
||||
|
||||
Returns 0 on success, with *is_set set to 1 when the path was part of a
|
||||
multi-file set (non-NULL only). Returns -1 on failure and, when `err` is
|
||||
non-NULL, stores a malloc'd message the caller must free -- an incomplete
|
||||
set reports the missing volume by name. */
|
||||
int sevenz_volstream_open(sevenz_volstream **out, const char *path, int *is_set,
|
||||
char **err);
|
||||
|
||||
/* The stream to hand to SzArEx_Open(); valid until sevenz_volstream_free(). */
|
||||
ISeekInStream *sevenz_volstream_stream(sevenz_volstream *v);
|
||||
|
||||
/* Number of files backing the stream (1 for an ordinary archive). */
|
||||
uint32_t sevenz_volstream_count(const sevenz_volstream *v);
|
||||
|
||||
/* Size of the whole logical archive. */
|
||||
uint64_t sevenz_volstream_size(const sevenz_volstream *v);
|
||||
|
||||
/* Human readable description, e.g. "name.7z (3 volumes)"; writes into buf and
|
||||
returns buf. */
|
||||
const char *sevenz_volstream_describe(const sevenz_volstream *v, char *buf,
|
||||
unsigned size);
|
||||
|
||||
void sevenz_volstream_free(sevenz_volstream *v);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* /api/version -- hands the build's VERSION_TAG to the web UI.
|
||||
*
|
||||
* The footer in the browser shows a version string, and for a long time that
|
||||
* string was a literal in assets/main.js, so it drifted out of sync the moment
|
||||
* the Makefile moved on (v1.9 stayed on screen through the whole v1.9.1
|
||||
* release). Exposing it over the API keeps a single source of truth: bump
|
||||
* VERSION_TAG in the Makefile and every surface -- startup notification
|
||||
* (src/main.c), stdout banner, ELF file name and the UI footer -- follows.
|
||||
*
|
||||
* The response is tiny and immutable, so the client caches it for the session.
|
||||
*/
|
||||
|
||||
#include "filemgr_internal.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "json_util.h"
|
||||
|
||||
#ifndef VERSION_TAG
|
||||
#define VERSION_TAG "unknown"
|
||||
#endif
|
||||
|
||||
enum MHD_Result
|
||||
api_version(struct MHD_Connection *conn) {
|
||||
strbuf_t b = {0};
|
||||
|
||||
strbuf_append(&b, "{\"ok\":true,\"version\":");
|
||||
json_escape(&b, VERSION_TAG);
|
||||
strbuf_append(&b, ",\"titleId\":");
|
||||
json_escape(&b, TITLE_ID);
|
||||
strbuf_append(&b, "}");
|
||||
return send_buffer(conn, MHD_HTTP_OK, b.data, "application/json");
|
||||
}
|
||||
+198
-99
@@ -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
|
||||
@@ -33,69 +36,8 @@
|
||||
#define ZIPX_PUBLISH_MAX_DEPTH 128
|
||||
#define ZIPX_SPACE_SLACK_PER_ENTRY 512
|
||||
|
||||
static const zipx_limits_t k_default_limits = {
|
||||
.max_entries = 200000,
|
||||
.max_total_bytes = 512ULL * 1024 * 1024 * 1024,
|
||||
.max_file_bytes = 64ULL * 1024 * 1024 * 1024,
|
||||
.max_ratio = 200,
|
||||
.max_depth = 32,
|
||||
.max_name_len = 255,
|
||||
.max_path_len = 1024
|
||||
};
|
||||
|
||||
/* Large profile for archives that exceed the safe limits (e.g. 200 GB system
|
||||
images). The relaxed ratio still rejects obvious bombs; the size caps
|
||||
require the user to opt in via the web UI before they take effect. */
|
||||
static const zipx_limits_t k_large_limits = {
|
||||
.max_entries = 500000,
|
||||
.max_total_bytes = 2ULL * 1024 * 1024 * 1024 * 1024,
|
||||
.max_file_bytes = 1ULL * 1024 * 1024 * 1024 * 1024,
|
||||
.max_ratio = 1000,
|
||||
.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
|
||||
@@ -703,10 +645,14 @@ scan_archive(void *zip, zipx_ctx_t *c) {
|
||||
break;
|
||||
}
|
||||
if(c->limits.max_ratio && info->compressed_size > 0 &&
|
||||
uncompressed >= c->limits.ratio_min_bytes &&
|
||||
uncompressed > (uint64_t)info->compressed_size *
|
||||
c->limits.max_ratio) {
|
||||
ret = ctx_fail(c, ZIPX_ERR_LIMIT_RATIO, name,
|
||||
"compression ratio is above %u", c->limits.max_ratio);
|
||||
"compression ratio is above %u (%llu -> %llu bytes)",
|
||||
c->limits.max_ratio,
|
||||
(unsigned long long)info->compressed_size,
|
||||
(unsigned long long)uncompressed);
|
||||
break;
|
||||
}
|
||||
if(uncompressed >= c->limits.max_total_bytes ||
|
||||
@@ -748,11 +694,34 @@ scan_archive(void *zip, zipx_ctx_t *c) {
|
||||
* extract phase
|
||||
**************************************************************************/
|
||||
|
||||
/* Builds "<staging>/<rel>[/<leaf>]" for the plain-call fallbacks below.
|
||||
rel may be empty (staging root); leaf may be NULL. */
|
||||
static void
|
||||
staging_path(const zipx_ctx_t *c, const char *rel, const char *leaf,
|
||||
char *out, size_t cap) {
|
||||
if(rel[0] && leaf) {
|
||||
snprintf(out, cap, "%s/%s/%s", c->staging, rel, leaf);
|
||||
} else if(rel[0]) {
|
||||
snprintf(out, cap, "%s/%s", c->staging, rel);
|
||||
} else if(leaf) {
|
||||
snprintf(out, cap, "%s/%s", c->staging, leaf);
|
||||
} else {
|
||||
snprintf(out, cap, "%s", c->staging);
|
||||
}
|
||||
}
|
||||
|
||||
/* The *at() family can be present in the target libc yet fail at runtime
|
||||
without setting errno (observed on PS5 hardware: mkdirat() returns -1
|
||||
with errno 0, while plain path-based calls work). Every *at() call in
|
||||
the extract phase therefore falls back to a full-path call built from
|
||||
the staging root before reporting an I/O error. */
|
||||
|
||||
/* Opens (creating when needed) every directory of rel below root_fd.
|
||||
Returns an open descriptor for the deepest directory. */
|
||||
static int
|
||||
open_parent_dirs(int root_fd, const char *rel, zipx_ctx_t *c) {
|
||||
char buf[ZIPX_PATH_MAX];
|
||||
char cur[ZIPX_PATH_MAX] = "";
|
||||
int fd = root_fd;
|
||||
char *seg;
|
||||
char *save = NULL;
|
||||
@@ -765,31 +734,48 @@ open_parent_dirs(int root_fd, const char *rel, zipx_ctx_t *c) {
|
||||
|
||||
for(seg = strtok_r(buf, "/", &save); seg; seg = strtok_r(NULL, "/", &save)) {
|
||||
int next;
|
||||
int made = 0;
|
||||
|
||||
if(cur[0]) {
|
||||
strncat(cur, "/", sizeof(cur) - strlen(cur) - 1);
|
||||
}
|
||||
strncat(cur, seg, sizeof(cur) - strlen(cur) - 1);
|
||||
|
||||
if(!mkdirat(fd, seg, 0777)) {
|
||||
next = openat(fd, seg, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||
if(next >= 0) {
|
||||
chmod_0777_fd(next);
|
||||
c->dirs_created++;
|
||||
made = 1;
|
||||
} else if(errno != EEXIST) {
|
||||
char full[ZIPX_PATH_MAX];
|
||||
|
||||
staging_path(c, cur, NULL, full, sizeof(full));
|
||||
if(mkdir(full, 0777) && errno != EEXIST) {
|
||||
ctx_fail(c, ZIPX_ERR_IO, rel, "cannot create directory '%s': %s "
|
||||
"(errno=%d)", seg, strerror(errno), errno);
|
||||
if(fd != root_fd) {
|
||||
close(fd);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
} else if(errno == EEXIST) {
|
||||
next = openat(fd, seg, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||
} else {
|
||||
ctx_fail(c, ZIPX_ERR_IO, rel, "cannot create directory '%s': %s", seg,
|
||||
strerror(errno));
|
||||
if(fd != root_fd) {
|
||||
close(fd);
|
||||
}
|
||||
return -1;
|
||||
made = 1;
|
||||
}
|
||||
|
||||
next = openat(fd, seg, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||
if(next < 0) {
|
||||
ctx_fail(c, ZIPX_ERR_IO, rel, "cannot open directory '%s': %s", seg,
|
||||
strerror(errno));
|
||||
if(fd != root_fd) {
|
||||
close(fd);
|
||||
char full[ZIPX_PATH_MAX];
|
||||
|
||||
staging_path(c, cur, NULL, full, sizeof(full));
|
||||
next = open(full, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||
if(next < 0) {
|
||||
ctx_fail(c, ZIPX_ERR_IO, rel, "cannot open directory '%s': %s "
|
||||
"(errno=%d)", seg, strerror(errno), errno);
|
||||
if(fd != root_fd) {
|
||||
close(fd);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if(made) {
|
||||
chmod_0777_fd(next);
|
||||
c->dirs_created++;
|
||||
}
|
||||
if(fd != root_fd) {
|
||||
close(fd);
|
||||
@@ -839,7 +825,15 @@ write_entry(void *zip, zipx_ctx_t *c, int root_fd, const char *name,
|
||||
snprintf(tmp, sizeof(tmp), "%s%u", ZIPX_PART_PREFIX, ++c->part_counter);
|
||||
fd = openat(dir_fd, tmp, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0600);
|
||||
if(fd < 0) {
|
||||
ctx_fail(c, ZIPX_ERR_IO, name, "cannot create file: %s", strerror(errno));
|
||||
/* *at() fallback (see open_parent_dirs). */
|
||||
char full[ZIPX_PATH_MAX];
|
||||
|
||||
staging_path(c, dir_part, tmp, full, sizeof(full));
|
||||
fd = open(full, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0600);
|
||||
}
|
||||
if(fd < 0) {
|
||||
ctx_fail(c, ZIPX_ERR_IO, name, "cannot create file '%s': %s (errno=%d)",
|
||||
tmp, strerror(errno), errno);
|
||||
if(dir_fd != root_fd) {
|
||||
close(dir_fd);
|
||||
}
|
||||
@@ -918,22 +912,43 @@ write_entry(void *zip, zipx_ctx_t *c, int root_fd, const char *name,
|
||||
|
||||
done:
|
||||
if(!ret) {
|
||||
if(fsync(fd) || close(fd)) {
|
||||
ctx_fail(c, ZIPX_ERR_IO, name, "cannot flush file: %s", strerror(errno));
|
||||
/* No fsync here, on purpose. The per-entry flush used to cost 20-30
|
||||
minutes on a 95k-file archive (measured on PS5-class storage) and buys
|
||||
nothing the design needs: a crash mid-extract leaves the staging tree,
|
||||
which is discarded on the next run, and publish is a rename-only phase
|
||||
(see publish_entry). The RAR and 7z engines never flushed per entry
|
||||
either; all three now share the same "sync nothing, rename everything"
|
||||
policy. */
|
||||
if(close(fd)) {
|
||||
ctx_fail(c, ZIPX_ERR_IO, name, "cannot close file: %s", strerror(errno));
|
||||
ret = -1;
|
||||
}
|
||||
fd = -1;
|
||||
}
|
||||
if(!ret && renameat(dir_fd, tmp, dir_fd, base)) {
|
||||
ctx_fail(c, ZIPX_ERR_IO, name, "cannot move file into place: %s",
|
||||
strerror(errno));
|
||||
ret = -1;
|
||||
/* *at() fallback (see open_parent_dirs). */
|
||||
char src_full[ZIPX_PATH_MAX];
|
||||
char dst_full[ZIPX_PATH_MAX];
|
||||
|
||||
staging_path(c, dir_part, tmp, src_full, sizeof(src_full));
|
||||
staging_path(c, dir_part, base, dst_full, sizeof(dst_full));
|
||||
if(rename(src_full, dst_full)) {
|
||||
ctx_fail(c, ZIPX_ERR_IO, name, "cannot move file into place: %s "
|
||||
"(errno=%d)", strerror(errno), errno);
|
||||
ret = -1;
|
||||
}
|
||||
}
|
||||
if(fd >= 0) {
|
||||
close(fd);
|
||||
}
|
||||
if(ret) {
|
||||
unlinkat(dir_fd, tmp, 0);
|
||||
if(unlinkat(dir_fd, tmp, 0)) {
|
||||
/* *at() fallback (see open_parent_dirs). */
|
||||
char full[ZIPX_PATH_MAX];
|
||||
|
||||
staging_path(c, dir_part, tmp, full, sizeof(full));
|
||||
unlink(full);
|
||||
}
|
||||
} else {
|
||||
c->files_created++;
|
||||
}
|
||||
@@ -1158,6 +1173,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,
|
||||
@@ -1173,6 +1258,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) {
|
||||
@@ -1184,10 +1271,12 @@ 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;
|
||||
c->limits = limits ? *limits : k_default_limits;
|
||||
c->limits = limits ? *limits : *zipx_default_limits();
|
||||
c->cancel = cancel;
|
||||
c->progress = progress;
|
||||
c->userdata = userdata;
|
||||
@@ -1215,16 +1304,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;
|
||||
}
|
||||
|
||||
@@ -1285,6 +1383,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;
|
||||
|
||||
@@ -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,
|
||||
@@ -48,6 +50,13 @@ typedef struct {
|
||||
uint64_t max_total_bytes;
|
||||
uint64_t max_file_bytes;
|
||||
uint32_t max_ratio; /* uncompressed/compressed, 0 disables */
|
||||
/* Entries whose uncompressed size is below this are never ratio-screened.
|
||||
Small highly-compressible entries are common in legitimate archives
|
||||
(zero-filled placeholders, sparse blobs) and are harmless because the
|
||||
actual bytes written are bounded by the declared size and by the real
|
||||
free-space check; the ratio screen only needs to catch entries large
|
||||
enough to matter. */
|
||||
uint64_t ratio_min_bytes;
|
||||
uint32_t max_depth;
|
||||
uint32_t max_name_len;
|
||||
uint32_t max_path_len;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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,56 @@
|
||||
/* 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>
|
||||
|
||||
#include "zipx_volume.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 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,600 @@
|
||||
/* 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_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,58 @@
|
||||
/* 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
|
||||
|
||||
/* How the set is split. A byte split (`name.zip.001`, `name.7z.001`,
|
||||
`name.part1.zip`) is the concatenation of its parts with absolute offsets;
|
||||
zip split disks (`name.z01` + `name.zip`) store per-disk offsets instead.
|
||||
The constants live here because they describe the *set*, and every consumer
|
||||
of zipx_volume_t needs them. */
|
||||
#define ZIPX_VOL_MODE_CONCAT 0
|
||||
#define ZIPX_VOL_MODE_DISK 1
|
||||
|
||||
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,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Thin wrapper -- the actual driver is tests/bench_driver.py.
|
||||
#
|
||||
# /usr/bin/bash tests/bench-sevenz.sh [--big] [--runs N]
|
||||
#
|
||||
# bash is deliberately not used for timing here. In this sandbox every `date`
|
||||
# costs ~350 ms, so a t0/t1 pair injects ~700 ms of overhead into a
|
||||
# measurement whose real value is ~600 ms, and `time`'s user/sys accounting
|
||||
# does not see into the native child at all (it reported 31 ms of CPU for a
|
||||
# run that demonstrably decodes 82 MiB). Python reads a monotonic clock
|
||||
# around a single spawn per sample instead.
|
||||
|
||||
set -e
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -W 2>/dev/null || pwd)"
|
||||
exec python "$ROOT/tests/bench_driver.py" "$@"
|
||||
@@ -0,0 +1,314 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Performance baseline for the extraction engines.
|
||||
|
||||
python tests/bench_driver.py # 82 MiB fixture, fast
|
||||
python tests/bench_driver.py --big # 320 MiB fixture, accurate
|
||||
python tests/bench_driver.py --format zip
|
||||
python tests/bench_driver.py --format rar
|
||||
python tests/bench_driver.py --runs 5
|
||||
|
||||
Times our facades (src/zip_extract.c, src/rar_extract.c,
|
||||
src/sevenz_extract.c) against the external references that matter: the
|
||||
vendored SDK's own SzArEx path, and the official 7-Zip binary -- the latter
|
||||
being what upstream v1.8 gets by shelling out to a helper, so it doubles as
|
||||
the "how fast could we be" ceiling.
|
||||
|
||||
The three formats are packed from one shared payload, so the rows are
|
||||
comparable across formats and not just within one engine.
|
||||
|
||||
Why Python drives the measurement: in this sandbox a single `date` costs
|
||||
~350 ms, so the usual `t0=$(date)` / `t1=$(date)` pair adds ~700 ms of pure
|
||||
overhead to a measurement whose real value is ~600 ms, and `time`'s user/sys
|
||||
accounting does not see into the native child at all. Python spawns each
|
||||
child once and reads a monotonic clock around it, which leaves a small,
|
||||
constant "spawn tax" that is measured and subtracted (see the report).
|
||||
|
||||
The candidate binaries print their own in-process timing, which excludes the
|
||||
spawn tax entirely -- that is the most trustworthy figure for our side.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
BUILD = os.path.join(REPO, ".build", "bench")
|
||||
SZ_BUILD = os.path.join(REPO, ".build", "sevenz-test")
|
||||
HT_BUILD = os.path.join(REPO, ".build", "host-test")
|
||||
SEVENZ_BIN = os.path.join(REPO, ".build", "7zdl", "extra", "x64", "7za.exe")
|
||||
BENCH_BIN = os.path.join(BUILD, "bench_extract.exe")
|
||||
SDK_BIN = os.path.join(SZ_BUILD, "sevenz_e2e.exe")
|
||||
# RAR is write-only in WinRAR (7-Zip can read the format but not create it),
|
||||
# so a RAR fixture needs rar.exe. Missing means the rar fixture is skipped,
|
||||
# never that the run fails.
|
||||
RAR_BIN = next((p for p in (
|
||||
os.environ.get("WFM_RAR"),
|
||||
r"C:\Program Files\WinRAR\rar.exe",
|
||||
r"C:\Program Files (x86)\WinRAR\rar.exe",
|
||||
"/usr/bin/rar", "/usr/local/bin/rar") if p and os.path.exists(p)), None)
|
||||
|
||||
# Object lists mirror what tests/run-sevenz-tests.sh and tests/run-tests.sh
|
||||
# build; those scripts must have run once before this can link.
|
||||
VENDOR_7Z = ["7zAlloc", "7zArcIn", "7zBuf", "7zBuf2", "7zCrc", "7zCrcOpt",
|
||||
"7zDec", "7zFile", "7zStream", "Aes", "AesOpt", "Alloc", "Bcj2",
|
||||
"Bra", "Bra86", "BraIA64", "CpuArch", "Delta", "DllSecur",
|
||||
"Lzma2Dec", "LzmaDec", "Lzma2DecMt", "MtDec", "Threads", "Ppmd7",
|
||||
"Ppmd7Dec", "Sha256", "Sha256Opt", "SwapBytes"]
|
||||
ZLIB = ["adler32", "crc32", "deflate", "inffast", "inflate", "inftrees",
|
||||
"trees", "zutil"]
|
||||
MINIZIP = ["mz_crypt", "mz_os", "mz_os_posix", "mz_strm", "mz_strm_mem",
|
||||
"mz_strm_os_posix", "mz_strm_zlib", "mz_zip"]
|
||||
EXTRA_LIBS = ["-lole32", "-loleaut32", "-luuid", "-ladvapi32", "-luser32",
|
||||
"-lshell32"]
|
||||
|
||||
|
||||
def objs(base, names):
|
||||
return [os.path.join(base, n + ".o") for n in names]
|
||||
|
||||
|
||||
def build_bench():
|
||||
"""Link tests/bench_extract.c against the prebuilt engine objects."""
|
||||
if os.path.exists(BENCH_BIN):
|
||||
return True
|
||||
|
||||
# The RAR engine is C++ (vendored UnRAR), so bench_extract.c is compiled
|
||||
# with gcc and the link goes through g++.
|
||||
unrar = sorted(glob.glob(os.path.join(HT_BUILD, "unrar7_*.o")))
|
||||
needed = (objs(SZ_BUILD, ["sevenz_extract", "sevenz_chain",
|
||||
"sevenz_mt", "sevenz_volstream", "zipx_common",
|
||||
"zipx_volume"])
|
||||
+ objs(HT_BUILD, ["zip_extract", "zipx_volstream", "rar_extract"])
|
||||
+ objs(HT_BUILD, ZLIB) + objs(HT_BUILD, MINIZIP)
|
||||
+ objs(SZ_BUILD, VENDOR_7Z) + unrar)
|
||||
missing = [p for p in needed if not os.path.exists(p)]
|
||||
if missing:
|
||||
print("missing engine objects, run these first:")
|
||||
print(" /usr/bin/bash tests/run-sevenz-tests.sh")
|
||||
print(" /usr/bin/bash tests/run-tests.sh --rebuild")
|
||||
print("first missing: %s" % missing[0])
|
||||
return False
|
||||
|
||||
includes = ["-I" + os.path.join(REPO, "third_party", "7z"),
|
||||
"-I" + os.path.join(REPO, "third_party", "minizip-ng", "include"),
|
||||
"-I" + os.path.join(REPO, "third_party", "zlib", "include"),
|
||||
"-I" + os.path.join(REPO, "src"),
|
||||
"-I" + os.path.join(REPO, "tests", "compat"),
|
||||
"-include", os.path.join(REPO, "tests", "posix_compat.h")]
|
||||
obj = os.path.join(BUILD, "bench_extract.o")
|
||||
print("== compiling bench harness ==")
|
||||
if subprocess.run(["gcc", "-O2", "-w"] + includes
|
||||
+ ["-c", "-o", obj,
|
||||
os.path.join(REPO, "tests", "bench_extract.c")],
|
||||
cwd=REPO).returncode != 0:
|
||||
return False
|
||||
|
||||
libs = list(EXTRA_LIBS)
|
||||
if os.name == "nt":
|
||||
# Windows unrar system.cpp references SetSuspendState (PowrProf).
|
||||
libs.append("-lpowrprof")
|
||||
if subprocess.run(["g++", "-O2", "-o", BENCH_BIN, obj] + needed + libs,
|
||||
cwd=REPO).returncode != 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def make_fixture(big, fmt):
|
||||
"""Build the archive if absent: repeated source text plus a real PE file.
|
||||
|
||||
The blend matters -- compression throughput depends heavily on match
|
||||
length, so a pure-text corpus would flatter every decoder equally and a
|
||||
pure-random one would measure nothing but copying.
|
||||
"""
|
||||
stem = "big4" if big else "big"
|
||||
archive = os.path.join(BUILD, "%s.%s" % (stem, fmt))
|
||||
if os.path.exists(archive):
|
||||
return archive
|
||||
|
||||
os.makedirs(BUILD, exist_ok=True)
|
||||
tp = os.path.join(REPO, "third_party")
|
||||
parts = []
|
||||
for sub in ("zlib/src", "7z", "minizip-ng/src"):
|
||||
d = os.path.join(tp, sub)
|
||||
if os.path.isdir(d):
|
||||
for name in sorted(os.listdir(d)):
|
||||
if name.endswith((".c", ".h")):
|
||||
parts.append(os.path.join(d, name))
|
||||
if not parts:
|
||||
print("no source available to build a fixture from")
|
||||
return None
|
||||
|
||||
repeat = 240 if big else 60
|
||||
src = os.path.join(BUILD, "payload_src.bin")
|
||||
with open(src, "wb") as out:
|
||||
for _ in range(repeat):
|
||||
for path in parts:
|
||||
with open(path, "rb") as fh:
|
||||
out.write(fh.read())
|
||||
|
||||
payload = src
|
||||
pe = r"C:\Windows\System32\ntoskrnl.exe"
|
||||
if os.path.exists(pe):
|
||||
binary = os.path.join(BUILD, "payload_bin.bin")
|
||||
with open(binary, "wb") as out:
|
||||
for _ in range(30 if big else 3):
|
||||
with open(pe, "rb") as fh:
|
||||
out.write(fh.read())
|
||||
payload = os.path.join(BUILD, "payload_mix.bin")
|
||||
with open(payload, "wb") as out:
|
||||
for _ in range(4 if big else 1):
|
||||
for path in (src, binary):
|
||||
with open(path, "rb") as fh:
|
||||
out.write(fh.read())
|
||||
|
||||
print("== creating fixture (first run only) ==")
|
||||
if fmt == "rar":
|
||||
# RAR needs WinRAR's rar.exe; 7-Zip cannot write the format.
|
||||
rar = RAR_BIN
|
||||
if not rar:
|
||||
print("WinRAR (rar.exe) not found; cannot create a RAR fixture")
|
||||
return None
|
||||
add = ["a", "-m3", "-idq", "-ep1"]
|
||||
subprocess.run([rar] + add + [archive, payload], cwd=REPO,
|
||||
stdout=subprocess.DEVNULL)
|
||||
else:
|
||||
if fmt == "7z":
|
||||
add = ["a", "-t7z", "-m0=lzma2", "-mx=5", "-ms=on"]
|
||||
else:
|
||||
add = ["a", "-tzip", "-mx=5", "-mm=Deflate"]
|
||||
subprocess.run([SEVENZ_BIN] + add + [archive, payload], cwd=REPO,
|
||||
stdout=subprocess.DEVNULL)
|
||||
return archive
|
||||
|
||||
|
||||
def sample(argv, runs, workdir):
|
||||
"""Run argv `runs` times; return (best wall ms, best in-process ms|None).
|
||||
|
||||
Every run writes to its own path (`workdir0`, `workdir1`, ...) that does
|
||||
not exist yet. Reusing one output directory is not an option: publishing
|
||||
into a tree left by the previous run charges the rename step for the
|
||||
collision, and that alone moved the same archive from 0.77 s to 1.28 s.
|
||||
The placeholder `{out}` in argv marks where the run directory goes.
|
||||
|
||||
Nothing is deleted between runs either -- clearing these trees is a bulk
|
||||
delete this host blocks -- so `.build/bench/W*` does accumulate and is
|
||||
worth clearing by hand now and then.
|
||||
"""
|
||||
best = None
|
||||
internal = None
|
||||
for i in range(runs):
|
||||
# Forward slashes on purpose: the engines derive the destination's
|
||||
# parent with a '/' scan (they only ever see POSIX paths on the PS5),
|
||||
# so a Windows-style relative or absolute path is rejected outright.
|
||||
run_dir = ("%s%d" % (workdir, i)).replace("\\", "/")
|
||||
cmd = [arg.replace("{out}", run_dir) for arg in argv]
|
||||
started = time.perf_counter()
|
||||
proc = subprocess.run(cmd, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, cwd=REPO, timeout=1800)
|
||||
elapsed = (time.perf_counter() - started) * 1000.0
|
||||
if proc.returncode != 0:
|
||||
sys.stdout.write(proc.stdout.decode("utf-8", "replace")[:300])
|
||||
return None, None
|
||||
if best is None or elapsed < best:
|
||||
best = elapsed
|
||||
match = re.search(rb"wall\s*:\s*([0-9.]+)\s*s", proc.stdout)
|
||||
if match:
|
||||
value = float(match.group(1)) * 1000.0
|
||||
if internal is None or value < internal:
|
||||
internal = value
|
||||
return best, internal
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--big", action="store_true",
|
||||
help="4x fixture (~320 MiB) for accurate ratios")
|
||||
ap.add_argument("--runs", type=int, default=3)
|
||||
ap.add_argument("--format", choices=["7z", "zip", "rar"], default="7z",
|
||||
help="which engine to benchmark (default 7z)")
|
||||
args = ap.parse_args()
|
||||
|
||||
# For ZIP there is no useful SDK reference: the vendored SDK is 7z-only,
|
||||
# so the comparison is just ours versus the official binary.
|
||||
for path in (SEVENZ_BIN, SDK_BIN if args.format == "7z" else SEVENZ_BIN):
|
||||
if not os.path.exists(path):
|
||||
print("missing: %s" % path)
|
||||
return 1
|
||||
if not build_bench():
|
||||
return 1
|
||||
|
||||
archive = make_fixture(args.big, args.format)
|
||||
if not archive:
|
||||
return 1
|
||||
raw = os.path.getsize(archive)
|
||||
|
||||
print()
|
||||
print("archive : %s (%.0f MiB packed), best of %d runs"
|
||||
% (os.path.basename(archive), raw / 1048576.0, args.runs))
|
||||
print()
|
||||
|
||||
rows = []
|
||||
wall, inner = sample([BENCH_BIN, archive, "{out}"], args.runs,
|
||||
os.path.join(BUILD, "W7"))
|
||||
if wall:
|
||||
rows.append(("ours / " + args.format, wall, inner))
|
||||
|
||||
if args.format == "7z":
|
||||
wall, _ = sample([SDK_BIN, archive, "{out}"], args.runs,
|
||||
os.path.join(BUILD, "W8"))
|
||||
if wall:
|
||||
rows.append(("sdk SzArEx", wall, None))
|
||||
variants = (("7za 1 thread", ["-mmt=off"]),
|
||||
("7za 8 threads", ["-mmt=8"]),
|
||||
("7za all cores", []))
|
||||
elif args.format == "rar":
|
||||
# 7-Zip reads RAR, so it is a valid cross-check on the same archive.
|
||||
variants = (("7za 1 thread", ["-mmt=off"]),
|
||||
("7za all cores", []))
|
||||
else:
|
||||
variants = (("7za 1 thread", ["-mmt=off"]),)
|
||||
|
||||
for label, extra in variants:
|
||||
wall, _ = sample([SEVENZ_BIN, "x", "-y", "-aoa"] + extra
|
||||
+ ["-o{out}", archive], args.runs,
|
||||
os.path.join(BUILD, "W9"))
|
||||
if wall:
|
||||
rows.append((label, wall, None))
|
||||
|
||||
if args.format == "rar" and RAR_BIN:
|
||||
# rar.exe treats the trailing separator as "this is the target
|
||||
# directory"; without it the path is parsed as a file mask and the
|
||||
# command reports that there is nothing to extract.
|
||||
wall, _ = sample([RAR_BIN, "x", "-y", "-o+", archive, "{out}/"],
|
||||
args.runs, os.path.join(BUILD, "W10"))
|
||||
if wall:
|
||||
rows.append(("winrar x", wall, None))
|
||||
|
||||
print(" %-18s %10s %12s" % ("configuration", "external", "internal"))
|
||||
for label, wall, inner in rows:
|
||||
print(" %-18s %9.0f ms %12s"
|
||||
% (label, wall,
|
||||
"%9.0f ms" % inner if inner else " -"))
|
||||
|
||||
tax = None
|
||||
ours = [r for r in rows if r[0].startswith("ours")]
|
||||
if ours and ours[0][2]:
|
||||
tax = ours[0][1] - ours[0][2]
|
||||
ref = ours[0][2]
|
||||
print()
|
||||
print(" spawn tax (ours external - internal): %.0f ms" % tax)
|
||||
print()
|
||||
print(" %-18s %12s %9s" % ("configuration", "net", "vs ours"))
|
||||
for label, wall, inner in rows:
|
||||
net = inner if inner else max(wall - tax, 1.0)
|
||||
print(" %-18s %9.0f ms %8.2fx" % (label, net, ref / net))
|
||||
print()
|
||||
print(" net = external minus spawn tax; >1x means faster than ours")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,131 @@
|
||||
/* Wall-clock benchmark for the extraction paths, ZIP, RAR and 7z.
|
||||
*
|
||||
* bench_extract <archive> <out-dir> [password]
|
||||
*
|
||||
* Reports how long the facade takes end to end -- decode, staging writes,
|
||||
* fsync, publish -- which is exactly what a PS5 user waits for. It is
|
||||
* deliberately separate from the correctness drivers: those assert on bytes,
|
||||
* this one only prints numbers, and it is not part of the test matrix.
|
||||
*
|
||||
* Keeping it in-tree matters because "is our engine fast?" is a question that
|
||||
* will come up again, and the answer should be a command anyone can rerun
|
||||
* rather than a number somebody remembers. The format is picked from the
|
||||
* suffix so the same binary covers all three engines.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "sevenz_extract.h"
|
||||
|
||||
#ifndef BENCH_NO_RAR
|
||||
#include "rar_extract.h"
|
||||
#endif
|
||||
|
||||
static int
|
||||
has_suffix(const char *path, const char *suffix) {
|
||||
size_t path_len;
|
||||
size_t suffix_len;
|
||||
|
||||
if(!path || !suffix) return 0;
|
||||
path_len = strlen(path);
|
||||
suffix_len = strlen(suffix);
|
||||
if(path_len < suffix_len) return 0;
|
||||
for(size_t i = 0; i < suffix_len; i++) {
|
||||
char a = path[path_len - suffix_len + i];
|
||||
char b = suffix[i];
|
||||
if(a >= 'A' && a <= 'Z') a = (char)(a - 'A' + 'a');
|
||||
if(b >= 'A' && b <= 'Z') b = (char)(b - 'A' + 'a');
|
||||
if(a != b) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static double
|
||||
now_seconds(void) {
|
||||
struct timespec ts;
|
||||
|
||||
if(timespec_get(&ts, TIME_UTC) != TIME_UTC) return 0.0;
|
||||
return (double)ts.tv_sec + (double)ts.tv_nsec / 1000000000.0;
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char **argv) {
|
||||
zipx_result_t result;
|
||||
zipx_status_t status;
|
||||
const char *archive;
|
||||
const char *out_dir;
|
||||
const char *password;
|
||||
const char *format;
|
||||
double started;
|
||||
double elapsed;
|
||||
double mebibytes;
|
||||
|
||||
if(argc < 3) {
|
||||
fprintf(stderr, "usage: %s <archive> <out-dir> [password]\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
archive = argv[1];
|
||||
out_dir = argv[2];
|
||||
password = argc > 3 ? argv[3] : NULL;
|
||||
|
||||
if(has_suffix(archive, ".7z") || has_suffix(archive, ".7z.001") ||
|
||||
has_suffix(archive, ".001")) {
|
||||
format = "7z";
|
||||
}
|
||||
#ifndef BENCH_SEVENZ_ONLY
|
||||
else if(has_suffix(archive, ".zip") || has_suffix(archive, ".zip.001") ||
|
||||
has_suffix(archive, ".z01")) {
|
||||
format = "zip";
|
||||
}
|
||||
#endif
|
||||
#ifndef BENCH_NO_RAR
|
||||
else if(has_suffix(archive, ".rar") || has_suffix(archive, ".part1.rar") ||
|
||||
has_suffix(archive, ".r00")) {
|
||||
format = "rar";
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
fprintf(stderr, "unsupported benchmark format: %s\n", archive);
|
||||
return 2;
|
||||
}
|
||||
|
||||
memset(&result, 0, sizeof(result));
|
||||
started = now_seconds();
|
||||
#ifndef BENCH_NO_RAR
|
||||
if(!strcmp(format, "rar")) {
|
||||
status = rar_extract(archive, out_dir, ZIPX_CONFLICT_OVERWRITE,
|
||||
zipx_default_limits(), NULL, NULL, NULL, &result);
|
||||
} else
|
||||
#endif
|
||||
#ifndef BENCH_SEVENZ_ONLY
|
||||
if(!strcmp(format, "zip")) {
|
||||
status = zipx_extract(archive, out_dir, ZIPX_CONFLICT_OVERWRITE,
|
||||
zipx_default_limits(), NULL, NULL, NULL, &result);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
status = sevenz_extract(archive, out_dir, ZIPX_CONFLICT_OVERWRITE,
|
||||
zipx_default_limits(), NULL, NULL, NULL,
|
||||
password, &result);
|
||||
}
|
||||
elapsed = now_seconds() - started;
|
||||
|
||||
mebibytes = (double)result.bytes_total / (1024.0 * 1024.0);
|
||||
|
||||
printf("format : %s\n", format);
|
||||
printf("status : %s\n", zipx_status_string(status));
|
||||
printf("entries : %llu\n", (unsigned long long)result.entries_total);
|
||||
printf("unpacked : %.1f MiB\n", mebibytes);
|
||||
printf("wall : %.3f s\n", elapsed);
|
||||
if(elapsed > 0.0) {
|
||||
printf("through : %.1f MiB/s\n", mebibytes / elapsed);
|
||||
}
|
||||
if(status != ZIPX_OK) {
|
||||
printf("detail : %s\n", result.detail[0] ? result.detail : "(none)");
|
||||
printf("message : %s\n", result.message[0] ? result.message : "(none)");
|
||||
}
|
||||
return status == ZIPX_OK ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cross-format extraction benchmark: same payload, three containers.
|
||||
|
||||
python tests/bench_formats.py # one 329 MiB file, 3 runs
|
||||
python tests/bench_formats.py --source .build/bench/payload_bin.bin \\
|
||||
--stem bin --runs 5 # 39 MiB of machine code
|
||||
python tests/bench_formats.py --source .build/bench/manyfiles_src --stem mf
|
||||
|
||||
bench_driver.py answers "how do we compare with 7-Zip for one format"; this
|
||||
answers "which container should a user expect to unpack fastest", which is a
|
||||
different question and needs the three archives to hold the same bytes.
|
||||
|
||||
Method notes that took a while to get right, so they are pinned here:
|
||||
|
||||
* Fresh output directory per run. Publishing on top of the previous run's
|
||||
tree took the same 7z archive from 0.77 s to 1.28 s.
|
||||
* Forward slashes in every path. The engines derive the destination parent
|
||||
with a '/' scan, so "C:\\...\\W0" is rejected as an invalid destination.
|
||||
* Best of N, because a single run varies widely on this host: extraction
|
||||
creates hundreds of MiB that the on-access scanner inspects and the page
|
||||
cache has to write back, and neither is under our control.
|
||||
* Corpus matters as much as container. Deflate decodes faster than LZMA2 on
|
||||
ordinary data, but on highly repetitive text LZMA2 finds long matches
|
||||
where deflate only finds 32 KiB ones, and the order flips. Run the same
|
||||
archive set on more than one source before concluding anything.
|
||||
|
||||
Packed sizes are printed next to the times on purpose: a container that packs
|
||||
5x smaller also reads 5x less from the card, which is why the ranking on the
|
||||
PS5 -- where storage is the slow part -- can differ from the ranking here.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
BUILD = os.path.join(REPO, ".build", "bench")
|
||||
BENCH_BIN = os.path.join(BUILD, "bench_extract.exe")
|
||||
SEVENZ_BIN = os.path.join(REPO, ".build", "7zdl", "extra", "x64", "7za.exe")
|
||||
RAR_BIN = next((p for p in (
|
||||
os.environ.get("WFM_RAR"),
|
||||
r"C:\Program Files\WinRAR\rar.exe",
|
||||
r"C:\Program Files (x86)\WinRAR\rar.exe",
|
||||
"/usr/bin/rar", "/usr/local/bin/rar") if p and os.path.exists(p)), None)
|
||||
DEFAULT_SOURCE = os.path.join(BUILD, "payload4.bin")
|
||||
|
||||
# Nominal level 5 in both packers, which is the GUI default of each:
|
||||
# 7-Zip -mx=5, WinRAR -m3 ("Normal"). They are not equivalent amounts of
|
||||
# work -- LZMA2 at level 5 is a far stronger compressor than deflate at 5 --
|
||||
# but they are what a user who never opens the advanced panel ends up with.
|
||||
FORMATS = ("zip", "7z", "rar")
|
||||
|
||||
|
||||
def pack(stem, payload):
|
||||
"""Create stem.{zip,7z,rar} from payload if they are not there yet."""
|
||||
made = []
|
||||
for fmt in FORMATS:
|
||||
archive = os.path.join(BUILD, "%s.%s" % (stem, fmt))
|
||||
if os.path.exists(archive):
|
||||
continue
|
||||
if fmt == "rar":
|
||||
if not RAR_BIN:
|
||||
print("skip %s: WinRAR (rar.exe) not found" % fmt)
|
||||
continue
|
||||
cmd = [RAR_BIN, "a", "-m3", "-idq"]
|
||||
if os.path.isfile(payload):
|
||||
cmd.append("-ep1")
|
||||
cmd += [archive, payload]
|
||||
else:
|
||||
cmd = [SEVENZ_BIN, "a", "-t" + fmt] + (
|
||||
["-m0=lzma2", "-mx=5", "-ms=on"] if fmt == "7z"
|
||||
else ["-mx=5", "-mm=Deflate"]) + [archive, payload]
|
||||
print("== packing %s ==" % os.path.basename(archive))
|
||||
if subprocess.run(cmd, cwd=REPO, stdout=subprocess.DEVNULL).returncode:
|
||||
print("packing failed")
|
||||
return None
|
||||
made.append(archive)
|
||||
return [os.path.join(BUILD, "%s.%s" % (stem, f)) for f in FORMATS]
|
||||
|
||||
|
||||
def time_archive(archive, runs):
|
||||
"""Best in-process wall time over `runs` fresh-directory extractions."""
|
||||
tag = os.path.splitext(os.path.basename(archive))[0]
|
||||
times = []
|
||||
unpacked = entries = 0
|
||||
for i in range(runs):
|
||||
# A path that does not exist yet: the engines publish with a rename,
|
||||
# and a rename into a tree that already has the file costs extra.
|
||||
out = os.path.join(BUILD, "F-%s-%d" % (tag, i)).replace("\\", "/")
|
||||
started = time.perf_counter()
|
||||
proc = subprocess.run([BENCH_BIN, archive, out], cwd=REPO,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, timeout=1800)
|
||||
external = time.perf_counter() - started
|
||||
if proc.returncode != 0:
|
||||
sys.stdout.write(proc.stdout.decode("utf-8", "replace")[:400])
|
||||
return None
|
||||
text = proc.stdout.decode("utf-8", "replace")
|
||||
match = re.search(r"wall\s*:\s*([0-9.]+)", text)
|
||||
if not match:
|
||||
return None
|
||||
times.append(float(match.group(1)))
|
||||
unpacked = float(re.search(r"unpacked\s*:\s*([0-9.]+)", text).group(1))
|
||||
entries = int(re.search(r"entries\s*:\s*(\d+)", text).group(1))
|
||||
return {"times": sorted(times), "unpacked": unpacked, "entries": entries}
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--runs", type=int, default=3)
|
||||
ap.add_argument("--source", default=DEFAULT_SOURCE,
|
||||
help="file or directory to pack (default: %s)" % DEFAULT_SOURCE)
|
||||
ap.add_argument("--stem", help="archive base name (default: source basename)")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.exists(BENCH_BIN):
|
||||
print("missing %s -- run: python tests/bench_driver.py" % BENCH_BIN)
|
||||
return 1
|
||||
|
||||
payload = args.source
|
||||
if not os.path.isabs(payload):
|
||||
payload = os.path.join(REPO, payload)
|
||||
if not os.path.exists(payload):
|
||||
print("missing payload: %s" % payload)
|
||||
return 1
|
||||
stem = args.stem or os.path.splitext(os.path.basename(payload))[0]
|
||||
|
||||
archives = pack(stem, payload)
|
||||
if not archives:
|
||||
return 1
|
||||
|
||||
rows = []
|
||||
for archive in archives:
|
||||
if not os.path.exists(archive):
|
||||
continue
|
||||
result = time_archive(archive, args.runs)
|
||||
if result is None:
|
||||
print("failed: %s" % archive)
|
||||
continue
|
||||
rows.append((os.path.splitext(archive)[1][1:], archive, result))
|
||||
|
||||
if not rows:
|
||||
return 1
|
||||
|
||||
print()
|
||||
print("best of %d runs, fresh output directory each time" % args.runs)
|
||||
print("payload: %d entries, %.1f MiB unpacked"
|
||||
% (rows[0][2]["entries"], rows[0][2]["unpacked"]))
|
||||
print()
|
||||
print(" %-5s %10s %9s %12s %12s" %
|
||||
("fmt", "packed", "best", "median", "throughput"))
|
||||
best = min(r[2]["times"][0] for r in rows)
|
||||
for fmt, archive, result in sorted(rows, key=lambda r: r[2]["times"][0]):
|
||||
packed = os.path.getsize(archive) / 1048576.0
|
||||
times = result["times"]
|
||||
median = times[len(times) // 2]
|
||||
rate = result["unpacked"] / times[0]
|
||||
print(" %-5s %8.1f M %7.0f ms %9.0f ms %8.0f MiB/s"
|
||||
% (fmt, packed, times[0] * 1000.0, median * 1000.0, rate))
|
||||
print()
|
||||
for fmt, archive, result in sorted(rows, key=lambda r: r[2]["times"][0]):
|
||||
print(" %-5s vs fastest: %.2fx" % (fmt, result["times"][0] / best))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,161 @@
|
||||
/* Prints every extraction progress report, with a monotonic timestamp.
|
||||
*
|
||||
* bench_progress <archive> <out-dir> [--mode print|empty|none]
|
||||
*
|
||||
* bench_extract answers "how long"; this answers "did the UI move while it
|
||||
* took that long". A report that sits at entries=0/N for minutes reads to the
|
||||
* user as a hang even though bytes are still flowing, so being able to see the
|
||||
* phase/entries/bytes sequence is what turns "the progress bar freezes" into a
|
||||
* named phase. Format is picked from the suffix, same as bench_extract.
|
||||
*
|
||||
* --mode exists to answer a second question: "does reporting itself cost
|
||||
* time?". The engine's report() helper runs on the hot path -- unrar calls it
|
||||
* once per decompressed chunk -- and it reads the clock before it decides
|
||||
* whether to throttle, so the cost is paid even when no report goes out.
|
||||
* print a callback that formats and prints every report (diagnostic)
|
||||
* empty a callback that returns immediately (engine cost, no consumer)
|
||||
* none no callback at all (the engine skips report() entirely)
|
||||
* Comparing wall time across the three separates "our bookkeeping" from
|
||||
* "the file I/O we cannot avoid".
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "zip_extract.h"
|
||||
#include "rar_extract.h"
|
||||
#include "sevenz_extract.h"
|
||||
|
||||
static double g_t0;
|
||||
|
||||
static int
|
||||
has_suffix(const char *path, const char *suffix) {
|
||||
size_t path_len, suffix_len, i;
|
||||
|
||||
if(!path || !suffix) return 0;
|
||||
path_len = strlen(path);
|
||||
suffix_len = strlen(suffix);
|
||||
if(path_len < suffix_len) return 0;
|
||||
for(i = 0; i < suffix_len; i++) {
|
||||
char a = path[path_len - suffix_len + i];
|
||||
char b = suffix[i];
|
||||
|
||||
if(a >= 'A' && a <= 'Z') a = (char)(a - 'A' + 'a');
|
||||
if(b >= 'A' && b <= 'Z') b = (char)(b - 'A' + 'a');
|
||||
if(a != b) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static double
|
||||
now_seconds(void) {
|
||||
struct timespec ts;
|
||||
|
||||
if(timespec_get(&ts, TIME_UTC) != TIME_UTC) return 0.0;
|
||||
return (double)ts.tv_sec + (double)ts.tv_nsec / 1000000000.0;
|
||||
}
|
||||
|
||||
static const char *
|
||||
phase_name(int phase) {
|
||||
switch(phase) {
|
||||
case ZIPX_PHASE_SCAN: return "scan";
|
||||
case ZIPX_PHASE_EXTRACT: return "extract";
|
||||
case ZIPX_PHASE_PUBLISH: return "publish";
|
||||
case ZIPX_PHASE_CLEANUP: return "cleanup";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
/* Costs exactly what the engine's report() costs, without a consumer. */
|
||||
static void
|
||||
on_progress_empty(void *userdata, const zipx_progress_t *p) {
|
||||
(void)userdata;
|
||||
(void)p;
|
||||
}
|
||||
|
||||
static void
|
||||
on_progress(void *userdata, const zipx_progress_t *p) {
|
||||
(void)userdata;
|
||||
printf("%8.3f %-7s entries=%llu/%llu bytes=%llu/%llu %s\n",
|
||||
now_seconds() - g_t0, phase_name(p->phase),
|
||||
(unsigned long long)p->entries_done,
|
||||
(unsigned long long)p->entries_total,
|
||||
(unsigned long long)p->bytes_done,
|
||||
(unsigned long long)p->bytes_total,
|
||||
p->current ? p->current : "");
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char **argv) {
|
||||
zipx_result_t result;
|
||||
zipx_status_t status;
|
||||
const char *archive;
|
||||
const char *out_dir;
|
||||
const char *format;
|
||||
const char *mode = "print";
|
||||
zipx_progress_fn on_report = on_progress;
|
||||
double elapsed;
|
||||
int i;
|
||||
|
||||
if(argc < 3) {
|
||||
fprintf(stderr, "usage: %s <archive> <out-dir> [--mode print|empty|none]\n",
|
||||
argv[0]);
|
||||
return 2;
|
||||
}
|
||||
archive = argv[1];
|
||||
out_dir = argv[2];
|
||||
|
||||
for(i = 3; i < argc; i++) {
|
||||
if(!strcmp(argv[i], "--mode") && i + 1 < argc) {
|
||||
mode = argv[++i];
|
||||
} else {
|
||||
fprintf(stderr, "unknown argument: %s\n", argv[i]);
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
if(!strcmp(mode, "empty")) {
|
||||
on_report = on_progress_empty;
|
||||
} else if(!strcmp(mode, "none")) {
|
||||
on_report = NULL;
|
||||
} else if(strcmp(mode, "print")) {
|
||||
fprintf(stderr, "unknown --mode: %s\n", mode);
|
||||
return 2;
|
||||
}
|
||||
|
||||
if(has_suffix(archive, ".rar") || has_suffix(archive, ".part1.rar") ||
|
||||
has_suffix(archive, ".r00")) {
|
||||
format = "rar";
|
||||
} else if(has_suffix(archive, ".zip") || has_suffix(archive, ".z01") ||
|
||||
has_suffix(archive, ".zip.001")) {
|
||||
format = "zip";
|
||||
} else {
|
||||
format = "7z";
|
||||
}
|
||||
|
||||
memset(&result, 0, sizeof(result));
|
||||
g_t0 = now_seconds();
|
||||
|
||||
if(!strcmp(format, "rar")) {
|
||||
status = rar_extract(archive, out_dir, ZIPX_CONFLICT_OVERWRITE,
|
||||
zipx_default_limits(), NULL, on_report, NULL,
|
||||
&result);
|
||||
} else if(!strcmp(format, "zip")) {
|
||||
status = zipx_extract(archive, out_dir, ZIPX_CONFLICT_OVERWRITE,
|
||||
zipx_default_limits(), NULL, on_report, NULL,
|
||||
&result);
|
||||
} else {
|
||||
status = sevenz_extract(archive, out_dir, ZIPX_CONFLICT_OVERWRITE,
|
||||
zipx_default_limits(), NULL, on_report, NULL,
|
||||
NULL, &result);
|
||||
}
|
||||
|
||||
elapsed = now_seconds() - g_t0;
|
||||
printf("---- done: format=%s mode=%s status=%s entries=%llu bytes=%llu wall=%.3fs\n",
|
||||
format, mode, zipx_status_string(status),
|
||||
(unsigned long long)result.entries_total,
|
||||
(unsigned long long)result.bytes_total, elapsed);
|
||||
return status == ZIPX_OK ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/* Standalone big-file e2e: extract one large zip64 archive on the host and
|
||||
* report the engine result. Verification (size + sha256/cmp) is done by the
|
||||
* caller with shell tools.
|
||||
*
|
||||
* cc -O2 -Isrc -Ithird_party/minizip-ng/include \
|
||||
* -include tests/posix_compat.h \
|
||||
* -o bigfile_e2e bigfile_e2e.c zip_extract.o <minizip+zlib objs>
|
||||
*
|
||||
* ./bigfile_e2e <archive.zip> <out-dir>
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "zip_extract.h"
|
||||
|
||||
int
|
||||
main(int argc, char **argv) {
|
||||
zipx_result_t res;
|
||||
zipx_status_t st;
|
||||
|
||||
if(argc != 3) {
|
||||
fprintf(stderr, "usage: %s <archive.zip> <out-dir>\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
st = zipx_extract(argv[1], argv[2], ZIPX_CONFLICT_FAIL,
|
||||
zipx_limits_profile(ZIPX_LIMITS_LARGE),
|
||||
NULL, NULL, NULL, &res);
|
||||
printf("status=%d (%s)\n", (int)st, zipx_status_string(st));
|
||||
printf("sys_errno=%d entries=%llu/%llu files=%llu dirs=%llu\n",
|
||||
res.sys_errno, (unsigned long long)res.entries_done,
|
||||
(unsigned long long)res.entries_total,
|
||||
(unsigned long long)res.files_created,
|
||||
(unsigned long long)res.dirs_created);
|
||||
printf("bytes_total=%llu detail=%s\n",
|
||||
(unsigned long long)res.bytes_total, res.detail);
|
||||
if(res.message[0]) {
|
||||
printf("message=%s\n", res.message);
|
||||
}
|
||||
return st == ZIPX_OK ? 0 : 1;
|
||||
}
|
||||
@@ -18,16 +18,26 @@ static int
|
||||
statvfs(const char *path, struct statvfs *buf) {
|
||||
ULARGE_INTEGER total;
|
||||
ULARGE_INTEGER free_bytes;
|
||||
char full[MAX_PATH];
|
||||
char root[8];
|
||||
|
||||
snprintf(root, sizeof(root), "%.3s", path);
|
||||
/* Resolve to an absolute path first: the drive-letter extraction below
|
||||
only works for "X:\..." style paths, and callers may pass relative
|
||||
paths (e.g. the standalone bigfile_e2e driver). */
|
||||
if(!GetFullPathNameA(path, (DWORD)sizeof(full), full, NULL)) {
|
||||
return -1;
|
||||
}
|
||||
snprintf(root, sizeof(root), "%.3s", full);
|
||||
if(!GetDiskFreeSpaceExA(root, &free_bytes, &total, NULL)) {
|
||||
return -1;
|
||||
}
|
||||
memset(buf, 0, sizeof(*buf));
|
||||
buf->f_bsize = 1;
|
||||
buf->f_frsize = 1;
|
||||
buf->f_bavail = free_bytes.QuadPart;
|
||||
/* `unsigned long` is 32-bit on Windows: store free space scaled by 4096
|
||||
so archives up to 16 TiB don't overflow (real 64-bit hosts are LP64
|
||||
and unaffected; PS5 SDK is LP64 too). */
|
||||
buf->f_bsize = 4096;
|
||||
buf->f_frsize = 4096;
|
||||
buf->f_bavail = free_bytes.QuadPart / 4096;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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.
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,82 @@
|
||||
@echo off
|
||||
REM Generate real RAR fixtures for the v1.9 host test suite.
|
||||
REM Requires WinRAR command-line Rar.exe (ships with normal WinRAR install).
|
||||
REM Outputs into tests\fixtures-real\ - kept OUT of tests\fixtures\ because
|
||||
REM tests\make_fixtures.py wipes that directory on every test run.
|
||||
REM Rerun any time fixture needs change. NOTE: keep this file pure ASCII.
|
||||
setlocal EnableDelayedExpansion
|
||||
|
||||
set RAREXE=
|
||||
if exist "%ProgramFiles%\WinRAR\Rar.exe" set RAREXE=%ProgramFiles%\WinRAR\Rar.exe
|
||||
if exist "%ProgramFiles(x86)%\WinRAR\Rar.exe" set RAREXE=%ProgramFiles(x86)%\WinRAR\Rar.exe
|
||||
if exist "%~dp0Rar.exe" set RAREXE=%~dp0Rar.exe
|
||||
if "%RAREXE%"=="" (
|
||||
echo [ERROR] Rar.exe not found. Install WinRAR or drop Rar.exe next to this script.
|
||||
exit /b 1
|
||||
)
|
||||
echo Using: %RAREXE%
|
||||
"%RAREXE%" 2>nul | findstr /c:"RAR " >nul || (echo [ERROR] %RAREXE% does not look like WinRAR & exit /b 1)
|
||||
|
||||
set FIX=%~dp0fixtures-real
|
||||
if exist "%FIX%" rmdir /s /q "%FIX%"
|
||||
mkdir "%FIX%"
|
||||
set STAGE=%~dp0fixture-stage
|
||||
if exist "%STAGE%" rmdir /s /q "%STAGE%"
|
||||
mkdir "%STAGE%\dir" 2>nul
|
||||
|
||||
echo ############### > "%STAGE%\root.txt"
|
||||
echo rar v1.9 fixture root content >> "%STAGE%\root.txt"
|
||||
echo nested payload line one > "%STAGE%\dir\nested.txt"
|
||||
echo nested payload line two >> "%STAGE%\dir\nested.txt"
|
||||
echo file-b 0102030405060708090a > "%STAGE%\b.bin"
|
||||
|
||||
REM Work from inside the stage dir with RELATIVE names so the archive keeps
|
||||
REM the dir\ structure (no -ep1 stripping).
|
||||
pushd "%STAGE%"
|
||||
|
||||
echo.
|
||||
echo [1/4] RAR5 v6 single volume - basic-v6.rar
|
||||
"%RAREXE%" a -m2 -ma5 -idq "%FIX%\basic-v6.rar" root.txt dir\nested.txt b.bin
|
||||
if errorlevel 1 echo [FAIL] & goto :badpop
|
||||
|
||||
echo [2/4] RAR5 v6 multi-volume - vol.part1.rar + vol.part2.rar + ...
|
||||
REM Create a 512 KB incompressible payload so -v200k actually splits it.
|
||||
fsutil file createnew big.bin 524288 >nul 2>nul
|
||||
if not exist big.bin (
|
||||
echo [WARN] fsutil unavailable - cannot force multi-volume; skipping
|
||||
goto :aftervol
|
||||
)
|
||||
REM rar a updates an existing archive instead of re-splitting it, so any
|
||||
REM stale volume files must be removed first. Target name is plain
|
||||
REM "vol.rar": RAR5 numbering then yields vol.part1/2/3.rar (naming the
|
||||
REM target vol.part1.rar would double up to vol.part1.partN.rar).
|
||||
del "%FIX%\vol*.rar" 2>nul
|
||||
"%RAREXE%" a -m0 -ma5 -v200k -idq "%FIX%\vol.rar" big.bin root.txt
|
||||
if errorlevel 1 echo [FAIL] & goto :badpop
|
||||
echo wrote: & dir /b "%FIX%\vol.part*.rar" 2>nul
|
||||
:aftervol
|
||||
|
||||
echo [3/4] RAR5 v6 encrypted (password: secret123) - enc-v6.rar
|
||||
"%RAREXE%" a -m2 -ma5 -psecret123 -idq "%FIX%\enc-v6.rar" root.txt dir\nested.txt
|
||||
if errorlevel 1 echo [FAIL] & goto :badpop
|
||||
|
||||
echo [4/4] RAR4 legacy (optional - some WinRAR builds dropped RAR4 writing)
|
||||
"%RAREXE%" a -m2 -ma4 -idq "%FIX%\basic-rar4.rar" root.txt dir\nested.txt
|
||||
if errorlevel 1 (
|
||||
echo [WARN] RAR4 creation not supported by this Rar.exe - skipping basic-rar4.rar
|
||||
) else (
|
||||
echo basic-rar4.rar written
|
||||
)
|
||||
|
||||
popd
|
||||
rmdir /s /q "%STAGE%" 2>nul
|
||||
echo.
|
||||
echo OK. Fixtures written to %FIX%:
|
||||
dir /b "%FIX%\*.rar" 2>nul
|
||||
exit /b 0
|
||||
|
||||
:badpop
|
||||
popd
|
||||
:bad
|
||||
echo [ERROR] WinRAR command failed. Is this WinRAR 5+ with command line support?
|
||||
exit /b 1
|
||||
@@ -0,0 +1,258 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate real .7z fixtures for the host test suite.
|
||||
|
||||
The fixtures are produced by an actual 7-Zip binary so that the archives the
|
||||
engine has to read are genuine, not hand-rolled. The tool is looked up in this
|
||||
order:
|
||||
|
||||
1. ``--tool <path>``
|
||||
2. ``$SEVENZ_TOOL``
|
||||
3. the full ``7za.exe`` from the "7-Zip Extra" package under ``.build/7zdl/``
|
||||
4. the reduced ``7zr.exe`` under ``.build/7zdl/``
|
||||
5. ``7z`` / ``7zr`` / ``7za`` on PATH
|
||||
|
||||
Both binaries come from https://github.com/ip7z/7zip/releases (7-Zip is public
|
||||
domain). The reduced ``7zr.exe`` has **no PPMd encoder**, so that fixture is
|
||||
skipped (with a warning) unless a full build is available.
|
||||
|
||||
Output goes to ``tests/fixtures-7z/``. Only files this script owns are removed
|
||||
on a re-run (identified by an ``OWN_`` prefix list), never the whole directory.
|
||||
|
||||
python tests/make_sevenz_fixtures.py [--big]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(HERE)
|
||||
OUT = os.path.join(HERE, "fixtures-7z")
|
||||
SRC = os.path.join(OUT, "_src")
|
||||
|
||||
# Everything this script may delete on a re-run.
|
||||
OWN_PREFIXES = (
|
||||
"store", "lzma", "lzma2", "ppmd", "bcj", "delta", "aes", "vol", "utf8",
|
||||
"big", "empty", "_src",
|
||||
)
|
||||
|
||||
PASSWORD = "Secret123"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# tool discovery / process helpers
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def find_tool(explicit: str | None) -> str:
|
||||
if explicit:
|
||||
if os.path.exists(explicit):
|
||||
return explicit
|
||||
raise SystemExit(f"7z tool not found: {explicit}")
|
||||
|
||||
env = os.environ.get("SEVENZ_TOOL")
|
||||
if env and os.path.exists(env):
|
||||
return env
|
||||
|
||||
# Prefer the full 7za.exe (has PPMd); fall back to the reduced 7zr.exe.
|
||||
for rel in ("7zdl/extra/x64/7za.exe", "7zdl/extra/7za.exe", "7zdl/7zr.exe"):
|
||||
local = os.path.join(ROOT, ".build", rel.replace("/", os.sep))
|
||||
if os.path.exists(local):
|
||||
return local
|
||||
|
||||
for name in ("7z", "7zr", "7za"):
|
||||
found = shutil.which(name)
|
||||
if found:
|
||||
return found
|
||||
|
||||
raise SystemExit(
|
||||
"no 7z tool found; pass --tool, set SEVENZ_TOOL, or drop 7zr.exe into "
|
||||
".build/7zdl/"
|
||||
)
|
||||
|
||||
|
||||
def run(tool: str, args: list[str], optional: bool = False) -> bool:
|
||||
"""Run the 7z tool. Returns True on success; raises unless ``optional``."""
|
||||
proc = subprocess.run(
|
||||
[tool] + args, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
||||
)
|
||||
if proc.returncode == 0:
|
||||
return True
|
||||
out = proc.stdout.decode("utf-8", "replace")
|
||||
# 7-Zip writes its diagnostics in the OEM code page; try the common ones.
|
||||
for enc in ("gbk", "cp936", "utf-8"):
|
||||
try:
|
||||
out = proc.stdout.decode(enc)
|
||||
break
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
if optional:
|
||||
print(f" !! skipped: {' '.join(args[:4])}\n {out.strip()[-200:]}")
|
||||
return False
|
||||
raise SystemExit(
|
||||
f"7z failed ({proc.returncode}): {tool} {' '.join(args)}\n{out}"
|
||||
)
|
||||
|
||||
|
||||
def win(path: str) -> str:
|
||||
"""7zr.exe is a native Windows binary: hand it a Windows path."""
|
||||
return path.replace("/", "\\")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# source tree
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def x86ish(size: int) -> bytes:
|
||||
"""Bytes that look like x86 code so BCJ/BCJ2 filters are actually useful."""
|
||||
out = bytearray()
|
||||
rnd = random.Random(1234)
|
||||
while len(out) < size:
|
||||
op = rnd.choice((0xE8, 0xE9, 0xE8, 0xE9, 0x0F, 0x8B, 0xC3, 0x90))
|
||||
out.append(op)
|
||||
if op in (0xE8, 0xE9):
|
||||
out += rnd.randrange(0, 1 << 24).to_bytes(4, "little")
|
||||
elif op == 0x0F:
|
||||
out.append(0x8B)
|
||||
else:
|
||||
out += bytes(rnd.randrange(256) for _ in range(rnd.randrange(1, 6)))
|
||||
return bytes(out[:size])
|
||||
|
||||
|
||||
def build_source(sizes: dict[str, int]) -> None:
|
||||
os.makedirs(os.path.join(SRC, "sub"), exist_ok=True)
|
||||
rnd = random.Random(7)
|
||||
|
||||
with open(os.path.join(SRC, "readme.txt"), "wb") as fh:
|
||||
fh.write(b"sevenz fixture\n" * 20)
|
||||
|
||||
with open(os.path.join(SRC, "binary.bin"), "wb") as fh:
|
||||
fh.write(bytes(rnd.randrange(256) for _ in range(sizes["binary"])))
|
||||
|
||||
# Highly compressible: exercises the LZMA/LZMA2/PPMd long-range paths.
|
||||
with open(os.path.join(SRC, "zeros.bin"), "wb") as fh:
|
||||
fh.write(b"\0" * sizes["zeros"])
|
||||
|
||||
with open(os.path.join(SRC, "sub", "nested.txt"), "wb") as fh:
|
||||
fh.write(b"nested entry\n" * 100)
|
||||
|
||||
with open(os.path.join(SRC, "sub", "code.bin"), "wb") as fh:
|
||||
fh.write(x86ish(sizes["code"]))
|
||||
|
||||
# Non-ASCII names must survive the UTF-16 name table round trip.
|
||||
with open(os.path.join(SRC, "sub", "\u4e2d\u6587-\u30c6\u30b9\u30c8.txt"), "wb") as fh:
|
||||
fh.write("unicode name\n".encode("utf-8") * 30)
|
||||
|
||||
|
||||
def write_big_file(path: str, size: int) -> None:
|
||||
"""A large, semi-compressible file: forces multi-chunk streaming decode."""
|
||||
rnd = random.Random(99)
|
||||
block = bytes(rnd.randrange(256) for _ in range(64 * 1024))
|
||||
with open(path, "wb") as fh:
|
||||
written = 0
|
||||
while written < size:
|
||||
n = min(len(block), size - written)
|
||||
fh.write(block[:n])
|
||||
written += n
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# archives
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
# name -> (extra 7z arguments, takes password, required)
|
||||
VARIANTS: list[tuple[str, list[str], bool, bool]] = [
|
||||
("store", ["-mx0"], False, True),
|
||||
("lzma2", ["-m0=lzma2", "-mx5"], False, True),
|
||||
("lzma", ["-m0=lzma"], False, True),
|
||||
# The reduced 7zr.exe has no PPMd encoder, so this one is best-effort.
|
||||
("ppmd", ["-m0=ppmd"], False, False),
|
||||
("bcj", ["-m0=bcj", "-m1=lzma2"], False, True),
|
||||
("delta", ["-m0=delta:4", "-m1=lzma2"], False, True),
|
||||
("bcj2", ["-m0=bcj2", "-m1=lzma2", "-m2=lzma2", "-m3=lzma2", "-m4=lzma2"], False, True),
|
||||
("aes", ["-m0=lzma2", "-mx5"], True, True),
|
||||
("utf8", ["-m0=lzma2", "-mx5"], False, True),
|
||||
# Not solid: one folder per file. That is what exercises the multi-folder
|
||||
# walk and the per-folder slice of the packed-stream table.
|
||||
("solidoff", ["-m0=lzma2", "-mx5", "-ms=off"], False, True),
|
||||
("bcj2off", ["-m0=bcj2", "-m1=lzma2", "-m2=lzma2", "-m3=lzma2",
|
||||
"-m4=lzma2", "-ms=off"], False, True),
|
||||
]
|
||||
|
||||
|
||||
def build_archives(tool: str, big: bool) -> None:
|
||||
for name, extra, secret, required in VARIANTS:
|
||||
arch = os.path.join(OUT, f"{name}.7z")
|
||||
if os.path.exists(arch):
|
||||
os.remove(arch)
|
||||
args = ["a", "-t7z", win(arch), win(SRC), "-y"] + extra
|
||||
if secret:
|
||||
args.append(f"-p{PASSWORD}")
|
||||
if name == "aes":
|
||||
# Header stays readable: only the streams are encrypted.
|
||||
args.append("-mhe=off")
|
||||
run(tool, args, optional=not required)
|
||||
|
||||
# Encrypted header (-mhe=on): the archive cannot even be listed without
|
||||
# the password, so the engine must ask for it up front.
|
||||
run(tool, ["a", "-t7z", win(os.path.join(OUT, "aeshe.7z")), win(SRC),
|
||||
"-y", "-m0=lzma2", "-mx5", f"-p{PASSWORD}", "-mhe=on"])
|
||||
|
||||
# Multi-volume (-v): 100 KiB parts force the fixture across several files.
|
||||
vols = os.path.join(OUT, "vol.7z")
|
||||
for stale in sorted(os.listdir(OUT)):
|
||||
if stale.startswith("vol.7z"):
|
||||
os.remove(os.path.join(OUT, stale))
|
||||
run(tool, ["a", "-t7z", win(vols), win(SRC), "-y", "-m0=lzma2", "-mx5",
|
||||
"-v100k"])
|
||||
|
||||
if big:
|
||||
bigsrc = os.path.join(OUT, "_big")
|
||||
os.makedirs(bigsrc, exist_ok=True)
|
||||
write_big_file(os.path.join(bigsrc, "big.bin"), 96 * 1024 * 1024)
|
||||
run(tool, ["a", "-t7z", win(os.path.join(OUT, "big.7z")), win(bigsrc),
|
||||
"-y", "-m0=lzma2", "-mx1"])
|
||||
shutil.rmtree(bigsrc, ignore_errors=True)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def clean() -> None:
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
for entry in os.listdir(OUT):
|
||||
if entry.startswith(OWN_PREFIXES):
|
||||
path = os.path.join(OUT, entry)
|
||||
if os.path.isdir(path):
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
else:
|
||||
os.remove(path)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--tool", help="path to 7zr.exe / 7z.exe / 7zr")
|
||||
ap.add_argument("--big", action="store_true",
|
||||
help="also build a ~96 MiB single-file archive")
|
||||
opts = ap.parse_args()
|
||||
|
||||
tool = find_tool(opts.tool)
|
||||
print(f"7z tool: {tool}")
|
||||
|
||||
clean()
|
||||
build_source({"binary": 300_000, "zeros": 2_000_000, "code": 400_000})
|
||||
build_archives(tool, opts.big)
|
||||
|
||||
print(f"fixtures written to {OUT}")
|
||||
for name in sorted(os.listdir(OUT)):
|
||||
path = os.path.join(OUT, name)
|
||||
if os.path.isfile(path):
|
||||
print(f" {name} ({os.path.getsize(path)} bytes)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -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()
|
||||
+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 */
|
||||
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env bash
|
||||
# Host test runner for the 7z engine.
|
||||
#
|
||||
# ./tests/run-sevenz-tests.sh
|
||||
#
|
||||
# Builds the vendored LZMA SDK subset plus the project's own folder decoder
|
||||
# (src/sevenz_chain.c), generates real .7z fixtures with a 7-Zip binary,
|
||||
# extracts every fixture through that decoder and compares the result byte for
|
||||
# byte against the source tree.
|
||||
#
|
||||
# On Windows this expects MinGW gcc in PATH and must be started with the MSYS
|
||||
# bash explicitly (`/usr/bin/bash tests/run-sevenz-tests.sh`) -- a bare `bash`
|
||||
# can resolve to C:\Windows\System32\bash.exe, i.e. the WSL launcher.
|
||||
|
||||
set -e
|
||||
|
||||
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}"
|
||||
|
||||
# Archives the engine cannot read yet. Each entry needs a reason; when one of
|
||||
# them starts passing the script says so, so the list cannot rot.
|
||||
KNOWN_GAPS="aeshe"
|
||||
# aeshe - the header itself is encrypted (-mhe=on). Reading it means
|
||||
# decrypting a standalone 7z stream *before* any folder is
|
||||
# known, i.e. a header parser of our own; the vendored SDK
|
||||
# refuses with SZ_ERROR_UNSUPPORTED before we are involved.
|
||||
|
||||
# Must match PASSWORD in tests/make_sevenz_fixtures.py.
|
||||
FIXTURE_PASSWORD="Secret123"
|
||||
|
||||
find "$BUILD" -maxdepth 1 -type f \( -name '*.o' -o -name '*.exe' \) -delete 2>/dev/null || true
|
||||
mkdir -p "$BUILD"
|
||||
|
||||
# ---------------------------------------------------------------- vendor
|
||||
CFLAGS_7Z=(-O2 -w -DZ7_PPMD_SUPPORT -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE
|
||||
-DNDEBUG -D_REENTRANT)
|
||||
|
||||
VENDOR_OBJS=()
|
||||
for src in "$SEVENZ_DIR"/*.c; do
|
||||
name="$(basename "$src" .c)"
|
||||
"$CC" -c "${CFLAGS_7Z[@]}" -o "$BUILD/$name.o" "$src"
|
||||
VENDOR_OBJS+=("$BUILD/$name.o")
|
||||
done
|
||||
|
||||
# The engine modules are held to the same strictness as the rest of src/.
|
||||
# sevenz_volstream reuses the ZIP side's volume-set detector, so zipx_volume
|
||||
# is built here as well.
|
||||
"$CC" -c -O2 -Wall -Wextra -Werror -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE \
|
||||
-I"$SEVENZ_DIR" -I"$ROOT/src" -o "$BUILD/sevenz_chain.o" \
|
||||
"$ROOT/src/sevenz_chain.c"
|
||||
"$CC" -c -O2 -Wall -Wextra -Werror -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE \
|
||||
-I"$SEVENZ_DIR" -I"$ROOT/src" -o "$BUILD/sevenz_volstream.o" \
|
||||
"$ROOT/src/sevenz_volstream.c"
|
||||
"$CC" -c -O2 -Wall -Wextra -Werror -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE \
|
||||
-I"$SEVENZ_DIR" -I"$ROOT/src" -o "$BUILD/sevenz_mt.o" "$ROOT/src/sevenz_mt.c"
|
||||
"$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/sevenz_mt.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=()
|
||||
case "$(uname -s)" in
|
||||
MINGW*|MSYS*|CYGWIN*) EXTRA_LIBS=(-lole32 -loleaut32 -luuid -ladvapi32 -luser32 -lshell32) ;;
|
||||
esac
|
||||
|
||||
"$CC" -O2 -w -I"$SEVENZ_DIR" -I"$ROOT/src" -o "$BUILD/sevenz_chain_e2e" \
|
||||
"$ROOT/tests/sevenz_chain_e2e.c" "${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" \
|
||||
"$ROOT/tests/sevenz_e2e.c" "${VENDOR_OBJS[@]}" "${EXTRA_LIBS[@]}"
|
||||
|
||||
# ---------------------------------------------------------------- fixtures
|
||||
if [ ! -f "$FIXTURES/lzma2.7z" ]; then
|
||||
echo "== generating 7z fixtures =="
|
||||
if ! "$PYTHON" "$ROOT/tests/make_sevenz_fixtures.py"; then
|
||||
echo "SKIP: no 7z tool available, cannot build fixtures" >&2
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------- matrix
|
||||
pass=0
|
||||
fail=0
|
||||
|
||||
is_gap() {
|
||||
for g in $KNOWN_GAPS; do
|
||||
[ "$g" = "$1" ] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
run_case() {
|
||||
local name="$1" archive="$2" password="${3:-}"
|
||||
local out log rc
|
||||
|
||||
# A fresh directory per case keeps the run repeatable without deleting a tree
|
||||
# of previous results, which guarded shells refuse to do.
|
||||
out="$(mktemp -d "$BUILD/out/XXXXXX")" || return
|
||||
log="$out.log"
|
||||
|
||||
rc=0
|
||||
if [ -n "$password" ]; then
|
||||
"$BUILD/sevenz_chain_e2e" "$archive" "$out" "$password" >"$log" 2>&1 || rc=$?
|
||||
else
|
||||
"$BUILD/sevenz_chain_e2e" "$archive" "$out" >"$log" 2>&1 || rc=$?
|
||||
fi
|
||||
|
||||
if [ "$rc" -eq 0 ] &&
|
||||
diff -r "$FIXTURES/_src" "$out/_src" >/dev/null 2>&1; then
|
||||
if is_gap "$name"; then
|
||||
printf ' %-12s GAP CLOSED (remove from KNOWN_GAPS)\n' "$name"
|
||||
fail=$((fail + 1))
|
||||
else
|
||||
printf ' %-12s ok\n' "$name"
|
||||
pass=$((pass + 1))
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
if is_gap "$name"; then
|
||||
printf ' %-12s known gap (%s)\n' "$name" "$(head -1 "$log")"
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
printf ' %-12s FAIL\n' "$name"
|
||||
sed -n '1,20p' "$log" | sed 's/^/ /'
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "== 7z fixture matrix (engine: src/sevenz_chain.c) =="
|
||||
for a in store lzma2 lzma ppmd bcj delta utf8 bcj2 solidoff bcj2off aes aeshe; do
|
||||
[ -f "$FIXTURES/$a.7z" ] || continue
|
||||
case "$a" in
|
||||
aes|aeshe) run_case "$a" "$FIXTURES/$a.7z" "$FIXTURE_PASSWORD" ;;
|
||||
*) run_case "$a" "$FIXTURES/$a.7z" ;;
|
||||
esac
|
||||
done
|
||||
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 ]
|
||||
+53
-12
@@ -14,12 +14,18 @@ BUILD="$ROOT/.build/host-test"
|
||||
PYTHON="${PYTHON:-python3}"
|
||||
CC="${CC:-gcc}"
|
||||
|
||||
rm -rf "$BUILD"
|
||||
# Clean previous build outputs without nuking the whole tree (avoids
|
||||
# bulk-delete guards); stale fixture copies in work-* dirs are fine because
|
||||
# make_fixtures.py rewrites fixtures/ and the suites recreate their workdirs.
|
||||
find "$BUILD" -maxdepth 1 -type f -name '*.o' -delete 2>/dev/null || true
|
||||
find "$BUILD" -maxdepth 1 -type f -name 'test-*' -delete 2>/dev/null || true
|
||||
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)
|
||||
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)
|
||||
HOST_KIND=posix
|
||||
# MinGW has no O_NOFOLLOW; the flag is only a host build workaround.
|
||||
@@ -39,9 +45,19 @@ for src in "$ROOT"/third_party/zlib/src/*.c "$ROOT"/third_party/minizip-ng/src/*
|
||||
"${MZ_CFLAGS[@]}" "${extra[@]}" -o "$BUILD/$name.o" "$src"
|
||||
done
|
||||
|
||||
# dmc_unrar (single-file; uses stdio fopen by default on non-Windows)
|
||||
"$CC" -c -O2 -w "${RAR_CFLAGS[@]}" \
|
||||
-o "$BUILD/dmc_unrar.o" "$ROOT/third_party/unrar/dmc_unrar.c"
|
||||
# unrar 7.20.1 (RARDLL source set; compiled with the host C++ compiler).
|
||||
UNRAR7_SRCS="$ROOT/third_party/unrar7"
|
||||
UNRAR7_CFLAGS=(-O2 -w -std=c++17 -DRARDLL -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE)
|
||||
CXX="${CXX:-g++}"
|
||||
for src in \
|
||||
archive arcread blake2s cmddata consio crc crypt dll encname errhnd extinfo \
|
||||
extract filcreat file filefn filestr find getbits global hash headers isnt \
|
||||
largepage match motw options pathfn qopen rar rarpch rarvm rawread rdwrfn \
|
||||
rijndael rs rs16 scantree secpassword sha1 sha256 smallfn strfn strlist \
|
||||
system threadpool timefn ui unicode unpack volume; do
|
||||
"$CXX" -c "${UNRAR7_CFLAGS[@]}" -o "$BUILD/unrar7_$src.o" \
|
||||
"$UNRAR7_SRCS/$src.cpp" || exit 1
|
||||
done
|
||||
|
||||
COMPAT_INC="$ROOT/tests/compat"
|
||||
|
||||
@@ -51,9 +67,24 @@ 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" \
|
||||
"${RAR_CFLAGS[@]}" -include "$ROOT/tests/posix_compat.h" \
|
||||
-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" \
|
||||
-o "$BUILD/rar_extract.o" "$ROOT/src/rar_extract.c"
|
||||
|
||||
"$CC" -c -O2 -Wall -Wextra -Wno-unused-parameter -I"$ROOT/src" \
|
||||
@@ -61,23 +92,33 @@ COMPAT_INC="$ROOT/tests/compat"
|
||||
-o "$BUILD/test_zip_extract.o" "$ROOT/tests/test_zip_extract.c"
|
||||
|
||||
"$CC" -c -O2 -Wall -Wextra -Wno-unused-parameter -I"$ROOT/src" \
|
||||
-I"$COMPAT_INC" "${RAR_CFLAGS[@]}" -include "$ROOT/tests/posix_compat.h" \
|
||||
-I"$COMPAT_INC" -include "$ROOT/tests/posix_compat.h" \
|
||||
-o "$BUILD/test_rar_extract.o" "$ROOT/tests/test_rar_extract.c"
|
||||
|
||||
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[@]}"
|
||||
|
||||
"$CC" -O2 -o "$BUILD/test-rar-extract" \
|
||||
# The RAR test links the unrar7 objects, so it needs the C++ driver.
|
||||
# Windows unrar system.cpp references SetSuspendState (PowrProf).
|
||||
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[@]}"
|
||||
"$BUILD/zip_extract.o" "$BUILD/zipx_common.o" "${objs[@]}" "${rar_objs[@]}" "${RAR_LIBS[@]}"
|
||||
|
||||
"$BUILD/test-zip-extract" "$ROOT/tests/fixtures" "$BUILD/work-zip"
|
||||
"$BUILD/test-rar-extract" "$ROOT/tests/fixtures" "$BUILD/work-rar"
|
||||
# Real RAR fixtures (v6 / multi-volume / encrypted) live in fixtures-real/,
|
||||
# generated by tests/make-rar-fixtures.bat (WinRAR required); fixtures/
|
||||
# itself is wiped by make_fixtures.py on every run.
|
||||
"$BUILD/test-rar-extract" "$ROOT/tests/fixtures" "$BUILD/work-rar" \
|
||||
"$ROOT/tests/fixtures-real"
|
||||
@@ -0,0 +1,491 @@
|
||||
/*
|
||||
* End-to-end driver for the project's own 7z folder decoder (src/sevenz_chain.c).
|
||||
*
|
||||
* sevenz_chain_e2e <archive.7z> <out-dir>
|
||||
*
|
||||
* Unlike tests/sevenz_e2e.c -- which drives the LZMA SDK's own CSzFolder based
|
||||
* path and therefore cannot handle BCJ2 or anything else the SDK caps at four
|
||||
* coders -- this one decodes *every* folder through src/sevenz_chain.c.
|
||||
*
|
||||
* It also mirrors the shape of the real extraction path: each folder is
|
||||
* decoded once, streamed to a sink, and the sink splits the byte stream across
|
||||
* the entries that live in that folder (that is what makes an archive solid).
|
||||
* Per-entry and per-folder CRCs are verified as the bytes go past.
|
||||
*
|
||||
* Exit status is non-zero if any folder or entry failed.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <direct.h>
|
||||
#include <wchar.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "7z.h"
|
||||
#include "7zAlloc.h"
|
||||
#include "7zCrc.h"
|
||||
#include "7zFile.h"
|
||||
|
||||
#include "sevenz_chain.h"
|
||||
#include "sevenz_volstream.h"
|
||||
|
||||
#define INPUT_BUF_SIZE (1u << 18)
|
||||
#define MAX_PATH_LEN 4096
|
||||
|
||||
static ISzAlloc g_alloc = { SzAlloc, SzFree };
|
||||
static ISzAlloc g_temp = { SzAlloc, SzFree };
|
||||
|
||||
static int g_failures = 0;
|
||||
|
||||
/* ---------------------------------------------------------------- paths */
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
static void utf8_to_utf16(const char *src, wchar_t *dst, size_t cap) {
|
||||
size_t out = 0;
|
||||
|
||||
while(*src && out + 2 < cap) {
|
||||
unsigned char c = (unsigned char)*src++;
|
||||
UInt32 cp;
|
||||
|
||||
if(c < 0x80) {
|
||||
cp = c;
|
||||
} else if((c & 0xE0) == 0xC0 && (src[0] & 0xC0) == 0x80) {
|
||||
cp = ((UInt32)(c & 0x1F) << 6) | (UInt32)(*src++ & 0x3F);
|
||||
} else if((c & 0xF0) == 0xE0 && (src[0] & 0xC0) == 0x80 &&
|
||||
(src[1] & 0xC0) == 0x80) {
|
||||
cp = ((UInt32)(c & 0x0F) << 12) | ((UInt32)(src[0] & 0x3F) << 6) |
|
||||
(UInt32)(src[1] & 0x3F);
|
||||
src += 2;
|
||||
} else if((c & 0xF8) == 0xF0 && (src[0] & 0xC0) == 0x80 &&
|
||||
(src[1] & 0xC0) == 0x80 && (src[2] & 0xC0) == 0x80) {
|
||||
cp = ((UInt32)(c & 0x07) << 18) | ((UInt32)(src[0] & 0x3F) << 12) |
|
||||
((UInt32)(src[1] & 0x3F) << 6) | (UInt32)(src[2] & 0x3F);
|
||||
src += 3;
|
||||
} else {
|
||||
cp = '?';
|
||||
}
|
||||
|
||||
if(cp >= 0x10000) {
|
||||
cp -= 0x10000;
|
||||
dst[out++] = (wchar_t)(0xD800 | (cp >> 10));
|
||||
dst[out++] = (wchar_t)(0xDC00 | (cp & 0x3FF));
|
||||
} else {
|
||||
dst[out++] = (wchar_t)cp;
|
||||
}
|
||||
}
|
||||
dst[out] = 0;
|
||||
}
|
||||
|
||||
static void path_mkdir(const char *path) {
|
||||
wchar_t wide[MAX_PATH_LEN];
|
||||
utf8_to_utf16(path, wide, MAX_PATH_LEN);
|
||||
_wmkdir(wide);
|
||||
}
|
||||
|
||||
static FILE *path_fopen_write(const char *path) {
|
||||
wchar_t wide[MAX_PATH_LEN];
|
||||
utf8_to_utf16(path, wide, MAX_PATH_LEN);
|
||||
return _wfopen(wide, L"wb");
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
static void path_mkdir(const char *path) { mkdir(path, 0755); }
|
||||
static FILE *path_fopen_write(const char *path) { return fopen(path, "wb"); }
|
||||
|
||||
#endif
|
||||
|
||||
static void make_dirs(const char *path) {
|
||||
char tmp[MAX_PATH_LEN];
|
||||
size_t i, n = strlen(path);
|
||||
|
||||
if(n + 1 > sizeof(tmp)) return;
|
||||
memcpy(tmp, path, n + 1);
|
||||
|
||||
for(i = 1; i < n; i++) {
|
||||
if(tmp[i] == '/' || tmp[i] == '\\') {
|
||||
char c = tmp[i];
|
||||
tmp[i] = 0;
|
||||
path_mkdir(tmp);
|
||||
tmp[i] = c;
|
||||
}
|
||||
}
|
||||
path_mkdir(tmp);
|
||||
}
|
||||
|
||||
/* UTF-16 (LE, as stored by the 7z name table) to UTF-8. */
|
||||
static void utf16_to_utf8(const UInt16 *src, char *dst, size_t dst_size) {
|
||||
size_t out = 0;
|
||||
|
||||
while(*src) {
|
||||
UInt32 c = *src++;
|
||||
|
||||
if(c >= 0xD800 && c <= 0xDBFF && *src >= 0xDC00 && *src <= 0xDFFF)
|
||||
c = 0x10000 + ((c - 0xD800) << 10) + (*src++ - 0xDC00);
|
||||
if(out + 5 >= dst_size) break;
|
||||
|
||||
if(c < 0x80) {
|
||||
dst[out++] = (char)c;
|
||||
} else if(c < 0x800) {
|
||||
dst[out++] = (char)(0xC0 | (c >> 6));
|
||||
dst[out++] = (char)(0x80 | (c & 0x3F));
|
||||
} else if(c < 0x10000) {
|
||||
dst[out++] = (char)(0xE0 | (c >> 12));
|
||||
dst[out++] = (char)(0x80 | ((c >> 6) & 0x3F));
|
||||
dst[out++] = (char)(0x80 | (c & 0x3F));
|
||||
} else {
|
||||
dst[out++] = (char)(0xF0 | (c >> 18));
|
||||
dst[out++] = (char)(0x80 | ((c >> 12) & 0x3F));
|
||||
dst[out++] = (char)(0x80 | ((c >> 6) & 0x3F));
|
||||
dst[out++] = (char)(0x80 | (c & 0x3F));
|
||||
}
|
||||
}
|
||||
dst[out] = 0;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- reader */
|
||||
|
||||
typedef struct {
|
||||
ISeekInStream *stream;
|
||||
UInt64 base; /* db.dataPos: packed offsets are relative to it */
|
||||
} reader_ctx;
|
||||
|
||||
static int reader_at(void *ctx, uint64_t offset, void *dst, size_t size) {
|
||||
reader_ctx *r = (reader_ctx *)ctx;
|
||||
Int64 pos = (Int64)(r->base + offset);
|
||||
size_t done = 0;
|
||||
|
||||
if(r->stream->Seek(r->stream, &pos, SZ_SEEK_SET) != SZ_OK) return -1;
|
||||
while(done < size) {
|
||||
size_t want = size - done;
|
||||
if(r->stream->Read(r->stream, (Byte *)dst + done, &want) != SZ_OK) return -1;
|
||||
if(want == 0) return -1;
|
||||
done += want;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------ folder -> sink */
|
||||
|
||||
typedef struct {
|
||||
UInt32 file_index;
|
||||
uint64_t size;
|
||||
} plan_entry;
|
||||
|
||||
typedef struct {
|
||||
const CSzArEx *db;
|
||||
char out_dir[MAX_PATH_LEN];
|
||||
char name[MAX_PATH_LEN];
|
||||
|
||||
plan_entry *plan;
|
||||
size_t plan_len;
|
||||
size_t plan_pos;
|
||||
uint64_t written;
|
||||
FILE *fh;
|
||||
|
||||
uint32_t entry_crc;
|
||||
uint64_t bytes_total;
|
||||
uint64_t bytes_ok;
|
||||
int failures;
|
||||
} sink_ctx;
|
||||
|
||||
static void plan_free(sink_ctx *s) {
|
||||
free(s->plan);
|
||||
s->plan = NULL;
|
||||
s->plan_len = s->plan_pos = 0;
|
||||
}
|
||||
|
||||
/* Builds the ordered list of non-empty entries living in folder `folder`. */
|
||||
static int plan_build(sink_ctx *s, UInt32 folder, sz_chain_err_t *cerr) {
|
||||
const CSzArEx *db = s->db;
|
||||
UInt32 first = db->FolderToFile[folder];
|
||||
UInt32 last = db->FolderToFile[(size_t)folder + 1];
|
||||
UInt32 i;
|
||||
|
||||
plan_free(s);
|
||||
/* A folder that failed mid-entry leaves `written` pointing into an entry it
|
||||
never finished; carrying that into the next folder makes every later entry
|
||||
look "already part written" and produces a cascade of bogus failures. */
|
||||
s->written = 0;
|
||||
if(last <= first) return 0;
|
||||
s->plan = (plan_entry *)malloc(sizeof(plan_entry) * (size_t)(last - first));
|
||||
if(!s->plan) return -1;
|
||||
|
||||
for(i = first; i < last; i++) {
|
||||
UInt64 size = db->UnpackPositions[(size_t)i + 1] - db->UnpackPositions[i];
|
||||
if(db->FileToFolder[i] != folder) continue;
|
||||
if(size == 0) continue;
|
||||
s->plan[s->plan_len].file_index = i;
|
||||
s->plan[s->plan_len].size = (uint64_t)size;
|
||||
s->plan_len++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void sink_close_entry(sink_ctx *s) {
|
||||
if(s->fh) {
|
||||
fclose(s->fh);
|
||||
s->fh = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static int sink_open_entry(sink_ctx *s, UInt32 file_index) {
|
||||
const CSzArEx *db = s->db;
|
||||
UInt16 *name16 = NULL;
|
||||
size_t len = SzArEx_GetFileNameUtf16(db, file_index, NULL);
|
||||
char rel[MAX_PATH_LEN];
|
||||
char full[MAX_PATH_LEN];
|
||||
char *slash;
|
||||
|
||||
name16 = (UInt16 *)malloc((len + 1) * sizeof(UInt16));
|
||||
if(!name16) return -1;
|
||||
SzArEx_GetFileNameUtf16(db, file_index, name16);
|
||||
utf16_to_utf8(name16, rel, sizeof(rel));
|
||||
free(name16);
|
||||
|
||||
if(snprintf(full, sizeof(full), "%s/%s", s->out_dir, rel) >=
|
||||
(int)sizeof(full)) {
|
||||
printf(" FAIL (path too long) %s\n", rel);
|
||||
return -1;
|
||||
}
|
||||
slash = strrchr(full, '/');
|
||||
if(slash) {
|
||||
*slash = 0;
|
||||
make_dirs(full);
|
||||
*slash = '/';
|
||||
}
|
||||
s->fh = path_fopen_write(full);
|
||||
if(!s->fh) {
|
||||
printf(" FAIL (cannot create) %s\n", rel);
|
||||
return -1;
|
||||
}
|
||||
s->entry_crc = CRC_INIT_VAL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int sink_write(void *ctx, const void *data, size_t size) {
|
||||
sink_ctx *s = (sink_ctx *)ctx;
|
||||
const Byte *p = (const Byte *)data;
|
||||
|
||||
while(size > 0) {
|
||||
plan_entry *e;
|
||||
uint64_t remain;
|
||||
size_t take;
|
||||
char rel[MAX_PATH_LEN];
|
||||
|
||||
if(s->plan_pos >= s->plan_len) {
|
||||
printf(" FAIL folder produced %llu bytes more than its entries hold\n",
|
||||
(unsigned long long)size);
|
||||
s->failures++;
|
||||
return -1;
|
||||
}
|
||||
e = &s->plan[s->plan_pos];
|
||||
remain = e->size - s->written;
|
||||
take = (size_t)((uint64_t)size < remain ? (uint64_t)size : remain);
|
||||
|
||||
if(!s->fh) {
|
||||
if(sink_open_entry(s, e->file_index) != 0) {
|
||||
s->failures++;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if(take && fwrite(p, 1, take, s->fh) != take) {
|
||||
printf(" FAIL (write error)\n");
|
||||
s->failures++;
|
||||
return -1;
|
||||
}
|
||||
s->entry_crc = CrcUpdate(s->entry_crc, p, take);
|
||||
s->written += take;
|
||||
s->bytes_total += take;
|
||||
p += take;
|
||||
size -= take;
|
||||
|
||||
if(s->written == e->size) {
|
||||
UInt16 *name16;
|
||||
size_t len;
|
||||
sink_close_entry(s);
|
||||
len = SzArEx_GetFileNameUtf16(s->db, e->file_index, NULL);
|
||||
name16 = (UInt16 *)malloc((len + 1) * sizeof(UInt16));
|
||||
if(name16) {
|
||||
SzArEx_GetFileNameUtf16(s->db, e->file_index, name16);
|
||||
utf16_to_utf8(name16, rel, sizeof(rel));
|
||||
free(name16);
|
||||
if(SzBitWithVals_Check(&s->db->CRCs, e->file_index) &&
|
||||
CRC_GET_DIGEST(s->entry_crc) != s->db->CRCs.Vals[e->file_index]) {
|
||||
printf(" FAIL %s (crc mismatch)\n", rel);
|
||||
s->failures++;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
s->bytes_ok += e->size;
|
||||
s->plan_pos++;
|
||||
s->written = 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ main */
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
CLookToRead2 look_stream;
|
||||
CSzArEx db;
|
||||
SRes res;
|
||||
UInt32 folder;
|
||||
reader_ctx reader;
|
||||
sink_ctx sink;
|
||||
sevenz_volstream *vol = NULL;
|
||||
char *vol_err = NULL;
|
||||
char vol_desc[512];
|
||||
int is_set = 0;
|
||||
int rc = 0;
|
||||
|
||||
if(argc < 3) {
|
||||
fprintf(stderr, "usage: %s <archive.7z> <out-dir> [password]\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
if(sevenz_volstream_open(&vol, argv[1], &is_set, &vol_err) != 0) {
|
||||
fprintf(stderr, "%s\n", vol_err ? vol_err : "cannot open the archive");
|
||||
free(vol_err);
|
||||
return 1;
|
||||
}
|
||||
sevenz_volstream_describe(vol, vol_desc, sizeof(vol_desc));
|
||||
|
||||
LookToRead2_CreateVTable(&look_stream, 0);
|
||||
look_stream.buf = (Byte *)ISzAlloc_Alloc(&g_alloc, INPUT_BUF_SIZE);
|
||||
if(!look_stream.buf) {
|
||||
fprintf(stderr, "out of memory\n");
|
||||
sevenz_volstream_free(vol);
|
||||
return 1;
|
||||
}
|
||||
look_stream.bufSize = INPUT_BUF_SIZE;
|
||||
look_stream.realStream = sevenz_volstream_stream(vol);
|
||||
LookToRead2_INIT(&look_stream)
|
||||
|
||||
CrcGenerateTable();
|
||||
SzArEx_Init(&db);
|
||||
|
||||
res = SzArEx_Open(&db, &look_stream.vt, &g_alloc, &g_temp);
|
||||
if(res != SZ_OK) {
|
||||
if(res == SZ_ERROR_UNSUPPORTED)
|
||||
fprintf(stderr,
|
||||
"%s: the archive header is encrypted (-mhe=on); this build reads "
|
||||
"encrypted *streams* only, so the header itself cannot be "
|
||||
"decoded\n",
|
||||
vol_desc);
|
||||
else
|
||||
fprintf(stderr, "%s: cannot read the 7z header (res=%d)\n", vol_desc,
|
||||
(int)res);
|
||||
sevenz_volstream_free(vol);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("archive: %s, %llu bytes, entries: %u, folders: %u, packed streams: %u\n",
|
||||
vol_desc, (unsigned long long)sevenz_volstream_size(vol),
|
||||
(unsigned)db.NumFiles, (unsigned)db.db.NumFolders,
|
||||
(unsigned)db.db.NumPackStreams);
|
||||
|
||||
memset(&sink, 0, sizeof(sink));
|
||||
sink.db = &db;
|
||||
snprintf(sink.out_dir, sizeof(sink.out_dir), "%s", argv[2]);
|
||||
|
||||
reader.stream = sevenz_volstream_stream(vol);
|
||||
reader.base = db.dataPos;
|
||||
|
||||
for(folder = 0; folder < db.db.NumFolders; folder++) {
|
||||
const UInt32 pack_first = db.db.FoStartPackStreamIndex[folder];
|
||||
const UInt32 pack_count = db.db.FoStartPackStreamIndex[(size_t)folder + 1] -
|
||||
pack_first;
|
||||
uint64_t pack_positions[SZ_CHAIN_MAX_STREAMS + 1];
|
||||
sz_chain *chain = NULL;
|
||||
sz_chain_err_t cerr;
|
||||
char desc[256];
|
||||
uint32_t folder_crc = 0;
|
||||
UInt32 k;
|
||||
uint64_t unpack_size = SzAr_GetFolderUnpackSize(&db.db, folder);
|
||||
const uint8_t *blob = db.db.CodersData + db.db.FoCodersOffsets[folder];
|
||||
size_t blob_size = db.db.FoCodersOffsets[(size_t)folder + 1] -
|
||||
db.db.FoCodersOffsets[folder];
|
||||
const uint64_t *cu =
|
||||
&db.db.CoderUnpackSizes[db.db.FoToCoderUnpackSizes[folder]];
|
||||
|
||||
for(k = 0; k <= pack_count; k++)
|
||||
pack_positions[k] = db.db.PackPositions[pack_first + k];
|
||||
|
||||
sink.bytes_total = 0;
|
||||
sink.bytes_ok = 0;
|
||||
if(plan_build(&sink, folder, &cerr) != 0) {
|
||||
fprintf(stderr, "folder %u: cannot build the entry plan\n",
|
||||
(unsigned)folder);
|
||||
rc = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(sz_chain_parse(&chain, blob, blob_size, pack_positions, pack_count, cu,
|
||||
unpack_size, sz_chain_default_limits(), &cerr) != 0) {
|
||||
printf("folder %-2u FAIL parse: %s: %s\n", (unsigned)folder,
|
||||
sz_chain_status_string(cerr.status), cerr.message);
|
||||
g_failures++;
|
||||
rc = 1;
|
||||
plan_free(&sink);
|
||||
continue;
|
||||
}
|
||||
sz_chain_describe(chain, desc, sizeof(desc));
|
||||
|
||||
{
|
||||
sz_chain_err_t derr;
|
||||
if(sz_chain_decode(chain, reader_at, &reader, sink_write, &sink, NULL,
|
||||
NULL, argc > 3 ? argv[3] : NULL, &folder_crc,
|
||||
&derr) != 0) {
|
||||
printf("folder %-2u FAIL decode [%s]: %s: %s (offset %llu)\n",
|
||||
(unsigned)folder, desc, sz_chain_status_string(derr.status),
|
||||
derr.message, (unsigned long long)derr.offset);
|
||||
g_failures++;
|
||||
rc = 1;
|
||||
} else if(sink.bytes_total != unpack_size) {
|
||||
printf("folder %-2u FAIL size: %llu decoded, %llu declared\n",
|
||||
(unsigned)folder, (unsigned long long)sink.bytes_total,
|
||||
(unsigned long long)unpack_size);
|
||||
g_failures++;
|
||||
rc = 1;
|
||||
} else if(SzBitWithVals_Check(&db.db.FolderCRCs, folder) &&
|
||||
folder_crc != db.db.FolderCRCs.Vals[folder]) {
|
||||
printf("folder %-2u FAIL crc: got %08X, expected %08X\n",
|
||||
(unsigned)folder, (unsigned)folder_crc,
|
||||
(unsigned)db.db.FolderCRCs.Vals[folder]);
|
||||
g_failures++;
|
||||
rc = 1;
|
||||
} else {
|
||||
printf("folder %-2u ok [%s] %llu bytes, %u entr%s\n",
|
||||
(unsigned)folder, desc, (unsigned long long)sink.bytes_total,
|
||||
(unsigned)sink.plan_len, sink.plan_len == 1 ? "y" : "ies");
|
||||
}
|
||||
}
|
||||
|
||||
if(sink.plan_pos != sink.plan_len) {
|
||||
printf("folder %-2u FAIL only %u of %u entries were produced\n",
|
||||
(unsigned)folder, (unsigned)sink.plan_pos,
|
||||
(unsigned)sink.plan_len);
|
||||
g_failures++;
|
||||
rc = 1;
|
||||
}
|
||||
sink_close_entry(&sink);
|
||||
sz_chain_free(chain);
|
||||
plan_free(&sink);
|
||||
}
|
||||
|
||||
ISzAlloc_Free(&g_alloc, look_stream.buf);
|
||||
SzArEx_Free(&db, &g_alloc);
|
||||
sevenz_volstream_free(vol);
|
||||
|
||||
printf("%s: %d failure(s)\n", argv[1], g_failures);
|
||||
return rc ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
* Standalone 7z driver for host end-to-end checks.
|
||||
*
|
||||
* sevenz_e2e <archive.7z> <out-dir>
|
||||
*
|
||||
* Lists every entry, extracts the archive into <out-dir> and prints the
|
||||
* per-entry result. Exit status is non-zero if any entry fails.
|
||||
*
|
||||
* This is a *diagnostic* tool: it exercises the same container layer as the
|
||||
* real extraction path, so it is the quickest way to tell whether a failure
|
||||
* comes from the container/codec layer or from the web server around it.
|
||||
*
|
||||
* Entry names are UTF-16 inside a 7z archive; on a Windows host the file APIs
|
||||
* must therefore be called with the wide-character variants or non-ASCII names
|
||||
* silently fail to be created.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <direct.h>
|
||||
#include <wchar.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "7z.h"
|
||||
#include "7zAlloc.h"
|
||||
#include "7zCrc.h"
|
||||
#include "7zFile.h"
|
||||
|
||||
#define INPUT_BUF_SIZE (1u << 18)
|
||||
#define MAX_PATH_LEN 4096
|
||||
|
||||
static ISzAlloc g_alloc = { SzAlloc, SzFree };
|
||||
static ISzAlloc g_temp = { SzAlloc, SzFree };
|
||||
|
||||
static int g_failures = 0;
|
||||
|
||||
/* ---------------------------------------------------------------- paths */
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
/* MinGW's wchar_t is 16 bit, matching UTF-16. */
|
||||
static void utf8_to_utf16(const char *src, wchar_t *dst, size_t cap)
|
||||
{
|
||||
size_t out = 0;
|
||||
|
||||
while (*src && out + 2 < cap) {
|
||||
unsigned char c = (unsigned char)*src++;
|
||||
UInt32 cp;
|
||||
|
||||
if (c < 0x80) {
|
||||
cp = c;
|
||||
} else if ((c & 0xE0) == 0xC0 && (src[0] & 0xC0) == 0x80) {
|
||||
cp = ((UInt32)(c & 0x1F) << 6) | (UInt32)(*src++ & 0x3F);
|
||||
} else if ((c & 0xF0) == 0xE0 && (src[0] & 0xC0) == 0x80 && (src[1] & 0xC0) == 0x80) {
|
||||
cp = ((UInt32)(c & 0x0F) << 12) | ((UInt32)(src[0] & 0x3F) << 6) | (UInt32)(src[1] & 0x3F);
|
||||
src += 2;
|
||||
} else if ((c & 0xF8) == 0xF0 && (src[0] & 0xC0) == 0x80 && (src[1] & 0xC0) == 0x80 && (src[2] & 0xC0) == 0x80) {
|
||||
cp = ((UInt32)(c & 0x07) << 18) | ((UInt32)(src[0] & 0x3F) << 12) |
|
||||
((UInt32)(src[1] & 0x3F) << 6) | (UInt32)(src[2] & 0x3F);
|
||||
src += 3;
|
||||
} else {
|
||||
cp = '?';
|
||||
}
|
||||
|
||||
if (cp >= 0x10000) {
|
||||
cp -= 0x10000;
|
||||
dst[out++] = (wchar_t)(0xD800 | (cp >> 10));
|
||||
dst[out++] = (wchar_t)(0xDC00 | (cp & 0x3FF));
|
||||
} else {
|
||||
dst[out++] = (wchar_t)cp;
|
||||
}
|
||||
}
|
||||
dst[out] = 0;
|
||||
}
|
||||
|
||||
static void path_mkdir(const char *path)
|
||||
{
|
||||
wchar_t wide[MAX_PATH_LEN];
|
||||
utf8_to_utf16(path, wide, MAX_PATH_LEN);
|
||||
_wmkdir(wide);
|
||||
}
|
||||
|
||||
static FILE *path_fopen_write(const char *path)
|
||||
{
|
||||
wchar_t wide[MAX_PATH_LEN];
|
||||
utf8_to_utf16(path, wide, MAX_PATH_LEN);
|
||||
return _wfopen(wide, L"wb");
|
||||
}
|
||||
|
||||
#else /* POSIX: byte paths are UTF-8 already */
|
||||
|
||||
static void path_mkdir(const char *path) { mkdir(path, 0755); }
|
||||
static FILE *path_fopen_write(const char *path) { return fopen(path, "wb"); }
|
||||
|
||||
#endif
|
||||
|
||||
static void make_dirs(const char *path)
|
||||
{
|
||||
char tmp[MAX_PATH_LEN];
|
||||
size_t i, n = strlen(path);
|
||||
|
||||
if (n + 1 > sizeof(tmp))
|
||||
return;
|
||||
memcpy(tmp, path, n + 1);
|
||||
|
||||
for (i = 1; i < n; i++) {
|
||||
if (tmp[i] == '/' || tmp[i] == '\\') {
|
||||
char c = tmp[i];
|
||||
tmp[i] = 0;
|
||||
path_mkdir(tmp);
|
||||
tmp[i] = c;
|
||||
}
|
||||
}
|
||||
/* The final component counts too: callers hand us either a directory entry
|
||||
or the parent directory of a file. */
|
||||
path_mkdir(tmp);
|
||||
}
|
||||
|
||||
static int write_file(const char *path, const Byte *data, size_t size)
|
||||
{
|
||||
FILE *fh = path_fopen_write(path);
|
||||
if (!fh)
|
||||
return -1;
|
||||
if (size && fwrite(data, 1, size, fh) != size) {
|
||||
fclose(fh);
|
||||
return -1;
|
||||
}
|
||||
return fclose(fh) == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
/* UTF-16 (LE, as stored by the 7z name table) to UTF-8. */
|
||||
static void utf16_to_utf8(const UInt16 *src, char *dst, size_t dst_size)
|
||||
{
|
||||
size_t out = 0;
|
||||
|
||||
while (*src) {
|
||||
UInt32 c = *src++;
|
||||
|
||||
if (c >= 0xD800 && c <= 0xDBFF && *src >= 0xDC00 && *src <= 0xDFFF)
|
||||
c = 0x10000 + ((c - 0xD800) << 10) + (*src++ - 0xDC00);
|
||||
|
||||
if (out + 5 >= dst_size)
|
||||
break;
|
||||
|
||||
if (c < 0x80) {
|
||||
dst[out++] = (char)c;
|
||||
} else if (c < 0x800) {
|
||||
dst[out++] = (char)(0xC0 | (c >> 6));
|
||||
dst[out++] = (char)(0x80 | (c & 0x3F));
|
||||
} else if (c < 0x10000) {
|
||||
dst[out++] = (char)(0xE0 | (c >> 12));
|
||||
dst[out++] = (char)(0x80 | ((c >> 6) & 0x3F));
|
||||
dst[out++] = (char)(0x80 | (c & 0x3F));
|
||||
} else {
|
||||
dst[out++] = (char)(0xF0 | (c >> 18));
|
||||
dst[out++] = (char)(0x80 | ((c >> 12) & 0x3F));
|
||||
dst[out++] = (char)(0x80 | ((c >> 6) & 0x3F));
|
||||
dst[out++] = (char)(0x80 | (c & 0x3F));
|
||||
}
|
||||
}
|
||||
dst[out] = 0;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- main */
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
CFileInStream archive_stream;
|
||||
CLookToRead2 look_stream;
|
||||
CSzArEx db;
|
||||
SRes res;
|
||||
UInt16 *name16 = NULL;
|
||||
size_t name16_cap = 0;
|
||||
UInt32 i;
|
||||
UInt32 block_index = 0xFFFFFFFF;
|
||||
Byte *out_buffer = NULL;
|
||||
size_t out_buffer_size = 0;
|
||||
const char *out_dir;
|
||||
|
||||
if (argc < 3) {
|
||||
fprintf(stderr, "usage: %s <archive.7z> <out-dir>\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
out_dir = argv[2];
|
||||
|
||||
if (InFile_Open(&archive_stream.file, argv[1]) != 0) {
|
||||
fprintf(stderr, "cannot open %s\n", argv[1]);
|
||||
return 1;
|
||||
}
|
||||
FileInStream_CreateVTable(&archive_stream);
|
||||
archive_stream.wres = 0;
|
||||
|
||||
LookToRead2_CreateVTable(&look_stream, 0);
|
||||
look_stream.buf = (Byte *)ISzAlloc_Alloc(&g_alloc, INPUT_BUF_SIZE);
|
||||
if (!look_stream.buf) {
|
||||
fprintf(stderr, "out of memory\n");
|
||||
return 1;
|
||||
}
|
||||
look_stream.bufSize = INPUT_BUF_SIZE;
|
||||
look_stream.realStream = &archive_stream.vt;
|
||||
LookToRead2_INIT(&look_stream)
|
||||
|
||||
CrcGenerateTable();
|
||||
SzArEx_Init(&db);
|
||||
|
||||
res = SzArEx_Open(&db, &look_stream.vt, &g_alloc, &g_temp);
|
||||
if (res != SZ_OK) {
|
||||
fprintf(stderr, "SzArEx_Open failed: res=%d\n", (int)res);
|
||||
if (res == SZ_ERROR_UNSUPPORTED)
|
||||
fprintf(stderr, " (unsupported coder - encrypted header or exotic method)\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("entries: %u\n", (unsigned)db.NumFiles);
|
||||
|
||||
for (i = 0; i < db.NumFiles; i++) {
|
||||
const int is_dir = SzArEx_IsDir(&db, i);
|
||||
const size_t len = SzArEx_GetFileNameUtf16(&db, i, NULL);
|
||||
char rel[MAX_PATH_LEN];
|
||||
char full[MAX_PATH_LEN];
|
||||
size_t offset = 0;
|
||||
size_t processed = 0;
|
||||
|
||||
if (len + 1 > name16_cap) {
|
||||
UInt16 *grown = (UInt16 *)realloc(name16, (len + 1) * sizeof(UInt16));
|
||||
if (!grown) {
|
||||
fprintf(stderr, "out of memory\n");
|
||||
return 1;
|
||||
}
|
||||
name16 = grown;
|
||||
name16_cap = len + 1;
|
||||
}
|
||||
SzArEx_GetFileNameUtf16(&db, i, name16);
|
||||
utf16_to_utf8(name16, rel, sizeof(rel));
|
||||
|
||||
if (snprintf(full, sizeof(full), "%s/%s", out_dir, rel) >= (int)sizeof(full)) {
|
||||
printf(" SKIP (path too long) %s\n", rel);
|
||||
g_failures++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_dir) {
|
||||
make_dirs(full);
|
||||
printf(" dir %s\n", rel);
|
||||
continue;
|
||||
}
|
||||
|
||||
res = SzArEx_Extract(&db, &look_stream.vt, i, &block_index, &out_buffer,
|
||||
&out_buffer_size, &offset, &processed, &g_alloc, &g_temp);
|
||||
if (res != SZ_OK) {
|
||||
printf(" FAIL %s (res=%d)\n", rel, (int)res);
|
||||
g_failures++;
|
||||
/* SzArEx_Extract leaves the block cache primed with a half-decoded
|
||||
buffer when the folder decode fails; drop it so the next entry
|
||||
reports its own error instead of a bogus CRC mismatch. */
|
||||
block_index = 0xFFFFFFFF;
|
||||
continue;
|
||||
}
|
||||
|
||||
{
|
||||
char *slash = strrchr(full, '/');
|
||||
if (slash) {
|
||||
*slash = 0;
|
||||
make_dirs(full);
|
||||
*slash = '/';
|
||||
}
|
||||
}
|
||||
if (write_file(full, out_buffer + offset, processed) != 0) {
|
||||
printf(" FAIL %s (write error)\n", rel);
|
||||
g_failures++;
|
||||
continue;
|
||||
}
|
||||
|
||||
{
|
||||
UInt32 crc = 0;
|
||||
if (SzBitWithVals_Check(&db.CRCs, i)) {
|
||||
crc = CrcCalc(out_buffer + offset, processed);
|
||||
if (crc != db.CRCs.Vals[i]) {
|
||||
printf(" FAIL %s (crc mismatch)\n", rel);
|
||||
g_failures++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
printf(" ok %s (%lu bytes)\n", rel, (unsigned long)processed);
|
||||
}
|
||||
|
||||
ISzAlloc_Free(&g_alloc, out_buffer);
|
||||
ISzAlloc_Free(&g_alloc, look_stream.buf);
|
||||
SzArEx_Free(&db, &g_alloc);
|
||||
File_Close(&archive_stream.file);
|
||||
free(name16);
|
||||
|
||||
printf("%s: %d failure(s)\n", argv[1], g_failures);
|
||||
return g_failures ? 1 : 0;
|
||||
}
|
||||
+154
-1
@@ -28,15 +28,39 @@
|
||||
#include "posix_compat.h"
|
||||
|
||||
static const char *g_fixtures;
|
||||
static const char *g_fixtures_real;
|
||||
static char g_work[4096];
|
||||
static int g_failures;
|
||||
static int g_checks;
|
||||
|
||||
/* Progress capture: g_pmid_extract is set when a progress event reports a
|
||||
partially complete EXTRACT phase (0 < bytes_done < bytes_total). Before
|
||||
the UCM_PROCESSDATA callback existed the RAR engine only accounted bytes
|
||||
after a whole entry finished, so a single entry spanning volumes produced
|
||||
no mid-file progress at all. */
|
||||
static int g_pmid_extract;
|
||||
static zipx_progress_t g_last_progress;
|
||||
|
||||
static void
|
||||
progress_cb(void *userdata, const zipx_progress_t *p) {
|
||||
(void)userdata;
|
||||
g_last_progress = *p;
|
||||
if(p->phase == ZIPX_PHASE_EXTRACT && p->bytes_total > 0 &&
|
||||
p->bytes_done > 0 && p->bytes_done < p->bytes_total) {
|
||||
g_pmid_extract = 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
fixture_path(char *out, size_t size, const char *name) {
|
||||
snprintf(out, size, "%s/%s", g_fixtures, name);
|
||||
}
|
||||
|
||||
static void
|
||||
fixture_real_path(char *out, size_t size, const char *name) {
|
||||
snprintf(out, size, "%s/%s", g_fixtures_real, name);
|
||||
}
|
||||
|
||||
static void
|
||||
work_path(char *out, size_t size, const char *name) {
|
||||
snprintf(out, size, "%s/%s", g_work, name);
|
||||
@@ -298,13 +322,141 @@ test_limits_handoff(void) {
|
||||
"default() matches ZIPX_LIMITS_DEFAULT");
|
||||
}
|
||||
|
||||
/* Real-archive happy paths (v1.9, unrar 7.20.1 engine).
|
||||
The fixtures are produced by tests/make-rar-fixtures.bat on a machine with
|
||||
WinRAR; when they are missing (plain CI checkout) these checks SKIP. */
|
||||
static void
|
||||
test_real_archives(void) {
|
||||
char dst[4096];
|
||||
char checkp[4096];
|
||||
|
||||
printf("test_real_archives\n");
|
||||
|
||||
/* RAR5 (WinRAR 6/7 "v6" compression) single volume. */
|
||||
work_path(dst, sizeof(dst), "rdst_v6");
|
||||
remove_dir(dst);
|
||||
{
|
||||
zipx_result_t res;
|
||||
zipx_status_t st;
|
||||
char src[4096];
|
||||
fixture_real_path(src, sizeof(src), "basic-v6.rar");
|
||||
if(exists(src)) {
|
||||
memset(&res, 0, sizeof(res));
|
||||
st = rar_extract(src, dst, ZIPX_CONFLICT_FAIL,
|
||||
zipx_default_limits(), NULL, NULL, NULL, &res);
|
||||
check(st == ZIPX_OK, "basic-v6.rar extracts (v6 RAR5)");
|
||||
if(st == ZIPX_OK) {
|
||||
snprintf(checkp, sizeof(checkp), "%s/root.txt", dst);
|
||||
check(exists(checkp), " root.txt present");
|
||||
snprintf(checkp, sizeof(checkp), "%s/dir/nested.txt", dst);
|
||||
check(exists(checkp), " dir/nested.txt present");
|
||||
check(res.entries_done >= 3, " entries_done >= 3");
|
||||
} else {
|
||||
printf(" message=%s\n", res.message);
|
||||
}
|
||||
} else {
|
||||
printf(" SKIP basic-v6.rar (missing — run tests/make-rar-fixtures.bat)\n");
|
||||
}
|
||||
}
|
||||
remove_dir(dst);
|
||||
|
||||
/* RAR4 legacy single volume. */
|
||||
work_path(dst, sizeof(dst), "rdst_r4");
|
||||
remove_dir(dst);
|
||||
{
|
||||
zipx_result_t res;
|
||||
zipx_status_t st;
|
||||
char src[4096];
|
||||
fixture_real_path(src, sizeof(src), "basic-rar4.rar");
|
||||
if(exists(src)) {
|
||||
memset(&res, 0, sizeof(res));
|
||||
st = rar_extract(src, dst, ZIPX_CONFLICT_FAIL,
|
||||
zipx_default_limits(), NULL, NULL, NULL, &res);
|
||||
check(st == ZIPX_OK, "basic-rar4.rar extracts (RAR4)");
|
||||
if(st == ZIPX_OK) {
|
||||
snprintf(checkp, sizeof(checkp), "%s/root.txt", dst);
|
||||
check(exists(checkp), " root.txt present");
|
||||
} else {
|
||||
printf(" message=%s\n", res.message);
|
||||
}
|
||||
} else {
|
||||
printf(" SKIP basic-rar4.rar (missing)\n");
|
||||
}
|
||||
}
|
||||
remove_dir(dst);
|
||||
|
||||
/* Multi-volume: opening vol.part1.rar must auto-merge part2 from the same
|
||||
directory (unrar drives the volume chain). Also asserts that progress
|
||||
is byte-accurate and updates mid-entry (see progress_cb). */
|
||||
work_path(dst, sizeof(dst), "rdst_vol");
|
||||
remove_dir(dst);
|
||||
{
|
||||
zipx_result_t res;
|
||||
zipx_status_t st;
|
||||
char src[4096];
|
||||
fixture_real_path(src, sizeof(src), "vol.part1.rar");
|
||||
if(exists(src)) {
|
||||
memset(&res, 0, sizeof(res));
|
||||
g_pmid_extract = 0;
|
||||
memset(&g_last_progress, 0, sizeof(g_last_progress));
|
||||
st = rar_extract(src, dst, ZIPX_CONFLICT_FAIL,
|
||||
zipx_default_limits(), NULL, progress_cb, NULL, &res);
|
||||
check(st == ZIPX_OK, "vol.part1.rar auto-merges volumes");
|
||||
if(st == ZIPX_OK) {
|
||||
snprintf(checkp, sizeof(checkp), "%s/root.txt", dst);
|
||||
check(exists(checkp), " root.txt present across volumes");
|
||||
snprintf(checkp, sizeof(checkp), "%s/big.bin", dst);
|
||||
check(exists(checkp), " big.bin (split file) present and whole");
|
||||
check(res.entries_done >= 2, " entries_done >= 2");
|
||||
check(g_last_progress.bytes_total > 0,
|
||||
" progress bytes_total reported");
|
||||
check(g_last_progress.bytes_total == g_last_progress.bytes_done,
|
||||
" final bytes_done reaches bytes_total");
|
||||
check(g_pmid_extract,
|
||||
" progress updates mid-entry (not only at entry end)");
|
||||
} else {
|
||||
printf(" message=%s\n", res.message);
|
||||
}
|
||||
} else {
|
||||
printf(" SKIP vol.part1.rar (missing)\n");
|
||||
}
|
||||
}
|
||||
remove_dir(dst);
|
||||
|
||||
/* Encrypted: engine can decrypt but the password channel is not wired yet,
|
||||
so encrypted entries must be rejected up front with UNSUPPORTED. */
|
||||
work_path(dst, sizeof(dst), "rdst_enc");
|
||||
remove_dir(dst);
|
||||
{
|
||||
zipx_result_t res;
|
||||
zipx_status_t st;
|
||||
char src[4096];
|
||||
fixture_real_path(src, sizeof(src), "enc-v6.rar");
|
||||
if(exists(src)) {
|
||||
memset(&res, 0, sizeof(res));
|
||||
st = rar_extract(src, dst, ZIPX_CONFLICT_FAIL,
|
||||
zipx_default_limits(), NULL, NULL, NULL, &res);
|
||||
check(st == ZIPX_ERR_UNSUPPORTED, "enc-v6.rar rejected (no password channel)");
|
||||
check(!exists(dst), " no files written for encrypted archive");
|
||||
if(st == ZIPX_OK) {
|
||||
remove_dir(dst);
|
||||
}
|
||||
} else {
|
||||
printf(" SKIP enc-v6.rar (missing)\n");
|
||||
}
|
||||
}
|
||||
remove_dir(dst);
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char **argv) {
|
||||
if(argc < 3) {
|
||||
fprintf(stderr, "usage: %s <fixtures-dir> <work-dir>\n", argv[0]);
|
||||
fprintf(stderr, "usage: %s <fixtures-dir> <work-dir> [real-fixtures-dir]\n",
|
||||
argv[0]);
|
||||
return 2;
|
||||
}
|
||||
g_fixtures = argv[1];
|
||||
g_fixtures_real = (argc >= 4) ? argv[3] : argv[1];
|
||||
snprintf(g_work, sizeof(g_work), "%s", argv[2]);
|
||||
mkdir(g_work, 0777);
|
||||
|
||||
@@ -341,6 +493,7 @@ main(int argc, char **argv) {
|
||||
test_engine_dispatch();
|
||||
test_format_translation();
|
||||
test_limits_handoff();
|
||||
test_real_archives();
|
||||
|
||||
printf("\nrar_extract: %d checks, %d failures\n", g_checks, g_failures);
|
||||
return g_failures == 0 ? 0 : 1;
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
/* The progress callback the dispatch driver installs fires the UI loop; it has
|
||||
to be called multiple times, with monotonic bytes_done and a non-zero
|
||||
bytes_total, or the front-end ends up with no spinner. These checks pin
|
||||
that down so a refactor that drops the callback returns the test set to
|
||||
red instead of "no progress shown" on the device. */
|
||||
typedef struct {
|
||||
int calls;
|
||||
unsigned long long prev_done;
|
||||
unsigned long long max_bytes_total;
|
||||
unsigned long long max_bytes_done;
|
||||
unsigned long long max_entries_total;
|
||||
unsigned long long max_entries_done;
|
||||
} progress_recorder_t;
|
||||
|
||||
static void
|
||||
recorder_progress(void *userdata, const zipx_progress_t *p) {
|
||||
progress_recorder_t *r = userdata;
|
||||
|
||||
r->calls++;
|
||||
if(p->bytes_total > r->max_bytes_total) r->max_bytes_total = p->bytes_total;
|
||||
if(p->bytes_done > r->max_bytes_done) r->max_bytes_done = p->bytes_done;
|
||||
if((unsigned long long)p->entries_total > r->max_entries_total) {
|
||||
r->max_entries_total = (unsigned long long)p->entries_total;
|
||||
}
|
||||
if((unsigned long long)p->entries_done > r->max_entries_done) {
|
||||
r->max_entries_done = (unsigned long long)p->entries_done;
|
||||
}
|
||||
if(p->bytes_done < r->prev_done) {
|
||||
printf(" FAIL progress: bytes_done went backwards (%llu -> %llu)\n",
|
||||
r->prev_done, p->bytes_done);
|
||||
g_failures++;
|
||||
}
|
||||
r->prev_done = p->bytes_done;
|
||||
}
|
||||
|
||||
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");
|
||||
{
|
||||
char b2[256];
|
||||
zipx_result_t r;
|
||||
(void)sevenz_extract(arc, dst, ZIPX_CONFLICT_FAIL,
|
||||
zipx_default_limits(), NULL, NULL, NULL,
|
||||
"NotThePassword", &r);
|
||||
snprintf(b2, sizeof(b2),
|
||||
"wrong password: detail names the archive (got '%s')", r.detail);
|
||||
check(strstr(r.detail, "aes.7z") != NULL, b2);
|
||||
}
|
||||
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");
|
||||
|
||||
/* --- progress callback ---------------------------------------------- */
|
||||
{
|
||||
progress_recorder_t rec = {0};
|
||||
zipx_result_t r;
|
||||
|
||||
snprintf(dst, sizeof(dst), "%s/progress", work);
|
||||
mkdir(dst, 0777);
|
||||
(void)sevenz_extract(arc, dst, ZIPX_CONFLICT_FAIL,
|
||||
zipx_default_limits(), NULL, recorder_progress,
|
||||
&rec, NULL, &r);
|
||||
check(rec.calls >= 2, "progress callback fires multiple times");
|
||||
check(rec.max_bytes_total > 0, "progress reports a non-zero bytes_total");
|
||||
check(rec.max_bytes_done > 0,
|
||||
"progress reports a non-zero bytes_done during extraction");
|
||||
check(rec.max_entries_total >= 1,
|
||||
"progress reports a non-zero entries_total");
|
||||
check(rec.max_bytes_done >= rec.max_bytes_total * 9 / 10,
|
||||
"progress reaches within 90% of the declared total");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
+781
-639
File diff suppressed because it is too large.
Load diff
Vendored
+204
@@ -0,0 +1,204 @@
|
||||
/* 7z.h -- 7z interface
|
||||
2023-04-02 : Igor Pavlov : Public domain */
|
||||
|
||||
#ifndef ZIP7_INC_7Z_H
|
||||
#define ZIP7_INC_7Z_H
|
||||
|
||||
#include "7zTypes.h"
|
||||
|
||||
EXTERN_C_BEGIN
|
||||
|
||||
#define k7zStartHeaderSize 0x20
|
||||
#define k7zSignatureSize 6
|
||||
|
||||
extern const Byte k7zSignature[k7zSignatureSize];
|
||||
|
||||
typedef struct
|
||||
{
|
||||
const Byte *Data;
|
||||
size_t Size;
|
||||
} CSzData;
|
||||
|
||||
/* CSzCoderInfo & CSzFolder support only default methods */
|
||||
|
||||
typedef struct
|
||||
{
|
||||
size_t PropsOffset;
|
||||
UInt32 MethodID;
|
||||
Byte NumStreams;
|
||||
Byte PropsSize;
|
||||
} CSzCoderInfo;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
UInt32 InIndex;
|
||||
UInt32 OutIndex;
|
||||
} CSzBond;
|
||||
|
||||
#define SZ_NUM_CODERS_IN_FOLDER_MAX 4
|
||||
#define SZ_NUM_BONDS_IN_FOLDER_MAX 3
|
||||
#define SZ_NUM_PACK_STREAMS_IN_FOLDER_MAX 4
|
||||
|
||||
typedef struct
|
||||
{
|
||||
UInt32 NumCoders;
|
||||
UInt32 NumBonds;
|
||||
UInt32 NumPackStreams;
|
||||
UInt32 UnpackStream;
|
||||
UInt32 PackStreams[SZ_NUM_PACK_STREAMS_IN_FOLDER_MAX];
|
||||
CSzBond Bonds[SZ_NUM_BONDS_IN_FOLDER_MAX];
|
||||
CSzCoderInfo Coders[SZ_NUM_CODERS_IN_FOLDER_MAX];
|
||||
} CSzFolder;
|
||||
|
||||
|
||||
SRes SzGetNextFolderItem(CSzFolder *f, CSzData *sd);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
UInt32 Low;
|
||||
UInt32 High;
|
||||
} CNtfsFileTime;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
Byte *Defs; /* MSB 0 bit numbering */
|
||||
UInt32 *Vals;
|
||||
} CSzBitUi32s;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
Byte *Defs; /* MSB 0 bit numbering */
|
||||
// UInt64 *Vals;
|
||||
CNtfsFileTime *Vals;
|
||||
} CSzBitUi64s;
|
||||
|
||||
#define SzBitArray_Check(p, i) (((p)[(i) >> 3] & (0x80 >> ((i) & 7))) != 0)
|
||||
|
||||
#define SzBitWithVals_Check(p, i) ((p)->Defs && ((p)->Defs[(i) >> 3] & (0x80 >> ((i) & 7))) != 0)
|
||||
|
||||
typedef struct
|
||||
{
|
||||
UInt32 NumPackStreams;
|
||||
UInt32 NumFolders;
|
||||
|
||||
UInt64 *PackPositions; // NumPackStreams + 1
|
||||
CSzBitUi32s FolderCRCs; // NumFolders
|
||||
|
||||
size_t *FoCodersOffsets; // NumFolders + 1
|
||||
UInt32 *FoStartPackStreamIndex; // NumFolders + 1
|
||||
UInt32 *FoToCoderUnpackSizes; // NumFolders + 1
|
||||
Byte *FoToMainUnpackSizeIndex; // NumFolders
|
||||
UInt64 *CoderUnpackSizes; // for all coders in all folders
|
||||
|
||||
Byte *CodersData;
|
||||
|
||||
UInt64 RangeLimit;
|
||||
} CSzAr;
|
||||
|
||||
UInt64 SzAr_GetFolderUnpackSize(const CSzAr *p, UInt32 folderIndex);
|
||||
|
||||
SRes SzAr_DecodeFolder(const CSzAr *p, UInt32 folderIndex,
|
||||
ILookInStreamPtr stream, UInt64 startPos,
|
||||
Byte *outBuffer, size_t outSize,
|
||||
ISzAllocPtr allocMain);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
CSzAr db;
|
||||
|
||||
UInt64 startPosAfterHeader;
|
||||
UInt64 dataPos;
|
||||
|
||||
UInt32 NumFiles;
|
||||
|
||||
UInt64 *UnpackPositions; // NumFiles + 1
|
||||
// Byte *IsEmptyFiles;
|
||||
Byte *IsDirs;
|
||||
CSzBitUi32s CRCs;
|
||||
|
||||
CSzBitUi32s Attribs;
|
||||
// CSzBitUi32s Parents;
|
||||
CSzBitUi64s MTime;
|
||||
CSzBitUi64s CTime;
|
||||
|
||||
UInt32 *FolderToFile; // NumFolders + 1
|
||||
UInt32 *FileToFolder; // NumFiles
|
||||
|
||||
size_t *FileNameOffsets; /* in 2-byte steps */
|
||||
Byte *FileNames; /* UTF-16-LE */
|
||||
} CSzArEx;
|
||||
|
||||
#define SzArEx_IsDir(p, i) (SzBitArray_Check((p)->IsDirs, i))
|
||||
|
||||
#define SzArEx_GetFileSize(p, i) ((p)->UnpackPositions[(i) + 1] - (p)->UnpackPositions[i])
|
||||
|
||||
void SzArEx_Init(CSzArEx *p);
|
||||
void SzArEx_Free(CSzArEx *p, ISzAllocPtr alloc);
|
||||
UInt64 SzArEx_GetFolderStreamPos(const CSzArEx *p, UInt32 folderIndex, UInt32 indexInFolder);
|
||||
int SzArEx_GetFolderFullPackSize(const CSzArEx *p, UInt32 folderIndex, UInt64 *resSize);
|
||||
|
||||
/*
|
||||
if dest == NULL, the return value specifies the required size of the buffer,
|
||||
in 16-bit characters, including the null-terminating character.
|
||||
if dest != NULL, the return value specifies the number of 16-bit characters that
|
||||
are written to the dest, including the null-terminating character. */
|
||||
|
||||
size_t SzArEx_GetFileNameUtf16(const CSzArEx *p, size_t fileIndex, UInt16 *dest);
|
||||
|
||||
/*
|
||||
size_t SzArEx_GetFullNameLen(const CSzArEx *p, size_t fileIndex);
|
||||
UInt16 *SzArEx_GetFullNameUtf16_Back(const CSzArEx *p, size_t fileIndex, UInt16 *dest);
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/*
|
||||
SzArEx_Extract extracts file from archive
|
||||
|
||||
*outBuffer must be 0 before first call for each new archive.
|
||||
|
||||
Extracting cache:
|
||||
If you need to decompress more than one file, you can send
|
||||
these values from previous call:
|
||||
*blockIndex,
|
||||
*outBuffer,
|
||||
*outBufferSize
|
||||
You can consider "*outBuffer" as cache of solid block. If your archive is solid,
|
||||
it will increase decompression speed.
|
||||
|
||||
If you use external function, you can declare these 3 cache variables
|
||||
(blockIndex, outBuffer, outBufferSize) as static in that external function.
|
||||
|
||||
Free *outBuffer and set *outBuffer to 0, if you want to flush cache.
|
||||
*/
|
||||
|
||||
SRes SzArEx_Extract(
|
||||
const CSzArEx *db,
|
||||
ILookInStreamPtr inStream,
|
||||
UInt32 fileIndex, /* index of file */
|
||||
UInt32 *blockIndex, /* index of solid block */
|
||||
Byte **outBuffer, /* pointer to pointer to output buffer (allocated with allocMain) */
|
||||
size_t *outBufferSize, /* buffer size for output buffer */
|
||||
size_t *offset, /* offset of stream for required file in *outBuffer */
|
||||
size_t *outSizeProcessed, /* size of file in *outBuffer */
|
||||
ISzAllocPtr allocMain,
|
||||
ISzAllocPtr allocTemp);
|
||||
|
||||
|
||||
/*
|
||||
SzArEx_Open Errors:
|
||||
SZ_ERROR_NO_ARCHIVE
|
||||
SZ_ERROR_ARCHIVE
|
||||
SZ_ERROR_UNSUPPORTED
|
||||
SZ_ERROR_MEM
|
||||
SZ_ERROR_CRC
|
||||
SZ_ERROR_INPUT_EOF
|
||||
SZ_ERROR_FAIL
|
||||
*/
|
||||
|
||||
SRes SzArEx_Open(CSzArEx *p, ILookInStreamPtr inStream,
|
||||
ISzAllocPtr allocMain, ISzAllocPtr allocTemp);
|
||||
|
||||
EXTERN_C_END
|
||||
|
||||
#endif
|
||||
Vendored
+89
@@ -0,0 +1,89 @@
|
||||
/* 7zAlloc.c -- Allocation functions for 7z processing
|
||||
2023-03-04 : Igor Pavlov : Public domain */
|
||||
|
||||
#include "Precomp.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "7zAlloc.h"
|
||||
|
||||
/* #define SZ_ALLOC_DEBUG */
|
||||
/* use SZ_ALLOC_DEBUG to debug alloc/free operations */
|
||||
|
||||
#ifdef SZ_ALLOC_DEBUG
|
||||
|
||||
/*
|
||||
#ifdef _WIN32
|
||||
#include "7zWindows.h"
|
||||
#endif
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
static int g_allocCount = 0;
|
||||
static int g_allocCountTemp = 0;
|
||||
|
||||
static void Print_Alloc(const char *s, size_t size, int *counter)
|
||||
{
|
||||
const unsigned size2 = (unsigned)size;
|
||||
fprintf(stderr, "\n%s count = %10d : %10u bytes; ", s, *counter, size2);
|
||||
(*counter)++;
|
||||
}
|
||||
static void Print_Free(const char *s, int *counter)
|
||||
{
|
||||
(*counter)--;
|
||||
fprintf(stderr, "\n%s count = %10d", s, *counter);
|
||||
}
|
||||
#endif
|
||||
|
||||
void *SzAlloc(ISzAllocPtr p, size_t size)
|
||||
{
|
||||
UNUSED_VAR(p)
|
||||
if (size == 0)
|
||||
return 0;
|
||||
#ifdef SZ_ALLOC_DEBUG
|
||||
Print_Alloc("Alloc", size, &g_allocCount);
|
||||
#endif
|
||||
return malloc(size);
|
||||
}
|
||||
|
||||
void SzFree(ISzAllocPtr p, void *address)
|
||||
{
|
||||
UNUSED_VAR(p)
|
||||
#ifdef SZ_ALLOC_DEBUG
|
||||
if (address)
|
||||
Print_Free("Free ", &g_allocCount);
|
||||
#endif
|
||||
free(address);
|
||||
}
|
||||
|
||||
void *SzAllocTemp(ISzAllocPtr p, size_t size)
|
||||
{
|
||||
UNUSED_VAR(p)
|
||||
if (size == 0)
|
||||
return 0;
|
||||
#ifdef SZ_ALLOC_DEBUG
|
||||
Print_Alloc("Alloc_temp", size, &g_allocCountTemp);
|
||||
/*
|
||||
#ifdef _WIN32
|
||||
return HeapAlloc(GetProcessHeap(), 0, size);
|
||||
#endif
|
||||
*/
|
||||
#endif
|
||||
return malloc(size);
|
||||
}
|
||||
|
||||
void SzFreeTemp(ISzAllocPtr p, void *address)
|
||||
{
|
||||
UNUSED_VAR(p)
|
||||
#ifdef SZ_ALLOC_DEBUG
|
||||
if (address)
|
||||
Print_Free("Free_temp ", &g_allocCountTemp);
|
||||
/*
|
||||
#ifdef _WIN32
|
||||
HeapFree(GetProcessHeap(), 0, address);
|
||||
return;
|
||||
#endif
|
||||
*/
|
||||
#endif
|
||||
free(address);
|
||||
}
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
/* 7zAlloc.h -- Allocation functions
|
||||
2023-03-04 : Igor Pavlov : Public domain */
|
||||
|
||||
#ifndef ZIP7_INC_7Z_ALLOC_H
|
||||
#define ZIP7_INC_7Z_ALLOC_H
|
||||
|
||||
#include "7zTypes.h"
|
||||
|
||||
EXTERN_C_BEGIN
|
||||
|
||||
void *SzAlloc(ISzAllocPtr p, size_t size);
|
||||
void SzFree(ISzAllocPtr p, void *address);
|
||||
|
||||
void *SzAllocTemp(ISzAllocPtr p, size_t size);
|
||||
void SzFreeTemp(ISzAllocPtr p, void *address);
|
||||
|
||||
EXTERN_C_END
|
||||
|
||||
#endif
|
||||
Vendored
+1701
File diff suppressed because it is too large.
Load diff
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
/* 7zBuf.c -- Byte Buffer
|
||||
2017-04-03 : Igor Pavlov : Public domain */
|
||||
|
||||
#include "Precomp.h"
|
||||
|
||||
#include "7zBuf.h"
|
||||
|
||||
void Buf_Init(CBuf *p)
|
||||
{
|
||||
p->data = 0;
|
||||
p->size = 0;
|
||||
}
|
||||
|
||||
int Buf_Create(CBuf *p, size_t size, ISzAllocPtr alloc)
|
||||
{
|
||||
p->size = 0;
|
||||
if (size == 0)
|
||||
{
|
||||
p->data = 0;
|
||||
return 1;
|
||||
}
|
||||
p->data = (Byte *)ISzAlloc_Alloc(alloc, size);
|
||||
if (p->data)
|
||||
{
|
||||
p->size = size;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Buf_Free(CBuf *p, ISzAllocPtr alloc)
|
||||
{
|
||||
ISzAlloc_Free(alloc, p->data);
|
||||
p->data = 0;
|
||||
p->size = 0;
|
||||
}
|
||||
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
/* 7zBuf.h -- Byte Buffer
|
||||
2023-03-04 : Igor Pavlov : Public domain */
|
||||
|
||||
#ifndef ZIP7_INC_7Z_BUF_H
|
||||
#define ZIP7_INC_7Z_BUF_H
|
||||
|
||||
#include "7zTypes.h"
|
||||
|
||||
EXTERN_C_BEGIN
|
||||
|
||||
typedef struct
|
||||
{
|
||||
Byte *data;
|
||||
size_t size;
|
||||
} CBuf;
|
||||
|
||||
void Buf_Init(CBuf *p);
|
||||
int Buf_Create(CBuf *p, size_t size, ISzAllocPtr alloc);
|
||||
void Buf_Free(CBuf *p, ISzAllocPtr alloc);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
Byte *data;
|
||||
size_t size;
|
||||
size_t pos;
|
||||
} CDynBuf;
|
||||
|
||||
void DynBuf_Construct(CDynBuf *p);
|
||||
void DynBuf_SeekToBeg(CDynBuf *p);
|
||||
int DynBuf_Write(CDynBuf *p, const Byte *buf, size_t size, ISzAllocPtr alloc);
|
||||
void DynBuf_Free(CDynBuf *p, ISzAllocPtr alloc);
|
||||
|
||||
EXTERN_C_END
|
||||
|
||||
#endif
|
||||
Vendored
+52
@@ -0,0 +1,52 @@
|
||||
/* 7zBuf2.c -- Byte Buffer
|
||||
2017-04-03 : Igor Pavlov : Public domain */
|
||||
|
||||
#include "Precomp.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "7zBuf.h"
|
||||
|
||||
void DynBuf_Construct(CDynBuf *p)
|
||||
{
|
||||
p->data = 0;
|
||||
p->size = 0;
|
||||
p->pos = 0;
|
||||
}
|
||||
|
||||
void DynBuf_SeekToBeg(CDynBuf *p)
|
||||
{
|
||||
p->pos = 0;
|
||||
}
|
||||
|
||||
int DynBuf_Write(CDynBuf *p, const Byte *buf, size_t size, ISzAllocPtr alloc)
|
||||
{
|
||||
if (size > p->size - p->pos)
|
||||
{
|
||||
size_t newSize = p->pos + size;
|
||||
Byte *data;
|
||||
newSize += newSize / 4;
|
||||
data = (Byte *)ISzAlloc_Alloc(alloc, newSize);
|
||||
if (!data)
|
||||
return 0;
|
||||
p->size = newSize;
|
||||
if (p->pos != 0)
|
||||
memcpy(data, p->data, p->pos);
|
||||
ISzAlloc_Free(alloc, p->data);
|
||||
p->data = data;
|
||||
}
|
||||
if (size != 0)
|
||||
{
|
||||
memcpy(p->data + p->pos, buf, size);
|
||||
p->pos += size;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void DynBuf_Free(CDynBuf *p, ISzAllocPtr alloc)
|
||||
{
|
||||
ISzAlloc_Free(alloc, p->data);
|
||||
p->data = 0;
|
||||
p->size = 0;
|
||||
p->pos = 0;
|
||||
}
|
||||
Vendored
+420
@@ -0,0 +1,420 @@
|
||||
/* 7zCrc.c -- CRC32 calculation and init
|
||||
2024-03-01 : Igor Pavlov : Public domain */
|
||||
|
||||
#include "Precomp.h"
|
||||
|
||||
#include "7zCrc.h"
|
||||
#include "CpuArch.h"
|
||||
|
||||
// for debug:
|
||||
// #define __ARM_FEATURE_CRC32 1
|
||||
|
||||
#ifdef __ARM_FEATURE_CRC32
|
||||
// #pragma message("__ARM_FEATURE_CRC32")
|
||||
#define Z7_CRC_HW_FORCE
|
||||
#endif
|
||||
|
||||
// #define Z7_CRC_DEBUG_BE
|
||||
#ifdef Z7_CRC_DEBUG_BE
|
||||
#undef MY_CPU_LE
|
||||
#define MY_CPU_BE
|
||||
#endif
|
||||
|
||||
#ifdef Z7_CRC_HW_FORCE
|
||||
#define Z7_CRC_NUM_TABLES_USE 1
|
||||
#else
|
||||
#ifdef Z7_CRC_NUM_TABLES
|
||||
#define Z7_CRC_NUM_TABLES_USE Z7_CRC_NUM_TABLES
|
||||
#else
|
||||
#define Z7_CRC_NUM_TABLES_USE 12
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if Z7_CRC_NUM_TABLES_USE < 1
|
||||
#error Stop_Compiling_Bad_Z7_CRC_NUM_TABLES
|
||||
#endif
|
||||
|
||||
#if defined(MY_CPU_LE) || (Z7_CRC_NUM_TABLES_USE == 1)
|
||||
#define Z7_CRC_NUM_TABLES_TOTAL Z7_CRC_NUM_TABLES_USE
|
||||
#else
|
||||
#define Z7_CRC_NUM_TABLES_TOTAL (Z7_CRC_NUM_TABLES_USE + 1)
|
||||
#endif
|
||||
|
||||
#ifndef Z7_CRC_HW_FORCE
|
||||
|
||||
#if Z7_CRC_NUM_TABLES_USE == 1 \
|
||||
|| (!defined(MY_CPU_LE) && !defined(MY_CPU_BE))
|
||||
#define CRC_UPDATE_BYTE_2(crc, b) (table[((crc) ^ (b)) & 0xFF] ^ ((crc) >> 8))
|
||||
#define Z7_CRC_UPDATE_T1_FUNC_NAME CrcUpdateGT1
|
||||
static UInt32 Z7_FASTCALL Z7_CRC_UPDATE_T1_FUNC_NAME(UInt32 v, const void *data, size_t size)
|
||||
{
|
||||
const UInt32 *table = g_CrcTable;
|
||||
const Byte *p = (const Byte *)data;
|
||||
const Byte *lim = p + size;
|
||||
for (; p != lim; p++)
|
||||
v = CRC_UPDATE_BYTE_2(v, *p);
|
||||
return v;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#if Z7_CRC_NUM_TABLES_USE != 1
|
||||
#ifndef MY_CPU_BE
|
||||
#define FUNC_NAME_LE_2(s) CrcUpdateT ## s
|
||||
#define FUNC_NAME_LE_1(s) FUNC_NAME_LE_2(s)
|
||||
#define FUNC_NAME_LE FUNC_NAME_LE_1(Z7_CRC_NUM_TABLES_USE)
|
||||
UInt32 Z7_FASTCALL FUNC_NAME_LE (UInt32 v, const void *data, size_t size, const UInt32 *table);
|
||||
#endif
|
||||
#ifndef MY_CPU_LE
|
||||
#define FUNC_NAME_BE_2(s) CrcUpdateT1_BeT ## s
|
||||
#define FUNC_NAME_BE_1(s) FUNC_NAME_BE_2(s)
|
||||
#define FUNC_NAME_BE FUNC_NAME_BE_1(Z7_CRC_NUM_TABLES_USE)
|
||||
UInt32 Z7_FASTCALL FUNC_NAME_BE (UInt32 v, const void *data, size_t size, const UInt32 *table);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#endif // Z7_CRC_HW_FORCE
|
||||
|
||||
/* ---------- hardware CRC ---------- */
|
||||
|
||||
#ifdef MY_CPU_LE
|
||||
|
||||
#if defined(MY_CPU_ARM_OR_ARM64)
|
||||
// #pragma message("ARM*")
|
||||
|
||||
#if (defined(__clang__) && (__clang_major__ >= 3)) \
|
||||
|| defined(__GNUC__) && (__GNUC__ >= 6) && defined(MY_CPU_ARM64) \
|
||||
|| defined(__GNUC__) && (__GNUC__ >= 8)
|
||||
#if !defined(__ARM_FEATURE_CRC32)
|
||||
// #pragma message("!defined(__ARM_FEATURE_CRC32)")
|
||||
Z7_DIAGNOSTIC_IGNORE_BEGIN_RESERVED_MACRO_IDENTIFIER
|
||||
#define __ARM_FEATURE_CRC32 1
|
||||
Z7_DIAGNOSTIC_IGNORE_END_RESERVED_MACRO_IDENTIFIER
|
||||
#define Z7_ARM_FEATURE_CRC32_WAS_SET
|
||||
#if defined(__clang__)
|
||||
#if defined(MY_CPU_ARM64)
|
||||
#define ATTRIB_CRC __attribute__((__target__("crc")))
|
||||
#else
|
||||
#define ATTRIB_CRC __attribute__((__target__("armv8-a,crc")))
|
||||
#endif
|
||||
#else
|
||||
#if defined(MY_CPU_ARM64)
|
||||
#if !defined(Z7_GCC_VERSION) || (Z7_GCC_VERSION >= 60000)
|
||||
#define ATTRIB_CRC __attribute__((__target__("+crc")))
|
||||
#endif
|
||||
#else
|
||||
#if !defined(Z7_GCC_VERSION) || (__GNUC__ >= 8)
|
||||
#if defined(__ARM_FP) && __GNUC__ >= 8
|
||||
// for -mfloat-abi=hard: similar to <arm_acle.h>
|
||||
#define ATTRIB_CRC __attribute__((__target__("arch=armv8-a+crc+simd")))
|
||||
#else
|
||||
#define ATTRIB_CRC __attribute__((__target__("arch=armv8-a+crc")))
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#if defined(__ARM_FEATURE_CRC32)
|
||||
// #pragma message("<arm_acle.h>")
|
||||
/*
|
||||
arm_acle.h (GGC):
|
||||
before Nov 17, 2017:
|
||||
#ifdef __ARM_FEATURE_CRC32
|
||||
|
||||
Nov 17, 2017: gcc10.0 (gcc 9.2.0) checked"
|
||||
#if __ARM_ARCH >= 8
|
||||
#pragma GCC target ("arch=armv8-a+crc")
|
||||
|
||||
Aug 22, 2019: GCC 8.4?, 9.2.1, 10.1:
|
||||
#ifdef __ARM_FEATURE_CRC32
|
||||
#ifdef __ARM_FP
|
||||
#pragma GCC target ("arch=armv8-a+crc+simd")
|
||||
#else
|
||||
#pragma GCC target ("arch=armv8-a+crc")
|
||||
#endif
|
||||
*/
|
||||
#if defined(__ARM_ARCH) && __ARM_ARCH < 8
|
||||
#if defined(Z7_GCC_VERSION) && (__GNUC__ == 8) && (Z7_GCC_VERSION < 80400) \
|
||||
|| defined(Z7_GCC_VERSION) && (__GNUC__ == 9) && (Z7_GCC_VERSION < 90201) \
|
||||
|| defined(Z7_GCC_VERSION) && (__GNUC__ == 10) && (Z7_GCC_VERSION < 100100)
|
||||
Z7_DIAGNOSTIC_IGNORE_BEGIN_RESERVED_MACRO_IDENTIFIER
|
||||
// #pragma message("#define __ARM_ARCH 8")
|
||||
#undef __ARM_ARCH
|
||||
#define __ARM_ARCH 8
|
||||
Z7_DIAGNOSTIC_IGNORE_END_RESERVED_MACRO_IDENTIFIER
|
||||
#endif
|
||||
#endif
|
||||
#define Z7_CRC_HW_USE
|
||||
#include <arm_acle.h>
|
||||
#endif
|
||||
#elif defined(_MSC_VER)
|
||||
#if defined(MY_CPU_ARM64)
|
||||
#if (_MSC_VER >= 1910)
|
||||
#ifdef __clang__
|
||||
// #define Z7_CRC_HW_USE
|
||||
// #include <arm_acle.h>
|
||||
#else
|
||||
#define Z7_CRC_HW_USE
|
||||
#include <intrin.h>
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#else // non-ARM*
|
||||
|
||||
// #define Z7_CRC_HW_USE // for debug : we can test HW-branch of code
|
||||
#ifdef Z7_CRC_HW_USE
|
||||
#include "7zCrcEmu.h"
|
||||
#endif
|
||||
|
||||
#endif // non-ARM*
|
||||
|
||||
|
||||
|
||||
#if defined(Z7_CRC_HW_USE)
|
||||
|
||||
// #pragma message("USE ARM HW CRC")
|
||||
|
||||
#ifdef MY_CPU_64BIT
|
||||
#define CRC_HW_WORD_TYPE UInt64
|
||||
#define CRC_HW_WORD_FUNC __crc32d
|
||||
#else
|
||||
#define CRC_HW_WORD_TYPE UInt32
|
||||
#define CRC_HW_WORD_FUNC __crc32w
|
||||
#endif
|
||||
|
||||
#define CRC_HW_UNROLL_BYTES (sizeof(CRC_HW_WORD_TYPE) * 4)
|
||||
|
||||
#ifdef ATTRIB_CRC
|
||||
ATTRIB_CRC
|
||||
#endif
|
||||
Z7_NO_INLINE
|
||||
#ifdef Z7_CRC_HW_FORCE
|
||||
UInt32 Z7_FASTCALL CrcUpdate
|
||||
#else
|
||||
static UInt32 Z7_FASTCALL CrcUpdate_HW
|
||||
#endif
|
||||
(UInt32 v, const void *data, size_t size)
|
||||
{
|
||||
const Byte *p = (const Byte *)data;
|
||||
for (; size != 0 && ((unsigned)(ptrdiff_t)p & (CRC_HW_UNROLL_BYTES - 1)) != 0; size--)
|
||||
v = __crc32b(v, *p++);
|
||||
if (size >= CRC_HW_UNROLL_BYTES)
|
||||
{
|
||||
const Byte *lim = p + size;
|
||||
size &= CRC_HW_UNROLL_BYTES - 1;
|
||||
lim -= size;
|
||||
do
|
||||
{
|
||||
v = CRC_HW_WORD_FUNC(v, *(const CRC_HW_WORD_TYPE *)(const void *)(p));
|
||||
v = CRC_HW_WORD_FUNC(v, *(const CRC_HW_WORD_TYPE *)(const void *)(p + sizeof(CRC_HW_WORD_TYPE)));
|
||||
p += 2 * sizeof(CRC_HW_WORD_TYPE);
|
||||
v = CRC_HW_WORD_FUNC(v, *(const CRC_HW_WORD_TYPE *)(const void *)(p));
|
||||
v = CRC_HW_WORD_FUNC(v, *(const CRC_HW_WORD_TYPE *)(const void *)(p + sizeof(CRC_HW_WORD_TYPE)));
|
||||
p += 2 * sizeof(CRC_HW_WORD_TYPE);
|
||||
}
|
||||
while (p != lim);
|
||||
}
|
||||
|
||||
for (; size != 0; size--)
|
||||
v = __crc32b(v, *p++);
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
#ifdef Z7_ARM_FEATURE_CRC32_WAS_SET
|
||||
Z7_DIAGNOSTIC_IGNORE_BEGIN_RESERVED_MACRO_IDENTIFIER
|
||||
#undef __ARM_FEATURE_CRC32
|
||||
Z7_DIAGNOSTIC_IGNORE_END_RESERVED_MACRO_IDENTIFIER
|
||||
#undef Z7_ARM_FEATURE_CRC32_WAS_SET
|
||||
#endif
|
||||
|
||||
#endif // defined(Z7_CRC_HW_USE)
|
||||
#endif // MY_CPU_LE
|
||||
|
||||
|
||||
|
||||
#ifndef Z7_CRC_HW_FORCE
|
||||
|
||||
#if defined(Z7_CRC_HW_USE) || defined(Z7_CRC_UPDATE_T1_FUNC_NAME)
|
||||
/*
|
||||
typedef UInt32 (Z7_FASTCALL *Z7_CRC_UPDATE_WITH_TABLE_FUNC)
|
||||
(UInt32 v, const void *data, size_t size, const UInt32 *table);
|
||||
Z7_CRC_UPDATE_WITH_TABLE_FUNC g_CrcUpdate;
|
||||
*/
|
||||
static unsigned g_Crc_Algo;
|
||||
#if (!defined(MY_CPU_LE) && !defined(MY_CPU_BE))
|
||||
static unsigned g_Crc_Be;
|
||||
#endif
|
||||
#endif // defined(Z7_CRC_HW_USE) || defined(Z7_CRC_UPDATE_T1_FUNC_NAME)
|
||||
|
||||
|
||||
|
||||
Z7_NO_INLINE
|
||||
#ifdef Z7_CRC_HW_USE
|
||||
static UInt32 Z7_FASTCALL CrcUpdate_Base
|
||||
#else
|
||||
UInt32 Z7_FASTCALL CrcUpdate
|
||||
#endif
|
||||
(UInt32 crc, const void *data, size_t size)
|
||||
{
|
||||
#if Z7_CRC_NUM_TABLES_USE == 1
|
||||
return Z7_CRC_UPDATE_T1_FUNC_NAME(crc, data, size);
|
||||
#else // Z7_CRC_NUM_TABLES_USE != 1
|
||||
#ifdef Z7_CRC_UPDATE_T1_FUNC_NAME
|
||||
if (g_Crc_Algo == 1)
|
||||
return Z7_CRC_UPDATE_T1_FUNC_NAME(crc, data, size);
|
||||
#endif
|
||||
|
||||
#ifdef MY_CPU_LE
|
||||
return FUNC_NAME_LE(crc, data, size, g_CrcTable);
|
||||
#elif defined(MY_CPU_BE)
|
||||
return FUNC_NAME_BE(crc, data, size, g_CrcTable);
|
||||
#else
|
||||
if (g_Crc_Be)
|
||||
return FUNC_NAME_BE(crc, data, size, g_CrcTable);
|
||||
else
|
||||
return FUNC_NAME_LE(crc, data, size, g_CrcTable);
|
||||
#endif
|
||||
#endif // Z7_CRC_NUM_TABLES_USE != 1
|
||||
}
|
||||
|
||||
|
||||
#ifdef Z7_CRC_HW_USE
|
||||
Z7_NO_INLINE
|
||||
UInt32 Z7_FASTCALL CrcUpdate(UInt32 crc, const void *data, size_t size)
|
||||
{
|
||||
if (g_Crc_Algo == 0)
|
||||
return CrcUpdate_HW(crc, data, size);
|
||||
return CrcUpdate_Base(crc, data, size);
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // !defined(Z7_CRC_HW_FORCE)
|
||||
|
||||
|
||||
|
||||
UInt32 Z7_FASTCALL CrcCalc(const void *data, size_t size)
|
||||
{
|
||||
return CrcUpdate(CRC_INIT_VAL, data, size) ^ CRC_INIT_VAL;
|
||||
}
|
||||
|
||||
|
||||
MY_ALIGN(64)
|
||||
UInt32 g_CrcTable[256 * Z7_CRC_NUM_TABLES_TOTAL];
|
||||
|
||||
|
||||
void Z7_FASTCALL CrcGenerateTable(void)
|
||||
{
|
||||
UInt32 i;
|
||||
for (i = 0; i < 256; i++)
|
||||
{
|
||||
#if defined(Z7_CRC_HW_FORCE)
|
||||
g_CrcTable[i] = __crc32b(i, 0);
|
||||
#else
|
||||
#define kCrcPoly 0xEDB88320
|
||||
UInt32 r = i;
|
||||
unsigned j;
|
||||
for (j = 0; j < 8; j++)
|
||||
r = (r >> 1) ^ (kCrcPoly & ((UInt32)0 - (r & 1)));
|
||||
g_CrcTable[i] = r;
|
||||
#endif
|
||||
}
|
||||
for (i = 256; i < 256 * Z7_CRC_NUM_TABLES_USE; i++)
|
||||
{
|
||||
const UInt32 r = g_CrcTable[(size_t)i - 256];
|
||||
g_CrcTable[i] = g_CrcTable[r & 0xFF] ^ (r >> 8);
|
||||
}
|
||||
|
||||
#if !defined(Z7_CRC_HW_FORCE) && \
|
||||
(defined(Z7_CRC_HW_USE) || defined(Z7_CRC_UPDATE_T1_FUNC_NAME) || defined(MY_CPU_BE))
|
||||
|
||||
#if Z7_CRC_NUM_TABLES_USE <= 1
|
||||
g_Crc_Algo = 1;
|
||||
#else // Z7_CRC_NUM_TABLES_USE <= 1
|
||||
|
||||
#if defined(MY_CPU_LE)
|
||||
g_Crc_Algo = Z7_CRC_NUM_TABLES_USE;
|
||||
#else // !defined(MY_CPU_LE)
|
||||
{
|
||||
#ifndef MY_CPU_BE
|
||||
UInt32 k = 0x01020304;
|
||||
const Byte *p = (const Byte *)&k;
|
||||
if (p[0] == 4 && p[1] == 3)
|
||||
g_Crc_Algo = Z7_CRC_NUM_TABLES_USE;
|
||||
else if (p[0] != 1 || p[1] != 2)
|
||||
g_Crc_Algo = 1;
|
||||
else
|
||||
#endif // MY_CPU_BE
|
||||
{
|
||||
for (i = 256 * Z7_CRC_NUM_TABLES_TOTAL - 1; i >= 256; i--)
|
||||
{
|
||||
const UInt32 x = g_CrcTable[(size_t)i - 256];
|
||||
g_CrcTable[i] = Z7_BSWAP32(x);
|
||||
}
|
||||
#if defined(Z7_CRC_UPDATE_T1_FUNC_NAME)
|
||||
g_Crc_Algo = Z7_CRC_NUM_TABLES_USE;
|
||||
#endif
|
||||
#if (!defined(MY_CPU_LE) && !defined(MY_CPU_BE))
|
||||
g_Crc_Be = 1;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif // !defined(MY_CPU_LE)
|
||||
|
||||
#ifdef MY_CPU_LE
|
||||
#ifdef Z7_CRC_HW_USE
|
||||
if (CPU_IsSupported_CRC32())
|
||||
g_Crc_Algo = 0;
|
||||
#endif // Z7_CRC_HW_USE
|
||||
#endif // MY_CPU_LE
|
||||
|
||||
#endif // Z7_CRC_NUM_TABLES_USE <= 1
|
||||
#endif // g_Crc_Algo was declared
|
||||
}
|
||||
|
||||
Z7_CRC_UPDATE_FUNC z7_GetFunc_CrcUpdate(unsigned algo)
|
||||
{
|
||||
if (algo == 0)
|
||||
return &CrcUpdate;
|
||||
|
||||
#if defined(Z7_CRC_HW_USE)
|
||||
if (algo == sizeof(CRC_HW_WORD_TYPE) * 8)
|
||||
{
|
||||
#ifdef Z7_CRC_HW_FORCE
|
||||
return &CrcUpdate;
|
||||
#else
|
||||
if (g_Crc_Algo == 0)
|
||||
return &CrcUpdate_HW;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef Z7_CRC_HW_FORCE
|
||||
if (algo == Z7_CRC_NUM_TABLES_USE)
|
||||
return
|
||||
#ifdef Z7_CRC_HW_USE
|
||||
&CrcUpdate_Base;
|
||||
#else
|
||||
&CrcUpdate;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#undef kCrcPoly
|
||||
#undef Z7_CRC_NUM_TABLES_USE
|
||||
#undef Z7_CRC_NUM_TABLES_TOTAL
|
||||
#undef CRC_UPDATE_BYTE_2
|
||||
#undef FUNC_NAME_LE_2
|
||||
#undef FUNC_NAME_LE_1
|
||||
#undef FUNC_NAME_LE
|
||||
#undef FUNC_NAME_BE_2
|
||||
#undef FUNC_NAME_BE_1
|
||||
#undef FUNC_NAME_BE
|
||||
|
||||
#undef CRC_HW_UNROLL_BYTES
|
||||
#undef CRC_HW_WORD_FUNC
|
||||
#undef CRC_HW_WORD_TYPE
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
/* 7zCrc.h -- CRC32 calculation
|
||||
2024-01-22 : Igor Pavlov : Public domain */
|
||||
|
||||
#ifndef ZIP7_INC_7Z_CRC_H
|
||||
#define ZIP7_INC_7Z_CRC_H
|
||||
|
||||
#include "7zTypes.h"
|
||||
|
||||
EXTERN_C_BEGIN
|
||||
|
||||
extern UInt32 g_CrcTable[];
|
||||
|
||||
/* Call CrcGenerateTable one time before other CRC functions */
|
||||
void Z7_FASTCALL CrcGenerateTable(void);
|
||||
|
||||
#define CRC_INIT_VAL 0xFFFFFFFF
|
||||
#define CRC_GET_DIGEST(crc) ((crc) ^ CRC_INIT_VAL)
|
||||
#define CRC_UPDATE_BYTE(crc, b) (g_CrcTable[((crc) ^ (b)) & 0xFF] ^ ((crc) >> 8))
|
||||
|
||||
UInt32 Z7_FASTCALL CrcUpdate(UInt32 crc, const void *data, size_t size);
|
||||
UInt32 Z7_FASTCALL CrcCalc(const void *data, size_t size);
|
||||
|
||||
typedef UInt32 (Z7_FASTCALL *Z7_CRC_UPDATE_FUNC)(UInt32 v, const void *data, size_t size);
|
||||
Z7_CRC_UPDATE_FUNC z7_GetFunc_CrcUpdate(unsigned algo);
|
||||
|
||||
EXTERN_C_END
|
||||
|
||||
#endif
|
||||
Vendored
+199
@@ -0,0 +1,199 @@
|
||||
/* 7zCrcOpt.c -- CRC32 calculation (optimized functions)
|
||||
2023-12-07 : Igor Pavlov : Public domain */
|
||||
|
||||
#include "Precomp.h"
|
||||
|
||||
#include "CpuArch.h"
|
||||
|
||||
#if !defined(Z7_CRC_NUM_TABLES) || Z7_CRC_NUM_TABLES > 1
|
||||
|
||||
// for debug only : define Z7_CRC_DEBUG_BE to test big-endian code in little-endian cpu
|
||||
// #define Z7_CRC_DEBUG_BE
|
||||
#ifdef Z7_CRC_DEBUG_BE
|
||||
#undef MY_CPU_LE
|
||||
#define MY_CPU_BE
|
||||
#endif
|
||||
|
||||
// the value Z7_CRC_NUM_TABLES_USE must be defined to same value as in 7zCrc.c
|
||||
#ifdef Z7_CRC_NUM_TABLES
|
||||
#define Z7_CRC_NUM_TABLES_USE Z7_CRC_NUM_TABLES
|
||||
#else
|
||||
#define Z7_CRC_NUM_TABLES_USE 12
|
||||
#endif
|
||||
|
||||
#if Z7_CRC_NUM_TABLES_USE % 4 || \
|
||||
Z7_CRC_NUM_TABLES_USE < 4 * 1 || \
|
||||
Z7_CRC_NUM_TABLES_USE > 4 * 6
|
||||
#error Stop_Compiling_Bad_Z7_CRC_NUM_TABLES
|
||||
#endif
|
||||
|
||||
|
||||
#ifndef MY_CPU_BE
|
||||
|
||||
#define CRC_UPDATE_BYTE_2(crc, b) (table[((crc) ^ (b)) & 0xFF] ^ ((crc) >> 8))
|
||||
|
||||
#define Q(n, d) \
|
||||
( (table + ((n) * 4 + 3) * 0x100)[(Byte)(d)] \
|
||||
^ (table + ((n) * 4 + 2) * 0x100)[((d) >> 1 * 8) & 0xFF] \
|
||||
^ (table + ((n) * 4 + 1) * 0x100)[((d) >> 2 * 8) & 0xFF] \
|
||||
^ (table + ((n) * 4 + 0) * 0x100)[((d) >> 3 * 8)] )
|
||||
|
||||
#define R(a) *((const UInt32 *)(const void *)p + (a))
|
||||
|
||||
#define CRC_FUNC_PRE_LE2(step) \
|
||||
UInt32 Z7_FASTCALL CrcUpdateT ## step (UInt32 v, const void *data, size_t size, const UInt32 *table)
|
||||
|
||||
#define CRC_FUNC_PRE_LE(step) \
|
||||
CRC_FUNC_PRE_LE2(step); \
|
||||
CRC_FUNC_PRE_LE2(step)
|
||||
|
||||
CRC_FUNC_PRE_LE(Z7_CRC_NUM_TABLES_USE)
|
||||
{
|
||||
const Byte *p = (const Byte *)data;
|
||||
const Byte *lim;
|
||||
for (; size && ((unsigned)(ptrdiff_t)p & (7 - (Z7_CRC_NUM_TABLES_USE & 4))) != 0; size--, p++)
|
||||
v = CRC_UPDATE_BYTE_2(v, *p);
|
||||
lim = p + size;
|
||||
if (size >= Z7_CRC_NUM_TABLES_USE)
|
||||
{
|
||||
lim -= Z7_CRC_NUM_TABLES_USE;
|
||||
do
|
||||
{
|
||||
v ^= R(0);
|
||||
{
|
||||
#if Z7_CRC_NUM_TABLES_USE == 1 * 4
|
||||
v = Q(0, v);
|
||||
#else
|
||||
#define U2(r, op) \
|
||||
{ d = R(r); x op Q(Z7_CRC_NUM_TABLES_USE / 4 - 1 - (r), d); }
|
||||
UInt32 d, x;
|
||||
U2(1, =)
|
||||
#if Z7_CRC_NUM_TABLES_USE >= 3 * 4
|
||||
#define U(r) U2(r, ^=)
|
||||
U(2)
|
||||
#if Z7_CRC_NUM_TABLES_USE >= 4 * 4
|
||||
U(3)
|
||||
#if Z7_CRC_NUM_TABLES_USE >= 5 * 4
|
||||
U(4)
|
||||
#if Z7_CRC_NUM_TABLES_USE >= 6 * 4
|
||||
U(5)
|
||||
#if Z7_CRC_NUM_TABLES_USE >= 7 * 4
|
||||
#error Stop_Compiling_Bad_Z7_CRC_NUM_TABLES
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#undef U
|
||||
#undef U2
|
||||
v = x ^ Q(Z7_CRC_NUM_TABLES_USE / 4 - 1, v);
|
||||
#endif
|
||||
}
|
||||
p += Z7_CRC_NUM_TABLES_USE;
|
||||
}
|
||||
while (p <= lim);
|
||||
lim += Z7_CRC_NUM_TABLES_USE;
|
||||
}
|
||||
for (; p < lim; p++)
|
||||
v = CRC_UPDATE_BYTE_2(v, *p);
|
||||
return v;
|
||||
}
|
||||
|
||||
#undef CRC_UPDATE_BYTE_2
|
||||
#undef R
|
||||
#undef Q
|
||||
#undef CRC_FUNC_PRE_LE
|
||||
#undef CRC_FUNC_PRE_LE2
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
|
||||
#ifndef MY_CPU_LE
|
||||
|
||||
#define CRC_UPDATE_BYTE_2_BE(crc, b) (table[((crc) >> 24) ^ (b)] ^ ((crc) << 8))
|
||||
|
||||
#define Q(n, d) \
|
||||
( (table + ((n) * 4 + 0) * 0x100)[((d)) & 0xFF] \
|
||||
^ (table + ((n) * 4 + 1) * 0x100)[((d) >> 1 * 8) & 0xFF] \
|
||||
^ (table + ((n) * 4 + 2) * 0x100)[((d) >> 2 * 8) & 0xFF] \
|
||||
^ (table + ((n) * 4 + 3) * 0x100)[((d) >> 3 * 8)] )
|
||||
|
||||
#ifdef Z7_CRC_DEBUG_BE
|
||||
#define R(a) GetBe32a((const UInt32 *)(const void *)p + (a))
|
||||
#else
|
||||
#define R(a) *((const UInt32 *)(const void *)p + (a))
|
||||
#endif
|
||||
|
||||
|
||||
#define CRC_FUNC_PRE_BE2(step) \
|
||||
UInt32 Z7_FASTCALL CrcUpdateT1_BeT ## step (UInt32 v, const void *data, size_t size, const UInt32 *table)
|
||||
|
||||
#define CRC_FUNC_PRE_BE(step) \
|
||||
CRC_FUNC_PRE_BE2(step); \
|
||||
CRC_FUNC_PRE_BE2(step)
|
||||
|
||||
CRC_FUNC_PRE_BE(Z7_CRC_NUM_TABLES_USE)
|
||||
{
|
||||
const Byte *p = (const Byte *)data;
|
||||
const Byte *lim;
|
||||
table += 0x100;
|
||||
v = Z7_BSWAP32(v);
|
||||
for (; size && ((unsigned)(ptrdiff_t)p & (7 - (Z7_CRC_NUM_TABLES_USE & 4))) != 0; size--, p++)
|
||||
v = CRC_UPDATE_BYTE_2_BE(v, *p);
|
||||
lim = p + size;
|
||||
if (size >= Z7_CRC_NUM_TABLES_USE)
|
||||
{
|
||||
lim -= Z7_CRC_NUM_TABLES_USE;
|
||||
do
|
||||
{
|
||||
v ^= R(0);
|
||||
{
|
||||
#if Z7_CRC_NUM_TABLES_USE == 1 * 4
|
||||
v = Q(0, v);
|
||||
#else
|
||||
#define U2(r, op) \
|
||||
{ d = R(r); x op Q(Z7_CRC_NUM_TABLES_USE / 4 - 1 - (r), d); }
|
||||
UInt32 d, x;
|
||||
U2(1, =)
|
||||
#if Z7_CRC_NUM_TABLES_USE >= 3 * 4
|
||||
#define U(r) U2(r, ^=)
|
||||
U(2)
|
||||
#if Z7_CRC_NUM_TABLES_USE >= 4 * 4
|
||||
U(3)
|
||||
#if Z7_CRC_NUM_TABLES_USE >= 5 * 4
|
||||
U(4)
|
||||
#if Z7_CRC_NUM_TABLES_USE >= 6 * 4
|
||||
U(5)
|
||||
#if Z7_CRC_NUM_TABLES_USE >= 7 * 4
|
||||
#error Stop_Compiling_Bad_Z7_CRC_NUM_TABLES
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#undef U
|
||||
#undef U2
|
||||
v = x ^ Q(Z7_CRC_NUM_TABLES_USE / 4 - 1, v);
|
||||
#endif
|
||||
}
|
||||
p += Z7_CRC_NUM_TABLES_USE;
|
||||
}
|
||||
while (p <= lim);
|
||||
lim += Z7_CRC_NUM_TABLES_USE;
|
||||
}
|
||||
for (; p < lim; p++)
|
||||
v = CRC_UPDATE_BYTE_2_BE(v, *p);
|
||||
return Z7_BSWAP32(v);
|
||||
}
|
||||
|
||||
#undef CRC_UPDATE_BYTE_2_BE
|
||||
#undef R
|
||||
#undef Q
|
||||
#undef CRC_FUNC_PRE_BE
|
||||
#undef CRC_FUNC_PRE_BE2
|
||||
|
||||
#endif
|
||||
#undef Z7_CRC_NUM_TABLES_USE
|
||||
#endif
|
||||
Vendored
+672
@@ -0,0 +1,672 @@
|
||||
/* 7zDec.c -- Decoding from 7z folder
|
||||
: Igor Pavlov : Public domain */
|
||||
|
||||
#include "Precomp.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* #define Z7_PPMD_SUPPORT */
|
||||
|
||||
#include "7z.h"
|
||||
#include "7zCrc.h"
|
||||
|
||||
#include "Bcj2.h"
|
||||
#include "Bra.h"
|
||||
#include "CpuArch.h"
|
||||
#include "Delta.h"
|
||||
#include "LzmaDec.h"
|
||||
#include "Lzma2Dec.h"
|
||||
#ifdef Z7_PPMD_SUPPORT
|
||||
#include "Ppmd7.h"
|
||||
#endif
|
||||
|
||||
#define k_Copy 0
|
||||
#ifndef Z7_NO_METHOD_LZMA2
|
||||
#define k_LZMA2 0x21
|
||||
#endif
|
||||
#define k_LZMA 0x30101
|
||||
#define k_BCJ2 0x303011B
|
||||
|
||||
#if !defined(Z7_NO_METHODS_FILTERS)
|
||||
#define Z7_USE_BRANCH_FILTER
|
||||
#endif
|
||||
|
||||
#if !defined(Z7_NO_METHODS_FILTERS) || \
|
||||
defined(Z7_USE_NATIVE_BRANCH_FILTER) && defined(MY_CPU_ARM64)
|
||||
#define Z7_USE_FILTER_ARM64
|
||||
#ifndef Z7_USE_BRANCH_FILTER
|
||||
#define Z7_USE_BRANCH_FILTER
|
||||
#endif
|
||||
#define k_ARM64 0xa
|
||||
#endif
|
||||
|
||||
#if !defined(Z7_NO_METHODS_FILTERS) || \
|
||||
defined(Z7_USE_NATIVE_BRANCH_FILTER) && defined(MY_CPU_ARMT)
|
||||
#define Z7_USE_FILTER_ARMT
|
||||
#ifndef Z7_USE_BRANCH_FILTER
|
||||
#define Z7_USE_BRANCH_FILTER
|
||||
#endif
|
||||
#define k_ARMT 0x3030701
|
||||
#endif
|
||||
|
||||
#ifndef Z7_NO_METHODS_FILTERS
|
||||
#define k_Delta 3
|
||||
#define k_RISCV 0xb
|
||||
#define k_BCJ 0x3030103
|
||||
#define k_PPC 0x3030205
|
||||
#define k_IA64 0x3030401
|
||||
#define k_ARM 0x3030501
|
||||
#define k_SPARC 0x3030805
|
||||
#endif
|
||||
|
||||
#ifdef Z7_PPMD_SUPPORT
|
||||
|
||||
#define k_PPMD 0x30401
|
||||
|
||||
typedef struct
|
||||
{
|
||||
IByteIn vt;
|
||||
const Byte *cur;
|
||||
const Byte *end;
|
||||
const Byte *begin;
|
||||
UInt64 processed;
|
||||
BoolInt extra;
|
||||
SRes res;
|
||||
ILookInStreamPtr inStream;
|
||||
} CByteInToLook;
|
||||
|
||||
static Byte ReadByte(IByteInPtr pp)
|
||||
{
|
||||
Z7_CONTAINER_FROM_VTBL_TO_DECL_VAR_pp_vt_p(CByteInToLook)
|
||||
if (p->cur != p->end)
|
||||
return *p->cur++;
|
||||
if (p->res == SZ_OK)
|
||||
{
|
||||
size_t size = (size_t)(p->cur - p->begin);
|
||||
p->processed += size;
|
||||
p->res = ILookInStream_Skip(p->inStream, size);
|
||||
size = (1 << 25);
|
||||
p->res = ILookInStream_Look(p->inStream, (const void **)&p->begin, &size);
|
||||
p->cur = p->begin;
|
||||
p->end = p->begin + size;
|
||||
if (size != 0)
|
||||
return *p->cur++;
|
||||
}
|
||||
p->extra = True;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static SRes SzDecodePpmd(const Byte *props, unsigned propsSize, UInt64 inSize, ILookInStreamPtr inStream,
|
||||
Byte *outBuffer, SizeT outSize, ISzAllocPtr allocMain)
|
||||
{
|
||||
CPpmd7 *ppmd;
|
||||
SRes res;
|
||||
unsigned order;
|
||||
UInt32 memSize;
|
||||
|
||||
if (propsSize != 5)
|
||||
return SZ_ERROR_UNSUPPORTED;
|
||||
order = props[0];
|
||||
memSize = GetUi32(props + 1);
|
||||
if (order < PPMD7_MIN_ORDER ||
|
||||
order > PPMD7_MAX_ORDER ||
|
||||
memSize < PPMD7_MIN_MEM_SIZE ||
|
||||
memSize > PPMD7_MAX_MEM_SIZE)
|
||||
return SZ_ERROR_UNSUPPORTED;
|
||||
if ((ppmd = (CPpmd7 *)ISzAlloc_Alloc(allocMain, sizeof(CPpmd7))) == NULL)
|
||||
return SZ_ERROR_MEM;
|
||||
Ppmd7_Construct(ppmd);
|
||||
res = SZ_ERROR_MEM;
|
||||
if (Ppmd7_Alloc(ppmd, memSize, allocMain))
|
||||
{
|
||||
CByteInToLook s;
|
||||
s.vt.Read = ReadByte;
|
||||
s.inStream = inStream;
|
||||
s.begin = s.end = s.cur = NULL;
|
||||
s.extra = False;
|
||||
s.res = SZ_OK;
|
||||
s.processed = 0;
|
||||
|
||||
Ppmd7_Init(ppmd, order);
|
||||
ppmd->rc.dec.Stream = &s.vt;
|
||||
res = SZ_ERROR_DATA;
|
||||
if (Ppmd7z_RangeDec_Init(&ppmd->rc.dec) && !s.extra)
|
||||
{
|
||||
Byte *buf = outBuffer;
|
||||
const Byte *lim = buf + outSize;
|
||||
for (; buf != lim; buf++)
|
||||
{
|
||||
int sym = Ppmd7z_DecodeSymbol(ppmd);
|
||||
if (s.extra || sym < 0)
|
||||
break;
|
||||
*buf = (Byte)sym;
|
||||
}
|
||||
if (buf == lim)
|
||||
if (Ppmd7z_RangeDec_IsFinishedOK(&ppmd->rc.dec)
|
||||
// || (Ppmd7z_DecodeSymbol(&ppmd) == PPMD7_SYM_END && Ppmd7z_RangeDec_IsFinishedOK(&ppmd.rc.dec))
|
||||
)
|
||||
res = SZ_OK;
|
||||
}
|
||||
if (s.extra)
|
||||
res = (s.res != SZ_OK ? s.res : SZ_ERROR_DATA);
|
||||
else if (s.processed + (size_t)(s.cur - s.begin) != inSize)
|
||||
res = SZ_ERROR_DATA;
|
||||
Ppmd7_Free(ppmd, allocMain);
|
||||
}
|
||||
ISzAlloc_Free(allocMain, ppmd);
|
||||
return res;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
static SRes SzDecodeLzma(const Byte *props, unsigned propsSize, UInt64 inSize, ILookInStreamPtr inStream,
|
||||
Byte *outBuffer, SizeT outSize, ISzAllocPtr allocMain)
|
||||
{
|
||||
CLzmaDec state;
|
||||
SRes res = SZ_OK;
|
||||
|
||||
LzmaDec_CONSTRUCT(&state)
|
||||
RINOK(LzmaDec_AllocateProbs(&state, props, propsSize, allocMain))
|
||||
state.dic = outBuffer;
|
||||
state.dicBufSize = outSize;
|
||||
LzmaDec_Init(&state);
|
||||
|
||||
for (;;)
|
||||
{
|
||||
const void *inBuf = NULL;
|
||||
size_t lookahead = (1 << 18);
|
||||
if (lookahead > inSize)
|
||||
lookahead = (size_t)inSize;
|
||||
res = ILookInStream_Look(inStream, &inBuf, &lookahead);
|
||||
if (res != SZ_OK)
|
||||
break;
|
||||
|
||||
{
|
||||
SizeT inProcessed = (SizeT)lookahead, dicPos = state.dicPos;
|
||||
ELzmaStatus status;
|
||||
res = LzmaDec_DecodeToDic(&state, outSize, (const Byte *)inBuf, &inProcessed, LZMA_FINISH_END, &status);
|
||||
lookahead -= inProcessed;
|
||||
inSize -= inProcessed;
|
||||
if (res != SZ_OK)
|
||||
break;
|
||||
|
||||
if (status == LZMA_STATUS_FINISHED_WITH_MARK)
|
||||
{
|
||||
if (outSize != state.dicPos || inSize != 0)
|
||||
res = SZ_ERROR_DATA;
|
||||
break;
|
||||
}
|
||||
|
||||
if (outSize == state.dicPos && inSize == 0 && status == LZMA_STATUS_MAYBE_FINISHED_WITHOUT_MARK)
|
||||
break;
|
||||
|
||||
if (inProcessed == 0 && dicPos == state.dicPos)
|
||||
{
|
||||
res = SZ_ERROR_DATA;
|
||||
break;
|
||||
}
|
||||
|
||||
res = ILookInStream_Skip(inStream, inProcessed);
|
||||
if (res != SZ_OK)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
LzmaDec_FreeProbs(&state, allocMain);
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
#ifndef Z7_NO_METHOD_LZMA2
|
||||
|
||||
static SRes SzDecodeLzma2(const Byte *props, unsigned propsSize, UInt64 inSize, ILookInStreamPtr inStream,
|
||||
Byte *outBuffer, SizeT outSize, ISzAllocPtr allocMain)
|
||||
{
|
||||
CLzma2Dec state;
|
||||
SRes res = SZ_OK;
|
||||
|
||||
Lzma2Dec_CONSTRUCT(&state)
|
||||
if (propsSize != 1)
|
||||
return SZ_ERROR_DATA;
|
||||
RINOK(Lzma2Dec_AllocateProbs(&state, props[0], allocMain))
|
||||
state.decoder.dic = outBuffer;
|
||||
state.decoder.dicBufSize = outSize;
|
||||
Lzma2Dec_Init(&state);
|
||||
|
||||
for (;;)
|
||||
{
|
||||
const void *inBuf = NULL;
|
||||
size_t lookahead = (1 << 18);
|
||||
if (lookahead > inSize)
|
||||
lookahead = (size_t)inSize;
|
||||
res = ILookInStream_Look(inStream, &inBuf, &lookahead);
|
||||
if (res != SZ_OK)
|
||||
break;
|
||||
|
||||
{
|
||||
SizeT inProcessed = (SizeT)lookahead, dicPos = state.decoder.dicPos;
|
||||
ELzmaStatus status;
|
||||
res = Lzma2Dec_DecodeToDic(&state, outSize, (const Byte *)inBuf, &inProcessed, LZMA_FINISH_END, &status);
|
||||
lookahead -= inProcessed;
|
||||
inSize -= inProcessed;
|
||||
if (res != SZ_OK)
|
||||
break;
|
||||
|
||||
if (status == LZMA_STATUS_FINISHED_WITH_MARK)
|
||||
{
|
||||
if (outSize != state.decoder.dicPos || inSize != 0)
|
||||
res = SZ_ERROR_DATA;
|
||||
break;
|
||||
}
|
||||
|
||||
if (inProcessed == 0 && dicPos == state.decoder.dicPos)
|
||||
{
|
||||
res = SZ_ERROR_DATA;
|
||||
break;
|
||||
}
|
||||
|
||||
res = ILookInStream_Skip(inStream, inProcessed);
|
||||
if (res != SZ_OK)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Lzma2Dec_FreeProbs(&state, allocMain);
|
||||
return res;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
static SRes SzDecodeCopy(UInt64 inSize, ILookInStreamPtr inStream, Byte *outBuffer)
|
||||
{
|
||||
while (inSize > 0)
|
||||
{
|
||||
const void *inBuf;
|
||||
size_t curSize = (1 << 18);
|
||||
if (curSize > inSize)
|
||||
curSize = (size_t)inSize;
|
||||
RINOK(ILookInStream_Look(inStream, &inBuf, &curSize))
|
||||
if (curSize == 0)
|
||||
return SZ_ERROR_INPUT_EOF;
|
||||
memcpy(outBuffer, inBuf, curSize);
|
||||
outBuffer += curSize;
|
||||
inSize -= curSize;
|
||||
RINOK(ILookInStream_Skip(inStream, curSize))
|
||||
}
|
||||
return SZ_OK;
|
||||
}
|
||||
|
||||
static BoolInt IS_MAIN_METHOD(UInt32 m)
|
||||
{
|
||||
switch (m)
|
||||
{
|
||||
case k_Copy:
|
||||
case k_LZMA:
|
||||
#ifndef Z7_NO_METHOD_LZMA2
|
||||
case k_LZMA2:
|
||||
#endif
|
||||
#ifdef Z7_PPMD_SUPPORT
|
||||
case k_PPMD:
|
||||
#endif
|
||||
return True;
|
||||
default:
|
||||
return False;
|
||||
}
|
||||
}
|
||||
|
||||
static BoolInt IS_SUPPORTED_CODER(const CSzCoderInfo *c)
|
||||
{
|
||||
return
|
||||
c->NumStreams == 1
|
||||
/* && c->MethodID <= (UInt32)0xFFFFFFFF */
|
||||
&& IS_MAIN_METHOD((UInt32)c->MethodID);
|
||||
}
|
||||
|
||||
#define IS_BCJ2(c) ((c)->MethodID == k_BCJ2 && (c)->NumStreams == 4)
|
||||
|
||||
static SRes CheckSupportedFolder(const CSzFolder *f)
|
||||
{
|
||||
if (f->NumCoders < 1 || f->NumCoders > 4)
|
||||
return SZ_ERROR_UNSUPPORTED;
|
||||
if (!IS_SUPPORTED_CODER(&f->Coders[0]))
|
||||
return SZ_ERROR_UNSUPPORTED;
|
||||
if (f->NumCoders == 1)
|
||||
{
|
||||
if (f->NumPackStreams != 1 || f->PackStreams[0] != 0 || f->NumBonds != 0)
|
||||
return SZ_ERROR_UNSUPPORTED;
|
||||
return SZ_OK;
|
||||
}
|
||||
|
||||
|
||||
#if defined(Z7_USE_BRANCH_FILTER)
|
||||
|
||||
if (f->NumCoders == 2)
|
||||
{
|
||||
const CSzCoderInfo *c = &f->Coders[1];
|
||||
if (
|
||||
/* c->MethodID > (UInt32)0xFFFFFFFF || */
|
||||
c->NumStreams != 1
|
||||
|| f->NumPackStreams != 1
|
||||
|| f->PackStreams[0] != 0
|
||||
|| f->NumBonds != 1
|
||||
|| f->Bonds[0].InIndex != 1
|
||||
|| f->Bonds[0].OutIndex != 0)
|
||||
return SZ_ERROR_UNSUPPORTED;
|
||||
switch ((UInt32)c->MethodID)
|
||||
{
|
||||
#if !defined(Z7_NO_METHODS_FILTERS)
|
||||
case k_Delta:
|
||||
case k_BCJ:
|
||||
case k_PPC:
|
||||
case k_IA64:
|
||||
case k_SPARC:
|
||||
case k_ARM:
|
||||
case k_RISCV:
|
||||
#endif
|
||||
#ifdef Z7_USE_FILTER_ARM64
|
||||
case k_ARM64:
|
||||
#endif
|
||||
#ifdef Z7_USE_FILTER_ARMT
|
||||
case k_ARMT:
|
||||
#endif
|
||||
break;
|
||||
default:
|
||||
return SZ_ERROR_UNSUPPORTED;
|
||||
}
|
||||
return SZ_OK;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
if (f->NumCoders == 4)
|
||||
{
|
||||
if (!IS_SUPPORTED_CODER(&f->Coders[1])
|
||||
|| !IS_SUPPORTED_CODER(&f->Coders[2])
|
||||
|| !IS_BCJ2(&f->Coders[3]))
|
||||
return SZ_ERROR_UNSUPPORTED;
|
||||
if (f->NumPackStreams != 4
|
||||
|| f->PackStreams[0] != 2
|
||||
|| f->PackStreams[1] != 6
|
||||
|| f->PackStreams[2] != 1
|
||||
|| f->PackStreams[3] != 0
|
||||
|| f->NumBonds != 3
|
||||
|| f->Bonds[0].InIndex != 5 || f->Bonds[0].OutIndex != 0
|
||||
|| f->Bonds[1].InIndex != 4 || f->Bonds[1].OutIndex != 1
|
||||
|| f->Bonds[2].InIndex != 3 || f->Bonds[2].OutIndex != 2)
|
||||
return SZ_ERROR_UNSUPPORTED;
|
||||
return SZ_OK;
|
||||
}
|
||||
|
||||
return SZ_ERROR_UNSUPPORTED;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
static SRes SzFolder_Decode2(const CSzFolder *folder,
|
||||
const Byte *propsData,
|
||||
const UInt64 *unpackSizes,
|
||||
const UInt64 *packPositions,
|
||||
ILookInStreamPtr inStream, UInt64 startPos,
|
||||
Byte *outBuffer, SizeT outSize, ISzAllocPtr allocMain,
|
||||
Byte *tempBuf[])
|
||||
{
|
||||
UInt32 ci;
|
||||
SizeT tempSizes[3] = { 0, 0, 0};
|
||||
SizeT tempSize3 = 0;
|
||||
Byte *tempBuf3 = 0;
|
||||
|
||||
RINOK(CheckSupportedFolder(folder))
|
||||
|
||||
for (ci = 0; ci < folder->NumCoders; ci++)
|
||||
{
|
||||
const CSzCoderInfo *coder = &folder->Coders[ci];
|
||||
|
||||
if (IS_MAIN_METHOD((UInt32)coder->MethodID))
|
||||
{
|
||||
UInt32 si = 0;
|
||||
UInt64 offset;
|
||||
UInt64 inSize;
|
||||
Byte *outBufCur = outBuffer;
|
||||
SizeT outSizeCur = outSize;
|
||||
if (folder->NumCoders == 4)
|
||||
{
|
||||
const UInt32 indices[] = { 3, 2, 0 };
|
||||
const UInt64 unpackSize = unpackSizes[ci];
|
||||
si = indices[ci];
|
||||
if (ci < 2)
|
||||
{
|
||||
Byte *temp;
|
||||
outSizeCur = (SizeT)unpackSize;
|
||||
if (outSizeCur != unpackSize)
|
||||
return SZ_ERROR_MEM;
|
||||
temp = (Byte *)ISzAlloc_Alloc(allocMain, outSizeCur);
|
||||
if (!temp && outSizeCur != 0)
|
||||
return SZ_ERROR_MEM;
|
||||
outBufCur = tempBuf[1 - ci] = temp;
|
||||
tempSizes[1 - ci] = outSizeCur;
|
||||
}
|
||||
else if (ci == 2)
|
||||
{
|
||||
if (unpackSize > outSize) /* check it */
|
||||
return SZ_ERROR_PARAM;
|
||||
tempBuf3 = outBufCur = outBuffer + (outSize - (size_t)unpackSize);
|
||||
tempSize3 = outSizeCur = (SizeT)unpackSize;
|
||||
}
|
||||
else
|
||||
return SZ_ERROR_UNSUPPORTED;
|
||||
}
|
||||
offset = packPositions[si];
|
||||
inSize = packPositions[(size_t)si + 1] - offset;
|
||||
RINOK(LookInStream_SeekTo(inStream, startPos + offset))
|
||||
|
||||
if (coder->MethodID == k_Copy)
|
||||
{
|
||||
if (inSize != outSizeCur) /* check it */
|
||||
return SZ_ERROR_DATA;
|
||||
RINOK(SzDecodeCopy(inSize, inStream, outBufCur))
|
||||
}
|
||||
else if (coder->MethodID == k_LZMA)
|
||||
{
|
||||
RINOK(SzDecodeLzma(propsData + coder->PropsOffset, coder->PropsSize, inSize, inStream, outBufCur, outSizeCur, allocMain))
|
||||
}
|
||||
#ifndef Z7_NO_METHOD_LZMA2
|
||||
else if (coder->MethodID == k_LZMA2)
|
||||
{
|
||||
RINOK(SzDecodeLzma2(propsData + coder->PropsOffset, coder->PropsSize, inSize, inStream, outBufCur, outSizeCur, allocMain))
|
||||
}
|
||||
#endif
|
||||
#ifdef Z7_PPMD_SUPPORT
|
||||
else if (coder->MethodID == k_PPMD)
|
||||
{
|
||||
RINOK(SzDecodePpmd(propsData + coder->PropsOffset, coder->PropsSize, inSize, inStream, outBufCur, outSizeCur, allocMain))
|
||||
}
|
||||
#endif
|
||||
else
|
||||
return SZ_ERROR_UNSUPPORTED;
|
||||
}
|
||||
else if (coder->MethodID == k_BCJ2)
|
||||
{
|
||||
const UInt64 offset = packPositions[1];
|
||||
const UInt64 s3Size = packPositions[2] - offset;
|
||||
|
||||
if (ci != 3)
|
||||
return SZ_ERROR_UNSUPPORTED;
|
||||
|
||||
tempSizes[2] = (SizeT)s3Size;
|
||||
if (tempSizes[2] != s3Size)
|
||||
return SZ_ERROR_MEM;
|
||||
tempBuf[2] = (Byte *)ISzAlloc_Alloc(allocMain, tempSizes[2]);
|
||||
if (!tempBuf[2] && tempSizes[2] != 0)
|
||||
return SZ_ERROR_MEM;
|
||||
|
||||
RINOK(LookInStream_SeekTo(inStream, startPos + offset))
|
||||
RINOK(SzDecodeCopy(s3Size, inStream, tempBuf[2]))
|
||||
|
||||
if ((tempSizes[0] & 3) != 0 ||
|
||||
(tempSizes[1] & 3) != 0 ||
|
||||
tempSize3 + tempSizes[0] + tempSizes[1] != outSize)
|
||||
return SZ_ERROR_DATA;
|
||||
|
||||
{
|
||||
CBcj2Dec p;
|
||||
|
||||
p.bufs[0] = tempBuf3; p.lims[0] = tempBuf3 + tempSize3;
|
||||
p.bufs[1] = tempBuf[0]; p.lims[1] = tempBuf[0] + tempSizes[0];
|
||||
p.bufs[2] = tempBuf[1]; p.lims[2] = tempBuf[1] + tempSizes[1];
|
||||
p.bufs[3] = tempBuf[2]; p.lims[3] = tempBuf[2] + tempSizes[2];
|
||||
|
||||
p.dest = outBuffer;
|
||||
p.destLim = outBuffer + outSize;
|
||||
|
||||
Bcj2Dec_Init(&p);
|
||||
RINOK(Bcj2Dec_Decode(&p))
|
||||
|
||||
{
|
||||
unsigned i;
|
||||
for (i = 0; i < 4; i++)
|
||||
if (p.bufs[i] != p.lims[i])
|
||||
return SZ_ERROR_DATA;
|
||||
if (p.dest != p.destLim || !Bcj2Dec_IsMaybeFinished(&p))
|
||||
return SZ_ERROR_DATA;
|
||||
}
|
||||
}
|
||||
}
|
||||
#if defined(Z7_USE_BRANCH_FILTER)
|
||||
else if (ci == 1)
|
||||
{
|
||||
#if !defined(Z7_NO_METHODS_FILTERS)
|
||||
if (coder->MethodID == k_Delta)
|
||||
{
|
||||
if (coder->PropsSize != 1)
|
||||
return SZ_ERROR_UNSUPPORTED;
|
||||
{
|
||||
Byte state[DELTA_STATE_SIZE];
|
||||
Delta_Init(state);
|
||||
Delta_Decode(state, (unsigned)(propsData[coder->PropsOffset]) + 1, outBuffer, outSize);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef Z7_USE_FILTER_ARM64
|
||||
if (coder->MethodID == k_ARM64)
|
||||
{
|
||||
UInt32 pc = 0;
|
||||
if (coder->PropsSize == 4)
|
||||
{
|
||||
pc = GetUi32(propsData + coder->PropsOffset);
|
||||
if (pc & 3)
|
||||
return SZ_ERROR_UNSUPPORTED;
|
||||
}
|
||||
else if (coder->PropsSize != 0)
|
||||
return SZ_ERROR_UNSUPPORTED;
|
||||
z7_BranchConv_ARM64_Dec(outBuffer, outSize, pc);
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !defined(Z7_NO_METHODS_FILTERS)
|
||||
if (coder->MethodID == k_RISCV)
|
||||
{
|
||||
UInt32 pc = 0;
|
||||
if (coder->PropsSize == 4)
|
||||
{
|
||||
pc = GetUi32(propsData + coder->PropsOffset);
|
||||
if (pc & 1)
|
||||
return SZ_ERROR_UNSUPPORTED;
|
||||
}
|
||||
else if (coder->PropsSize != 0)
|
||||
return SZ_ERROR_UNSUPPORTED;
|
||||
z7_BranchConv_RISCV_Dec(outBuffer, outSize, pc);
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !defined(Z7_NO_METHODS_FILTERS) || defined(Z7_USE_FILTER_ARMT)
|
||||
{
|
||||
if (coder->PropsSize != 0)
|
||||
return SZ_ERROR_UNSUPPORTED;
|
||||
#define CASE_BRA_CONV(isa) case k_ ## isa: Z7_BRANCH_CONV_DEC(isa)(outBuffer, outSize, 0); break; // pc = 0;
|
||||
switch (coder->MethodID)
|
||||
{
|
||||
#if !defined(Z7_NO_METHODS_FILTERS)
|
||||
case k_BCJ:
|
||||
{
|
||||
UInt32 state = Z7_BRANCH_CONV_ST_X86_STATE_INIT_VAL;
|
||||
z7_BranchConvSt_X86_Dec(outBuffer, outSize, 0, &state); // pc = 0
|
||||
break;
|
||||
}
|
||||
case k_PPC: Z7_BRANCH_CONV_DEC_2(BranchConv_PPC)(outBuffer, outSize, 0); break; // pc = 0;
|
||||
// CASE_BRA_CONV(PPC)
|
||||
CASE_BRA_CONV(IA64)
|
||||
CASE_BRA_CONV(SPARC)
|
||||
CASE_BRA_CONV(ARM)
|
||||
#endif
|
||||
#if !defined(Z7_NO_METHODS_FILTERS) || defined(Z7_USE_FILTER_ARMT)
|
||||
CASE_BRA_CONV(ARMT)
|
||||
#endif
|
||||
default:
|
||||
return SZ_ERROR_UNSUPPORTED;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
} // (c == 1)
|
||||
#endif // Z7_USE_BRANCH_FILTER
|
||||
else
|
||||
return SZ_ERROR_UNSUPPORTED;
|
||||
}
|
||||
|
||||
return SZ_OK;
|
||||
}
|
||||
|
||||
|
||||
SRes SzAr_DecodeFolder(const CSzAr *p, UInt32 folderIndex,
|
||||
ILookInStreamPtr inStream, UInt64 startPos,
|
||||
Byte *outBuffer, size_t outSize,
|
||||
ISzAllocPtr allocMain)
|
||||
{
|
||||
SRes res;
|
||||
CSzFolder folder;
|
||||
CSzData sd;
|
||||
|
||||
const Byte *data = p->CodersData + p->FoCodersOffsets[folderIndex];
|
||||
sd.Data = data;
|
||||
sd.Size = p->FoCodersOffsets[(size_t)folderIndex + 1] - p->FoCodersOffsets[folderIndex];
|
||||
|
||||
res = SzGetNextFolderItem(&folder, &sd);
|
||||
|
||||
if (res != SZ_OK)
|
||||
return res;
|
||||
|
||||
if (sd.Size != 0
|
||||
|| folder.UnpackStream != p->FoToMainUnpackSizeIndex[folderIndex]
|
||||
|| outSize != SzAr_GetFolderUnpackSize(p, folderIndex))
|
||||
return SZ_ERROR_FAIL;
|
||||
{
|
||||
unsigned i;
|
||||
Byte *tempBuf[3] = { 0, 0, 0};
|
||||
|
||||
res = SzFolder_Decode2(&folder, data,
|
||||
&p->CoderUnpackSizes[p->FoToCoderUnpackSizes[folderIndex]],
|
||||
p->PackPositions + p->FoStartPackStreamIndex[folderIndex],
|
||||
inStream, startPos,
|
||||
outBuffer, (SizeT)outSize, allocMain, tempBuf);
|
||||
|
||||
for (i = 0; i < 3; i++)
|
||||
ISzAlloc_Free(allocMain, tempBuf[i]);
|
||||
|
||||
if (res == SZ_OK)
|
||||
if (SzBitWithVals_Check(&p->FolderCRCs, folderIndex))
|
||||
if (CrcCalc(outBuffer, outSize) != p->FolderCRCs.Vals[folderIndex])
|
||||
res = SZ_ERROR_CRC;
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
Vendored
+443
@@ -0,0 +1,443 @@
|
||||
/* 7zFile.c -- File IO
|
||||
2023-04-02 : Igor Pavlov : Public domain */
|
||||
|
||||
#include "Precomp.h"
|
||||
|
||||
#include "7zFile.h"
|
||||
|
||||
#ifndef USE_WINDOWS_FILE
|
||||
|
||||
#include <errno.h>
|
||||
|
||||
#ifndef USE_FOPEN
|
||||
#include <stdio.h>
|
||||
#include <fcntl.h>
|
||||
#ifdef _WIN32
|
||||
#include <io.h>
|
||||
typedef int ssize_t;
|
||||
typedef int off_t;
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#else
|
||||
|
||||
/*
|
||||
ReadFile and WriteFile functions in Windows have BUG:
|
||||
If you Read or Write 64MB or more (probably min_failure_size = 64MB - 32KB + 1)
|
||||
from/to Network file, it returns ERROR_NO_SYSTEM_RESOURCES
|
||||
(Insufficient system resources exist to complete the requested service).
|
||||
Probably in some version of Windows there are problems with other sizes:
|
||||
for 32 MB (maybe also for 16 MB).
|
||||
And message can be "Network connection was lost"
|
||||
*/
|
||||
|
||||
#endif
|
||||
|
||||
#define kChunkSizeMax (1 << 22)
|
||||
|
||||
void File_Construct(CSzFile *p)
|
||||
{
|
||||
#ifdef USE_WINDOWS_FILE
|
||||
p->handle = INVALID_HANDLE_VALUE;
|
||||
#elif defined(USE_FOPEN)
|
||||
p->file = NULL;
|
||||
#else
|
||||
p->fd = -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !defined(UNDER_CE) || !defined(USE_WINDOWS_FILE)
|
||||
|
||||
static WRes File_Open(CSzFile *p, const char *name, int writeMode)
|
||||
{
|
||||
#ifdef USE_WINDOWS_FILE
|
||||
|
||||
p->handle = CreateFileA(name,
|
||||
writeMode ? GENERIC_WRITE : GENERIC_READ,
|
||||
FILE_SHARE_READ, NULL,
|
||||
writeMode ? CREATE_ALWAYS : OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
return (p->handle != INVALID_HANDLE_VALUE) ? 0 : GetLastError();
|
||||
|
||||
#elif defined(USE_FOPEN)
|
||||
|
||||
p->file = fopen(name, writeMode ? "wb+" : "rb");
|
||||
return (p->file != 0) ? 0 :
|
||||
#ifdef UNDER_CE
|
||||
2; /* ENOENT */
|
||||
#else
|
||||
errno;
|
||||
#endif
|
||||
|
||||
#else
|
||||
|
||||
int flags = (writeMode ? (O_CREAT | O_EXCL | O_WRONLY) : O_RDONLY);
|
||||
#ifdef O_BINARY
|
||||
flags |= O_BINARY;
|
||||
#endif
|
||||
p->fd = open(name, flags, 0666);
|
||||
return (p->fd != -1) ? 0 : errno;
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
WRes InFile_Open(CSzFile *p, const char *name) { return File_Open(p, name, 0); }
|
||||
|
||||
WRes OutFile_Open(CSzFile *p, const char *name)
|
||||
{
|
||||
#if defined(USE_WINDOWS_FILE) || defined(USE_FOPEN)
|
||||
return File_Open(p, name, 1);
|
||||
#else
|
||||
p->fd = creat(name, 0666);
|
||||
return (p->fd != -1) ? 0 : errno;
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef USE_WINDOWS_FILE
|
||||
static WRes File_OpenW(CSzFile *p, const WCHAR *name, int writeMode)
|
||||
{
|
||||
p->handle = CreateFileW(name,
|
||||
writeMode ? GENERIC_WRITE : GENERIC_READ,
|
||||
FILE_SHARE_READ, NULL,
|
||||
writeMode ? CREATE_ALWAYS : OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
return (p->handle != INVALID_HANDLE_VALUE) ? 0 : GetLastError();
|
||||
}
|
||||
WRes InFile_OpenW(CSzFile *p, const WCHAR *name) { return File_OpenW(p, name, 0); }
|
||||
WRes OutFile_OpenW(CSzFile *p, const WCHAR *name) { return File_OpenW(p, name, 1); }
|
||||
#endif
|
||||
|
||||
WRes File_Close(CSzFile *p)
|
||||
{
|
||||
#ifdef USE_WINDOWS_FILE
|
||||
|
||||
if (p->handle != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
if (!CloseHandle(p->handle))
|
||||
return GetLastError();
|
||||
p->handle = INVALID_HANDLE_VALUE;
|
||||
}
|
||||
|
||||
#elif defined(USE_FOPEN)
|
||||
|
||||
if (p->file != NULL)
|
||||
{
|
||||
int res = fclose(p->file);
|
||||
if (res != 0)
|
||||
{
|
||||
if (res == EOF)
|
||||
return errno;
|
||||
return res;
|
||||
}
|
||||
p->file = NULL;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
if (p->fd != -1)
|
||||
{
|
||||
if (close(p->fd) != 0)
|
||||
return errno;
|
||||
p->fd = -1;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
WRes File_Read(CSzFile *p, void *data, size_t *size)
|
||||
{
|
||||
size_t originalSize = *size;
|
||||
*size = 0;
|
||||
if (originalSize == 0)
|
||||
return 0;
|
||||
|
||||
#ifdef USE_WINDOWS_FILE
|
||||
|
||||
do
|
||||
{
|
||||
const DWORD curSize = (originalSize > kChunkSizeMax) ? kChunkSizeMax : (DWORD)originalSize;
|
||||
DWORD processed = 0;
|
||||
const BOOL res = ReadFile(p->handle, data, curSize, &processed, NULL);
|
||||
data = (void *)((Byte *)data + processed);
|
||||
originalSize -= processed;
|
||||
*size += processed;
|
||||
if (!res)
|
||||
return GetLastError();
|
||||
// debug : we can break here for partial reading mode
|
||||
if (processed == 0)
|
||||
break;
|
||||
}
|
||||
while (originalSize > 0);
|
||||
|
||||
#elif defined(USE_FOPEN)
|
||||
|
||||
do
|
||||
{
|
||||
const size_t curSize = (originalSize > kChunkSizeMax) ? kChunkSizeMax : originalSize;
|
||||
const size_t processed = fread(data, 1, curSize, p->file);
|
||||
data = (void *)((Byte *)data + (size_t)processed);
|
||||
originalSize -= processed;
|
||||
*size += processed;
|
||||
if (processed != curSize)
|
||||
return ferror(p->file);
|
||||
// debug : we can break here for partial reading mode
|
||||
if (processed == 0)
|
||||
break;
|
||||
}
|
||||
while (originalSize > 0);
|
||||
|
||||
#else
|
||||
|
||||
do
|
||||
{
|
||||
const size_t curSize = (originalSize > kChunkSizeMax) ? kChunkSizeMax : originalSize;
|
||||
const ssize_t processed = read(p->fd, data, curSize);
|
||||
if (processed == -1)
|
||||
return errno;
|
||||
if (processed == 0)
|
||||
break;
|
||||
data = (void *)((Byte *)data + (size_t)processed);
|
||||
originalSize -= (size_t)processed;
|
||||
*size += (size_t)processed;
|
||||
// debug : we can break here for partial reading mode
|
||||
// break;
|
||||
}
|
||||
while (originalSize > 0);
|
||||
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
WRes File_Write(CSzFile *p, const void *data, size_t *size)
|
||||
{
|
||||
size_t originalSize = *size;
|
||||
*size = 0;
|
||||
if (originalSize == 0)
|
||||
return 0;
|
||||
|
||||
#ifdef USE_WINDOWS_FILE
|
||||
|
||||
do
|
||||
{
|
||||
const DWORD curSize = (originalSize > kChunkSizeMax) ? kChunkSizeMax : (DWORD)originalSize;
|
||||
DWORD processed = 0;
|
||||
const BOOL res = WriteFile(p->handle, data, curSize, &processed, NULL);
|
||||
data = (const void *)((const Byte *)data + processed);
|
||||
originalSize -= processed;
|
||||
*size += processed;
|
||||
if (!res)
|
||||
return GetLastError();
|
||||
if (processed == 0)
|
||||
break;
|
||||
}
|
||||
while (originalSize > 0);
|
||||
|
||||
#elif defined(USE_FOPEN)
|
||||
|
||||
do
|
||||
{
|
||||
const size_t curSize = (originalSize > kChunkSizeMax) ? kChunkSizeMax : originalSize;
|
||||
const size_t processed = fwrite(data, 1, curSize, p->file);
|
||||
data = (void *)((Byte *)data + (size_t)processed);
|
||||
originalSize -= processed;
|
||||
*size += processed;
|
||||
if (processed != curSize)
|
||||
return ferror(p->file);
|
||||
if (processed == 0)
|
||||
break;
|
||||
}
|
||||
while (originalSize > 0);
|
||||
|
||||
#else
|
||||
|
||||
do
|
||||
{
|
||||
const size_t curSize = (originalSize > kChunkSizeMax) ? kChunkSizeMax : originalSize;
|
||||
const ssize_t processed = write(p->fd, data, curSize);
|
||||
if (processed == -1)
|
||||
return errno;
|
||||
if (processed == 0)
|
||||
break;
|
||||
data = (const void *)((const Byte *)data + (size_t)processed);
|
||||
originalSize -= (size_t)processed;
|
||||
*size += (size_t)processed;
|
||||
}
|
||||
while (originalSize > 0);
|
||||
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
WRes File_Seek(CSzFile *p, Int64 *pos, ESzSeek origin)
|
||||
{
|
||||
#ifdef USE_WINDOWS_FILE
|
||||
|
||||
DWORD moveMethod;
|
||||
UInt32 low = (UInt32)*pos;
|
||||
LONG high = (LONG)((UInt64)*pos >> 16 >> 16); /* for case when UInt64 is 32-bit only */
|
||||
// (int) to eliminate clang warning
|
||||
switch ((int)origin)
|
||||
{
|
||||
case SZ_SEEK_SET: moveMethod = FILE_BEGIN; break;
|
||||
case SZ_SEEK_CUR: moveMethod = FILE_CURRENT; break;
|
||||
case SZ_SEEK_END: moveMethod = FILE_END; break;
|
||||
default: return ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
low = SetFilePointer(p->handle, (LONG)low, &high, moveMethod);
|
||||
if (low == (UInt32)0xFFFFFFFF)
|
||||
{
|
||||
WRes res = GetLastError();
|
||||
if (res != NO_ERROR)
|
||||
return res;
|
||||
}
|
||||
*pos = ((Int64)high << 32) | low;
|
||||
return 0;
|
||||
|
||||
#else
|
||||
|
||||
int moveMethod; // = origin;
|
||||
|
||||
switch ((int)origin)
|
||||
{
|
||||
case SZ_SEEK_SET: moveMethod = SEEK_SET; break;
|
||||
case SZ_SEEK_CUR: moveMethod = SEEK_CUR; break;
|
||||
case SZ_SEEK_END: moveMethod = SEEK_END; break;
|
||||
default: return EINVAL;
|
||||
}
|
||||
|
||||
#if defined(USE_FOPEN)
|
||||
{
|
||||
int res = fseek(p->file, (long)*pos, moveMethod);
|
||||
if (res == -1)
|
||||
return errno;
|
||||
*pos = ftell(p->file);
|
||||
if (*pos == -1)
|
||||
return errno;
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
{
|
||||
off_t res = lseek(p->fd, (off_t)*pos, moveMethod);
|
||||
if (res == -1)
|
||||
return errno;
|
||||
*pos = res;
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif // USE_FOPEN
|
||||
#endif // USE_WINDOWS_FILE
|
||||
}
|
||||
|
||||
|
||||
WRes File_GetLength(CSzFile *p, UInt64 *length)
|
||||
{
|
||||
#ifdef USE_WINDOWS_FILE
|
||||
|
||||
DWORD sizeHigh;
|
||||
DWORD sizeLow = GetFileSize(p->handle, &sizeHigh);
|
||||
if (sizeLow == 0xFFFFFFFF)
|
||||
{
|
||||
DWORD res = GetLastError();
|
||||
if (res != NO_ERROR)
|
||||
return res;
|
||||
}
|
||||
*length = (((UInt64)sizeHigh) << 32) + sizeLow;
|
||||
return 0;
|
||||
|
||||
#elif defined(USE_FOPEN)
|
||||
|
||||
long pos = ftell(p->file);
|
||||
int res = fseek(p->file, 0, SEEK_END);
|
||||
*length = ftell(p->file);
|
||||
fseek(p->file, pos, SEEK_SET);
|
||||
return res;
|
||||
|
||||
#else
|
||||
|
||||
off_t pos;
|
||||
*length = 0;
|
||||
pos = lseek(p->fd, 0, SEEK_CUR);
|
||||
if (pos != -1)
|
||||
{
|
||||
const off_t len2 = lseek(p->fd, 0, SEEK_END);
|
||||
const off_t res2 = lseek(p->fd, pos, SEEK_SET);
|
||||
if (len2 != -1)
|
||||
{
|
||||
*length = (UInt64)len2;
|
||||
if (res2 != -1)
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return errno;
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
/* ---------- FileSeqInStream ---------- */
|
||||
|
||||
static SRes FileSeqInStream_Read(ISeqInStreamPtr pp, void *buf, size_t *size)
|
||||
{
|
||||
Z7_CONTAINER_FROM_VTBL_TO_DECL_VAR_pp_vt_p(CFileSeqInStream)
|
||||
const WRes wres = File_Read(&p->file, buf, size);
|
||||
p->wres = wres;
|
||||
return (wres == 0) ? SZ_OK : SZ_ERROR_READ;
|
||||
}
|
||||
|
||||
void FileSeqInStream_CreateVTable(CFileSeqInStream *p)
|
||||
{
|
||||
p->vt.Read = FileSeqInStream_Read;
|
||||
}
|
||||
|
||||
|
||||
/* ---------- FileInStream ---------- */
|
||||
|
||||
static SRes FileInStream_Read(ISeekInStreamPtr pp, void *buf, size_t *size)
|
||||
{
|
||||
Z7_CONTAINER_FROM_VTBL_TO_DECL_VAR_pp_vt_p(CFileInStream)
|
||||
const WRes wres = File_Read(&p->file, buf, size);
|
||||
p->wres = wres;
|
||||
return (wres == 0) ? SZ_OK : SZ_ERROR_READ;
|
||||
}
|
||||
|
||||
static SRes FileInStream_Seek(ISeekInStreamPtr pp, Int64 *pos, ESzSeek origin)
|
||||
{
|
||||
Z7_CONTAINER_FROM_VTBL_TO_DECL_VAR_pp_vt_p(CFileInStream)
|
||||
const WRes wres = File_Seek(&p->file, pos, origin);
|
||||
p->wres = wres;
|
||||
return (wres == 0) ? SZ_OK : SZ_ERROR_READ;
|
||||
}
|
||||
|
||||
void FileInStream_CreateVTable(CFileInStream *p)
|
||||
{
|
||||
p->vt.Read = FileInStream_Read;
|
||||
p->vt.Seek = FileInStream_Seek;
|
||||
}
|
||||
|
||||
|
||||
/* ---------- FileOutStream ---------- */
|
||||
|
||||
static size_t FileOutStream_Write(ISeqOutStreamPtr pp, const void *data, size_t size)
|
||||
{
|
||||
Z7_CONTAINER_FROM_VTBL_TO_DECL_VAR_pp_vt_p(CFileOutStream)
|
||||
const WRes wres = File_Write(&p->file, data, &size);
|
||||
p->wres = wres;
|
||||
return size;
|
||||
}
|
||||
|
||||
void FileOutStream_CreateVTable(CFileOutStream *p)
|
||||
{
|
||||
p->vt.Write = FileOutStream_Write;
|
||||
}
|
||||
Vendored
+91
@@ -0,0 +1,91 @@
|
||||
/* 7zFile.h -- File IO
|
||||
: Igor Pavlov : Public domain */
|
||||
|
||||
#ifndef ZIP7_INC_FILE_H
|
||||
#define ZIP7_INC_FILE_H
|
||||
|
||||
#ifdef _WIN32
|
||||
#define USE_WINDOWS_FILE
|
||||
#endif
|
||||
|
||||
#ifdef USE_WINDOWS_FILE
|
||||
#include "7zWindows.h"
|
||||
|
||||
#else
|
||||
// note: USE_FOPEN mode is limited to 32-bit file size
|
||||
// #define USE_FOPEN
|
||||
// #include <stdio.h>
|
||||
#endif
|
||||
|
||||
#include "7zTypes.h"
|
||||
|
||||
EXTERN_C_BEGIN
|
||||
|
||||
/* ---------- File ---------- */
|
||||
|
||||
typedef struct
|
||||
{
|
||||
#ifdef USE_WINDOWS_FILE
|
||||
HANDLE handle;
|
||||
#elif defined(USE_FOPEN)
|
||||
FILE *file;
|
||||
#else
|
||||
int fd;
|
||||
#endif
|
||||
} CSzFile;
|
||||
|
||||
void File_Construct(CSzFile *p);
|
||||
#if !defined(UNDER_CE) || !defined(USE_WINDOWS_FILE)
|
||||
WRes InFile_Open(CSzFile *p, const char *name);
|
||||
WRes OutFile_Open(CSzFile *p, const char *name);
|
||||
#endif
|
||||
#ifdef USE_WINDOWS_FILE
|
||||
WRes InFile_OpenW(CSzFile *p, const WCHAR *name);
|
||||
WRes OutFile_OpenW(CSzFile *p, const WCHAR *name);
|
||||
#endif
|
||||
WRes File_Close(CSzFile *p);
|
||||
|
||||
/* reads max(*size, remain file's size) bytes */
|
||||
WRes File_Read(CSzFile *p, void *data, size_t *size);
|
||||
|
||||
/* writes *size bytes */
|
||||
WRes File_Write(CSzFile *p, const void *data, size_t *size);
|
||||
|
||||
WRes File_Seek(CSzFile *p, Int64 *pos, ESzSeek origin);
|
||||
WRes File_GetLength(CSzFile *p, UInt64 *length);
|
||||
|
||||
|
||||
/* ---------- FileInStream ---------- */
|
||||
|
||||
typedef struct
|
||||
{
|
||||
ISeqInStream vt;
|
||||
CSzFile file;
|
||||
WRes wres;
|
||||
} CFileSeqInStream;
|
||||
|
||||
void FileSeqInStream_CreateVTable(CFileSeqInStream *p);
|
||||
|
||||
|
||||
typedef struct
|
||||
{
|
||||
ISeekInStream vt;
|
||||
CSzFile file;
|
||||
WRes wres;
|
||||
} CFileInStream;
|
||||
|
||||
void FileInStream_CreateVTable(CFileInStream *p);
|
||||
|
||||
|
||||
typedef struct
|
||||
{
|
||||
ISeqOutStream vt;
|
||||
CSzFile file;
|
||||
WRes wres;
|
||||
} CFileOutStream;
|
||||
|
||||
void FileOutStream_CreateVTable(CFileOutStream *p);
|
||||
|
||||
EXTERN_C_END
|
||||
|
||||
#endif
|
||||
Vendored
+199
@@ -0,0 +1,199 @@
|
||||
/* 7zStream.c -- 7z Stream functions
|
||||
2023-04-02 : Igor Pavlov : Public domain */
|
||||
|
||||
#include "Precomp.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "7zTypes.h"
|
||||
|
||||
|
||||
SRes SeqInStream_ReadMax(ISeqInStreamPtr stream, void *buf, size_t *processedSize)
|
||||
{
|
||||
size_t size = *processedSize;
|
||||
*processedSize = 0;
|
||||
while (size != 0)
|
||||
{
|
||||
size_t cur = size;
|
||||
const SRes res = ISeqInStream_Read(stream, buf, &cur);
|
||||
*processedSize += cur;
|
||||
buf = (void *)((Byte *)buf + cur);
|
||||
size -= cur;
|
||||
if (res != SZ_OK)
|
||||
return res;
|
||||
if (cur == 0)
|
||||
return SZ_OK;
|
||||
}
|
||||
return SZ_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
SRes SeqInStream_Read2(ISeqInStreamPtr stream, void *buf, size_t size, SRes errorType)
|
||||
{
|
||||
while (size != 0)
|
||||
{
|
||||
size_t processed = size;
|
||||
RINOK(ISeqInStream_Read(stream, buf, &processed))
|
||||
if (processed == 0)
|
||||
return errorType;
|
||||
buf = (void *)((Byte *)buf + processed);
|
||||
size -= processed;
|
||||
}
|
||||
return SZ_OK;
|
||||
}
|
||||
|
||||
SRes SeqInStream_Read(ISeqInStreamPtr stream, void *buf, size_t size)
|
||||
{
|
||||
return SeqInStream_Read2(stream, buf, size, SZ_ERROR_INPUT_EOF);
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
SRes SeqInStream_ReadByte(ISeqInStreamPtr stream, Byte *buf)
|
||||
{
|
||||
size_t processed = 1;
|
||||
RINOK(ISeqInStream_Read(stream, buf, &processed))
|
||||
return (processed == 1) ? SZ_OK : SZ_ERROR_INPUT_EOF;
|
||||
}
|
||||
|
||||
|
||||
|
||||
SRes LookInStream_SeekTo(ILookInStreamPtr stream, UInt64 offset)
|
||||
{
|
||||
Int64 t = (Int64)offset;
|
||||
return ILookInStream_Seek(stream, &t, SZ_SEEK_SET);
|
||||
}
|
||||
|
||||
SRes LookInStream_LookRead(ILookInStreamPtr stream, void *buf, size_t *size)
|
||||
{
|
||||
const void *lookBuf;
|
||||
if (*size == 0)
|
||||
return SZ_OK;
|
||||
RINOK(ILookInStream_Look(stream, &lookBuf, size))
|
||||
memcpy(buf, lookBuf, *size);
|
||||
return ILookInStream_Skip(stream, *size);
|
||||
}
|
||||
|
||||
SRes LookInStream_Read2(ILookInStreamPtr stream, void *buf, size_t size, SRes errorType)
|
||||
{
|
||||
while (size != 0)
|
||||
{
|
||||
size_t processed = size;
|
||||
RINOK(ILookInStream_Read(stream, buf, &processed))
|
||||
if (processed == 0)
|
||||
return errorType;
|
||||
buf = (void *)((Byte *)buf + processed);
|
||||
size -= processed;
|
||||
}
|
||||
return SZ_OK;
|
||||
}
|
||||
|
||||
SRes LookInStream_Read(ILookInStreamPtr stream, void *buf, size_t size)
|
||||
{
|
||||
return LookInStream_Read2(stream, buf, size, SZ_ERROR_INPUT_EOF);
|
||||
}
|
||||
|
||||
|
||||
|
||||
#define GET_LookToRead2 Z7_CONTAINER_FROM_VTBL_TO_DECL_VAR_pp_vt_p(CLookToRead2)
|
||||
|
||||
static SRes LookToRead2_Look_Lookahead(ILookInStreamPtr pp, const void **buf, size_t *size)
|
||||
{
|
||||
SRes res = SZ_OK;
|
||||
GET_LookToRead2
|
||||
size_t size2 = p->size - p->pos;
|
||||
if (size2 == 0 && *size != 0)
|
||||
{
|
||||
p->pos = 0;
|
||||
p->size = 0;
|
||||
size2 = p->bufSize;
|
||||
res = ISeekInStream_Read(p->realStream, p->buf, &size2);
|
||||
p->size = size2;
|
||||
}
|
||||
if (*size > size2)
|
||||
*size = size2;
|
||||
*buf = p->buf + p->pos;
|
||||
return res;
|
||||
}
|
||||
|
||||
static SRes LookToRead2_Look_Exact(ILookInStreamPtr pp, const void **buf, size_t *size)
|
||||
{
|
||||
SRes res = SZ_OK;
|
||||
GET_LookToRead2
|
||||
size_t size2 = p->size - p->pos;
|
||||
if (size2 == 0 && *size != 0)
|
||||
{
|
||||
p->pos = 0;
|
||||
p->size = 0;
|
||||
if (*size > p->bufSize)
|
||||
*size = p->bufSize;
|
||||
res = ISeekInStream_Read(p->realStream, p->buf, size);
|
||||
size2 = p->size = *size;
|
||||
}
|
||||
if (*size > size2)
|
||||
*size = size2;
|
||||
*buf = p->buf + p->pos;
|
||||
return res;
|
||||
}
|
||||
|
||||
static SRes LookToRead2_Skip(ILookInStreamPtr pp, size_t offset)
|
||||
{
|
||||
GET_LookToRead2
|
||||
p->pos += offset;
|
||||
return SZ_OK;
|
||||
}
|
||||
|
||||
static SRes LookToRead2_Read(ILookInStreamPtr pp, void *buf, size_t *size)
|
||||
{
|
||||
GET_LookToRead2
|
||||
size_t rem = p->size - p->pos;
|
||||
if (rem == 0)
|
||||
return ISeekInStream_Read(p->realStream, buf, size);
|
||||
if (rem > *size)
|
||||
rem = *size;
|
||||
memcpy(buf, p->buf + p->pos, rem);
|
||||
p->pos += rem;
|
||||
*size = rem;
|
||||
return SZ_OK;
|
||||
}
|
||||
|
||||
static SRes LookToRead2_Seek(ILookInStreamPtr pp, Int64 *pos, ESzSeek origin)
|
||||
{
|
||||
GET_LookToRead2
|
||||
p->pos = p->size = 0;
|
||||
return ISeekInStream_Seek(p->realStream, pos, origin);
|
||||
}
|
||||
|
||||
void LookToRead2_CreateVTable(CLookToRead2 *p, int lookahead)
|
||||
{
|
||||
p->vt.Look = lookahead ?
|
||||
LookToRead2_Look_Lookahead :
|
||||
LookToRead2_Look_Exact;
|
||||
p->vt.Skip = LookToRead2_Skip;
|
||||
p->vt.Read = LookToRead2_Read;
|
||||
p->vt.Seek = LookToRead2_Seek;
|
||||
}
|
||||
|
||||
|
||||
|
||||
static SRes SecToLook_Read(ISeqInStreamPtr pp, void *buf, size_t *size)
|
||||
{
|
||||
Z7_CONTAINER_FROM_VTBL_TO_DECL_VAR_pp_vt_p(CSecToLook)
|
||||
return LookInStream_LookRead(p->realStream, buf, size);
|
||||
}
|
||||
|
||||
void SecToLook_CreateVTable(CSecToLook *p)
|
||||
{
|
||||
p->vt.Read = SecToLook_Read;
|
||||
}
|
||||
|
||||
static SRes SecToRead_Read(ISeqInStreamPtr pp, void *buf, size_t *size)
|
||||
{
|
||||
Z7_CONTAINER_FROM_VTBL_TO_DECL_VAR_pp_vt_p(CSecToRead)
|
||||
return ILookInStream_Read(p->realStream, buf, size);
|
||||
}
|
||||
|
||||
void SecToRead_CreateVTable(CSecToRead *p)
|
||||
{
|
||||
p->vt.Read = SecToRead_Read;
|
||||
}
|
||||
Vendored
+600
@@ -0,0 +1,600 @@
|
||||
/* 7zTypes.h -- Basic types
|
||||
: Igor Pavlov : Public domain */
|
||||
|
||||
#ifndef ZIP7_7Z_TYPES_H
|
||||
#define ZIP7_7Z_TYPES_H
|
||||
|
||||
#ifdef _WIN32
|
||||
/* #include <windows.h> */
|
||||
#else
|
||||
#include <errno.h>
|
||||
#endif
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifndef EXTERN_C_BEGIN
|
||||
#ifdef __cplusplus
|
||||
#define EXTERN_C_BEGIN extern "C" {
|
||||
#define EXTERN_C_END }
|
||||
#else
|
||||
#define EXTERN_C_BEGIN
|
||||
#define EXTERN_C_END
|
||||
#endif
|
||||
#endif
|
||||
|
||||
EXTERN_C_BEGIN
|
||||
|
||||
#define SZ_OK 0
|
||||
|
||||
#define SZ_ERROR_DATA 1
|
||||
#define SZ_ERROR_MEM 2
|
||||
#define SZ_ERROR_CRC 3
|
||||
#define SZ_ERROR_UNSUPPORTED 4
|
||||
#define SZ_ERROR_PARAM 5
|
||||
#define SZ_ERROR_INPUT_EOF 6
|
||||
#define SZ_ERROR_OUTPUT_EOF 7
|
||||
#define SZ_ERROR_READ 8
|
||||
#define SZ_ERROR_WRITE 9
|
||||
#define SZ_ERROR_PROGRESS 10
|
||||
#define SZ_ERROR_FAIL 11
|
||||
#define SZ_ERROR_THREAD 12
|
||||
|
||||
#define SZ_ERROR_ARCHIVE 16
|
||||
#define SZ_ERROR_NO_ARCHIVE 17
|
||||
|
||||
typedef int SRes;
|
||||
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#define MY_ALIGN_IN_STRUCT(n) __declspec(align(n))
|
||||
#if _MSC_VER > 1200
|
||||
#define MY_ALIGN(n) MY_ALIGN_IN_STRUCT(n)
|
||||
#else
|
||||
#define MY_ALIGN(n)
|
||||
#endif
|
||||
#else
|
||||
/*
|
||||
// C11/C++11:
|
||||
#include <stdalign.h>
|
||||
#define MY_ALIGN(n) alignas(n)
|
||||
*/
|
||||
#define MY_ALIGN(n) __attribute__ ((aligned(n)))
|
||||
#define MY_ALIGN_IN_STRUCT(n) MY_ALIGN(n)
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
/* typedef DWORD WRes; */
|
||||
typedef unsigned WRes;
|
||||
#define MY_SRes_HRESULT_FROM_WRes(x) HRESULT_FROM_WIN32(x)
|
||||
|
||||
// #define MY_HRES_ERROR_INTERNAL_ERROR MY_SRes_HRESULT_FROM_WRes(ERROR_INTERNAL_ERROR)
|
||||
|
||||
#else // _WIN32
|
||||
|
||||
// #define ENV_HAVE_LSTAT
|
||||
typedef int WRes;
|
||||
|
||||
// (FACILITY_ERRNO = 0x800) is 7zip's FACILITY constant to represent (errno) errors in HRESULT
|
||||
#define MY_FACILITY_ERRNO 0x800
|
||||
#define MY_FACILITY_WIN32 7
|
||||
#define MY_FACILITY_WRes MY_FACILITY_ERRNO
|
||||
|
||||
#define MY_HRESULT_FROM_errno_CONST_ERROR(x) ((HRESULT)( \
|
||||
( (HRESULT)(x) & 0x0000FFFF) \
|
||||
| (MY_FACILITY_WRes << 16) \
|
||||
| (HRESULT)0x80000000 ))
|
||||
|
||||
#define MY_SRes_HRESULT_FROM_WRes(x) \
|
||||
((HRESULT)(x) <= 0 ? ((HRESULT)(x)) : MY_HRESULT_FROM_errno_CONST_ERROR(x))
|
||||
|
||||
// we call macro HRESULT_FROM_WIN32 for system errors (WRes) that are (errno)
|
||||
#define HRESULT_FROM_WIN32(x) MY_SRes_HRESULT_FROM_WRes(x)
|
||||
|
||||
/*
|
||||
#define ERROR_FILE_NOT_FOUND 2L
|
||||
#define ERROR_ACCESS_DENIED 5L
|
||||
#define ERROR_NO_MORE_FILES 18L
|
||||
#define ERROR_LOCK_VIOLATION 33L
|
||||
#define ERROR_FILE_EXISTS 80L
|
||||
#define ERROR_DISK_FULL 112L
|
||||
#define ERROR_NEGATIVE_SEEK 131L
|
||||
#define ERROR_ALREADY_EXISTS 183L
|
||||
#define ERROR_DIRECTORY 267L
|
||||
#define ERROR_TOO_MANY_POSTS 298L
|
||||
|
||||
#define ERROR_INTERNAL_ERROR 1359L
|
||||
#define ERROR_INVALID_REPARSE_DATA 4392L
|
||||
#define ERROR_REPARSE_TAG_INVALID 4393L
|
||||
#define ERROR_REPARSE_TAG_MISMATCH 4394L
|
||||
*/
|
||||
|
||||
// we use errno equivalents for some WIN32 errors:
|
||||
|
||||
#define ERROR_INVALID_PARAMETER EINVAL
|
||||
#define ERROR_INVALID_FUNCTION EINVAL
|
||||
#define ERROR_ALREADY_EXISTS EEXIST
|
||||
#define ERROR_FILE_EXISTS EEXIST
|
||||
#define ERROR_PATH_NOT_FOUND ENOENT
|
||||
#define ERROR_FILE_NOT_FOUND ENOENT
|
||||
#define ERROR_DISK_FULL ENOSPC
|
||||
// #define ERROR_INVALID_HANDLE EBADF
|
||||
|
||||
// we use FACILITY_WIN32 for errors that has no errno equivalent
|
||||
// Too many posts were made to a semaphore.
|
||||
#define ERROR_TOO_MANY_POSTS ((HRESULT)0x8007012AL)
|
||||
#define ERROR_INVALID_REPARSE_DATA ((HRESULT)0x80071128L)
|
||||
#define ERROR_REPARSE_TAG_INVALID ((HRESULT)0x80071129L)
|
||||
|
||||
// if (MY_FACILITY_WRes != FACILITY_WIN32),
|
||||
// we use FACILITY_WIN32 for COM errors:
|
||||
#define E_OUTOFMEMORY ((HRESULT)0x8007000EL)
|
||||
#define E_INVALIDARG ((HRESULT)0x80070057L)
|
||||
#define MY_E_ERROR_NEGATIVE_SEEK ((HRESULT)0x80070083L)
|
||||
|
||||
/*
|
||||
// we can use FACILITY_ERRNO for some COM errors, that have errno equivalents:
|
||||
#define E_OUTOFMEMORY MY_HRESULT_FROM_errno_CONST_ERROR(ENOMEM)
|
||||
#define E_INVALIDARG MY_HRESULT_FROM_errno_CONST_ERROR(EINVAL)
|
||||
#define MY_E_ERROR_NEGATIVE_SEEK MY_HRESULT_FROM_errno_CONST_ERROR(EINVAL)
|
||||
*/
|
||||
|
||||
#define TEXT(quote) quote
|
||||
|
||||
#define FILE_ATTRIBUTE_READONLY 0x0001
|
||||
#define FILE_ATTRIBUTE_HIDDEN 0x0002
|
||||
#define FILE_ATTRIBUTE_SYSTEM 0x0004
|
||||
#define FILE_ATTRIBUTE_DIRECTORY 0x0010
|
||||
#define FILE_ATTRIBUTE_ARCHIVE 0x0020
|
||||
#define FILE_ATTRIBUTE_DEVICE 0x0040
|
||||
#define FILE_ATTRIBUTE_NORMAL 0x0080
|
||||
#define FILE_ATTRIBUTE_TEMPORARY 0x0100
|
||||
#define FILE_ATTRIBUTE_SPARSE_FILE 0x0200
|
||||
#define FILE_ATTRIBUTE_REPARSE_POINT 0x0400
|
||||
#define FILE_ATTRIBUTE_COMPRESSED 0x0800
|
||||
#define FILE_ATTRIBUTE_OFFLINE 0x1000
|
||||
#define FILE_ATTRIBUTE_NOT_CONTENT_INDEXED 0x2000
|
||||
#define FILE_ATTRIBUTE_ENCRYPTED 0x4000
|
||||
|
||||
#define FILE_ATTRIBUTE_UNIX_EXTENSION 0x8000 /* trick for Unix */
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
#ifndef RINOK
|
||||
#define RINOK(x) { const int _result_ = (x); if (_result_ != 0) return _result_; }
|
||||
#endif
|
||||
|
||||
#ifndef RINOK_WRes
|
||||
#define RINOK_WRes(x) { const WRes _result_ = (x); if (_result_ != 0) return _result_; }
|
||||
#endif
|
||||
|
||||
typedef unsigned char Byte;
|
||||
typedef short Int16;
|
||||
typedef unsigned short UInt16;
|
||||
|
||||
#ifdef Z7_DECL_Int32_AS_long
|
||||
typedef long Int32;
|
||||
typedef unsigned long UInt32;
|
||||
#else
|
||||
typedef int Int32;
|
||||
typedef unsigned int UInt32;
|
||||
#endif
|
||||
|
||||
|
||||
#ifndef _WIN32
|
||||
|
||||
typedef int INT;
|
||||
typedef Int32 INT32;
|
||||
typedef unsigned int UINT;
|
||||
typedef UInt32 UINT32;
|
||||
typedef INT32 LONG; // LONG, ULONG and DWORD must be 32-bit for _WIN32 compatibility
|
||||
typedef UINT32 ULONG;
|
||||
|
||||
#undef DWORD
|
||||
typedef UINT32 DWORD;
|
||||
|
||||
#define VOID void
|
||||
|
||||
#define HRESULT LONG
|
||||
|
||||
typedef void *LPVOID;
|
||||
// typedef void VOID;
|
||||
// typedef ULONG_PTR DWORD_PTR, *PDWORD_PTR;
|
||||
// gcc / clang on Unix : sizeof(long==sizeof(void*) in 32 or 64 bits)
|
||||
typedef long INT_PTR;
|
||||
typedef unsigned long UINT_PTR;
|
||||
typedef long LONG_PTR;
|
||||
typedef unsigned long DWORD_PTR;
|
||||
|
||||
typedef size_t SIZE_T;
|
||||
|
||||
#endif // _WIN32
|
||||
|
||||
|
||||
#define MY_HRES_ERROR_INTERNAL_ERROR ((HRESULT)0x8007054FL)
|
||||
|
||||
|
||||
#ifdef Z7_DECL_Int64_AS_long
|
||||
|
||||
typedef long Int64;
|
||||
typedef unsigned long UInt64;
|
||||
|
||||
#else
|
||||
|
||||
#if (defined(_MSC_VER) || defined(__BORLANDC__)) && !defined(__clang__)
|
||||
typedef __int64 Int64;
|
||||
typedef unsigned __int64 UInt64;
|
||||
#else
|
||||
#if defined(__clang__) || defined(__GNUC__)
|
||||
#include <stdint.h>
|
||||
typedef int64_t Int64;
|
||||
typedef uint64_t UInt64;
|
||||
#else
|
||||
typedef long long int Int64;
|
||||
typedef unsigned long long int UInt64;
|
||||
// #define UINT64_CONST(n) n ## ULL
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
#define UINT64_CONST(n) n
|
||||
|
||||
|
||||
#ifdef Z7_DECL_SizeT_AS_unsigned_int
|
||||
typedef unsigned int SizeT;
|
||||
#else
|
||||
typedef size_t SizeT;
|
||||
#endif
|
||||
|
||||
/*
|
||||
#if (defined(_MSC_VER) && _MSC_VER <= 1200)
|
||||
typedef size_t MY_uintptr_t;
|
||||
#else
|
||||
#include <stdint.h>
|
||||
typedef uintptr_t MY_uintptr_t;
|
||||
#endif
|
||||
*/
|
||||
|
||||
typedef int BoolInt;
|
||||
/* typedef BoolInt Bool; */
|
||||
#define True 1
|
||||
#define False 0
|
||||
|
||||
|
||||
#ifdef _WIN32
|
||||
#define Z7_STDCALL __stdcall
|
||||
#else
|
||||
#define Z7_STDCALL
|
||||
#endif
|
||||
|
||||
#ifdef _MSC_VER
|
||||
|
||||
#if _MSC_VER >= 1300
|
||||
#define Z7_NO_INLINE __declspec(noinline)
|
||||
#else
|
||||
#define Z7_NO_INLINE
|
||||
#endif
|
||||
|
||||
#define Z7_FORCE_INLINE __forceinline
|
||||
|
||||
#define Z7_CDECL __cdecl
|
||||
#define Z7_FASTCALL __fastcall
|
||||
|
||||
#else // _MSC_VER
|
||||
|
||||
#if (defined(__GNUC__) && (__GNUC__ >= 4)) \
|
||||
|| (defined(__clang__) && (__clang_major__ >= 4)) \
|
||||
|| defined(__INTEL_COMPILER) \
|
||||
|| defined(__xlC__)
|
||||
#define Z7_NO_INLINE __attribute__((noinline))
|
||||
#define Z7_FORCE_INLINE __attribute__((always_inline)) inline
|
||||
#else
|
||||
#define Z7_NO_INLINE
|
||||
#define Z7_FORCE_INLINE
|
||||
#endif
|
||||
|
||||
#define Z7_CDECL
|
||||
|
||||
#if defined(_M_IX86) \
|
||||
|| defined(__i386__)
|
||||
// #define Z7_FASTCALL __attribute__((fastcall))
|
||||
// #define Z7_FASTCALL __attribute__((cdecl))
|
||||
#define Z7_FASTCALL
|
||||
#elif defined(MY_CPU_AMD64)
|
||||
// #define Z7_FASTCALL __attribute__((ms_abi))
|
||||
#define Z7_FASTCALL
|
||||
#else
|
||||
#define Z7_FASTCALL
|
||||
#endif
|
||||
|
||||
#endif // _MSC_VER
|
||||
|
||||
|
||||
/* The following interfaces use first parameter as pointer to structure */
|
||||
|
||||
// #define Z7_C_IFACE_CONST_QUAL
|
||||
#define Z7_C_IFACE_CONST_QUAL const
|
||||
|
||||
#define Z7_C_IFACE_DECL(a) \
|
||||
struct a ## _; \
|
||||
typedef Z7_C_IFACE_CONST_QUAL struct a ## _ * a ## Ptr; \
|
||||
typedef struct a ## _ a; \
|
||||
struct a ## _
|
||||
|
||||
|
||||
Z7_C_IFACE_DECL (IByteIn)
|
||||
{
|
||||
Byte (*Read)(IByteInPtr p); /* reads one byte, returns 0 in case of EOF or error */
|
||||
};
|
||||
#define IByteIn_Read(p) (p)->Read(p)
|
||||
|
||||
|
||||
Z7_C_IFACE_DECL (IByteOut)
|
||||
{
|
||||
void (*Write)(IByteOutPtr p, Byte b);
|
||||
};
|
||||
#define IByteOut_Write(p, b) (p)->Write(p, b)
|
||||
|
||||
|
||||
Z7_C_IFACE_DECL (ISeqInStream)
|
||||
{
|
||||
SRes (*Read)(ISeqInStreamPtr p, void *buf, size_t *size);
|
||||
/* if (input(*size) != 0 && output(*size) == 0) means end_of_stream.
|
||||
(output(*size) < input(*size)) is allowed */
|
||||
};
|
||||
#define ISeqInStream_Read(p, buf, size) (p)->Read(p, buf, size)
|
||||
|
||||
/* try to read as much as avail in stream and limited by (*processedSize) */
|
||||
SRes SeqInStream_ReadMax(ISeqInStreamPtr stream, void *buf, size_t *processedSize);
|
||||
/* it can return SZ_ERROR_INPUT_EOF */
|
||||
// SRes SeqInStream_Read(ISeqInStreamPtr stream, void *buf, size_t size);
|
||||
// SRes SeqInStream_Read2(ISeqInStreamPtr stream, void *buf, size_t size, SRes errorType);
|
||||
SRes SeqInStream_ReadByte(ISeqInStreamPtr stream, Byte *buf);
|
||||
|
||||
|
||||
Z7_C_IFACE_DECL (ISeqOutStream)
|
||||
{
|
||||
size_t (*Write)(ISeqOutStreamPtr p, const void *buf, size_t size);
|
||||
/* Returns: result - the number of actually written bytes.
|
||||
(result < size) means error */
|
||||
};
|
||||
#define ISeqOutStream_Write(p, buf, size) (p)->Write(p, buf, size)
|
||||
|
||||
typedef enum
|
||||
{
|
||||
SZ_SEEK_SET = 0,
|
||||
SZ_SEEK_CUR = 1,
|
||||
SZ_SEEK_END = 2
|
||||
} ESzSeek;
|
||||
|
||||
|
||||
Z7_C_IFACE_DECL (ISeekInStream)
|
||||
{
|
||||
SRes (*Read)(ISeekInStreamPtr p, void *buf, size_t *size); /* same as ISeqInStream::Read */
|
||||
SRes (*Seek)(ISeekInStreamPtr p, Int64 *pos, ESzSeek origin);
|
||||
};
|
||||
#define ISeekInStream_Read(p, buf, size) (p)->Read(p, buf, size)
|
||||
#define ISeekInStream_Seek(p, pos, origin) (p)->Seek(p, pos, origin)
|
||||
|
||||
|
||||
Z7_C_IFACE_DECL (ILookInStream)
|
||||
{
|
||||
SRes (*Look)(ILookInStreamPtr p, const void **buf, size_t *size);
|
||||
/* if (input(*size) != 0 && output(*size) == 0) means end_of_stream.
|
||||
(output(*size) > input(*size)) is not allowed
|
||||
(output(*size) < input(*size)) is allowed */
|
||||
SRes (*Skip)(ILookInStreamPtr p, size_t offset);
|
||||
/* offset must be <= output(*size) of Look */
|
||||
SRes (*Read)(ILookInStreamPtr p, void *buf, size_t *size);
|
||||
/* reads directly (without buffer). It's same as ISeqInStream::Read */
|
||||
SRes (*Seek)(ILookInStreamPtr p, Int64 *pos, ESzSeek origin);
|
||||
};
|
||||
|
||||
#define ILookInStream_Look(p, buf, size) (p)->Look(p, buf, size)
|
||||
#define ILookInStream_Skip(p, offset) (p)->Skip(p, offset)
|
||||
#define ILookInStream_Read(p, buf, size) (p)->Read(p, buf, size)
|
||||
#define ILookInStream_Seek(p, pos, origin) (p)->Seek(p, pos, origin)
|
||||
|
||||
|
||||
SRes LookInStream_LookRead(ILookInStreamPtr stream, void *buf, size_t *size);
|
||||
SRes LookInStream_SeekTo(ILookInStreamPtr stream, UInt64 offset);
|
||||
|
||||
/* reads via ILookInStream::Read */
|
||||
SRes LookInStream_Read2(ILookInStreamPtr stream, void *buf, size_t size, SRes errorType);
|
||||
SRes LookInStream_Read(ILookInStreamPtr stream, void *buf, size_t size);
|
||||
|
||||
|
||||
typedef struct
|
||||
{
|
||||
ILookInStream vt;
|
||||
ISeekInStreamPtr realStream;
|
||||
|
||||
size_t pos;
|
||||
size_t size; /* it's data size */
|
||||
|
||||
/* the following variables must be set outside */
|
||||
Byte *buf;
|
||||
size_t bufSize;
|
||||
} CLookToRead2;
|
||||
|
||||
void LookToRead2_CreateVTable(CLookToRead2 *p, int lookahead);
|
||||
|
||||
#define LookToRead2_INIT(p) { (p)->pos = (p)->size = 0; }
|
||||
|
||||
|
||||
typedef struct
|
||||
{
|
||||
ISeqInStream vt;
|
||||
ILookInStreamPtr realStream;
|
||||
} CSecToLook;
|
||||
|
||||
void SecToLook_CreateVTable(CSecToLook *p);
|
||||
|
||||
|
||||
|
||||
typedef struct
|
||||
{
|
||||
ISeqInStream vt;
|
||||
ILookInStreamPtr realStream;
|
||||
} CSecToRead;
|
||||
|
||||
void SecToRead_CreateVTable(CSecToRead *p);
|
||||
|
||||
|
||||
Z7_C_IFACE_DECL (ICompressProgress)
|
||||
{
|
||||
SRes (*Progress)(ICompressProgressPtr p, UInt64 inSize, UInt64 outSize);
|
||||
/* Returns: result. (result != SZ_OK) means break.
|
||||
Value (UInt64)(Int64)-1 for size means unknown value. */
|
||||
};
|
||||
|
||||
#define ICompressProgress_Progress(p, inSize, outSize) (p)->Progress(p, inSize, outSize)
|
||||
|
||||
|
||||
|
||||
typedef struct ISzAlloc ISzAlloc;
|
||||
typedef const ISzAlloc * ISzAllocPtr;
|
||||
|
||||
struct ISzAlloc
|
||||
{
|
||||
void *(*Alloc)(ISzAllocPtr p, size_t size);
|
||||
void (*Free)(ISzAllocPtr p, void *address); /* address can be 0 */
|
||||
};
|
||||
|
||||
#define ISzAlloc_Alloc(p, size) (p)->Alloc(p, size)
|
||||
#define ISzAlloc_Free(p, a) (p)->Free(p, a)
|
||||
|
||||
/* deprecated */
|
||||
#define IAlloc_Alloc(p, size) ISzAlloc_Alloc(p, size)
|
||||
#define IAlloc_Free(p, a) ISzAlloc_Free(p, a)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#ifndef MY_offsetof
|
||||
#ifdef offsetof
|
||||
#define MY_offsetof(type, m) offsetof(type, m)
|
||||
/*
|
||||
#define MY_offsetof(type, m) FIELD_OFFSET(type, m)
|
||||
*/
|
||||
#else
|
||||
#define MY_offsetof(type, m) ((size_t)&(((type *)0)->m))
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
#ifndef Z7_container_of
|
||||
|
||||
/*
|
||||
#define Z7_container_of(ptr, type, m) container_of(ptr, type, m)
|
||||
#define Z7_container_of(ptr, type, m) CONTAINING_RECORD(ptr, type, m)
|
||||
#define Z7_container_of(ptr, type, m) ((type *)((char *)(ptr) - offsetof(type, m)))
|
||||
#define Z7_container_of(ptr, type, m) (&((type *)0)->m == (ptr), ((type *)(((char *)(ptr)) - MY_offsetof(type, m))))
|
||||
*/
|
||||
|
||||
/*
|
||||
GCC shows warning: "perhaps the 'offsetof' macro was used incorrectly"
|
||||
GCC 3.4.4 : classes with constructor
|
||||
GCC 4.8.1 : classes with non-public variable members"
|
||||
*/
|
||||
|
||||
#define Z7_container_of(ptr, type, m) \
|
||||
((type *)(void *)((char *)(void *) \
|
||||
(1 ? (ptr) : &((type *)NULL)->m) - MY_offsetof(type, m)))
|
||||
|
||||
#define Z7_container_of_CONST(ptr, type, m) \
|
||||
((const type *)(const void *)((const char *)(const void *) \
|
||||
(1 ? (ptr) : &((type *)NULL)->m) - MY_offsetof(type, m)))
|
||||
|
||||
/*
|
||||
#define Z7_container_of_NON_CONST_FROM_CONST(ptr, type, m) \
|
||||
((type *)(void *)(const void *)((const char *)(const void *) \
|
||||
(1 ? (ptr) : &((type *)NULL)->m) - MY_offsetof(type, m)))
|
||||
*/
|
||||
|
||||
#endif
|
||||
|
||||
#define Z7_CONTAINER_FROM_VTBL_SIMPLE(ptr, type, m) ((type *)(void *)(ptr))
|
||||
|
||||
// #define Z7_CONTAINER_FROM_VTBL(ptr, type, m) Z7_CONTAINER_FROM_VTBL_SIMPLE(ptr, type, m)
|
||||
#define Z7_CONTAINER_FROM_VTBL(ptr, type, m) Z7_container_of(ptr, type, m)
|
||||
// #define Z7_CONTAINER_FROM_VTBL(ptr, type, m) Z7_container_of_NON_CONST_FROM_CONST(ptr, type, m)
|
||||
|
||||
#define Z7_CONTAINER_FROM_VTBL_CONST(ptr, type, m) Z7_container_of_CONST(ptr, type, m)
|
||||
|
||||
#define Z7_CONTAINER_FROM_VTBL_CLS(ptr, type, m) Z7_CONTAINER_FROM_VTBL_SIMPLE(ptr, type, m)
|
||||
/*
|
||||
#define Z7_CONTAINER_FROM_VTBL_CLS(ptr, type, m) Z7_CONTAINER_FROM_VTBL(ptr, type, m)
|
||||
*/
|
||||
#if defined (__clang__) || defined(__GNUC__)
|
||||
#define Z7_DIAGNOSTIC_IGNORE_BEGIN_CAST_QUAL \
|
||||
_Pragma("GCC diagnostic push") \
|
||||
_Pragma("GCC diagnostic ignored \"-Wcast-qual\"")
|
||||
#define Z7_DIAGNOSTIC_IGNORE_END_CAST_QUAL \
|
||||
_Pragma("GCC diagnostic pop")
|
||||
#else
|
||||
#define Z7_DIAGNOSTIC_IGNORE_BEGIN_CAST_QUAL
|
||||
#define Z7_DIAGNOSTIC_IGNORE_END_CAST_QUAL
|
||||
#endif
|
||||
|
||||
#define Z7_CONTAINER_FROM_VTBL_TO_DECL_VAR(ptr, type, m, p) \
|
||||
Z7_DIAGNOSTIC_IGNORE_BEGIN_CAST_QUAL \
|
||||
type *p = Z7_CONTAINER_FROM_VTBL(ptr, type, m); \
|
||||
Z7_DIAGNOSTIC_IGNORE_END_CAST_QUAL
|
||||
|
||||
#define Z7_CONTAINER_FROM_VTBL_TO_DECL_VAR_pp_vt_p(type) \
|
||||
Z7_CONTAINER_FROM_VTBL_TO_DECL_VAR(pp, type, vt, p)
|
||||
|
||||
|
||||
// #define ZIP7_DECLARE_HANDLE(name) typedef void *name;
|
||||
#define Z7_DECLARE_HANDLE(name) struct name##_dummy{int unused;}; typedef struct name##_dummy *name;
|
||||
|
||||
|
||||
#define Z7_memset_0_ARRAY(a) memset((a), 0, sizeof(a))
|
||||
#define Z7_memset_0_VAR(a) memset(&(a), 0, sizeof(a))
|
||||
|
||||
#ifndef Z7_ARRAY_SIZE
|
||||
#define Z7_ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
#define CHAR_PATH_SEPARATOR '\\'
|
||||
#define WCHAR_PATH_SEPARATOR L'\\'
|
||||
#define STRING_PATH_SEPARATOR "\\"
|
||||
#define WSTRING_PATH_SEPARATOR L"\\"
|
||||
|
||||
#else
|
||||
|
||||
#define CHAR_PATH_SEPARATOR '/'
|
||||
#define WCHAR_PATH_SEPARATOR L'/'
|
||||
#define STRING_PATH_SEPARATOR "/"
|
||||
#define WSTRING_PATH_SEPARATOR L"/"
|
||||
|
||||
#endif
|
||||
|
||||
#define k_PropVar_TimePrec_0 0
|
||||
#define k_PropVar_TimePrec_Unix 1
|
||||
#define k_PropVar_TimePrec_DOS 2
|
||||
#define k_PropVar_TimePrec_HighPrec 3
|
||||
#define k_PropVar_TimePrec_Base 16
|
||||
#define k_PropVar_TimePrec_100ns (k_PropVar_TimePrec_Base + 7)
|
||||
#define k_PropVar_TimePrec_1ns (k_PropVar_TimePrec_Base + 9)
|
||||
|
||||
EXTERN_C_END
|
||||
|
||||
#endif
|
||||
|
||||
/*
|
||||
#ifndef Z7_ST
|
||||
#ifdef _7ZIP_ST
|
||||
#define Z7_ST
|
||||
#endif
|
||||
#endif
|
||||
*/
|
||||
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
#define MY_VER_MAJOR 26
|
||||
#define MY_VER_MINOR 3
|
||||
#define MY_VER_BUILD 0
|
||||
#define MY_VERSION_NUMBERS "26.03"
|
||||
#define MY_VERSION MY_VERSION_NUMBERS
|
||||
|
||||
#ifdef MY_CPU_NAME
|
||||
#define MY_VERSION_CPU MY_VERSION " (" MY_CPU_NAME ")"
|
||||
#else
|
||||
#define MY_VERSION_CPU MY_VERSION
|
||||
#endif
|
||||
|
||||
#define MY_DATE "2026-09-03"
|
||||
#undef MY_COPYRIGHT
|
||||
#undef MY_VERSION_COPYRIGHT_DATE
|
||||
#define MY_AUTHOR_NAME "Igor Pavlov"
|
||||
#define MY_COPYRIGHT_PD "Igor Pavlov : Public domain"
|
||||
#define MY_COPYRIGHT_CR "Copyright (c) 1999-2026 Igor Pavlov"
|
||||
|
||||
#ifdef USE_COPYRIGHT_CR
|
||||
#define MY_COPYRIGHT MY_COPYRIGHT_CR
|
||||
#else
|
||||
#define MY_COPYRIGHT MY_COPYRIGHT_PD
|
||||
#endif
|
||||
|
||||
#define MY_COPYRIGHT_DATE MY_COPYRIGHT " : " MY_DATE
|
||||
#define MY_VERSION_COPYRIGHT_DATE MY_VERSION_CPU " : " MY_COPYRIGHT " : " MY_DATE
|
||||
Vendored
+107
@@ -0,0 +1,107 @@
|
||||
/* 7zWindows.h -- Windows.h and related code
|
||||
Igor Pavlov : Public domain */
|
||||
|
||||
#ifndef ZIP7_INC_7Z_WINDOWS_H
|
||||
#define ZIP7_INC_7Z_WINDOWS_H
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
#if defined(_MSC_VER) && _MSC_VER >= 1950 && !defined(__clang__) // VS2026
|
||||
// <Windows.h> and some another windows files need that option
|
||||
// VS2026: wtypesbase.h: warning C4865: 'tagCLSCTX': the underlying type will change from 'int' to 'unsigned int' when '/Zc:enumTypes' is specified on the command line
|
||||
#pragma warning(disable : 4865)
|
||||
#endif
|
||||
|
||||
#if defined(__clang__)
|
||||
# pragma clang diagnostic push
|
||||
#endif
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 4668) // '_WIN32_WINNT' is not defined as a preprocessor macro, replacing with '0' for '#if/#elif'
|
||||
|
||||
#if _MSC_VER == 1900
|
||||
// for old kit10 versions
|
||||
// #pragma warning(disable : 4255) // winuser.h(13979): warning C4255: 'GetThreadDpiAwarenessContext':
|
||||
#endif
|
||||
// win10 Windows Kit:
|
||||
#endif // _MSC_VER
|
||||
|
||||
#if defined(_MSC_VER) && _MSC_VER <= 1200 && !defined(_WIN64)
|
||||
// for msvc6 without sdk2003
|
||||
#define RPC_NO_WINDOWS_H
|
||||
#endif
|
||||
|
||||
#if defined(__MINGW32__) || defined(__MINGW64__)
|
||||
// #if defined(__GNUC__) && !defined(__clang__)
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
// #include <basetsd.h>
|
||||
// #include <wtypes.h>
|
||||
|
||||
// but if precompiled with clang-cl then we need
|
||||
// #include <windows.h>
|
||||
#if defined(_MSC_VER)
|
||||
#pragma warning(pop)
|
||||
#endif
|
||||
|
||||
#if defined(__clang__)
|
||||
# pragma clang diagnostic pop
|
||||
#endif
|
||||
|
||||
#if defined(_MSC_VER) && _MSC_VER <= 1200 && !defined(_WIN64)
|
||||
#ifndef _W64
|
||||
|
||||
typedef long LONG_PTR, *PLONG_PTR;
|
||||
typedef unsigned long ULONG_PTR, *PULONG_PTR;
|
||||
typedef ULONG_PTR DWORD_PTR, *PDWORD_PTR;
|
||||
|
||||
#define Z7_OLD_WIN_SDK
|
||||
#endif // _W64
|
||||
#endif // _MSC_VER == 1200
|
||||
|
||||
#ifdef Z7_OLD_WIN_SDK
|
||||
|
||||
#ifndef INVALID_FILE_ATTRIBUTES
|
||||
#define INVALID_FILE_ATTRIBUTES ((DWORD)-1)
|
||||
#endif
|
||||
#ifndef INVALID_SET_FILE_POINTER
|
||||
#define INVALID_SET_FILE_POINTER ((DWORD)-1)
|
||||
#endif
|
||||
#ifndef FILE_SPECIAL_ACCESS
|
||||
#define FILE_SPECIAL_ACCESS (FILE_ANY_ACCESS)
|
||||
#endif
|
||||
|
||||
// ShlObj.h:
|
||||
// #define BIF_NEWDIALOGSTYLE 0x0040
|
||||
|
||||
#pragma warning(disable : 4201)
|
||||
// #pragma warning(disable : 4115)
|
||||
|
||||
#undef VARIANT_TRUE
|
||||
#define VARIANT_TRUE ((VARIANT_BOOL)-1)
|
||||
#endif
|
||||
|
||||
#endif // Z7_OLD_WIN_SDK
|
||||
|
||||
#ifdef UNDER_CE
|
||||
#undef VARIANT_TRUE
|
||||
#define VARIANT_TRUE ((VARIANT_BOOL)-1)
|
||||
#endif
|
||||
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
#if _MSC_VER >= 1400 && _MSC_VER <= 1600
|
||||
// BaseTsd.h(148) : 'HandleToULong' : unreferenced inline function has been removed
|
||||
// string.h
|
||||
// #pragma warning(disable : 4514)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
/* #include "7zTypes.h" */
|
||||
|
||||
#endif
|
||||
Vendored
+360
@@ -0,0 +1,360 @@
|
||||
|
||||
MY_ARCH_2 = $(MY_ARCH)
|
||||
|
||||
MY_ASM = jwasm
|
||||
MY_ASM = asmc
|
||||
|
||||
ifndef RC
|
||||
#RC=windres.exe --target=pe-x86-64
|
||||
#RC=windres.exe -F pe-i386
|
||||
RC=windres.exe
|
||||
endif
|
||||
|
||||
PROGPATH = $(O)/$(PROG)
|
||||
PROGPATH_STATIC = $(O)/$(PROG)s
|
||||
|
||||
ifneq ($(CC), xlc)
|
||||
CFLAGS_WARN_WALL = -Wall -Werror -Wextra
|
||||
endif
|
||||
|
||||
# for object file
|
||||
CFLAGS_BASE_LIST = -c
|
||||
# for ASM file
|
||||
# CFLAGS_BASE_LIST = -S
|
||||
|
||||
FLAGS_FLTO = -flto
|
||||
FLAGS_FLTO =
|
||||
|
||||
CFLAGS_BASE = $(MY_ARCH_2) -O2 $(CFLAGS_BASE_LIST) $(CFLAGS_WARN_WALL) $(CFLAGS_WARN) \
|
||||
-DNDEBUG -D_REENTRANT -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE
|
||||
|
||||
|
||||
ifdef SystemDrive
|
||||
IS_MINGW = 1
|
||||
else
|
||||
ifdef SYSTEMDRIVE
|
||||
# ifdef OS
|
||||
IS_MINGW = 1
|
||||
endif
|
||||
endif
|
||||
|
||||
ifdef IS_MINGW
|
||||
LDFLAGS_STATIC_2 = -static
|
||||
else
|
||||
ifndef DEF_FILE
|
||||
ifndef IS_NOT_STANDALONE
|
||||
ifndef MY_DYNAMIC_LINK
|
||||
ifneq ($(CC), clang)
|
||||
LDFLAGS_STATIC_2 =
|
||||
# -static
|
||||
# -static-libstdc++ -static-libgcc
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
LDFLAGS_STATIC = -DNDEBUG $(LDFLAGS_STATIC_2)
|
||||
|
||||
ifdef DEF_FILE
|
||||
|
||||
|
||||
ifdef IS_MINGW
|
||||
SHARED_EXT=.dll
|
||||
LDFLAGS = -shared -DEF $(DEF_FILE) $(LDFLAGS_STATIC)
|
||||
else
|
||||
SHARED_EXT=.so
|
||||
LDFLAGS = -shared -fPIC $(LDFLAGS_STATIC)
|
||||
CC_SHARED=-fPIC
|
||||
endif
|
||||
|
||||
|
||||
else
|
||||
|
||||
LDFLAGS = $(LDFLAGS_STATIC)
|
||||
# -s is not required for clang, do we need it for GGC ???
|
||||
# -s
|
||||
|
||||
#-static -static-libgcc -static-libstdc++
|
||||
|
||||
ifdef IS_MINGW
|
||||
SHARED_EXT=.exe
|
||||
else
|
||||
SHARED_EXT=
|
||||
endif
|
||||
|
||||
endif
|
||||
|
||||
|
||||
PROGPATH = $(O)/$(PROG)$(SHARED_EXT)
|
||||
PROGPATH_STATIC = $(O)/$(PROG)s$(SHARED_EXT)
|
||||
|
||||
ifndef O
|
||||
O=_o
|
||||
endif
|
||||
|
||||
ifdef IS_MINGW
|
||||
|
||||
ifdef MSYSTEM
|
||||
RM = rm -f
|
||||
MY_MKDIR=mkdir -p
|
||||
DEL_OBJ_EXE = -$(RM) $(PROGPATH) $(PROGPATH_STATIC) $(OBJS)
|
||||
else
|
||||
RM = del
|
||||
MY_MKDIR=mkdir
|
||||
DEL_OBJ_EXE = -$(RM) $(O)\*.o $(O)\$(PROG).exe $(O)\$(PROG).dll
|
||||
endif
|
||||
|
||||
|
||||
LIB2 = -lole32 -loleaut32 -luuid -ladvapi32 -luser32 -lshell32
|
||||
|
||||
CFLAGS_EXTRA = -DUNICODE -D_UNICODE
|
||||
# -Wno-delete-non-virtual-dtor
|
||||
|
||||
|
||||
else
|
||||
|
||||
RM = rm -f
|
||||
MY_MKDIR=mkdir -p
|
||||
# CFLAGS_BASE := $(CFLAGS_BASE) -DZ7_ST
|
||||
# CFLAGS_EXTRA = -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE
|
||||
|
||||
# LOCAL_LIBS=-lpthread
|
||||
# LOCAL_LIBS_DLL=$(LOCAL_LIBS) -ldl
|
||||
LIB2 = -lpthread -ldl
|
||||
|
||||
DEL_OBJ_EXE = -$(RM) $(PROGPATH) $(PROGPATH_STATIC) $(OBJS)
|
||||
|
||||
endif
|
||||
|
||||
|
||||
ifdef IS_X64
|
||||
AFLAGS_ABI = -elf64 -DABI_LINUX
|
||||
else
|
||||
AFLAGS_ABI = -elf -DABI_LINUX -DABI_CDECL
|
||||
# -DABI_CDECL
|
||||
# -DABI_LINUX
|
||||
# -DABI_CDECL
|
||||
endif
|
||||
AFLAGS = $(AFLAGS_ABI) -Fo$(O)/
|
||||
|
||||
C_WARN_FLAGS =
|
||||
|
||||
CFLAGS = $(LOCAL_FLAGS) $(CFLAGS_BASE2) $(CFLAGS_BASE) $(CFLAGS_EXTRA) $(C_WARN_FLAGS) $(FLAGS_FLTO) $(CC_SHARED) -o $@
|
||||
|
||||
STATIC_TARGET=
|
||||
ifdef COMPL_STATIC
|
||||
STATIC_TARGET=$(PROGPATH_STATIC)
|
||||
endif
|
||||
|
||||
|
||||
all: $(O) $(PROGPATH) $(STATIC_TARGET)
|
||||
|
||||
$(O):
|
||||
$(MY_MKDIR) $(O)
|
||||
|
||||
ifneq ($(CC), $(CROSS_COMPILE)clang)
|
||||
LFLAGS_STRIP = -s
|
||||
endif
|
||||
|
||||
LFLAGS_ALL = $(LFLAGS_STRIP) $(MY_ARCH_2) $(LDFLAGS) $(FLAGS_FLTO) $(LD_arch) $(OBJS) $(MY_LIBS) $(LIB2)
|
||||
$(PROGPATH): $(OBJS)
|
||||
$(CC) -o $(PROGPATH) $(LFLAGS_ALL)
|
||||
|
||||
$(PROGPATH_STATIC): $(OBJS)
|
||||
$(CC) -static -o $(PROGPATH_STATIC) $(LFLAGS_ALL)
|
||||
|
||||
|
||||
ifndef NO_DEFAULT_RES
|
||||
# old mingw without -FO
|
||||
# windres.exe $(RFLAGS) resource.rc $O/resource.o
|
||||
$O/resource.o: resource.rc
|
||||
$(RC) $(RFLAGS) resource.rc $(O)/resource.o
|
||||
endif
|
||||
# windres.exe $(RFLAGS) resource.rc $(O)\resource.o
|
||||
# windres.exe $(RFLAGS) resource.rc -FO $(O)/resource.o
|
||||
# $(RC) $(RFLAGS) resource.rc -FO $(O)/resource.o
|
||||
|
||||
|
||||
|
||||
$O/7zAlloc.o: ../../../C/7zAlloc.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/7zArcIn.o: ../../../C/7zArcIn.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/7zBuf.o: ../../../C/7zBuf.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/7zBuf2.o: ../../../C/7zBuf2.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/7zCrc.o: ../../../C/7zCrc.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/7zDec.o: ../../../C/7zDec.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/7zFile.o: ../../../C/7zFile.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/7zStream.o: ../../../C/7zStream.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Aes.o: ../../../C/Aes.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Alloc.o: ../../../C/Alloc.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Bcj2.o: ../../../C/Bcj2.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Bcj2Enc.o: ../../../C/Bcj2Enc.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Blake2s.o: ../../../C/Blake2s.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Bra.o: ../../../C/Bra.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Bra86.o: ../../../C/Bra86.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/BraIA64.o: ../../../C/BraIA64.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/BwtSort.o: ../../../C/BwtSort.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
|
||||
$O/CpuArch.o: ../../../C/CpuArch.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Delta.o: ../../../C/Delta.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/DllSecur.o: ../../../C/DllSecur.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/HuffEnc.o: ../../../C/HuffEnc.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/LzFind.o: ../../../C/LzFind.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
|
||||
# ifdef MT_FILES
|
||||
$O/LzFindMt.o: ../../../C/LzFindMt.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/LzFindOpt.o: ../../../C/LzFindOpt.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
|
||||
$O/Threads.o: ../../../C/Threads.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
# endif
|
||||
|
||||
$O/LzmaEnc.o: ../../../C/LzmaEnc.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Lzma86Dec.o: ../../../C/Lzma86Dec.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Lzma86Enc.o: ../../../C/Lzma86Enc.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Lzma2Dec.o: ../../../C/Lzma2Dec.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Lzma2DecMt.o: ../../../C/Lzma2DecMt.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Lzma2Enc.o: ../../../C/Lzma2Enc.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/LzmaLib.o: ../../../C/LzmaLib.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/MtCoder.o: ../../../C/MtCoder.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/MtDec.o: ../../../C/MtDec.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Ppmd7.o: ../../../C/Ppmd7.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Ppmd7aDec.o: ../../../C/Ppmd7aDec.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Ppmd7Dec.o: ../../../C/Ppmd7Dec.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Ppmd7Enc.o: ../../../C/Ppmd7Enc.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Ppmd8.o: ../../../C/Ppmd8.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Ppmd8Dec.o: ../../../C/Ppmd8Dec.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Ppmd8Enc.o: ../../../C/Ppmd8Enc.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Sha1.o: ../../../C/Sha1.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Sha256.o: ../../../C/Sha256.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Sort.o: ../../../C/Sort.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/SwapBytes.o: ../../../C/SwapBytes.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Xz.o: ../../../C/Xz.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/XzCrc64.o: ../../../C/XzCrc64.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/XzDec.o: ../../../C/XzDec.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/XzEnc.o: ../../../C/XzEnc.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/XzIn.o: ../../../C/XzIn.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
|
||||
|
||||
ifdef USE_ASM
|
||||
ifdef IS_X64
|
||||
USE_X86_ASM=1
|
||||
else
|
||||
ifdef IS_X86
|
||||
USE_X86_ASM=1
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
ifdef USE_X86_ASM
|
||||
$O/7zCrcOpt.o: ../../../Asm/x86/7zCrcOpt.asm
|
||||
$(MY_ASM) $(AFLAGS) $<
|
||||
$O/XzCrc64Opt.o: ../../../Asm/x86/XzCrc64Opt.asm
|
||||
$(MY_ASM) $(AFLAGS) $<
|
||||
$O/AesOpt.o: ../../../Asm/x86/AesOpt.asm
|
||||
$(MY_ASM) $(AFLAGS) $<
|
||||
$O/Sha1Opt.o: ../../../Asm/x86/Sha1Opt.asm
|
||||
$(MY_ASM) $(AFLAGS) $<
|
||||
$O/Sha256Opt.o: ../../../Asm/x86/Sha256Opt.asm
|
||||
$(MY_ASM) $(AFLAGS) $<
|
||||
else
|
||||
$O/7zCrcOpt.o: ../../7zCrcOpt.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/XzCrc64Opt.o: ../../XzCrc64Opt.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Sha1Opt.o: ../../Sha1Opt.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/Sha256Opt.o: ../../Sha256Opt.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/AesOpt.o: ../../AesOpt.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
endif
|
||||
|
||||
|
||||
ifdef USE_LZMA_DEC_ASM
|
||||
|
||||
ifdef IS_X64
|
||||
$O/LzmaDecOpt.o: ../../../Asm/x86/LzmaDecOpt.asm
|
||||
$(MY_ASM) $(AFLAGS) $<
|
||||
endif
|
||||
|
||||
ifdef IS_ARM64
|
||||
$O/LzmaDecOpt.o: ../../../Asm/arm64/LzmaDecOpt.S ../../../Asm/arm64/7zAsm.S
|
||||
$(CC) $(CFLAGS) $(ASM_FLAGS) $<
|
||||
endif
|
||||
|
||||
$O/LzmaDec.o: ../../LzmaDec.c
|
||||
$(CC) $(CFLAGS) -DZ7_LZMA_DEC_OPT $<
|
||||
|
||||
else
|
||||
|
||||
$O/LzmaDec.o: ../../LzmaDec.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
|
||||
endif
|
||||
|
||||
|
||||
|
||||
$O/7zMain.o: ../../../C/Util/7z/7zMain.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/7zipInstall.o: ../../../C/Util/7zipInstall/7zipInstall.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/7zipUninstall.o: ../../../C/Util/7zipUninstall/7zipUninstall.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/LzmaUtil.o: ../../../C/Util/Lzma/LzmaUtil.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
$O/XzUtil.o: ../../../C/Util/Xz/XzUtil.c
|
||||
$(CC) $(CFLAGS) $<
|
||||
|
||||
|
||||
clean:
|
||||
-$(DEL_OBJ_EXE)
|
||||
Vendored
+429
@@ -0,0 +1,429 @@
|
||||
/* Aes.c -- AES encryption / decryption
|
||||
2024-03-01 : Igor Pavlov : Public domain */
|
||||
|
||||
#include "Precomp.h"
|
||||
|
||||
#include "CpuArch.h"
|
||||
#include "Aes.h"
|
||||
|
||||
AES_CODE_FUNC g_AesCbc_Decode;
|
||||
#ifndef Z7_SFX
|
||||
AES_CODE_FUNC g_AesCbc_Encode;
|
||||
AES_CODE_FUNC g_AesCtr_Code;
|
||||
UInt32 g_Aes_SupportedFunctions_Flags;
|
||||
#endif
|
||||
|
||||
MY_ALIGN(64)
|
||||
static UInt32 T[256 * 4];
|
||||
MY_ALIGN(64)
|
||||
static const Byte Sbox[256] = {
|
||||
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
|
||||
0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
|
||||
0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
|
||||
0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
|
||||
0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
|
||||
0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
|
||||
0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
|
||||
0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
|
||||
0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
|
||||
0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
|
||||
0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
|
||||
0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
|
||||
0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
|
||||
0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
|
||||
0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
|
||||
0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16};
|
||||
|
||||
|
||||
MY_ALIGN(64)
|
||||
static UInt32 D[256 * 4];
|
||||
MY_ALIGN(64)
|
||||
static Byte InvS[256];
|
||||
|
||||
#define xtime(x) ((((x) << 1) ^ (((x) & 0x80) != 0 ? 0x1B : 0)) & 0xFF)
|
||||
|
||||
#define Ui32(a0, a1, a2, a3) ((UInt32)(a0) | ((UInt32)(a1) << 8) | ((UInt32)(a2) << 16) | ((UInt32)(a3) << 24))
|
||||
|
||||
#define gb0(x) ( (x) & 0xFF)
|
||||
#define gb1(x) (((x) >> ( 8)) & 0xFF)
|
||||
#define gb2(x) (((x) >> (16)) & 0xFF)
|
||||
#define gb3(x) (((x) >> (24)))
|
||||
|
||||
#define gb(n, x) gb ## n(x)
|
||||
|
||||
#define TT(x) (T + (x << 8))
|
||||
#define DD(x) (D + (x << 8))
|
||||
|
||||
|
||||
// #define Z7_SHOW_AES_STATUS
|
||||
|
||||
#ifdef MY_CPU_X86_OR_AMD64
|
||||
|
||||
#if defined(__INTEL_COMPILER)
|
||||
#if (__INTEL_COMPILER >= 1110)
|
||||
#define USE_HW_AES
|
||||
#if (__INTEL_COMPILER >= 1900)
|
||||
#define USE_HW_VAES
|
||||
#endif
|
||||
#endif
|
||||
#elif defined(Z7_CLANG_VERSION) && (Z7_CLANG_VERSION >= 30800) \
|
||||
|| defined(Z7_GCC_VERSION) && (Z7_GCC_VERSION >= 40400)
|
||||
#define USE_HW_AES
|
||||
#if defined(__clang__) && (__clang_major__ >= 8) \
|
||||
|| defined(__GNUC__) && (__GNUC__ >= 8)
|
||||
#define USE_HW_VAES
|
||||
#endif
|
||||
#elif defined(_MSC_VER)
|
||||
#define USE_HW_AES
|
||||
#define USE_HW_VAES
|
||||
#endif
|
||||
|
||||
#elif defined(MY_CPU_ARM_OR_ARM64) && defined(MY_CPU_LE)
|
||||
|
||||
#if defined(__ARM_FEATURE_AES) \
|
||||
|| defined(__ARM_FEATURE_CRYPTO)
|
||||
#define USE_HW_AES
|
||||
#else
|
||||
#if defined(MY_CPU_ARM64) \
|
||||
|| defined(__ARM_ARCH) && (__ARM_ARCH >= 4) \
|
||||
|| defined(Z7_MSC_VER_ORIGINAL)
|
||||
#if defined(__ARM_FP) && \
|
||||
( defined(Z7_CLANG_VERSION) && (Z7_CLANG_VERSION >= 30800) \
|
||||
|| defined(__GNUC__) && (__GNUC__ >= 6) \
|
||||
) \
|
||||
|| defined(Z7_MSC_VER_ORIGINAL) && (_MSC_VER >= 1910)
|
||||
#if defined(MY_CPU_ARM64) \
|
||||
|| !defined(Z7_CLANG_VERSION) \
|
||||
|| defined(__ARM_NEON) && \
|
||||
(Z7_CLANG_VERSION < 170000 || \
|
||||
Z7_CLANG_VERSION > 170001)
|
||||
#define USE_HW_AES
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef USE_HW_AES
|
||||
// #pragma message("=== Aes.c USE_HW_AES === ")
|
||||
#ifdef Z7_SHOW_AES_STATUS
|
||||
#include <stdio.h>
|
||||
#define PRF(x) x
|
||||
#else
|
||||
#define PRF(x)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
void AesGenTables(void)
|
||||
{
|
||||
unsigned i;
|
||||
for (i = 0; i < 256; i++)
|
||||
InvS[Sbox[i]] = (Byte)i;
|
||||
|
||||
for (i = 0; i < 256; i++)
|
||||
{
|
||||
{
|
||||
const UInt32 a1 = Sbox[i];
|
||||
const UInt32 a2 = xtime(a1);
|
||||
const UInt32 a3 = a2 ^ a1;
|
||||
TT(0)[i] = Ui32(a2, a1, a1, a3);
|
||||
TT(1)[i] = Ui32(a3, a2, a1, a1);
|
||||
TT(2)[i] = Ui32(a1, a3, a2, a1);
|
||||
TT(3)[i] = Ui32(a1, a1, a3, a2);
|
||||
}
|
||||
{
|
||||
const UInt32 a1 = InvS[i];
|
||||
const UInt32 a2 = xtime(a1);
|
||||
const UInt32 a4 = xtime(a2);
|
||||
const UInt32 a8 = xtime(a4);
|
||||
const UInt32 a9 = a8 ^ a1;
|
||||
const UInt32 aB = a8 ^ a2 ^ a1;
|
||||
const UInt32 aD = a8 ^ a4 ^ a1;
|
||||
const UInt32 aE = a8 ^ a4 ^ a2;
|
||||
DD(0)[i] = Ui32(aE, a9, aD, aB);
|
||||
DD(1)[i] = Ui32(aB, aE, a9, aD);
|
||||
DD(2)[i] = Ui32(aD, aB, aE, a9);
|
||||
DD(3)[i] = Ui32(a9, aD, aB, aE);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
AES_CODE_FUNC d = AesCbc_Decode;
|
||||
#ifndef Z7_SFX
|
||||
AES_CODE_FUNC e = AesCbc_Encode;
|
||||
AES_CODE_FUNC c = AesCtr_Code;
|
||||
UInt32 flags = 0;
|
||||
#endif
|
||||
|
||||
#ifdef USE_HW_AES
|
||||
if (CPU_IsSupported_AES())
|
||||
{
|
||||
// #pragma message ("AES HW")
|
||||
PRF(printf("\n===AES HW\n"));
|
||||
d = AesCbc_Decode_HW;
|
||||
|
||||
#ifndef Z7_SFX
|
||||
e = AesCbc_Encode_HW;
|
||||
c = AesCtr_Code_HW;
|
||||
flags = k_Aes_SupportedFunctions_HW;
|
||||
#endif
|
||||
|
||||
#ifdef MY_CPU_X86_OR_AMD64
|
||||
#ifdef USE_HW_VAES
|
||||
if (CPU_IsSupported_VAES_AVX2())
|
||||
{
|
||||
PRF(printf("\n===vaes avx2\n"));
|
||||
d = AesCbc_Decode_HW_256;
|
||||
#ifndef Z7_SFX
|
||||
c = AesCtr_Code_HW_256;
|
||||
flags |= k_Aes_SupportedFunctions_HW_256;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
g_AesCbc_Decode = d;
|
||||
#ifndef Z7_SFX
|
||||
g_AesCbc_Encode = e;
|
||||
g_AesCtr_Code = c;
|
||||
g_Aes_SupportedFunctions_Flags = flags;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#define HT(i, x, s) TT(x)[gb(x, s[(i + x) & 3])]
|
||||
|
||||
#define HT4(m, i, s, p) m[i] = \
|
||||
HT(i, 0, s) ^ \
|
||||
HT(i, 1, s) ^ \
|
||||
HT(i, 2, s) ^ \
|
||||
HT(i, 3, s) ^ w[p + i]
|
||||
|
||||
#define HT16(m, s, p) \
|
||||
HT4(m, 0, s, p); \
|
||||
HT4(m, 1, s, p); \
|
||||
HT4(m, 2, s, p); \
|
||||
HT4(m, 3, s, p); \
|
||||
|
||||
#define FT(i, x) Sbox[gb(x, m[(i + x) & 3])]
|
||||
#define FT4(i) dest[i] = Ui32(FT(i, 0), FT(i, 1), FT(i, 2), FT(i, 3)) ^ w[i];
|
||||
|
||||
|
||||
#define HD(i, x, s) DD(x)[gb(x, s[(i - x) & 3])]
|
||||
|
||||
#define HD4(m, i, s, p) m[i] = \
|
||||
HD(i, 0, s) ^ \
|
||||
HD(i, 1, s) ^ \
|
||||
HD(i, 2, s) ^ \
|
||||
HD(i, 3, s) ^ w[p + i];
|
||||
|
||||
#define HD16(m, s, p) \
|
||||
HD4(m, 0, s, p); \
|
||||
HD4(m, 1, s, p); \
|
||||
HD4(m, 2, s, p); \
|
||||
HD4(m, 3, s, p); \
|
||||
|
||||
#define FD(i, x) InvS[gb(x, m[(i - x) & 3])]
|
||||
#define FD4(i) dest[i] = Ui32(FD(i, 0), FD(i, 1), FD(i, 2), FD(i, 3)) ^ w[i];
|
||||
|
||||
void Z7_FASTCALL Aes_SetKey_Enc(UInt32 *w, const Byte *key, unsigned keySize)
|
||||
{
|
||||
unsigned i, m;
|
||||
const UInt32 *wLim;
|
||||
UInt32 t;
|
||||
UInt32 rcon = 1;
|
||||
|
||||
keySize /= 4;
|
||||
w[0] = ((UInt32)keySize / 2) + 3;
|
||||
w += 4;
|
||||
|
||||
for (i = 0; i < keySize; i++, key += 4)
|
||||
w[i] = GetUi32(key);
|
||||
|
||||
t = w[(size_t)keySize - 1];
|
||||
wLim = w + (size_t)keySize * 3 + 28;
|
||||
m = 0;
|
||||
do
|
||||
{
|
||||
if (m == 0)
|
||||
{
|
||||
t = Ui32(Sbox[gb1(t)] ^ rcon, Sbox[gb2(t)], Sbox[gb3(t)], Sbox[gb0(t)]);
|
||||
rcon <<= 1;
|
||||
if (rcon & 0x100)
|
||||
rcon = 0x1b;
|
||||
m = keySize;
|
||||
}
|
||||
else if (m == 4 && keySize > 6)
|
||||
t = Ui32(Sbox[gb0(t)], Sbox[gb1(t)], Sbox[gb2(t)], Sbox[gb3(t)]);
|
||||
m--;
|
||||
t ^= w[0];
|
||||
w[keySize] = t;
|
||||
}
|
||||
while (++w != wLim);
|
||||
}
|
||||
|
||||
void Z7_FASTCALL Aes_SetKey_Dec(UInt32 *w, const Byte *key, unsigned keySize)
|
||||
{
|
||||
unsigned i, num;
|
||||
Aes_SetKey_Enc(w, key, keySize);
|
||||
num = keySize + 20;
|
||||
w += 8;
|
||||
for (i = 0; i < num; i++)
|
||||
{
|
||||
UInt32 r = w[i];
|
||||
w[i] =
|
||||
DD(0)[Sbox[gb0(r)]] ^
|
||||
DD(1)[Sbox[gb1(r)]] ^
|
||||
DD(2)[Sbox[gb2(r)]] ^
|
||||
DD(3)[Sbox[gb3(r)]];
|
||||
}
|
||||
}
|
||||
|
||||
/* Aes_Encode and Aes_Decode functions work with little-endian words.
|
||||
src and dest are pointers to 4 UInt32 words.
|
||||
src and dest can point to same block */
|
||||
|
||||
// Z7_FORCE_INLINE
|
||||
static void Aes_Encode(const UInt32 *w, UInt32 *dest, const UInt32 *src)
|
||||
{
|
||||
UInt32 s[4];
|
||||
UInt32 m[4];
|
||||
UInt32 numRounds2 = w[0];
|
||||
w += 4;
|
||||
s[0] = src[0] ^ w[0];
|
||||
s[1] = src[1] ^ w[1];
|
||||
s[2] = src[2] ^ w[2];
|
||||
s[3] = src[3] ^ w[3];
|
||||
w += 4;
|
||||
for (;;)
|
||||
{
|
||||
HT16(m, s, 0)
|
||||
if (--numRounds2 == 0)
|
||||
break;
|
||||
HT16(s, m, 4)
|
||||
w += 8;
|
||||
}
|
||||
w += 4;
|
||||
FT4(0)
|
||||
FT4(1)
|
||||
FT4(2)
|
||||
FT4(3)
|
||||
}
|
||||
|
||||
Z7_FORCE_INLINE
|
||||
static void Aes_Decode(const UInt32 *w, UInt32 *dest, const UInt32 *src)
|
||||
{
|
||||
UInt32 s[4];
|
||||
UInt32 m[4];
|
||||
UInt32 numRounds2 = w[0];
|
||||
w += 4 + numRounds2 * 8;
|
||||
s[0] = src[0] ^ w[0];
|
||||
s[1] = src[1] ^ w[1];
|
||||
s[2] = src[2] ^ w[2];
|
||||
s[3] = src[3] ^ w[3];
|
||||
for (;;)
|
||||
{
|
||||
w -= 8;
|
||||
HD16(m, s, 4)
|
||||
if (--numRounds2 == 0)
|
||||
break;
|
||||
HD16(s, m, 0)
|
||||
}
|
||||
FD4(0)
|
||||
FD4(1)
|
||||
FD4(2)
|
||||
FD4(3)
|
||||
}
|
||||
|
||||
void AesCbc_Init(UInt32 *p, const Byte *iv)
|
||||
{
|
||||
unsigned i;
|
||||
for (i = 0; i < 4; i++)
|
||||
p[i] = GetUi32(iv + i * 4);
|
||||
}
|
||||
|
||||
void Z7_FASTCALL AesCbc_Encode(UInt32 *p, Byte *data, size_t numBlocks)
|
||||
{
|
||||
for (; numBlocks != 0; numBlocks--, data += AES_BLOCK_SIZE)
|
||||
{
|
||||
p[0] ^= GetUi32(data);
|
||||
p[1] ^= GetUi32(data + 4);
|
||||
p[2] ^= GetUi32(data + 8);
|
||||
p[3] ^= GetUi32(data + 12);
|
||||
|
||||
Aes_Encode(p + 4, p, p);
|
||||
|
||||
SetUi32(data, p[0])
|
||||
SetUi32(data + 4, p[1])
|
||||
SetUi32(data + 8, p[2])
|
||||
SetUi32(data + 12, p[3])
|
||||
}
|
||||
}
|
||||
|
||||
void Z7_FASTCALL AesCbc_Decode(UInt32 *p, Byte *data, size_t numBlocks)
|
||||
{
|
||||
UInt32 in[4], out[4];
|
||||
for (; numBlocks != 0; numBlocks--, data += AES_BLOCK_SIZE)
|
||||
{
|
||||
in[0] = GetUi32(data);
|
||||
in[1] = GetUi32(data + 4);
|
||||
in[2] = GetUi32(data + 8);
|
||||
in[3] = GetUi32(data + 12);
|
||||
|
||||
Aes_Decode(p + 4, out, in);
|
||||
|
||||
SetUi32(data, p[0] ^ out[0])
|
||||
SetUi32(data + 4, p[1] ^ out[1])
|
||||
SetUi32(data + 8, p[2] ^ out[2])
|
||||
SetUi32(data + 12, p[3] ^ out[3])
|
||||
|
||||
p[0] = in[0];
|
||||
p[1] = in[1];
|
||||
p[2] = in[2];
|
||||
p[3] = in[3];
|
||||
}
|
||||
}
|
||||
|
||||
void Z7_FASTCALL AesCtr_Code(UInt32 *p, Byte *data, size_t numBlocks)
|
||||
{
|
||||
for (; numBlocks != 0; numBlocks--)
|
||||
{
|
||||
UInt32 temp[4];
|
||||
unsigned i;
|
||||
|
||||
if (++p[0] == 0)
|
||||
p[1]++;
|
||||
|
||||
Aes_Encode(p + 4, temp, p);
|
||||
|
||||
for (i = 0; i < 4; i++, data += 4)
|
||||
{
|
||||
const UInt32 t = temp[i];
|
||||
|
||||
#ifdef MY_CPU_LE_UNALIGN
|
||||
*((UInt32 *)(void *)data) ^= t;
|
||||
#else
|
||||
data[0] = (Byte)(data[0] ^ (t & 0xFF));
|
||||
data[1] = (Byte)(data[1] ^ ((t >> 8) & 0xFF));
|
||||
data[2] = (Byte)(data[2] ^ ((t >> 16) & 0xFF));
|
||||
data[3] = (Byte)(data[3] ^ ((t >> 24)));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#undef xtime
|
||||
#undef Ui32
|
||||
#undef gb0
|
||||
#undef gb1
|
||||
#undef gb2
|
||||
#undef gb3
|
||||
#undef gb
|
||||
#undef TT
|
||||
#undef DD
|
||||
#undef USE_HW_AES
|
||||
#undef PRF
|
||||
Vendored
+60
@@ -0,0 +1,60 @@
|
||||
/* Aes.h -- AES encryption / decryption
|
||||
2023-04-02 : Igor Pavlov : Public domain */
|
||||
|
||||
#ifndef ZIP7_INC_AES_H
|
||||
#define ZIP7_INC_AES_H
|
||||
|
||||
#include "7zTypes.h"
|
||||
|
||||
EXTERN_C_BEGIN
|
||||
|
||||
#define AES_BLOCK_SIZE 16
|
||||
|
||||
/* Call AesGenTables one time before other AES functions */
|
||||
void AesGenTables(void);
|
||||
|
||||
/* UInt32 pointers must be 16-byte aligned */
|
||||
|
||||
/* 16-byte (4 * 32-bit words) blocks: 1 (IV) + 1 (keyMode) + 15 (AES-256 roundKeys) */
|
||||
#define AES_NUM_IVMRK_WORDS ((1 + 1 + 15) * 4)
|
||||
|
||||
/* aes - 16-byte aligned pointer to keyMode+roundKeys sequence */
|
||||
/* keySize = 16 or 24 or 32 (bytes) */
|
||||
typedef void (Z7_FASTCALL *AES_SET_KEY_FUNC)(UInt32 *aes, const Byte *key, unsigned keySize);
|
||||
void Z7_FASTCALL Aes_SetKey_Enc(UInt32 *aes, const Byte *key, unsigned keySize);
|
||||
void Z7_FASTCALL Aes_SetKey_Dec(UInt32 *aes, const Byte *key, unsigned keySize);
|
||||
|
||||
/* ivAes - 16-byte aligned pointer to iv+keyMode+roundKeys sequence: UInt32[AES_NUM_IVMRK_WORDS] */
|
||||
void AesCbc_Init(UInt32 *ivAes, const Byte *iv); /* iv size is AES_BLOCK_SIZE */
|
||||
|
||||
/* data - 16-byte aligned pointer to data */
|
||||
/* numBlocks - the number of 16-byte blocks in data array */
|
||||
typedef void (Z7_FASTCALL *AES_CODE_FUNC)(UInt32 *ivAes, Byte *data, size_t numBlocks);
|
||||
|
||||
extern AES_CODE_FUNC g_AesCbc_Decode;
|
||||
#ifndef Z7_SFX
|
||||
extern AES_CODE_FUNC g_AesCbc_Encode;
|
||||
extern AES_CODE_FUNC g_AesCtr_Code;
|
||||
#define k_Aes_SupportedFunctions_HW (1 << 2)
|
||||
#define k_Aes_SupportedFunctions_HW_256 (1 << 3)
|
||||
extern UInt32 g_Aes_SupportedFunctions_Flags;
|
||||
#endif
|
||||
|
||||
|
||||
#define Z7_DECLARE_AES_CODE_FUNC(funcName) \
|
||||
void Z7_FASTCALL funcName(UInt32 *ivAes, Byte *data, size_t numBlocks);
|
||||
|
||||
Z7_DECLARE_AES_CODE_FUNC (AesCbc_Encode)
|
||||
Z7_DECLARE_AES_CODE_FUNC (AesCbc_Decode)
|
||||
Z7_DECLARE_AES_CODE_FUNC (AesCtr_Code)
|
||||
|
||||
Z7_DECLARE_AES_CODE_FUNC (AesCbc_Encode_HW)
|
||||
Z7_DECLARE_AES_CODE_FUNC (AesCbc_Decode_HW)
|
||||
Z7_DECLARE_AES_CODE_FUNC (AesCtr_Code_HW)
|
||||
|
||||
Z7_DECLARE_AES_CODE_FUNC (AesCbc_Decode_HW_256)
|
||||
Z7_DECLARE_AES_CODE_FUNC (AesCtr_Code_HW_256)
|
||||
|
||||
EXTERN_C_END
|
||||
|
||||
#endif
|
||||
Loaded 100 of 310 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user