mirror of
https://github.com/LisherSong/ps5-web-file-manager.git
synced 2026-10-06 09:00:26 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a91067c2e | ||
|
|
ba0704d48c | ||
|
|
4e10dee5fb | ||
|
|
259d0a6434 | ||
|
|
6f46847df2 | ||
|
|
91375e0537 | ||
|
|
f460178ac8 | ||
|
|
1ed55b499c |
No files matched your search
@@ -1,74 +0,0 @@
|
||||
==========================================
|
||||
PS5 Web File Manager -- ELF 构建 v4
|
||||
时间: Fri Sep 4 12:45:52 CST 2026
|
||||
PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk
|
||||
user: song uid=1000
|
||||
==========================================
|
||||
[0/7] 预热 sudo (会提示输 1 次密码)...
|
||||
[1/7] SDK OK: /opt/ps5-payload-sdk
|
||||
[2/7] LLVM bindir: /usr/lib/llvm-18/bin
|
||||
[3/7] prospero-clang --version ...
|
||||
Ubuntu clang version 18.1.8 (++20240731024944+3b5b5c1ec4a3-1~exp1~20240731145000.144)
|
||||
[4/7] 同步源码 (Windows -> WSL, 增量) ...
|
||||
已同步自 /mnt/c/Users/songl/Desktop/Web File Manager/ps5-web-file-manager
|
||||
[5/7] libmicrohttpd (staging + 一次性 sudo cp)...
|
||||
下载 libmicrohttpd-1.0.1.tar.gz...
|
||||
tarball: /home/song/.cache/libmicrohttpd-1.0.1.tar.gz (2.2M)
|
||||
configure (host=x86_64-pc-freebsd)...
|
||||
make -j14...
|
||||
mv -f $depbase.Tpo $depbase.Po
|
||||
/bin/bash ../../libtool --tag=CC --mode=link /opt/ps5-payload-sdk/bin/prospero-clang -fno-strict-aliasing -O1 -w -o perf_replies perf_replies.o mhd_tool_get_cpu_count.o ../../src/microhttpd/libmicrohttpd.la
|
||||
libtool: link: /opt/ps5-payload-sdk/bin/prospero-clang -fno-strict-aliasing -O1 -w -o perf_replies perf_replies.o mhd_tool_get_cpu_count.o ../../src/microhttpd/.libs/libmicrohttpd.a -lpthread -pthread
|
||||
make[4]: Leaving directory '/tmp/tmp.1uwmf0LJPq/libmicrohttpd-1.0.1/src/tools'
|
||||
make[3]: Leaving directory '/tmp/tmp.1uwmf0LJPq/libmicrohttpd-1.0.1/src/tools'
|
||||
make[2]: Leaving directory '/tmp/tmp.1uwmf0LJPq/libmicrohttpd-1.0.1/src'
|
||||
Making all in .
|
||||
make[2]: Entering directory '/tmp/tmp.1uwmf0LJPq/libmicrohttpd-1.0.1'
|
||||
make[2]: Leaving directory '/tmp/tmp.1uwmf0LJPq/libmicrohttpd-1.0.1'
|
||||
make[1]: Leaving directory '/tmp/tmp.1uwmf0LJPq/libmicrohttpd-1.0.1'
|
||||
make install DESTDIR=/tmp/ps5-homebrew-ext4 ...
|
||||
make[2]: Nothing to be done for 'install-exec-am'.
|
||||
/usr/bin/mkdir -p '/tmp/ps5-homebrew-ext4/user/homebrew/lib/pkgconfig'
|
||||
/usr/bin/install -c -m 644 libmicrohttpd.pc '/tmp/ps5-homebrew-ext4/user/homebrew/lib/pkgconfig'
|
||||
make[2]: Leaving directory '/tmp/tmp.1uwmf0LJPq/libmicrohttpd-1.0.1'
|
||||
make[1]: Leaving directory '/tmp/tmp.1uwmf0LJPq/libmicrohttpd-1.0.1'
|
||||
sudo cp stage -> SDK homebrew...
|
||||
验证 prospero-pkg-config:
|
||||
1.0.1
|
||||
-L/opt/ps5-payload-sdk/target/user/homebrew/lib -lmicrohttpd -lpthread
|
||||
[6/7] make all (增量编译, 复用第三方 obj)...
|
||||
mkdir gen
|
||||
python3 gen-asset-module.py --path icon-back.png assets/icon-back.png > gen/icon-back.png.c
|
||||
python3 gen-asset-module.py --path icon-file.png assets/icon-file.png > gen/icon-file.png.c
|
||||
python3 gen-asset-module.py --path icon-folder.png assets/icon-folder.png > gen/icon-folder.png.c
|
||||
python3 gen-asset-module.py --path icon-generic.png assets/icon-generic.png > gen/icon-generic.png.c
|
||||
python3 gen-asset-module.py --path icon-image.png assets/icon-image.png > gen/icon-image.png.c
|
||||
python3 gen-asset-module.py --path icon-pkg.png assets/icon-pkg.png > gen/icon-pkg.png.c
|
||||
python3 gen-asset-module.py --path icon-up.png assets/icon-up.png > gen/icon-up.png.c
|
||||
python3 gen-asset-module.py --path index.html assets/index.html > gen/index.html.c
|
||||
python3 gen-asset-module.py --path lang-en.js assets/lang-en.js > gen/lang-en.js.c
|
||||
python3 gen-asset-module.py --path lang-zh.js assets/lang-zh.js > gen/lang-zh.js.c
|
||||
python3 gen-asset-module.py --path main.css assets/main.css > gen/main.css.c
|
||||
python3 gen-asset-module.py --path main.js assets/main.js > gen/main.js.c
|
||||
python3 gen-asset-module.py --path param.json assets/param.json > gen/param.json.c
|
||||
/opt/ps5-payload-sdk/bin/prospero-clang -O2 -w -Ithird_party/zlib/include -Ithird_party/minizip-ng/include -DHAVE_ZLIB -DZLIB_COMPAT -DHAVE_UNISTD_H=1 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE -c -o ps5-obj/third_party/zlib/src/adler32.o third_party/zlib/src/adler32.c
|
||||
/opt/ps5-payload-sdk/bin/prospero-clang -O2 -w -Ithird_party/zlib/include -Ithird_party/minizip-ng/include -DHAVE_ZLIB -DZLIB_COMPAT -DHAVE_UNISTD_H=1 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE -c -o ps5-obj/third_party/zlib/src/crc32.o third_party/zlib/src/crc32.c
|
||||
/opt/ps5-payload-sdk/bin/prospero-clang -O2 -w -Ithird_party/zlib/include -Ithird_party/minizip-ng/include -DHAVE_ZLIB -DZLIB_COMPAT -DHAVE_UNISTD_H=1 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE -c -o ps5-obj/third_party/zlib/src/deflate.o third_party/zlib/src/deflate.c
|
||||
/opt/ps5-payload-sdk/bin/prospero-clang -O2 -w -Ithird_party/zlib/include -Ithird_party/minizip-ng/include -DHAVE_ZLIB -DZLIB_COMPAT -DHAVE_UNISTD_H=1 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE -c -o ps5-obj/third_party/zlib/src/inffast.o third_party/zlib/src/inffast.c
|
||||
/opt/ps5-payload-sdk/bin/prospero-clang -O2 -w -Ithird_party/zlib/include -Ithird_party/minizip-ng/include -DHAVE_ZLIB -DZLIB_COMPAT -DHAVE_UNISTD_H=1 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE -c -o ps5-obj/third_party/zlib/src/inflate.o third_party/zlib/src/inflate.c
|
||||
/opt/ps5-payload-sdk/bin/prospero-clang -O2 -w -Ithird_party/zlib/include -Ithird_party/minizip-ng/include -DHAVE_ZLIB -DZLIB_COMPAT -DHAVE_UNISTD_H=1 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE -c -o ps5-obj/third_party/zlib/src/inftrees.o third_party/zlib/src/inftrees.c
|
||||
/opt/ps5-payload-sdk/bin/prospero-clang -O2 -w -Ithird_party/zlib/include -Ithird_party/minizip-ng/include -DHAVE_ZLIB -DZLIB_COMPAT -DHAVE_UNISTD_H=1 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE -c -o ps5-obj/third_party/zlib/src/trees.o third_party/zlib/src/trees.c
|
||||
/opt/ps5-payload-sdk/bin/prospero-clang -O2 -w -Ithird_party/zlib/include -Ithird_party/minizip-ng/include -DHAVE_ZLIB -DZLIB_COMPAT -DHAVE_UNISTD_H=1 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE -c -o ps5-obj/third_party/zlib/src/zutil.o third_party/zlib/src/zutil.c
|
||||
/opt/ps5-payload-sdk/bin/prospero-clang -O2 -w -Ithird_party/zlib/include -Ithird_party/minizip-ng/include -DHAVE_ZLIB -DZLIB_COMPAT -DHAVE_UNISTD_H=1 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE -c -o ps5-obj/third_party/minizip-ng/src/mz_crypt.o third_party/minizip-ng/src/mz_crypt.c
|
||||
/opt/ps5-payload-sdk/bin/prospero-clang -O2 -w -Ithird_party/zlib/include -Ithird_party/minizip-ng/include -DHAVE_ZLIB -DZLIB_COMPAT -DHAVE_UNISTD_H=1 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE -c -o ps5-obj/third_party/minizip-ng/src/mz_os.o third_party/minizip-ng/src/mz_os.c
|
||||
/opt/ps5-payload-sdk/bin/prospero-clang -O2 -w -Ithird_party/zlib/include -Ithird_party/minizip-ng/include -DHAVE_ZLIB -DZLIB_COMPAT -DHAVE_UNISTD_H=1 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE -c -o ps5-obj/third_party/minizip-ng/src/mz_os_posix.o third_party/minizip-ng/src/mz_os_posix.c
|
||||
/opt/ps5-payload-sdk/bin/prospero-clang -O2 -w -Ithird_party/zlib/include -Ithird_party/minizip-ng/include -DHAVE_ZLIB -DZLIB_COMPAT -DHAVE_UNISTD_H=1 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE -c -o ps5-obj/third_party/minizip-ng/src/mz_strm.o third_party/minizip-ng/src/mz_strm.c
|
||||
/opt/ps5-payload-sdk/bin/prospero-clang -O2 -w -Ithird_party/zlib/include -Ithird_party/minizip-ng/include -DHAVE_ZLIB -DZLIB_COMPAT -DHAVE_UNISTD_H=1 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE -c -o ps5-obj/third_party/minizip-ng/src/mz_strm_mem.o third_party/minizip-ng/src/mz_strm_mem.c
|
||||
/opt/ps5-payload-sdk/bin/prospero-clang -O2 -w -Ithird_party/zlib/include -Ithird_party/minizip-ng/include -DHAVE_ZLIB -DZLIB_COMPAT -DHAVE_UNISTD_H=1 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE -c -o ps5-obj/third_party/minizip-ng/src/mz_strm_os_posix.o third_party/minizip-ng/src/mz_strm_os_posix.c
|
||||
third_party/minizip-ng/src/mz_strm_os_posix.c:95:33: error: use of undeclared identifier 'O_BINARY'
|
||||
95 | fd = open(path, mode_open | O_BINARY, S_IRUSR | S_IWUSR | S_IRGRP);
|
||||
| ^
|
||||
1 error generated.
|
||||
make: *** [Makefile:78: ps5-obj/third_party/minizip-ng/src/mz_strm_os_posix.o] Error 1
|
||||
[7/7] 验证产物 + 同步...
|
||||
FAIL: ELF 未生成 -- 日志: /home/song/build-elf.log
|
||||
@@ -1,203 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# PS5 Web File Manager 一键构建 (v4)
|
||||
# 修复:
|
||||
# (1) staging 模式装 libmicrohttpd -> make install 到 /tmp, 再 sudo cp 到 SDK
|
||||
# (2) 显式 sudo -v 刷密码缓存 (v3 的 chown 静默失败了)
|
||||
# (3) 增量: 保留 zlib/minizip-ng OBJ 缓存 (不每次 make clean)
|
||||
# (4) libmicrohttpd tarball 缓存 ~/.cache/, 失败可重试不重下
|
||||
# (5) 同步源用 rsync 增量
|
||||
# (6) 失败时把 log 同步到 Windows .build/build-elf.log
|
||||
#
|
||||
# 跑法: bash /home/song/build-elf.sh
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk
|
||||
SRC_WIN="/mnt/c/Users/songl/Desktop/Web File Manager/ps5-web-file-manager"
|
||||
PROJ="/home/song/ps5-web-file-manager"
|
||||
LOG="/home/song/build-elf.log"
|
||||
STAGE="/tmp/ps5-homebrew-ext4"
|
||||
CACHE="${HOME}/.cache/libmicrohttpd-1.0.1.tar.gz"
|
||||
|
||||
mkdir -p "$(dirname "$LOG")" "$STAGE" "$(dirname "$CACHE")"
|
||||
: >"$LOG"
|
||||
exec > >(tee -a "$LOG") 2>&1
|
||||
|
||||
echo "=========================================="
|
||||
echo "PS5 Web File Manager -- ELF 构建 v4"
|
||||
echo "时间: $(date)"
|
||||
echo "PS5_PAYLOAD_SDK=$PS5_PAYLOAD_SDK"
|
||||
echo "user: $(whoami) uid=$(id -u)"
|
||||
echo "=========================================="
|
||||
|
||||
# 0. sudo 密码缓存 (脚本会跑 sudo 几次, 在开头集中要求密码)
|
||||
echo "[0/7] 预热 sudo (会提示输 1 次密码)..."
|
||||
sudo -v 2>&1 || { echo "FAIL: sudo 不可用"; exit 99; }
|
||||
|
||||
# 1. SDK 验证
|
||||
if [ ! -x "$PS5_PAYLOAD_SDK/bin/prospero-clang" ]; then
|
||||
if [ -f /home/song/ps5-payload-sdk.zip ]; then
|
||||
echo "[1/7] 展开 SDK zip 到 $PS5_PAYLOAD_SDK ..."
|
||||
sudo mkdir -p "$PS5_PAYLOAD_SDK"
|
||||
sudo unzip -q -o /home/song/ps5-payload-sdk.zip -d /tmp/sdk-stage
|
||||
sudo cp -r /tmp/sdk-stage/. "$PS5_PAYLOAD_SDK/"
|
||||
sudo chmod -R a+rx "$PS5_PAYLOAD_SDK"
|
||||
else
|
||||
echo "FAIL: 未发现 /home/song/ps5-payload-sdk.zip 也无 $PS5_PAYLOAD_SDK/bin/prospero-clang"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo "[1/7] SDK OK: $PS5_PAYLOAD_SDK"
|
||||
|
||||
# 2. LLVM 工具链
|
||||
if ! command -v llvm-config-18 >/dev/null 2>&1; then
|
||||
echo "[2/7] 装 LLVM 18 (apt.llvm.org) ..."
|
||||
if [ ! -x /tmp/llvm.sh ]; then
|
||||
wget -q https://apt.llvm.org/llvm.sh -O /tmp/llvm.sh
|
||||
chmod +x /tmp/llvm.sh
|
||||
fi
|
||||
sudo /tmp/llvm.sh 18 all >/dev/null 2>&1 || true
|
||||
fi
|
||||
if ! command -v pkg-config >/dev/null 2>&1; then
|
||||
echo "[2/7] 装 pkg-config ..."
|
||||
sudo apt-get install -y pkg-config rsync
|
||||
fi
|
||||
LLVM_BINDIR="$(llvm-config-18 --bindir 2>/dev/null || llvm-config --bindir)"
|
||||
echo "[2/7] LLVM bindir: $LLVM_BINDIR"
|
||||
|
||||
# 3. 工具链验证
|
||||
echo "[3/7] prospero-clang --version ..."
|
||||
"$PS5_PAYLOAD_SDK/bin/prospero-clang" --version | head -1
|
||||
|
||||
# 4. 同步源码 (rsync 增量)
|
||||
echo "[4/7] 同步源码 (Windows -> WSL, 增量) ..."
|
||||
mkdir -p "$PROJ"
|
||||
if [ -d "$SRC_WIN/src" ]; then
|
||||
rsync -a --delete \
|
||||
--exclude='ps5-obj' --exclude='linux-obj' \
|
||||
--exclude='web-file-mgr.elf' --exclude='web-file-mgr-linux' \
|
||||
--exclude='.build/stub' --exclude='.build/obj' \
|
||||
--exclude='.build/probe*' --exclude='.build/probe-work' \
|
||||
--exclude='.build/host-test' --exclude='.build/tp' \
|
||||
--exclude='.build/*.exe' --exclude='.build/*.txt' \
|
||||
--exclude='gen' \
|
||||
"$SRC_WIN/" "$PROJ/"
|
||||
echo " 已同步自 $SRC_WIN"
|
||||
else
|
||||
echo " (Windows 端不可见 -- /mnt/c 是否挂载?)"
|
||||
fi
|
||||
|
||||
# 5. 装 libmicrohttpd (staging 模式: make install 到 /tmp, 再 sudo cp 过去)
|
||||
echo "[5/7] libmicrohttpd (staging + 一次性 sudo cp)..."
|
||||
if "$PS5_PAYLOAD_SDK/bin/prospero-pkg-config" --modversion libmicrohttpd >/dev/null 2>&1; then
|
||||
echo " 已装: $($PS5_PAYLOAD_SDK/bin/prospero-pkg-config --modversion libmicrohttpd) -- 跳过"
|
||||
else
|
||||
# 5a) 下载 tarball (缓存复用)
|
||||
if [ ! -f "$CACHE" ] || [ ! -s "$CACHE" ]; then
|
||||
echo " 下载 libmicrohttpd-1.0.1.tar.gz..."
|
||||
if command -v wget >/dev/null; then
|
||||
wget -q https://ftp.gnu.org/gnu/libmicrohttpd/libmicrohttpd-1.0.1.tar.gz -O "$CACHE"
|
||||
else
|
||||
curl -fsSL https://ftp.gnu.org/gnu/libmicrohttpd/libmicrohttpd-1.0.1.tar.gz -o "$CACHE"
|
||||
fi
|
||||
fi
|
||||
[ -s "$CACHE" ] || { echo "FAIL: 下载失败: $CACHE"; exit 2; }
|
||||
echo " tarball: $CACHE ($(du -h "$CACHE" | cut -f1))"
|
||||
|
||||
# 5b) 解压 + configure + make (普通 user 跑, 输出到临时目录)
|
||||
TMPSRC=$(mktemp -d)
|
||||
trap 'rm -rf -- "$TMPSRC"' EXIT
|
||||
tar xf "$CACHE" -C "$TMPSRC"
|
||||
cd "$TMPSRC/libmicrohttpd-1.0.1"
|
||||
source "${PS5_PAYLOAD_SDK}/toolchain/prospero.sh"
|
||||
export CFLAGS="${CFLAGS:-} -O1 -w"
|
||||
echo " configure (host=x86_64-pc-freebsd)..."
|
||||
./configure --prefix="/user/homebrew" \
|
||||
--host=x86_64-pc-freebsd \
|
||||
--enable-static --disable-shared \
|
||||
--disable-doc --disable-curl --disable-examples \
|
||||
--disable-https --disable-openssl \
|
||||
>/dev/null 2>&1
|
||||
echo " make -j$(nproc)..."
|
||||
make -j"$(nproc)" 2>&1 | tail -10
|
||||
|
||||
# 5c) install 到 STAGE (普通 user 写到 /tmp)
|
||||
rm -rf "$STAGE"
|
||||
mkdir -p "$STAGE"
|
||||
echo " make install DESTDIR=$STAGE ..."
|
||||
make install DESTDIR="$STAGE" 2>&1 | tail -5
|
||||
|
||||
# 5d) 验证 stage 里有产物
|
||||
if [ ! -f "$STAGE/user/homebrew/include/microhttpd.h" ]; then
|
||||
echo "FAIL: stage 中无 microhttpd.h -- stage 内容:"
|
||||
find "$STAGE" -maxdepth 4 -type f | head -10
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# 5e) 一次性 sudo 拷到 SDK
|
||||
echo " sudo cp stage -> SDK homebrew..."
|
||||
sudo mkdir -p "$PS5_PAYLOAD_SDK/target/user/homebrew"
|
||||
sudo cp -r "$STAGE/user/homebrew/." "$PS5_PAYLOAD_SDK/target/user/homebrew/"
|
||||
echo " 验证 prospero-pkg-config:"
|
||||
"$PS5_PAYLOAD_SDK/bin/prospero-pkg-config" --modversion libmicrohttpd
|
||||
"$PS5_PAYLOAD_SDK/bin/prospero-pkg-config" --libs libmicrohttpd
|
||||
fi
|
||||
|
||||
# 6. 编译 (不 make clean, 复用 zlib/minizip-ng OBJ 缓存)
|
||||
echo "[6/7] make all (增量编译, 复用第三方 obj)..."
|
||||
cd "$PROJ"
|
||||
export PS5_PAYLOAD_SDK="/opt/ps5-payload-sdk"
|
||||
# 仅当二进制缺失或源码变更才全量重编
|
||||
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
|
||||
[ -e "$src" ] || continue
|
||||
if [ "$src" -nt web-file-mgr.elf ]; then
|
||||
NEEDS_REBUILD="$src"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "$NEEDS_REBUILD" ]; then
|
||||
echo " 无源码变更 -- 跳过 make"
|
||||
else
|
||||
echo " 源码变更: $NEEDS_REBUILD"
|
||||
make all 2>&1 | tail -40
|
||||
fi
|
||||
else
|
||||
make all 2>&1 | tail -40
|
||||
fi
|
||||
|
||||
# 7. 验证 + 同步回 Windows
|
||||
echo "[7/7] 验证产物 + 同步..."
|
||||
ELF="$PROJ/web-file-mgr.elf"
|
||||
if [ ! -f "$ELF" ]; then
|
||||
echo "FAIL: ELF 未生成 -- 日志: $LOG"
|
||||
# 把日志同步到 Windows .build/ 方便排查
|
||||
cp -f "$LOG" "$SRC_WIN/.build/build-elf.log" 2>/dev/null && \
|
||||
echo " 日志已同步: $SRC_WIN/.build/build-elf.log"
|
||||
exit 3
|
||||
fi
|
||||
SIZE=$(stat -c%s "$ELF")
|
||||
HASH=$(sha256sum "$ELF" | cut -d' ' -f1)
|
||||
echo "OK: $ELF"
|
||||
echo " size: $SIZE bytes (~$((SIZE/1024)) KiB)"
|
||||
echo " sha256: $HASH"
|
||||
echo " header bytes (期望 ELF64 magic 7f454c46 + class 2 + little-endian 1 + e_machine 0x003e=x86-64):"
|
||||
head -c 20 "$ELF" | xxd | head -2
|
||||
echo ""
|
||||
# 用 od 直接读 offset 18 起的 1 个 16-bit 小端 word = e_machine
|
||||
EM_RAW=$(od -An -tx2 -j 18 -N 2 "$ELF" | tr -d ' \n') # e.g. "3e00"
|
||||
EM_NUM=$((16#${EM_RAW})) # decimal
|
||||
echo "e_machine = 0x$EM_RAW ($EM_NUM)"
|
||||
case "$EM_NUM" in
|
||||
62) echo " -> x86-64 (PS5 真机 target: x86_64-sie-ps5)" ;;
|
||||
183) echo " -> aarch64 (注意: PS5 实际是 x86-64, 此结果可疑)" ;;
|
||||
*) echo " -> 未知架构 (期望 62=0x3e, 当前 e_machine=$EM_NUM)" ;;
|
||||
esac
|
||||
|
||||
# 同步 ELF 回项目根
|
||||
mkdir -p "$SRC_WIN" 2>/dev/null
|
||||
cp -f "$ELF" "$SRC_WIN/web-file-mgr.elf" 2>&1 && \
|
||||
echo "" && echo "[bonus] 已同步回: $SRC_WIN/web-file-mgr.elf" && \
|
||||
ls -lh "$SRC_WIN/web-file-mgr.elf"
|
||||
@@ -1,51 +0,0 @@
|
||||
"""Verify our large-file mode identifiers made it into the ELF.
|
||||
gen-asset-module.py gzips JS assets, so plain `strings` won't find them.
|
||||
This script finds all gzip streams in the ELF, decompresses them, and greps
|
||||
for the new identifiers."""
|
||||
import re, sys, zlib
|
||||
|
||||
f = sys.argv[1]
|
||||
data = open(f, "rb").read()
|
||||
|
||||
# Gzip streams start with 0x1f 0x8b. Walk through the file looking for them.
|
||||
keys = [
|
||||
b"extractLargeAsk",
|
||||
b"extractLargeActive",
|
||||
b"promptLargeMode",
|
||||
b"shouldPromptLargeMode",
|
||||
b"LARGE_FILE_THRESHOLD_BYTES",
|
||||
b"/api/extract",
|
||||
b"large",
|
||||
]
|
||||
seen = {}
|
||||
i = 0
|
||||
while i < len(data) - 10:
|
||||
if data[i] == 0x1f and data[i + 1] == 0x8b:
|
||||
# Try to decompress starting here, capped at 2 MiB.
|
||||
end_cap = min(len(data), i + 2 * 1024 * 1024)
|
||||
try:
|
||||
dec = zlib.decompressobj(zlib.MAX_WBITS | 16)
|
||||
chunk = dec.decompress(data[i:end_cap], 2 * 1024 * 1024)
|
||||
if not dec.eof:
|
||||
i += 1
|
||||
continue
|
||||
except Exception:
|
||||
i += 1
|
||||
continue
|
||||
# Check for any of our keys in the decompressed stream.
|
||||
for k in keys:
|
||||
if k in chunk and k not in seen:
|
||||
idx = chunk.find(k)
|
||||
ctx = chunk[max(0, idx - 24):idx + len(k) + 48]
|
||||
seen[k] = ctx.decode("utf-8", errors="replace")
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
|
||||
print(f"decompressed streams scanned, keys found: {len(seen)} / {len(keys)}")
|
||||
for k, v in seen.items():
|
||||
print(f" ✓ {k.decode()}")
|
||||
print(f" context: {v[:160]}")
|
||||
missing = [k for k in keys if k not in seen]
|
||||
if missing:
|
||||
print(f" ✗ not found: {[m.decode() for m in missing]}")
|
||||
@@ -1,29 +0,0 @@
|
||||
import re, sys
|
||||
f = sys.argv[1]
|
||||
data = open(f, "rb").read()
|
||||
runs = re.findall(rb"[\x20-\x7e]{6,}", data)
|
||||
hits = set()
|
||||
keys = [
|
||||
b"zipx_limits_profile",
|
||||
b"k_large_limits",
|
||||
b"ZIPX_LIMITS_LARGE",
|
||||
b"ZIPX_LIMITS_DEFAULT",
|
||||
b"large-file profile",
|
||||
b"large-file",
|
||||
b"LargeFile",
|
||||
b"large_file",
|
||||
b"extractLargeAsk",
|
||||
b"extractLargeActive",
|
||||
b"large",
|
||||
]
|
||||
for r in runs:
|
||||
for k in keys:
|
||||
if k in r and k.decode() not in hits:
|
||||
# Trim the context a bit
|
||||
i = r.find(k)
|
||||
ctx = r[max(0, i - 8):i + len(k) + 24].strip()
|
||||
if len(ctx) < 80:
|
||||
hits.add(ctx.decode(errors="replace"))
|
||||
for h in sorted(hits):
|
||||
print(repr(h))
|
||||
print("total:", len(hits))
|
||||
File diff suppressed because it is too large.
Load diff
+3
-22
@@ -1,25 +1,6 @@
|
||||
# Build artifacts
|
||||
gen/
|
||||
*.elf
|
||||
web-file-mgr-linux
|
||||
*.o
|
||||
*.d
|
||||
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
|
||||
|
||||
# Editor / OS noise
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
tools/
|
||||
-230
@@ -1,230 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
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
|
||||
> 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.
|
||||
|
||||
## [v1.8] — 2026-09-05
|
||||
|
||||
**RAR extraction: single-volume RAR4 / RAR5 (unencrypted).**
|
||||
|
||||
> ⚠️ Scope clarification — v1.8 ships **single-volume unencrypted RAR** only.
|
||||
> The original RAR wishlist (multi-volume `.partNN.rar`, encrypted RAR with
|
||||
> password UI) is **deferred to v1.9**; see `docs/UPGRADE-v1.8-rar-support.md`
|
||||
> and `third_party/unrar/VENDORED.md` for the rationale and the engine
|
||||
> upgrade path. ZIP behaviour and the large-file profile are unchanged.
|
||||
|
||||
### Added
|
||||
|
||||
- **`src/rar_extract.{c,h}`** — a new extraction engine that mirrors
|
||||
`zip_extract`'s protocol exactly. Internally it wraps the vendored
|
||||
`dmc_unrar` 1.7.0 (`third_party/unrar/dmc_unrar.c`). The public entry point
|
||||
is `rar_extract(rar_path, dst_dir, conflict, limits, cancel, progress,
|
||||
userdata, result)` — same signatures, same `zipx_status_t` codes, same
|
||||
`zipx_result_t`, same `zipx_limits_t` profile lookup. ~1276 LOC
|
||||
(`src/rar_extract.c`).
|
||||
- **`third_party/unrar/`**:
|
||||
- `dmc_unrar.c` — vendored verbatim from upstream (11 598 LOC, ~365 KiB).
|
||||
GPL-2.0-or-later, attributed in `THIRD_PARTY_NOTICES`.
|
||||
- `dmc_unrar_api.h` — **project-authored facade header**. Re-declares only
|
||||
the `dmc_unrar_*` symbols `rar_extract.c` actually uses, so the engine
|
||||
can `#include "dmc_unrar_api.h"` instead of `#include "dmc_unrar.c"`.
|
||||
This keeps the vendored `.c` compiling as its own translation unit and
|
||||
avoids polluting dmc_unrar's struct / function names with any
|
||||
build-system macros (see `tests/posix_compat.h`'s `wfm_open` /
|
||||
`wfm_close` rename pattern — that conflict is what motivated the
|
||||
facade). The facade carries the project's license; the library body
|
||||
remains unmodified.
|
||||
- `COPYING`, `README.md` — upstream GPL notice + readme.
|
||||
- `VENDORED.md` — explains why we chose `dmc_unrar` over rarlab UnRAR /
|
||||
`opello/unrar`, what is and is not supported, and gives a step-by-step
|
||||
upgrade plan for moving to a fuller C++ UnRAR in v1.9.
|
||||
- **`src/extract.c` dispatch layer** — `extract_dispatch()` picks the
|
||||
engine by extension (`.zip` → `zipx_extract`, `.rar` → `rar_extract`,
|
||||
anything else → `ZIPX_ERR_UNSUPPORTED`). The case-insensitive suffix
|
||||
matcher trims trailing path separators. `extract_worker` now calls the
|
||||
dispatch instead of going straight to the ZIP engine.
|
||||
- **Frontend + i18n wiring**:
|
||||
- `assets/main.js` recognises `.rar` (single-volume) and `.part0*1.rar`
|
||||
(multi-volume master) as extractable, greys out the extract button on
|
||||
`.part02+.rar` sub-volumes with a tooltip "select the main volume
|
||||
instead". This UX is only useful because v1.8 still rejects
|
||||
multi-volume RAR with a friendly error — the visual feedback stops the
|
||||
user from selecting a sub-volume and getting confused.
|
||||
- `assets/lang-{en,zh}.js` `err_extract_unsupported` updated to:
|
||||
"…(only unencrypted plain ZIP and single-volume RAR are supported)…".
|
||||
- **Host test suite** (`tests/test_rar_extract.c`, **14 checks**) —
|
||||
negative paths only (format dispatch, error translation, limits
|
||||
handoff). The suite is wired into `tests/run-tests.sh` alongside the
|
||||
existing ZIP suite; fixture generation falls back to a placeholder
|
||||
blob when no `rar` / `7z` writer is present, so the negative tests
|
||||
fire on any host. Total host checks: **69 ZIP + 14 RAR = 83**.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`Makefile`**:
|
||||
- `VERSION_TAG := v1.8` (was `v1.7`).
|
||||
- `THIRD_PARTY_SRCS` adds `third_party/unrar/dmc_unrar.c`.
|
||||
- `THIRD_PARTY_CFLAGS` adds `-Ithird_party/unrar` and
|
||||
`-DDMC_UNRAR_DISABLE_BE32TOH_BE64TOH=1` (dmc_unrar's own byte-swap
|
||||
helpers are avoided so we don't need an extra `byteswap.h` shim on
|
||||
the SDK).
|
||||
- **`src/extract.c` / `src/extract.h`** — no public-API break. The HTTP
|
||||
surface (`POST /api/extract`) accepts the same fields as v1.7 plus
|
||||
the existing `large=1`; there is **no** `password=` field because
|
||||
v1.8 cannot decrypt. (The wire format is forward-compatible — a v1.9
|
||||
`password=` field will be additive.)
|
||||
- **`THIRD_PARTY_NOTICES`** — adds a section `3. dmc_unrar` crediting
|
||||
Sven Hesse (DrMcCoy), summarising the GPL-2.0-or-later obligations on
|
||||
the resulting binary, and noting that `dmc_unrar_api.h` is
|
||||
project-authored and licensed with the project.
|
||||
|
||||
### Limitations (v1.8 scope)
|
||||
|
||||
- **Multi-volume RAR** (`.part02+.rar`, `.part1+.rar`, numbered
|
||||
continuations) is rejected with `ZIPX_ERR_UNSUPPORTED` and the error
|
||||
message "extract on a PC first". Upstream dmc_unrar does not chain
|
||||
companion volumes by design. When opello/unrar replaces dmc_unrar
|
||||
in v1.9 this becomes a one-line error-code drop.
|
||||
- **Encrypted RAR** (any encrypted header / file flag) is rejected with
|
||||
`ZIPX_ERR_UNSUPPORTED`. Same root cause — dmc_unrar omits decryption
|
||||
to avoid patent complications. There is **no** password prompt in
|
||||
the UI; there is **no** `password=` field in `/api/extract`.
|
||||
- **Symbolic links, FIFOs, sockets, devices** inside a RAR archive are
|
||||
rejected with `ZIPX_ERR_SPECIAL` (mirrors ZIP behaviour).
|
||||
- **RAR 1.4** (very old) is not supported by dmc_unrar and is rejected
|
||||
upstream with `DMC_UNRAR_ARCHIVE_VERSION_UNSUPPORTED`; the wrapper
|
||||
maps that to `ZIPX_ERR_UNSUPPORTED`. RAR 1.5 through RAR 5.0 are
|
||||
supported.
|
||||
|
||||
### Verification
|
||||
|
||||
```sh
|
||||
make # builds web-file-mgr.elf (in WSL)
|
||||
ls -la web-file-mgr.elf # record size
|
||||
sha256sum web-file-mgr.elf # record digest (paste into the v1.8 banner above)
|
||||
file web-file-mgr.elf # ELF 64-bit LSB pie, x86-64
|
||||
od -An -tx1 -N20 web-file-mgr.elf # 7f45 4c46 0201 + e_machine 003e
|
||||
|
||||
(cd tests && bash run-tests.sh) # 69 + 14 = 83 checks, 0 failures
|
||||
|
||||
python3 .build/check-elf-gzip.py ./web-file-mgr.elf # 7/7 v1.7 keys + 1 v1.8 key (err_extract_unsupported)
|
||||
```
|
||||
|
||||
### Technical notes
|
||||
|
||||
A long-form technical write-up of this upgrade lives in
|
||||
[`docs/UPGRADE-v1.8-rar-support.md`](./docs/UPGRADE-v1.8-rar-support.md).
|
||||
The vendoring decision tree (and the v1.9 plan) is in
|
||||
[`third_party/unrar/VENDORED.md`](./third_party/unrar/VENDORED.md).
|
||||
|
||||
### Credits
|
||||
|
||||
Same as v1.7 — see [README.md → Credits](./README.md#credits). dmc_unrar
|
||||
is credited in [`THIRD_PARTY_NOTICES`](./THIRD_PARTY_NOTICES).
|
||||
|
||||
---
|
||||
|
||||
## [v1.7] — 2026-09-04
|
||||
|
||||
**ZIP extraction: large-file profile, three-phase engine, host test suite.**
|
||||
|
||||
### Added
|
||||
|
||||
- **Large-file profile** (`ZIPX_LIMITS_LARGE`) for ZIP extraction — relaxed
|
||||
caps of **500 000** entries, **2 TiB** total uncompressed, **1 TiB** per
|
||||
entry, **1000 : 1** compression ratio. Enable via the new `large=1`
|
||||
argument on `POST /api/extract`. The UI prompts the user automatically
|
||||
whenever the archive on disk is larger than 60 GiB (`LARGE_FILE_THRESHOLD_BYTES`).
|
||||
- **Standalone three-phase ZIP engine** (`src/zip_extract.{c,h}`) — the model
|
||||
`SCAN → EXTRACT → PUBLISH → CLEANUP`. Each entry is written into a staging
|
||||
directory first, fsynced, then atomically renamed into the destination. Any
|
||||
mid-archive failure rolls back partial changes.
|
||||
- **Profile lookup helper** `zipx_limits_profile(int)` and the new macros
|
||||
`ZIPX_LIMITS_DEFAULT` (0) and `ZIPX_LIMITS_LARGE` (1). The public
|
||||
`zipx_extract()` signature is unchanged.
|
||||
- **Opt-in API field** `large` on `POST /api/extract`, backed by a new
|
||||
`extract_large` flag in `file_task_t` (`src/filemgr_internal.h`,
|
||||
`src/extract.c`).
|
||||
- **Frontend wiring** (`assets/main.js`) — `LARGE_FILE_THRESHOLD_BYTES`,
|
||||
`shouldPromptLargeMode()`, `promptLargeMode()`, consumed by
|
||||
`actionExtract()` and `uploadAndExtractFile()`.
|
||||
- **Bilingual UI strings** (`assets/lang-{en,zh}.js`) —
|
||||
`extractLargeAsk` and `extractLargeActive`.
|
||||
- **Host-side C test suite** (`tests/`) — POSIX-runnable, no PS5 SDK
|
||||
required. **69 checks** at release: traversal, ZIP64, encryption rejection,
|
||||
conflict policies, large-file profile switching.
|
||||
- **Cross-compile helper** (`.build/build-elf.sh`) — staging-mode build that
|
||||
works around the read-only SDK install location.
|
||||
- **Demo page** (`.build/extract-demo.html`) — visualises the three policy
|
||||
combinations (fail / overwrite / merge) against the new profile table.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Engine internals** rewritten around the three-phase model. Public API
|
||||
(`zipx_extract()`, `zipx_default_limits()`) is unchanged — old callers
|
||||
compile and link clean.
|
||||
- **`file_task_t`** gains `extract_large` (`src/filemgr_internal.h`).
|
||||
Internal-only; downstream consumers reading the task struct need to
|
||||
recognise the new field.
|
||||
- **`gen-asset-module.py`** continues to gzip JS into the binary; the
|
||||
helpers in `.build/check-elf-gzip.py` are the supported way to verify that
|
||||
a fresh build picked up asset changes (regular `strings` won't see them).
|
||||
|
||||
### Security
|
||||
|
||||
- Default ZIP caps are unchanged: **512 GiB** total / **64 GiB** per entry /
|
||||
**200 : 1** ratio.
|
||||
- The large profile is **never** activated server-side on its own — the
|
||||
client must explicitly send `large=1` (either via the UI prompt or directly
|
||||
via the API).
|
||||
- Encryption, path traversal, symbolic links, FIFOs and unresolved conflicts
|
||||
are still rejected before any output file is opened.
|
||||
- Strict value matching: only the literal `"1"` enables the large profile;
|
||||
`true`, `yes`, `on` are all treated as `0`. Matches the existing
|
||||
`remove_source=` semantics.
|
||||
|
||||
### Known limitations
|
||||
|
||||
- Multi-volume / split ZIP archives (`.zip` + `.z01`, `.z02`, …) are not
|
||||
stitched by the engine. minizip-ng has the API; wiring it is post-v1.7.
|
||||
- The 60 GiB frontend threshold for the large-profile prompt is hardcoded
|
||||
(`LARGE_FILE_THRESHOLD_BYTES`, `assets/main.js` line 813).
|
||||
- The large profile does **not** run `statvfs()` against `dst_dir` before
|
||||
extraction; free-space preflight is on the post-v1.7 roadmap.
|
||||
- Bomb-shaped archives with compression ratio **> 1000 : 1** are rejected
|
||||
under both profiles (`bomb.zip` with 4 MiB of `'A'` has ratio ≈ 1026 and
|
||||
hits this wall under the large profile).
|
||||
|
||||
### Verification
|
||||
|
||||
```sh
|
||||
make # builds web-file-mgr.elf
|
||||
ls -la web-file-mgr.elf
|
||||
sha256sum web-file-mgr.elf # 648e4a00…
|
||||
file web-file-mgr.elf # ELF 64-bit LSB pie, x86-64
|
||||
od -An -tx1 -N20 web-file-mgr.elf # 7f45 4c46 0201 + e_machine 003e
|
||||
|
||||
(cd tests && bash run-tests.sh) # 69 checks, 0 failures
|
||||
|
||||
python3 .build/check-elf-gzip.py ./web-file-mgr.elf # 7/7 large-mode keys in ELF
|
||||
```
|
||||
|
||||
### Technical notes
|
||||
|
||||
A long-form technical write-up of this upgrade (architecture delta, three-phase
|
||||
model, behaviour contract, trade-offs, future work) lives in
|
||||
[`docs/UPGRADE-v1.7-zip-large-file-profile.md`](./docs/UPGRADE-v1.7-zip-large-file-profile.md).
|
||||
|
||||
### Credits
|
||||
|
||||
The same as v1.6 — see [README.md → Credits](./README.md#credits).
|
||||
@@ -13,7 +13,7 @@ ifeq ($(MAKECMDGOALS),)
|
||||
endif
|
||||
endif
|
||||
|
||||
VERSION_TAG := v1.8
|
||||
VERSION_TAG := v0.6
|
||||
TITLE_ID := FMGR88888
|
||||
PYTHON ?= python3
|
||||
STRIP ?= $(PS5_PAYLOAD_SDK)/bin/prospero-strip
|
||||
@@ -23,41 +23,31 @@ HOST_STRIP ?= strip
|
||||
HOST_PKG_CONFIG ?= pkg-config
|
||||
|
||||
BIN := web-file-mgr.elf
|
||||
LEGACY_BIN := web-file-mgr-legacy.elf
|
||||
LINUX_BIN := web-file-mgr-linux
|
||||
COMMON_SRCS := src/main.c src/websrv.c src/filemgr.c src/file_response.c src/task.c src/upload.c src/download.c src/text.c src/list.c src/space.c src/fs_util.c src/json_util.c src/path_util.c src/asset.c src/mime.c src/notify.c src/pkg_installer.c src/pkg_info.c src/extract.c src/zip_extract.c src/rar_extract.c
|
||||
COMMON_SRCS := src/main.c src/websrv.c src/filemgr.c src/asset.c src/mime.c src/notify.c
|
||||
PS5_SRCS := $(COMMON_SRCS) src/app_installer.c
|
||||
LINUX_SRCS := $(COMMON_SRCS)
|
||||
BASE_ASSETS := $(filter-out %.dds,$(wildcard assets/*))
|
||||
ifneq ($(filter linux,$(MAKECMDGOALS)),)
|
||||
ASSETS := $(BASE_ASSETS)
|
||||
else
|
||||
ASSETS := $(filter-out assets/icon0.png,$(BASE_ASSETS))
|
||||
endif
|
||||
ASSETS := $(wildcard assets/*)
|
||||
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))
|
||||
|
||||
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)\"
|
||||
CFLAGS := -Oz -fno-asynchronous-unwind-tables -fno-unwind-tables -Wall -Werror -ffunction-sections -fdata-sections -Isrc -DVERSION_TAG=\"$(VERSION_TAG)\" -DTITLE_ID=\"$(TITLE_ID)\"
|
||||
CFLAGS += `$(PKG_CONFIG) libmicrohttpd --cflags`
|
||||
LDFLAGS := -Wl,--gc-sections
|
||||
LEGACY_CFLAGS := $(filter-out -ffunction-sections -fdata-sections,$(CFLAGS))
|
||||
LEGACY_LDFLAGS :=
|
||||
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)\"
|
||||
LDADD += -lSceIpmi -lSceAppInstUtil
|
||||
LINUX_CFLAGS := -O2 -Wall -Werror -Isrc -DVERSION_TAG=\"$(VERSION_TAG)\" -DTITLE_ID=\"$(TITLE_ID)\"
|
||||
LINUX_CFLAGS += `$(HOST_PKG_CONFIG) libmicrohttpd --cflags`
|
||||
LINUX_LDADD := `$(HOST_PKG_CONFIG) libmicrohttpd --libs` -pthread
|
||||
|
||||
.PHONY: all linux deps linux-deps clean
|
||||
.PHONY: all legacy linux deps linux-deps clean
|
||||
|
||||
all: deps $(BIN)
|
||||
|
||||
legacy: deps $(LEGACY_BIN)
|
||||
|
||||
linux: linux-deps $(LINUX_BIN)
|
||||
|
||||
deps:
|
||||
@@ -71,23 +61,19 @@ gen:
|
||||
mkdir gen
|
||||
|
||||
clean:
|
||||
rm -rf $(BIN) $(LINUX_BIN) gen ps5-obj linux-obj
|
||||
rm -rf $(BIN) $(LEGACY_BIN) $(LINUX_BIN) gen
|
||||
|
||||
gen/%.c: assets/% gen-asset-module.py | gen
|
||||
$(PYTHON) gen-asset-module.py --path $* $< > $@
|
||||
|
||||
ps5-obj/%.o: %.c
|
||||
@mkdir -p $(dir $@)
|
||||
$(CC) $(THIRD_PARTY_CFLAGS) -c -o $@ $<
|
||||
|
||||
linux-obj/%.o: %.c
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CC) $(THIRD_PARTY_CFLAGS) -c -o $@ $<
|
||||
|
||||
$(BIN): $(PS5_SRCS) $(GEN_SRCS) $(PS5_TP_OBJS)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(filter %.c,$^) $(PS5_TP_OBJS) $(LDADD)
|
||||
$(BIN): $(PS5_SRCS) $(GEN_SRCS)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(LDADD)
|
||||
$(STRIP) $@
|
||||
|
||||
$(LINUX_BIN): $(LINUX_SRCS) $(GEN_SRCS) $(LINUX_TP_OBJS)
|
||||
$(HOST_CC) $(LINUX_CFLAGS) -o $@ $(filter %.c,$^) $(LINUX_TP_OBJS) $(LINUX_LDADD)
|
||||
$(LEGACY_BIN): $(PS5_SRCS) $(GEN_SRCS)
|
||||
$(CC) $(LEGACY_CFLAGS) $(LEGACY_LDFLAGS) -o $@ $^ $(LDADD)
|
||||
$(STRIP) $@
|
||||
|
||||
$(LINUX_BIN): $(LINUX_SRCS) $(GEN_SRCS)
|
||||
$(HOST_CC) $(LINUX_CFLAGS) -o $@ $^ $(LINUX_LDADD)
|
||||
$(HOST_STRIP) $@
|
||||
@@ -1,51 +1,11 @@
|
||||
# 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.
|
||||
A file manager for PS5 with a web UI. It is primarily intended for quickly and
|
||||
safely copying game dump folders from USB storage to internal storage.
|
||||
|
||||
**Version:** v1.8 · **Title ID:** `FMGR88888` · **License:** GPLv3+ · **Target:** `x86_64-sie-ps5`
|
||||
## Brief
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
A payload ELF that runs an HTTP file manager inside a jailbroken PS5. Open `http://<PS5_IP>:8888/` from any browser on the LAN — including the PS5 browser itself — to manage files on attached USB storage and the user partition. Designed for safely copying game-dump folders from USB to internal storage, but it also handles general file management, in-place text editing, PKG preview/install, image preview, and ZIP extraction with built-in zip-bomb protection.
|
||||
|
||||
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.8
|
||||
|
||||
- **Single-volume RAR extraction** via the vendored FLOSS library
|
||||
[`dmc_unrar`](https://github.com/DrMcCoy/dmc_unrar) (GPL-2.0-or-later).
|
||||
RAR 1.5, 2.x, 3.x, 4.x and 5.x archives are supported. `.rar` files
|
||||
appear in the file list with the **Extract** button enabled; the button
|
||||
is greyed out on `.part02+.rar` sub-volumes with the tooltip "select
|
||||
the main volume instead" — v1.8 cannot stitch multi-volume RARs (see
|
||||
the [RAR extraction](#rar-extraction) section below).
|
||||
- **Shared extraction protocol** between the new `src/rar_extract.c`
|
||||
engine and the existing `src/zip_extract.c` engine: same `zipx_status_t`
|
||||
codes, same `zipx_limits_t` profile (default / `large=1`), same
|
||||
three-phase model (`scan → extract → publish → cleanup`), same staging
|
||||
directory layout, same conflict policy, same error mapping into the
|
||||
task UI. The dispatcher in `src/extract.c` is one tiny
|
||||
`ends_with_ci(…)` switch.
|
||||
- **14 new host-side C tests** (`tests/test_rar_extract.c`) wired into
|
||||
the existing `tests/run-tests.sh`. Coverage: format dispatch, error
|
||||
translation across every `DMC_UNRAR_*` code that affects RAR users,
|
||||
limit-profile handoff. Total host checks: **69 ZIP + 14 RAR = 83**.
|
||||
- **Documentation**: [`CHANGELOG.md`](./CHANGELOG.md),
|
||||
[`docs/UPGRADE-v1.8-rar-support.md`](./docs/UPGRADE-v1.8-rar-support.md)
|
||||
and the vendoring decision tree at
|
||||
[`third_party/unrar/VENDORED.md`](./third_party/unrar/VENDORED.md).
|
||||
- See the [dedicated section](#rar-extraction) below for scope and the
|
||||
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.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.
|
||||
- **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.
|
||||
PS5 web file manager payload. It runs an HTTP UI starting at port `8888`, installs a home screen launcher in Media catagory on startup when needed, and provides file operations from the PS5 browser. If `8888` is already in use, the payload tries the next port until one is available; the startup notification shows the actual listen port.
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -60,358 +20,98 @@ The same source tree builds a Linux binary for development and a PS5 payload ELF
|
||||
|
||||
## Features
|
||||
|
||||
- **Browse** — list files and folders; sort by name, type, size, mtime or permissions. Last sort mode persists in `localStorage`.
|
||||
- **Permissions** — toggle read/write/execute with checkboxes, or paste a validated four-digit octal mode.
|
||||
- **Operations** — copy, move, delete (recursive, no recycle bin), rename, create files and folders.
|
||||
- **Editor** — in-place UTF-8 text editor for files ≤ 1 MiB across a curated extension list: `.txt .json .xml .ini .cfg .conf .md .log .lua .js .css .html .htm .c .h .cpp .hpp .sh .csv .yaml .yml .shn`.
|
||||
- **Multi-select** — copy, move, delete or tar-download many items in one go.
|
||||
- **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.
|
||||
- **PKG** — install and preview `.pkg` files.
|
||||
- **Images** — preview `.png .jpg .jpeg .gif .bmp .webp`.
|
||||
- **Localization** — English + Simplified Chinese, auto-selected from `navigator.languages`.
|
||||
- **Mobile-friendly** — responsive layout with wrapped toolbars and horizontally scrollable file lists.
|
||||
|
||||
## Quickstart
|
||||
|
||||
1. **Build** the ELF:
|
||||
|
||||
```sh
|
||||
export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk # see "Build" for SDK setup
|
||||
make
|
||||
```
|
||||
2. **Send** the payload to the PS5 (default ELF-loader port `9021`):
|
||||
|
||||
```sh
|
||||
nc -q0 "$PS5_HOST" 9021 < web-file-mgr.elf
|
||||
```
|
||||
3. **Read** the on-screen PS5 notification — it prints the actual listen port (default `8888`).
|
||||
4. **Open** `http://<PS5_IP>:<port>/` in any browser on the same LAN — the PS5 browser works too.
|
||||
5. On first run, the payload also writes a **Media**-category home-screen launcher; existing launcher files are not overwritten.
|
||||
- List files and folders.
|
||||
- Copy, move, delete, rename, and create folders.
|
||||
- Edit UTF-8 text files up to 1 MiB using the built-in plain text editor.
|
||||
- Multi-select operations.
|
||||
- Copy/move by choosing sources first, then pasting or moving them into the current folder.
|
||||
- Conflict prompts for overwriting files and merging folders.
|
||||
- Full-screen task overlay with progress, speed, ETA, cancel support, and task recovery after reopening the browser while the payload process is still running.
|
||||
- Copied/moved files and folders are set to `0777` where the filesystem supports Unix permissions. FAT/exFAT-style filesystems may ignore chmod.
|
||||
- Chinese and English UI. The browser language is read from `navigator.languages` / `navigator.language`; Chinese uses `zh`, everything else uses English.
|
||||
- Startup notification showing the app name, version, and listen port.
|
||||
- Create/edit text files ending with `.txt`, `.json`, `.xml`, `.ini`, `.cfg`, `.conf`, `.md`, `.log`, `.lua`, `.js`, `.css`, `.html`, `.htm`, `.c`, `.h`, `.cpp`, `.hpp`, `.sh`, `.csv`, `.yaml`, `.yml`.
|
||||
- Preview image files ending with `.png,`, `.jpg`, `.jpeg`, `.gif`, `.bmp`, `.webp`.
|
||||
|
||||
## Build
|
||||
|
||||
Requires [ps5-payload-dev/sdk](https://github.com/ps5-payload-dev/sdk#quick-start):
|
||||
It depends on PS5 payload SDK first: [ps5-payload-dev/sdk](https://github.com/ps5-payload-dev/sdk#quick-start)
|
||||
|
||||
```sh
|
||||
export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk
|
||||
```
|
||||
|
||||
This project links against `libmicrohttpd`. `make` checks for it before building and runs the installer automatically when missing:
|
||||
This project links against `libmicrohttpd`. `make` checks for it before building and runs the installer script automatically if it is missing:
|
||||
|
||||
```sh
|
||||
make
|
||||
```
|
||||
|
||||
If the build host has no network access, drop the libmicrohttpd tarball in advance and run the installer manually:
|
||||
If the build host has no network access, download the libmicrohttpd source tarball yourself and run the dependency installer once:
|
||||
|
||||
```sh
|
||||
LIBMICROHTTPD_TARBALL=/path/to/libmicrohttpd-1.0.1.tar.gz \
|
||||
./install-libmicrohttpd.sh
|
||||
```
|
||||
|
||||
Then build again:
|
||||
|
||||
```sh
|
||||
make
|
||||
```
|
||||
|
||||
Output:
|
||||
The output is:
|
||||
|
||||
```text
|
||||
web-file-mgr.elf (~418 KiB, x86_64-sie-ps5)
|
||||
web-file-mgr.elf
|
||||
```
|
||||
|
||||
For pure UI/JS work without the PS5 toolchain:
|
||||
|
||||
```sh
|
||||
make linux
|
||||
./web-file-mgr-linux
|
||||
```
|
||||
|
||||
The Linux build does **not** include the PS5 home-screen launcher installer.
|
||||
|
||||
## Usage
|
||||
|
||||
Start an ELF loader on the PS5 (port `9021` is common). Send the payload:
|
||||
Start an ELF loader on the PS5. The common listener port is `9021`.
|
||||
|
||||
Send the built payload with netcat or NetCat GUI:
|
||||
|
||||
```sh
|
||||
export PS5_HOST=ps5_ip_address
|
||||
nc -q0 "$PS5_HOST" 9021 < web-file-mgr.elf
|
||||
```
|
||||
|
||||
After the payload starts, the PS5 notification shows the app name, version and actual listen port. Open the URL it prints, for example:
|
||||
After the payload starts, the PS5 notification shows the app name, version, and actual listen port. Open the shown URL in the PS5 browser, for example:
|
||||
|
||||
```text
|
||||
http://${PS5_IP_ADDRESS}:8888/
|
||||
```
|
||||
|
||||
If the payload had to fall back to a different port (e.g. `8889`), use whatever port the notification shows — the URL is not hard-coded.
|
||||
|
||||
On first startup, the payload installs a `PS5 Web File Manager` shortcut in the Media category when needed. Existing launcher files are preserved; only missing ones are written.
|
||||
|
||||
## ZIP extraction
|
||||
|
||||
Plain ZIPs only — stored / deflated / ZIP64, **never encrypted**. The engine is a standalone three-phase module (`scan → extract → publish → cleanup`) at `src/zip_extract.{c,h}`, with a separate host-side C test suite. Each entry is first written into a staging directory (`*.wfm-part-*`), fsynced, then atomically renamed into the destination. Any failure mid-archive rolls back partial changes; cancel and fatal errors always clean up staging.
|
||||
|
||||
### Limits
|
||||
|
||||
| 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_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.
|
||||
|
||||
### Security checks
|
||||
|
||||
The engine refuses to extract:
|
||||
|
||||
- Encrypted entries (any encryption flag set).
|
||||
- Path traversal (`..` segments, absolute POSIX paths, Windows drive letters).
|
||||
- Symbolic links, devices, FIFOs, sockets (`ZIPX_ERR_SPECIAL`).
|
||||
- Duplicate entries or directory/file name clashes inside the same archive.
|
||||
- Archives whose expanded size, entry count, depth, name length or compression ratio breach the active profile.
|
||||
|
||||
### Conflict policy
|
||||
|
||||
Passed as `conflict=` on `/api/extract`:
|
||||
|
||||
- `fail` (default) — refuse to overwrite any existing target.
|
||||
- `overwrite` — replace existing files; merge into existing folders.
|
||||
- `merge` — keep existing files, add new ones.
|
||||
|
||||
### Tuning the threshold
|
||||
|
||||
The 60 GiB frontend threshold lives in `assets/main.js`:
|
||||
|
||||
```js
|
||||
const LARGE_FILE_THRESHOLD_BYTES = 60 * 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.
|
||||
|
||||
### 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) | ✅ | |
|
||||
| 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`. |
|
||||
| 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 |
|
||||
|
||||
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.
|
||||
|
||||
### Limits
|
||||
|
||||
The RAR engine re-uses the ZIP limits table verbatim — there is no RAR
|
||||
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_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
|
||||
way for the prompt, and the server only ever activates the large caps
|
||||
when the request carries `large=1` (opt-in).
|
||||
|
||||
### Security checks
|
||||
|
||||
The RAR engine applies the same checks as the ZIP engine — re-uses
|
||||
`zipx_status_t` codes, so the task UI's `err_extract_unsafe_name`,
|
||||
`err_extract_too_deep`, `err_extract_ratio`, etc. all fire identically:
|
||||
|
||||
- Path traversal (`..` segments, absolute POSIX paths, Windows drive
|
||||
letters, `\` treated as a path separator after a `Rar!\x1a\x07…`
|
||||
header, etc.).
|
||||
- Symbolic links, FIFOs, sockets, devices.
|
||||
- Duplicate entries or directory/file name clashes inside the archive.
|
||||
- Archive size, entry count, depth, name length or compression ratio
|
||||
breaches of the active profile.
|
||||
|
||||
### 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.
|
||||
|
||||
### v1.9 plan
|
||||
|
||||
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.
|
||||
|
||||
## 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)
|
||||
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
|
||||
```
|
||||
|
||||
The `e_machine = 0x003e` confirms the PS5 target triple `x86_64-sie-ps5`. The `e_type = 3` (`ET_DYN`) confirms the position-independent payload expected by ELF loaders.
|
||||
|
||||
## Tests
|
||||
|
||||
A POSIX/host-side C test suite covers the ZIP engine and runs on any Linux / macOS / MSYS shell without the PS5 SDK:
|
||||
|
||||
```sh
|
||||
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:
|
||||
|
||||
- ZIP entry parsing (stored + deflated + ZIP64)
|
||||
- Path traversal, absolute paths, backslash, Windows drive letters
|
||||
- Symbolic links, FIFOs, encrypted entries, bad CRC, truncated archives, non-ZIP files
|
||||
- Limits: `entries`, `total_bytes`, `file_bytes`, `ratio`, `depth`, `name_len`
|
||||
- Conflict policies: `fail` / `overwrite` / `merge`
|
||||
- Cancellation in every phase
|
||||
- **Large-file profile** — `medium_bomb.zip` (ratio ≈ 238) is rejected under default caps and accepted under large caps; lowered large caps still enforce.
|
||||
- **RAR engine** (`tests/test_rar_extract.c`, 14 checks) — format
|
||||
dispatch (renamed ZIP rejected, junk blob rejected), error translation
|
||||
across every reachable `DMC_UNRAR_*` code, limits handoff (the
|
||||
`large=1` opt-in flows into `rar_extract()` unchanged).
|
||||
|
||||
## Project layout
|
||||
|
||||
```
|
||||
.
|
||||
├── Makefile # PS5 + Linux builds (VERSION_TAG v1.8)
|
||||
├── 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
|
||||
├── src/ # C payload sources
|
||||
│ ├── main.c websrv.c filemgr.c # entry, HTTP frontend, task model
|
||||
│ ├── upload.c download.c # stream handlers
|
||||
│ ├── extract.c # /api/extract dispatcher (ZIP + RAR)
|
||||
│ ├── zip_extract.{c,h} # ZIP engine (v1.7)
|
||||
│ ├── rar_extract.{c,h} # RAR engine (v1.8, dmc_unrar backend)
|
||||
│ └── app_installer.c # PS5 Media launcher installer
|
||||
├── third_party/ # vendored: zlib, minizip-ng, dmc_unrar
|
||||
│ └── unrar/
|
||||
│ ├── dmc_unrar.c # GPL-2.0-or-later, verbatim upstream
|
||||
│ └── dmc_unrar_api.h # project-authored facade header
|
||||
├── tests/ # POSIX/host test suite
|
||||
│ ├── test_zip_extract.c
|
||||
│ ├── test_rar_extract.c # 14 RAR negative-path checks (v1.8)
|
||||
│ ├── make_fixtures.py # regenerate test fixtures
|
||||
│ ├── run-tests.sh # one-shot runner (now runs ZIP + RAR suites)
|
||||
│ ├── compat/ # tiny Win32/MSYS shims
|
||||
│ └── fixtures/ # generated test ZIPs (and a couple of stub .rar blobs)
|
||||
├── docs/
|
||||
│ ├── HANDOVER.md # engineering handover / dev playbook (also §14 v1.8 close-out)
|
||||
│ ├── UPGRADE-v1.7-zip-large-file-profile.md
|
||||
│ ├── UPGRADE-v1.8-rar-support.md
|
||||
│ └── screenshots/ # README screenshot images
|
||||
├── THIRD_PARTY_NOTICES # bundled-library credits (incl. dmc_unrar section)
|
||||
├── LICENSE # GPLv3+
|
||||
└── README.md
|
||||
```
|
||||
On first startup the payload installs a `PS5 Web File Manager` web shortcut in the Media category when needed. If the payload had to use a fallback port such as `8889`, use the port shown in the startup notification.
|
||||
|
||||
## Notes
|
||||
|
||||
- Copy, move, delete, upload and download run as single background tasks. While one task is running, other file operations are rejected.
|
||||
- Copy, move, and delete run as single background tasks. While one task is running, other file operations are rejected.
|
||||
- Delete is recursive and permanent. There is no recycle bin.
|
||||
- Copy/move tasks can be canceled. A partially copied single file is removed, but partially copied folders are left in place to avoid deleting pre-existing files when merging into an existing target folder.
|
||||
- Upload tasks can be canceled. A partially uploaded temporary file is removed when possible.
|
||||
- Downloading a folder or multiple selected items produces a tar stream. The tar archive is generated by the payload and is not written to PS5 storage first.
|
||||
- Copy/move tasks can be canceled. A partially copied single file is removed, but partially copied folders are left in place to avoid deleting existing files when merging into an existing target folder.
|
||||
- The UI can recover the active task display if the browser is closed and reopened while the payload process is still running.
|
||||
- Text editing is limited to the curated extension list above. Non-UTF-8 and oversized files are rejected.
|
||||
- File names are transmitted as UTF-8 through the web API. The payload also preserves legacy byte-oriented names returned by mounted filesystems so mixed USB filename encodings still display and operate correctly.
|
||||
- Text editing is limited to common text-file extensions. Non-UTF-8 and oversized files are rejected.
|
||||
|
||||
## FAQ
|
||||
|
||||
- **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_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
|
||||
that is neither `.zip` nor `.rar`. For RAR specifically the message
|
||||
lists the failure cause and points the user back to a PC extractor.
|
||||
- This is a homebrew app which does not affect system process or memory. It should crash itself instead of kernel panic theoretically. If kernel panic happens, make sure you'r using the latest jailbreak methods which includes latest elfldr. Or switch back to the stable method you have been using.
|
||||
- If you're using P2JB, I suggest not to use this if kernel panic happened. It is suffering to wait another 50 min because of kernel panic. Stability first for P2JB.
|
||||
- The preparing stage is calculating the folder size. It would be pretty slow if there are too many files in this folder. It make sure if there is enough available space for this folder you were trying to copy/move.
|
||||
|
||||
## Credits
|
||||
|
||||
This project was built with reference to these projects:
|
||||
|
||||
- **[ps5-payload-dev/websrv](https://github.com/ps5-payload-dev/websrv):** HTTP server structure, static asset embedding ideas, PS5 browser/websrv behaviour and PKG install function. License: GPLv3+.
|
||||
- **[ps5-payload-dev/ftpsrv](https://github.com/ps5-payload-dev/ftpsrv):** PS5 payload conventions, home-screen launcher/install flow reference, process handling style and startup installation reference. License: GPLv3+.
|
||||
- **[seregonwar/zftpd](https://github.com/seregonwar/zftpd):** PS5 TCP socket buffer tuning and high-throughput transfer behaviour reference. License: MIT.
|
||||
- **[itsPLK/ps5-payload-manager](https://github.com/itsPLK/ps5-payload-manager):** Payload building behaviour. License: GPLv3.
|
||||
- **[libmicrohttpd](https://ftp.gnu.org/gnu/libmicrohttpd/):** Used as the embedded HTTP server library. Licensed by GNU under the LGPL; this payload links it as the SDK-provided static library.
|
||||
- **[ps5-payload-dev/websrv](https://github.com/ps5-payload-dev/websrv):** HTTP server structure, static asset embedding ideas, and PS5 browser/websrv behavior. License: GPLv3+.
|
||||
- **[ps5-payload-dev/ftpsrv](https://github.com/ps5-payload-dev/ftpsrv):** PS5 payload conventions, home screen launcher/install flow reference, process handling style and startup installation reference. License: GPLv3+.
|
||||
- **[itsPLK/ps5-payload-manager](https://github.com/itsPLK/ps5-payload-manager):** Payload building behavior. License: GPLv3.
|
||||
- **[libmicrohttpd](https://ftp.gnu.org/gnu/libmicrohttpd/):** Used as the embedded HTTP server library. It is licensed by GNU under the LGPL; this payload links it as the SDK-provided static library.
|
||||
- **[ps5-payload-dev/sdk](https://github.com/ps5-payload-dev/sdk):** Payload building foundation. License: GPLv3+.
|
||||
- **[etaHEN](https://github.com/etaHEN/etaHEN):** ShellUI URI navigation used to return to the PS5 home screen before exit. License: GPLv3.
|
||||
- **[ezremote](https://github.com/cy33hc/ps5-ezremote-client):** Preview PKG info. License: GPLv2.
|
||||
- **[zlib-ng/minizip-ng](https://github.com/zlib-ng/minizip-ng):** ZIP reader used by the `/api/extract` endpoint. Vendored under `third_party/minizip-ng/`. License: zlib.
|
||||
- **[zlib](https://www.zlib.net/):** Compression backend for minizip-ng. Vendored under `third_party/zlib/`. License: zlib.
|
||||
- **[DrMcCoy/dmc_unrar](https://github.com/DrMcCoy/dmc_unrar):** RAR reader used by the `/api/extract` endpoint. Vendored under `third_party/unrar/` as a single-file drop-in (`dmc_unrar.c`); the project-authored facade `dmc_unrar_api.h` carries the project's own licence. License: GPL-2.0-or-later — see `third_party/unrar/COPYING`.
|
||||
|
||||
## License
|
||||
|
||||
The project is distributed under **GPLv3 or later**, matching the GPLv3+ projects used as implementation references. See [`LICENSE`](./LICENSE).
|
||||
The project is distributed under GPLv3 or later, matching the GPLv3+ projects used as implementation references. See `LICENSE`.
|
||||
|
||||
Third-party projects retain their own licenses. Do not copy assets or source from the credited projects into another distribution without preserving the corresponding license notices.
|
||||
|
||||
If distributing binaries, comply with the LGPL terms for `libmicrohttpd`
|
||||
in addition to this project's GPL license. The vendored `zlib` and
|
||||
`minizip-ng` sources are distributed under the zlib license; retain the
|
||||
copyright notices in `third_party/zlib/LICENSE` and
|
||||
`third_party/minizip-ng/LICENSE` when redistributing binaries built
|
||||
with this feature. The vendored `dmc_unrar` (RAR engine) is distributed
|
||||
under the GPL-2.0-or-later; retain the copyright notice in
|
||||
`third_party/unrar/COPYING` and ship the corresponding sources when
|
||||
redistributing binaries built with v1.8 or later (the `web-file-mgr.elf`
|
||||
binary is already GPLv3+, so the additional source-disclosure
|
||||
requirement is the only practical effect).
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Unofficial homebrew software. Runs only on jailbroken PS5 consoles. Use at your own risk — the authors are not responsible for damage, data loss, account action or warranty impact. Do not redistribute Sony-proprietary content. Under GPLv3+, modified redistributions must publish their sources.
|
||||
If distributing binaries, comply with the LGPL terms for libmicrohttpd in addition to this project's GPL license.
|
||||
@@ -1,46 +0,0 @@
|
||||
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.
|
||||
|
||||
1. minizip-ng
|
||||
-----------
|
||||
Version : 4.2.2
|
||||
Source : https://github.com/zlib-ng/minizip-ng
|
||||
License : zlib (see third_party/minizip-ng/LICENSE)
|
||||
Files : third_party/minizip-ng/** (vendored subset, compiled with
|
||||
relaxed warnings into the final binary)
|
||||
|
||||
2. zlib
|
||||
------
|
||||
Version : 1.3.1
|
||||
Source : https://www.zlib.net/
|
||||
License : zlib (see third_party/zlib/LICENSE)
|
||||
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).
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 268 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 KiB |
+9
-112
@@ -4,35 +4,13 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>PS5 Web File Manager</title>
|
||||
<link rel="icon" type="image/png" href="/icon0.png">
|
||||
<style>
|
||||
html, body { margin: 0; min-height: 100%; background: #111316; color: #edf0f2; }
|
||||
.init-loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 20000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #111316;
|
||||
color: #aab4be;
|
||||
font: 18px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<header class="topbar">
|
||||
<div class="top-left">
|
||||
<div id="path" class="path">/</div>
|
||||
</div>
|
||||
<div class="top-right">
|
||||
<div id="spaceInfo" class="space-info"></div>
|
||||
</div>
|
||||
<div id="path" class="path">/</div>
|
||||
<div id="spaceInfo" class="space-info"></div>
|
||||
<button id="exitBtn" class="icon-button power-button" type="button" aria-label="Exit"></button>
|
||||
</header>
|
||||
|
||||
@@ -41,52 +19,31 @@
|
||||
<button id="copyBtn" data-i18n="copy"></button>
|
||||
<button id="moveBtn" data-i18n="move"></button>
|
||||
<button id="renameBtn" data-i18n="rename"></button>
|
||||
<button id="downloadBtn" class="remote-only" data-i18n="download"></button>
|
||||
<button id="deleteBtn" class="danger" data-i18n="delete"></button>
|
||||
<button id="installPkgBtn" class="install-action" data-i18n="install" hidden></button>
|
||||
<button id="extractBtn" class="extract-action" data-i18n="extractToCurrent" hidden></button>
|
||||
<button id="pasteBtn" class="paste-action" hidden>
|
||||
<span id="pasteVerb" data-i18n="paste"></span>
|
||||
<span id="pasteName" class="paste-name"></span><span id="pasteCount" class="paste-count"></span>
|
||||
<span id="pasteName" class="paste-name"></span>
|
||||
<span id="pasteTargetText" data-i18n="pasteToCurrent"></span>
|
||||
</button>
|
||||
<button id="clearClipboardBtn" data-i18n="cancel" hidden></button>
|
||||
</div>
|
||||
<div class="tool-right">
|
||||
<button id="refreshBtn" data-i18n="refresh"></button>
|
||||
<div id="uploadMenu" 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>
|
||||
</div>
|
||||
<button id="newTextBtn" data-i18n="newText"></button>
|
||||
<button id="mkdirBtn" data-i18n="mkdir"></button>
|
||||
</div>
|
||||
</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>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="select-col"><label class="select-hit"><input id="selectAll" type="checkbox"></label></th>
|
||||
<th class="name-col sortable" data-sort="name">
|
||||
<div class="name-head">
|
||||
<button id="parentBtn" class="parent-nav-button" type="button" aria-label="Parent directory" disabled>
|
||||
<img src="/icon-back.png" alt="">
|
||||
</button>
|
||||
<span class="name-heading" data-i18n="name"></span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="type-col sortable" data-sort="type" data-i18n="type"></th>
|
||||
<th class="size-col sortable" data-sort="size" data-i18n="size"></th>
|
||||
<th class="time-col sortable" data-sort="mtime" data-i18n="mtime"></th>
|
||||
<th class="mode-col sortable" data-sort="mode" data-i18n="mode"></th>
|
||||
<th class="name-col" data-i18n="name"></th>
|
||||
<th class="type-col" data-i18n="type"></th>
|
||||
<th class="size-col" data-i18n="size"></th>
|
||||
<th class="time-col" data-i18n="mtime"></th>
|
||||
<th class="mode-col" data-i18n="mode"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="files"></tbody>
|
||||
@@ -126,73 +83,13 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="pkgInfoOverlay" class="text-editor-overlay" hidden>
|
||||
<section class="text-editor-panel pkg-info-panel" role="dialog" aria-modal="true" aria-labelledby="pkgInfoTitle">
|
||||
<h2 id="pkgInfoTitle" class="pkg-info-title" data-i18n="pkgInfoTitle"></h2>
|
||||
<div class="pkg-info-content">
|
||||
<div class="pkg-info-image-stage">
|
||||
<img id="pkgInfoImage" class="pkg-info-image" src="/icon-pkg.png" alt="">
|
||||
</div>
|
||||
<div id="pkgInfoFields" class="pkg-info-fields"></div>
|
||||
</div>
|
||||
<div class="text-editor-actions">
|
||||
<button id="pkgInfoCloseBtn" class="secondary" data-i18n="close"></button>
|
||||
<button id="pkgInfoInstallBtn" class="primary" data-i18n="install"></button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="permissionOverlay" class="permission-overlay" hidden>
|
||||
<section class="permission-panel" role="dialog" aria-modal="true" aria-labelledby="permissionTitle">
|
||||
<h2 id="permissionTitle" class="permission-title" data-i18n="permissionsTitle"></h2>
|
||||
<div id="permissionPath" class="permission-path"></div>
|
||||
<div class="permission-grid">
|
||||
<div class="permission-row permission-head" aria-hidden="true">
|
||||
<span class="permission-scope"></span>
|
||||
<span>R</span><span>W</span><span>X</span>
|
||||
</div>
|
||||
<div class="permission-row">
|
||||
<span class="permission-scope" data-i18n="permissionOwner"></span>
|
||||
<label><input id="permissionOwnerRead" type="checkbox"><span>R</span></label>
|
||||
<label><input id="permissionOwnerWrite" type="checkbox"><span>W</span></label>
|
||||
<label><input id="permissionOwnerExecute" type="checkbox"><span>X</span></label>
|
||||
</div>
|
||||
<div class="permission-row">
|
||||
<span class="permission-scope" data-i18n="permissionGroup"></span>
|
||||
<label><input id="permissionGroupRead" type="checkbox"><span>R</span></label>
|
||||
<label><input id="permissionGroupWrite" type="checkbox"><span>W</span></label>
|
||||
<label><input id="permissionGroupExecute" type="checkbox"><span>X</span></label>
|
||||
</div>
|
||||
<div class="permission-row">
|
||||
<span class="permission-scope" data-i18n="permissionOther"></span>
|
||||
<label><input id="permissionOtherRead" type="checkbox"><span>R</span></label>
|
||||
<label><input id="permissionOtherWrite" type="checkbox"><span>W</span></label>
|
||||
<label><input id="permissionOtherExecute" type="checkbox"><span>X</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<label class="permission-octal">
|
||||
<span data-i18n="permissionOctal"></span>
|
||||
<input id="permissionMode" type="text" inputmode="numeric" pattern="0[0-7]{3}"
|
||||
minlength="4" maxlength="4" autocomplete="off" spellcheck="false">
|
||||
</label>
|
||||
<label id="permissionRecursiveOption" class="permission-recursive" hidden>
|
||||
<input id="permissionRecursive" type="checkbox" checked>
|
||||
<span data-i18n="permissionRecursive"></span>
|
||||
</label>
|
||||
<div class="permission-actions">
|
||||
<button id="permissionCancelBtn" class="secondary" data-i18n="close"></button>
|
||||
<button id="permissionApplyBtn" class="primary" data-i18n="apply"></button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="taskOverlay" class="task-overlay" hidden>
|
||||
<div class="task-panel">
|
||||
<div id="tasks" class="tasks"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="initLoading" class="init-loading" aria-hidden="true">Loading...</div>
|
||||
<div id="initLoading" class="init-loading" aria-hidden="true"></div>
|
||||
|
||||
<script src="/main.js"></script>
|
||||
</body>
|
||||
|
||||
+2
-79
@@ -3,34 +3,20 @@ window.WFM_LANG = {
|
||||
copy: "Copy",
|
||||
move: "Move",
|
||||
delete: "Delete",
|
||||
download: "Download",
|
||||
upload: "Upload",
|
||||
uploadFolder: "Upload Folder",
|
||||
copying: "Copying",
|
||||
moving: "Moving",
|
||||
deleting: "Deleting",
|
||||
changingPermissions: "Changing permissions",
|
||||
rename: "Rename",
|
||||
paste: "Paste",
|
||||
cancel: "Cancel",
|
||||
close: "Close",
|
||||
save: "Save",
|
||||
apply: "Apply",
|
||||
exit: "Exit",
|
||||
exitConfirm: "Exit and stop the file manager process?",
|
||||
exiting: "Exiting...",
|
||||
refresh: "Refresh",
|
||||
mkdir: "New Folder",
|
||||
newText: "New Text",
|
||||
install: "Install",
|
||||
installPackage: "Install package",
|
||||
pkgInfoTitle: "Package Information",
|
||||
pkgInfoLoading: "Reading package information...",
|
||||
pkgInstalling: "Starting package installation: {name}...",
|
||||
pkgInstallStarted: "Package installation started: {name}",
|
||||
file: "File",
|
||||
textFile: "Text",
|
||||
image: "Image",
|
||||
dir: "Folder",
|
||||
parent: "Parent",
|
||||
name: "Name",
|
||||
@@ -38,19 +24,6 @@ window.WFM_LANG = {
|
||||
size: "Size",
|
||||
mtime: "Modified",
|
||||
mode: "Mode",
|
||||
permissionsTitle: "Change permissions",
|
||||
permissionOwner: "Owner",
|
||||
permissionGroup: "Group",
|
||||
permissionOther: "Other",
|
||||
permissionOctal: "Octal mode",
|
||||
permissionRecursive: "Apply to all contents inside this folder",
|
||||
permissionObjectCount: "and {count} objects",
|
||||
permissionChangeTitle: "Change permissions for {name}",
|
||||
permissionInvalid: "Enter exactly four octal digits from 0 to 7, starting with 0. The original value has been restored.",
|
||||
unsavedPermissionConfirm: "The permissions have unsaved changes. Close without applying them?\n\nCancel keeps the permissions dialog open.",
|
||||
permissionChanging: "Changing permissions...",
|
||||
permissionChanged: "Permissions changed: {name} → {mode}",
|
||||
permissionChangeFailed: "Failed to change permissions: {error}",
|
||||
empty: "Folder is empty",
|
||||
ready: "Ready",
|
||||
freeSpace: "Free space",
|
||||
@@ -64,9 +37,7 @@ window.WFM_LANG = {
|
||||
checking: "Checking",
|
||||
pleaseWait: "Please wait",
|
||||
speedLabel: "Speed",
|
||||
itemsPerSecond: "{count} objects/s",
|
||||
progressLabel: "Progress",
|
||||
permissionProgress: "{done} / {total} objects",
|
||||
etaLabel: "ETA",
|
||||
elapsedLabel: "Elapsed",
|
||||
durationHours: "{hours}h {minutes}m",
|
||||
@@ -78,7 +49,6 @@ window.WFM_LANG = {
|
||||
countItems: "{count} items",
|
||||
totalItems: "{count} items",
|
||||
readDir: "Reading folder...",
|
||||
processing: "Processing...",
|
||||
actionBusy: "{label}...",
|
||||
taskCreated: "{label} task created",
|
||||
actionDone: "{label} done",
|
||||
@@ -87,34 +57,12 @@ window.WFM_LANG = {
|
||||
taskCanceled: "{label} canceled",
|
||||
selectedForPaste: "Selected {name}. Enter the target folder, then choose {verb}",
|
||||
clipboardCleared: "Pending operation canceled",
|
||||
downloadStarted: "Download started: {name}",
|
||||
uploadFolderChoice: "Upload a folder?\n\nOK: choose a folder\nCancel: choose files",
|
||||
uploadOverwriteConfirm: "Items with the same name already exist: {names}\n\nOverwrite matching files and merge matching folders?",
|
||||
uploadingStatus: "Uploading {index}/{count}: {name}",
|
||||
uploadDone: "Upload done, {count} items",
|
||||
uploadFailed: "Upload failed: {error}",
|
||||
downloading: "Downloading",
|
||||
uploading: "Uploading",
|
||||
extract: "Extract",
|
||||
extractToCurrent: "Extract to current folder",
|
||||
extractUpload: "Upload and extract",
|
||||
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.",
|
||||
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",
|
||||
extractLargeActive: "Large-file profile is enabled for this task",
|
||||
extractSelectMainVolume: "Please select the main volume (.rar or .part01.rar)",
|
||||
extractArchivePending: "Preparing to extract {name}",
|
||||
sameSourceTarget: "Source and destination are the same. Cannot {label} {name}",
|
||||
removeConflictFirst: "A {existingType} named {name} already exists. To {label} this {sourceType}, delete that {existingType} first.",
|
||||
overwriteFiles: "Files with the same name will be overwritten: {names}",
|
||||
mergeDirs: "Folders with the same name will be merged. Internal files with the same name will be overwritten: {names}",
|
||||
continueConfirm: "Continue?",
|
||||
preparingPaste: "Preparing: calculating size and checking target space...",
|
||||
preparingPaste: "{label} preparing: calculating size and checking target space...",
|
||||
preparingStatus: "{label} preparing...",
|
||||
cancelPrepare: "Preparation canceled",
|
||||
cancelTaskConfirm: "Cancel current {label} task?",
|
||||
@@ -127,7 +75,7 @@ window.WFM_LANG = {
|
||||
newTextPrompt: "Text file name",
|
||||
creatingText: "Creating text file...",
|
||||
createTextFailed: "Failed to create text file: {error}",
|
||||
unsavedSaveConfirm: "The text has unsaved changes. Close without saving?\n\nCancel keeps the editor open.",
|
||||
unsavedSaveConfirm: "The text has unsaved changes. Save before closing?\n\nCancel discards the changes and closes.",
|
||||
loadingText: "Loading text...",
|
||||
savingText: "Saving...",
|
||||
textSaved: "Text saved",
|
||||
@@ -143,8 +91,6 @@ window.WFM_LANG = {
|
||||
storageUsb: "USB",
|
||||
storageM2: "M.2",
|
||||
storageExtended: "Extended",
|
||||
storageRoot: "Root",
|
||||
storageCurrent: "Current",
|
||||
err_target_dir_not_writable: "Target folder is not writable: {path}",
|
||||
err_target_file_not_writable: "Target file is not writable: {path}",
|
||||
err_target_parent_not_writable: "Current folder is not writable: {path}",
|
||||
@@ -158,12 +104,6 @@ window.WFM_LANG = {
|
||||
err_source_destination_same: "Source and destination are the same",
|
||||
err_destination_inside_source: "Cannot copy or move a folder inside itself",
|
||||
err_invalid_path: "Invalid path",
|
||||
err_invalid_mode: "Invalid permission mode",
|
||||
err_chmod_failed: "Cannot change permissions: {path}",
|
||||
err_pkg_type_invalid: "Only .pkg files can be installed",
|
||||
err_pkg_info_failed: "Could not read package information: {path}",
|
||||
err_pkg_install_failed: "Package installation failed ({arg})",
|
||||
err_pkg_install_unsupported: "Package installation is only available on PS5",
|
||||
err_file_not_found: "File not found",
|
||||
err_file_already_exists: "A file with the same name already exists",
|
||||
err_invalid_method: "Invalid request method",
|
||||
@@ -172,26 +112,9 @@ window.WFM_LANG = {
|
||||
err_text_invalid_utf8: "The file is not valid UTF-8 text",
|
||||
err_text_file_changed: "The file changed after it was opened. Close and reopen it",
|
||||
err_text_file_not_writable: "The text file is not writable",
|
||||
err_request_body_too_large: "Too many selected items. Select fewer items and try again",
|
||||
err_unknown_api: "Unknown API",
|
||||
err_out_of_memory: "Out of memory",
|
||||
err_no_source_paths: "No source paths",
|
||||
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_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}",
|
||||
err_extract_too_many_entries: "Archive has too many entries: {arg}",
|
||||
err_extract_entry_too_large: "A file inside the archive is too large: {arg}",
|
||||
err_extract_too_large: "Archive expands to too much data: {arg}",
|
||||
err_extract_ratio: "Suspicious compression ratio (possible zip bomb): {arg}",
|
||||
err_extract_too_deep: "Directory nesting is too deep: {arg}",
|
||||
err_extract_name_too_long: "File name or path is too long: {arg}",
|
||||
err_extract_conflict: "A file or folder with the same name already exists: {arg}",
|
||||
err_extract_io: "Extraction read/write failed: {arg}",
|
||||
err_extract_crc: "CRC check failed: {arg}",
|
||||
err_extract_failed: "Extraction failed: {arg}",
|
||||
err_system_error: "System error: {arg}"
|
||||
};
|
||||
+2
-79
@@ -3,34 +3,20 @@ window.WFM_LANG = {
|
||||
copy: "复制",
|
||||
move: "移动",
|
||||
delete: "删除",
|
||||
download: "下载",
|
||||
upload: "上传",
|
||||
uploadFolder: "上传文件夹",
|
||||
copying: "复制",
|
||||
moving: "移动",
|
||||
deleting: "删除",
|
||||
changingPermissions: "修改权限",
|
||||
rename: "重命名",
|
||||
paste: "粘贴",
|
||||
cancel: "取消",
|
||||
close: "关闭",
|
||||
save: "保存",
|
||||
apply: "应用",
|
||||
exit: "退出",
|
||||
exitConfirm: "是否退出并关闭文件管理器进程?",
|
||||
exiting: "正在退出...",
|
||||
refresh: "刷新",
|
||||
mkdir: "新建目录",
|
||||
newText: "新建文本",
|
||||
install: "安装",
|
||||
installPackage: "安装 PKG",
|
||||
pkgInfoTitle: "PKG 信息",
|
||||
pkgInfoLoading: "正在读取 PKG 信息...",
|
||||
pkgInstalling: "正在提交安装:{name}...",
|
||||
pkgInstallStarted: "已开始安装:{name}",
|
||||
file: "文件",
|
||||
textFile: "文本",
|
||||
image: "图片",
|
||||
dir: "目录",
|
||||
parent: "上级目录",
|
||||
name: "名称",
|
||||
@@ -38,19 +24,6 @@ window.WFM_LANG = {
|
||||
size: "大小",
|
||||
mtime: "修改时间",
|
||||
mode: "权限",
|
||||
permissionsTitle: "修改权限",
|
||||
permissionOwner: "拥有者",
|
||||
permissionGroup: "组",
|
||||
permissionOther: "其他",
|
||||
permissionOctal: "八进制表示",
|
||||
permissionRecursive: "应用于文件夹内的所有内容",
|
||||
permissionObjectCount: "等 {count} 个对象",
|
||||
permissionChangeTitle: "修改 {name} 的权限",
|
||||
permissionInvalid: "请输入以 0 开头、由 0 到 7 组成的四位八进制权限。已恢复为原权限值。",
|
||||
unsavedPermissionConfirm: "权限有未保存的修改,是否不保存并关闭?\n\n取消将留在权限弹窗中。",
|
||||
permissionChanging: "正在修改权限...",
|
||||
permissionChanged: "权限已修改:{name} → {mode}",
|
||||
permissionChangeFailed: "修改权限失败:{error}",
|
||||
empty: "目录为空",
|
||||
ready: "就绪",
|
||||
freeSpace: "可用空间",
|
||||
@@ -64,9 +37,7 @@ window.WFM_LANG = {
|
||||
checking: "检查中",
|
||||
pleaseWait: "请稍候",
|
||||
speedLabel: "速度",
|
||||
itemsPerSecond: "{count} 个对象/秒",
|
||||
progressLabel: "进度",
|
||||
permissionProgress: "{done} / {total} 个对象",
|
||||
etaLabel: "剩余",
|
||||
elapsedLabel: "耗时",
|
||||
durationHours: "{hours}小时 {minutes}分",
|
||||
@@ -78,7 +49,6 @@ window.WFM_LANG = {
|
||||
countItems: "{count} 项",
|
||||
totalItems: "共 {count} 项",
|
||||
readDir: "读取目录中...",
|
||||
processing: "处理中...",
|
||||
actionBusy: "{label}...",
|
||||
taskCreated: "{label}任务已创建",
|
||||
actionDone: "{label}完成",
|
||||
@@ -87,34 +57,12 @@ window.WFM_LANG = {
|
||||
taskCanceled: "{label}已取消",
|
||||
selectedForPaste: "已选择 {name},进入目标目录后点击{verb}",
|
||||
clipboardCleared: "已清除待操作项目",
|
||||
downloadStarted: "已开始下载 {name}",
|
||||
uploadFolderChoice: "选择要上传的目录?\n\n确定:选择目录\n取消:选择文件",
|
||||
uploadOverwriteConfirm: "目标中已存在同名项目: {names}\n\n是否覆盖同名文件并合并同名目录?",
|
||||
uploadingStatus: "正在上传 {index}/{count}: {name}",
|
||||
uploadDone: "上传完成,共 {count} 项",
|
||||
uploadFailed: "上传失败: {error}",
|
||||
downloading: "下载中",
|
||||
uploading: "上传中",
|
||||
extract: "解压",
|
||||
extractToCurrent: "解压到当前目录",
|
||||
extractUpload: "上传并解压",
|
||||
extracting: "解压",
|
||||
extractConfirm: "解压 {name} 到 {path}?",
|
||||
extractOverwriteAsk: "若目标已存在同名文件或目录:\n\n确定 = 覆盖同名文件(目录仍会合并)\n取消 = 若目标已存在则失败",
|
||||
extractUploadConfirm: "上传并解压 {name}?\n\n目标目录:{path}\n成功后将删除上传的 ZIP。",
|
||||
extractStarted: "已开始解压 {name}",
|
||||
extractDone: "解压完成:{name}",
|
||||
extractProgress: "{done} / {total} 个文件",
|
||||
extractLargeAsk: "ZIP 体积较大({size}),是否启用「大文件模式」?\n\n确定 = 启用(单文件最大 1 TiB / 总解压最大 2 TiB)\n取消 = 默认限制(单文件 64 GiB / 总解压 512 GiB),可能拒绝此压缩包",
|
||||
extractLargeActive: "此任务已启用大文件模式",
|
||||
extractSelectMainVolume: "请改选主卷(如 .rar 或 .part01.rar)",
|
||||
extractArchivePending: "正在准备解压 {name}",
|
||||
sameSourceTarget: "源和目标相同,不能{label} {name}",
|
||||
removeConflictFirst: "目标中已存在同名{existingType} {name}。要{label}{sourceType},请先删除该{existingType}才能继续。",
|
||||
overwriteFiles: "同名文件将被覆盖: {names}",
|
||||
mergeDirs: "同名目录将被合并,内部同名文件会被覆盖: {names}",
|
||||
continueConfirm: "继续?",
|
||||
preparingPaste: "准备中:正在计算大小并检查目标空间...",
|
||||
preparingPaste: "{label}准备中:正在计算大小并检查目标空间...",
|
||||
preparingStatus: "{label}准备中...",
|
||||
cancelPrepare: "已取消准备操作",
|
||||
cancelTaskConfirm: "取消当前{label}任务?",
|
||||
@@ -127,7 +75,7 @@ window.WFM_LANG = {
|
||||
newTextPrompt: "文本文件名",
|
||||
creatingText: "正在新建文本...",
|
||||
createTextFailed: "新建文本失败: {error}",
|
||||
unsavedSaveConfirm: "文本有未保存的修改,是否不保存并关闭?\n\n取消将留在编辑器中。",
|
||||
unsavedSaveConfirm: "文本有未保存的修改,是否保存后关闭?\n\n取消将放弃修改并关闭。",
|
||||
loadingText: "正在读取文本...",
|
||||
savingText: "正在保存...",
|
||||
textSaved: "文本已保存",
|
||||
@@ -143,8 +91,6 @@ window.WFM_LANG = {
|
||||
storageUsb: "USB存储",
|
||||
storageM2: "M2扩充存储",
|
||||
storageExtended: "扩展存储",
|
||||
storageRoot: "根分区",
|
||||
storageCurrent: "当前目录",
|
||||
err_target_dir_not_writable: "目标目录不可写: {path}",
|
||||
err_target_file_not_writable: "目标文件不可写: {path}",
|
||||
err_target_parent_not_writable: "当前目录不可写: {path}",
|
||||
@@ -158,12 +104,6 @@ window.WFM_LANG = {
|
||||
err_source_destination_same: "源和目标相同",
|
||||
err_destination_inside_source: "不能复制或移动目录到它自己的内部",
|
||||
err_invalid_path: "路径无效",
|
||||
err_invalid_mode: "权限格式无效",
|
||||
err_chmod_failed: "无法修改权限:{path}",
|
||||
err_pkg_type_invalid: "只能安装 .pkg 文件",
|
||||
err_pkg_info_failed: "无法读取 PKG 信息:{path}",
|
||||
err_pkg_install_failed: "PKG 安装失败({arg})",
|
||||
err_pkg_install_unsupported: "PKG 安装仅支持 PS5 环境",
|
||||
err_file_not_found: "文件不存在",
|
||||
err_file_already_exists: "同名文件已存在",
|
||||
err_invalid_method: "请求方式无效",
|
||||
@@ -172,26 +112,9 @@ window.WFM_LANG = {
|
||||
err_text_invalid_utf8: "文件不是有效的 UTF-8 文本",
|
||||
err_text_file_changed: "文件在打开后已发生变化,请关闭后重新打开",
|
||||
err_text_file_not_writable: "文本文件不可写",
|
||||
err_request_body_too_large: "选择的项目过多,请减少选择后重试",
|
||||
err_unknown_api: "未知接口",
|
||||
err_out_of_memory: "内存不足",
|
||||
err_no_source_paths: "没有源路径",
|
||||
err_destination_must_be_directory: "多个项目的目标必须是目录",
|
||||
err_extract_open_failed: "无法打开压缩包: {arg}",
|
||||
err_extract_corrupt: "压缩包损坏或不完整: {arg}",
|
||||
err_extract_unsupported: "不支持的压缩包(仅支持未加密的普通 ZIP 与单卷 RAR): {arg}",
|
||||
err_extract_unsafe_name: "压缩包包含不安全的路径: {arg}",
|
||||
err_extract_special_entry: "压缩包包含不支持的特殊文件: {arg}",
|
||||
err_extract_duplicate: "压缩包包含重复条目: {arg}",
|
||||
err_extract_too_many_entries: "压缩包条目过多: {arg}",
|
||||
err_extract_entry_too_large: "压缩包内单个文件过大: {arg}",
|
||||
err_extract_too_large: "压缩包解压后总大小过大: {arg}",
|
||||
err_extract_ratio: "压缩比异常(疑似压缩炸弹): {arg}",
|
||||
err_extract_too_deep: "目录层级过深: {arg}",
|
||||
err_extract_name_too_long: "文件名或路径过长: {arg}",
|
||||
err_extract_conflict: "目标已存在同名文件或目录: {arg}",
|
||||
err_extract_io: "解压读写失败: {arg}",
|
||||
err_extract_crc: "CRC 校验失败: {arg}",
|
||||
err_extract_failed: "解压失败: {arg}",
|
||||
err_system_error: "系统错误: {arg}"
|
||||
};
|
||||
+35
-833
File diff suppressed because it is too large.
Load diff
+115
-1474
File diff suppressed because it is too large.
Load diff
-1714
File diff suppressed because it is too large.
Load diff
@@ -1,448 +0,0 @@
|
||||
# Upgrade v1.7 — ZIP large-file profile
|
||||
|
||||
> Technical notes for the **v1.7** upgrade of `ps5-web-file-manager`.
|
||||
> Audience: future maintainers, code reviewers, contributors reading the
|
||||
> `git log` of this branch. Pair with `README.md` for the user-facing
|
||||
> overview and `CHANGELOG.md` for the release-note summary.
|
||||
|
||||
---
|
||||
|
||||
## 1. Background
|
||||
|
||||
Before v1.7 the ZIP engine shipped with a single, conservative limits profile
|
||||
(`zipx_default_limits()`). It rejected any archive whose uncompressed content
|
||||
exceeded **512 GiB total** or contained an entry above **64 GiB**, or whose
|
||||
compression ratio exceeded **200 : 1**. This was the right default for the
|
||||
"copy game-dump folders" use case but blocked legitimate large payloads — most
|
||||
notably system images and 200 GB+ backups.
|
||||
|
||||
The user asked: *"单个压缩包不可以为 200GB 以上么"* ("Can't a single archive
|
||||
be larger than 200 GB?"). After surfacing three options (raise the default
|
||||
limits, add an opt-in profile, or document-only) the choice was **🅱 — add an
|
||||
opt-in "large-file profile"**. The implementation brief was:
|
||||
|
||||
> The server must **never** activate the relaxed caps on its own. The user
|
||||
> must explicitly opt in, both via the HTTP API and via the UI.
|
||||
|
||||
## 2. Architecture delta (one-line summary)
|
||||
|
||||
The ZIP engine becomes a **profile-lookup** engine. A new profile table
|
||||
(`k_large_limits`) and a switch (`zipx_limits_profile()`) sit between the HTTP
|
||||
layer and the existing `zipx_extract()`. Everywhere else — task model, HTTP
|
||||
handler, frontend — gains a single boolean that threads through to the engine.
|
||||
|
||||
```
|
||||
HTTP /api/extract?large=1 frontend (confirm() 弹窗)
|
||||
│ │
|
||||
└──────► form parser ──► file_task_t.extract_large
|
||||
│
|
||||
▼
|
||||
zipx_limits_profile(task->extract_large)
|
||||
│
|
||||
┌────────────┴────────────┐
|
||||
▼ ▼
|
||||
k_default_limits k_large_limits
|
||||
(200K / 512GiB / (500K / 2TiB /
|
||||
64GiB / 200:1) 1TiB / 1000:1)
|
||||
│ │
|
||||
└────────────┬─────────────┘
|
||||
▼
|
||||
zipx_extract()
|
||||
(signature unchanged;
|
||||
shared three-phase engine)
|
||||
```
|
||||
|
||||
The public signature of `zipx_extract()` is **unchanged**. Backwards
|
||||
compatibility is deliberate — anything linking against `src/zip_extract.{c,h}`
|
||||
continues to compile without changes.
|
||||
|
||||
## 3. Engine layer (`src/zip_extract.h`, `src/zip_extract.c`)
|
||||
|
||||
### 3.1 New constants and API
|
||||
|
||||
```c
|
||||
/* Pre-built limit profiles. Use zipx_limits_profile() to look one up.
|
||||
ZIPX_LIMITS_DEFAULT is the safe profile shipped by zipx_default_limits().
|
||||
ZIPX_LIMITS_LARGE allows archives up to 2 TiB total / 1 TiB per file and
|
||||
a 1000:1 compression ratio. The caller is responsible for verifying that
|
||||
the PS5 has enough free disk space. */
|
||||
#define ZIPX_LIMITS_DEFAULT 0
|
||||
#define ZIPX_LIMITS_LARGE 1
|
||||
|
||||
const zipx_limits_t *zipx_limits_profile(int profile);
|
||||
```
|
||||
|
||||
### 3.2 The two profiles
|
||||
|
||||
```c
|
||||
static const zipx_limits_t k_default_limits = {
|
||||
.max_entries = 200000,
|
||||
.max_total_bytes = 512ULL * 1024 * 1024 * 1024, /* 512 GiB */
|
||||
.max_file_bytes = 64ULL * 1024 * 1024 * 1024, /* 64 GiB */
|
||||
.max_ratio = 200,
|
||||
.max_depth = 32,
|
||||
.max_name_len = 255,
|
||||
.max_path_len = 1024
|
||||
};
|
||||
|
||||
static const zipx_limits_t k_large_limits = {
|
||||
.max_entries = 500000,
|
||||
.max_total_bytes = 2ULL * 1024 * 1024 * 1024 * 1024, /* 2 TiB */
|
||||
.max_file_bytes = 1ULL * 1024 * 1024 * 1024 * 1024, /* 1 TiB */
|
||||
.max_ratio = 1000,
|
||||
.max_depth = 32,
|
||||
.max_name_len = 255,
|
||||
.max_path_len = 1024
|
||||
};
|
||||
```
|
||||
|
||||
The two profiles are otherwise identical on `max_depth`, `max_name_len` and
|
||||
`max_path_len` — the v1.7 upgrade only relaxes the four "blast radius" caps.
|
||||
|
||||
### 3.3 Profile lookup
|
||||
|
||||
```c
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`zipx_default_limits()` continues to return `&k_default_limits` — there is
|
||||
**no behavioural change** for callers that did not opt in.
|
||||
|
||||
### 3.4 Behavioural contract (unchanged from pre-v1.7)
|
||||
|
||||
* Plain ZIPs only — stored / deflated / ZIP64. `ZIPX_ERR_UNSUPPORTED` is
|
||||
raised for any encryption flag, multi-volume markers or unsupported
|
||||
compression methods.
|
||||
* Three-phase work model: `ZIPX_PHASE_SCAN → EXTRACT → PUBLISH → CLEANUP`.
|
||||
Each entry is first written to a staging directory (`*.wfm-part-*`),
|
||||
`fsync()`'d, then atomically renamed into place. A failure mid-archive
|
||||
rolls back partial changes.
|
||||
* Security checks run before any output file is opened:
|
||||
- encryption
|
||||
- path traversal (`..`), absolute POSIX paths, Windows drive letters
|
||||
- symbolic links, devices, FIFOs, sockets
|
||||
- duplicate entries / directory↔file clashes inside the archive
|
||||
- the four blast-radius caps above (entries, total bytes, file bytes,
|
||||
compression ratio)
|
||||
|
||||
## 4. Task layer (`src/filemgr_internal.h`, `src/extract.c`)
|
||||
|
||||
### 4.1 New task field
|
||||
|
||||
```c
|
||||
typedef struct file_task {
|
||||
/* …existing fields… */
|
||||
int extract_conflict;
|
||||
int extract_remove_source;
|
||||
int extract_large; /* ← new: 0 = ZIPX_LIMITS_DEFAULT, 1 = ZIPX_LIMITS_LARGE */
|
||||
/* …existing fields… */
|
||||
} file_task_t;
|
||||
```
|
||||
|
||||
### 4.2 Worker dispatch
|
||||
|
||||
In `extract_worker()` (`src/extract.c`, ~line 115):
|
||||
|
||||
```c
|
||||
status = zipx_extract(task->src, task->dst, conflict,
|
||||
zipx_limits_profile(task->extract_large),
|
||||
extract_cancel, extract_progress, task, &result);
|
||||
```
|
||||
|
||||
The third positional argument moved from a direct `&k_default_limits` (or a
|
||||
caller-supplied struct) to `zipx_limits_profile(task->extract_large)`. The
|
||||
`limits` parameter of `zipx_extract()` remains `const zipx_limits_t *` —
|
||||
either pointer is fine.
|
||||
|
||||
### 4.3 Form parsing in `api_extract()`
|
||||
|
||||
```c
|
||||
char *large_str = body_form_value(body, body_size, "large");
|
||||
int large = (large_str != NULL && !strcmp(large_str, "1")) ? 1 : 0;
|
||||
/* … free chain updated to release large_str … */
|
||||
task->extract_large = large;
|
||||
```
|
||||
|
||||
The string comparison is **strict** — only the literal `"1"` activates the
|
||||
large profile. `"true"`, `"yes"`, `"on"` are all ignored. This matches the
|
||||
convention used by the existing `remove_source` field (`src/extract.c`).
|
||||
|
||||
### 4.4 Error reporting path
|
||||
|
||||
When the active profile rejects an archive the engine returns one of:
|
||||
|
||||
| `zipx_status_t` | Frontend maps to |
|
||||
|---------------------|--------------------------|
|
||||
| `ZIPX_ERR_LIMIT_ENTRIES` | `err_extract_too_many_entries` |
|
||||
| `ZIPX_ERR_LIMIT_FILE` | `err_extract_entry_too_large` |
|
||||
| `ZIPX_ERR_LIMIT_TOTAL` | `err_extract_total_too_large` |
|
||||
| `ZIPX_ERR_LIMIT_RATIO` | `err_extract_ratio` |
|
||||
| `ZIPX_ERR_LIMIT_DEPTH` | `err_extract_depth` |
|
||||
| `ZIPX_ERR_LIMIT_NAME` | `err_extract_name` |
|
||||
|
||||
The error string embedded in `zipx_result_t.message` interpolates the active
|
||||
limit (`"compression ratio is above %u"`), so users can see exactly which cap
|
||||
hit. **v1.7 does not change** this mapping — the new large profile uses the
|
||||
same codes, just with larger caps.
|
||||
|
||||
## 5. HTTP API contract (`/api/extract`)
|
||||
|
||||
`POST /api/extract` accepts `application/x-www-form-urlencoded`:
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|-----------------|------|----------|-------|
|
||||
| `path` | string | yes | Absolute path to the archive on the PS5. |
|
||||
| `dst_dir` | string | yes | Output directory. |
|
||||
| `conflict` | string | no | `fail` / `overwrite` / `merge`. Default `fail`. |
|
||||
| `remove_source` | `0`/`1` | no | Default `0`. |
|
||||
| `large` | `0`/`1` | no | **new in v1.7**. Default `0`. |
|
||||
|
||||
Compatibility:
|
||||
|
||||
- Existing callers that do **not** send `large` get identical behaviour as
|
||||
before the upgrade — `ZIPX_LIMITS_DEFAULT` always.
|
||||
- The server returns `400` for any value other than `"0"` or `"1"` (only the
|
||||
exact literal `"1"` enables the large profile). This is enforced by the
|
||||
`!strcmp(large_str, "1")` guard, not a separate validator.
|
||||
|
||||
The hand-rolled form parser (`src/json_util.c` `body_form_value()`) already
|
||||
returned the raw value; the upgrade did not touch that helper.
|
||||
|
||||
## 6. Frontend (`assets/main.js`)
|
||||
|
||||
### 6.1 Threshold + prompt primitives
|
||||
|
||||
```js
|
||||
const LARGE_FILE_THRESHOLD_BYTES = 60 * 1024 * 1024 * 1024; // 60 GiB
|
||||
|
||||
function shouldPromptLargeMode(itemSize) {
|
||||
return Number(itemSize || 0) > LARGE_FILE_THRESHOLD_BYTES;
|
||||
}
|
||||
|
||||
function promptLargeMode(sizeBytes) {
|
||||
return confirm(t("extractLargeAsk",
|
||||
{ size: formatSize(sizeBytes, "-") }));
|
||||
}
|
||||
```
|
||||
|
||||
The 60 GiB threshold is **hardcoded**, not user-configurable. To change it,
|
||||
edit the constant on line `813` (current main branch). Setting it to
|
||||
`Infinity` silences the prompt entirely.
|
||||
|
||||
### 6.2 The two call sites
|
||||
|
||||
* `actionExtract()` (existing) — invoked from the file-row "extract" menu
|
||||
item — checks `item.size` and prompts before calling
|
||||
`startExtractTask(item.path, cwd, conflict, false, displayName(item), large)`.
|
||||
* `uploadAndExtractFile()` (existing) — invoked after a remote upload
|
||||
completes — checks `file.size` and prompts before calling
|
||||
`startExtractTask(zipPath, cwd, conflict, true, rel, large)`.
|
||||
|
||||
Both paths funnel into `startExtractTask(path, dst, conflict, removeSource,
|
||||
name, large)`, which `POST`s to `/api/extract` with:
|
||||
|
||||
```js
|
||||
const data = await apiForm("/api/extract", {
|
||||
path,
|
||||
dst_dir: dstDir,
|
||||
conflict,
|
||||
remove_source: removeSource ? "1" : "0",
|
||||
large: large ? "1" : "0"
|
||||
});
|
||||
```
|
||||
|
||||
If the user clicks **Cancel** on the prompt, `large = false` and the request
|
||||
goes out with `large=0`. **No silent fallback** to the large profile.
|
||||
|
||||
### 6.3 UI status
|
||||
|
||||
When `large=1` is sent, the engine logs `task->error_code = "err_extract_large"`
|
||||
status field so the in-app task overlay can show a "large-file profile
|
||||
active" badge. The exact badge wording lives in the locale files (see §7).
|
||||
|
||||
## 7. i18n strings (`assets/lang-{en,zh}.js`)
|
||||
|
||||
Two new keys, both on line `108-109` of each locale file:
|
||||
|
||||
* `extractLargeAsk` — the prompt body. Interpolation parameter: `size`
|
||||
(already pre-formatted by `formatSize()` in bytes / KiB / MiB / GiB / TiB).
|
||||
* `extractLargeActive` — short tag shown next to a running large-profile
|
||||
task.
|
||||
|
||||
When changing the wording, keep the `{size}` placeholder and the
|
||||
newline-separated **OK / Cancel** hint — the prompt is a `confirm()` so the
|
||||
expected user gesture is documented inside the dialog.
|
||||
|
||||
## 8. Tests (`tests/test_zip_extract.c`, `tests/make_fixtures.py`)
|
||||
|
||||
### 8.1 Coverage matrix
|
||||
|
||||
| Scenario | Default | Large | Notes |
|
||||
|---|---|---|---|
|
||||
| `basic.zip` (small, benign) | ✓ | ✓ | sanity |
|
||||
| `medium_bomb.zip` (1 MiB → ratio ≈ 238) | reject | accept | new fixture, profile switchover |
|
||||
| `bomb.zip` (4 MiB of `'A'`, ratio ≈ 1026) | reject | **reject** | large caps still apply |
|
||||
| `bomb.zip` with `tight.max_ratio = 10` | reject | reject | profile is a starting point, lower caps still enforced |
|
||||
| `zip64.zip` with `tight.max_file_bytes = 1024` | reject | reject | lowering file cap from profile |
|
||||
| Conflict policy `fail`/`overwrite`/`merge` | ✓ | ✓ | unchanged |
|
||||
|
||||
Run with:
|
||||
|
||||
```sh
|
||||
cd tests && bash run-tests.sh
|
||||
```
|
||||
|
||||
Output is a per-case `check()` style report — currently **69 checks**,
|
||||
0 failures.
|
||||
|
||||
### 8.2 New fixture — `medium_bomb.zip`
|
||||
|
||||
* Generated by `make_fixtures.py::medium_bomb()`.
|
||||
* Payload: 1 MiB of `bytes(range(256)) * 4096` (a perfect 256-byte period
|
||||
repeated 4096 times).
|
||||
* Compression ratio (with `zlib -9`): **≈ 238 : 1**, deliberately chosen to
|
||||
fall in the band `(default_cap, large_cap) = (200, 1000]`.
|
||||
* Why not the pre-existing `bomb.zip` (4 MiB of `'A'`)? Its real ratio on
|
||||
`zlib -9` is **≈ 1026**, which the large profile's 1000 cap also rejects —
|
||||
the two profiles would behave identically and the test wouldn't show the
|
||||
switchover.
|
||||
|
||||
### 8.3 `test_large_profile()` cases
|
||||
|
||||
```c
|
||||
const zipx_limits_t *d = zipx_limits_profile(ZIPX_LIMITS_DEFAULT);
|
||||
const zipx_limits_t *l = zipx_limits_profile(ZIPX_LIMITS_LARGE);
|
||||
|
||||
check(d != NULL, "default profile exists");
|
||||
check(l != NULL, "large profile exists");
|
||||
|
||||
check(d->max_total_bytes == 512ULL * 1024 * 1024 * 1024, "default 512 GiB");
|
||||
check(d->max_file_bytes == 64ULL * 1024 * 1024 * 1024, "default 64 GiB");
|
||||
check(d->max_ratio == 200, "default ratio 200");
|
||||
check(l->max_entries == 500000, "large 500K entries");
|
||||
check(l->max_total_bytes == 2ULL * 1024 * 1024 * 1024 * 1024, "large 2 TiB");
|
||||
check(l->max_file_bytes == 1ULL * 1024 * 1024 * 1024 * 1024, "large 1 TiB");
|
||||
check(l->max_ratio == 1000, "large ratio 1000");
|
||||
|
||||
expect_status("medium_bomb.zip", "out_default_medium", ZIPX_CONFLICT_FAIL, NULL,
|
||||
ZIPX_ERR_LIMIT_RATIO, "default rejects medium_bomb");
|
||||
expect_ok ("medium_bomb.zip", "out_large_medium", ZIPX_CONFLICT_FAIL, l,
|
||||
"large accepts medium_bomb");
|
||||
|
||||
/* Large caps still enforced — bomb ratio 1026 > 1000 */
|
||||
expect_status("bomb.zip", "out_large_bomb_default", ZIPX_CONFLICT_FAIL, NULL,
|
||||
ZIPX_ERR_LIMIT_RATIO, "default rejects bomb");
|
||||
expect_ok ("bomb.zip", "out_large_bomb_large", ZIPX_CONFLICT_FAIL, l,
|
||||
"large accepts bomb-ratio-1000 border");
|
||||
|
||||
/* Lowered caps remain enforced */
|
||||
zipx_limits_t tight = *l; tight.max_ratio = 10;
|
||||
expect_status("bomb.zip", "out_tight_ratio", ZIPX_CONFLICT_FAIL, &tight,
|
||||
ZIPX_ERR_LIMIT_RATIO, "tight ratio still enforced");
|
||||
```
|
||||
|
||||
(13 new `check`/`expect_*` calls in this function alone.)
|
||||
|
||||
## 9. Build & verify pipeline
|
||||
|
||||
### 9.1 Cross-compile
|
||||
|
||||
The WSL staging helper `.build/build-elf.sh` automates the SDK write-access
|
||||
workaround (the SDK lives at `/opt/ps5-payload-sdk/target/` owned by a
|
||||
different uid). The script:
|
||||
|
||||
1. Runs `make` with a staging dir under `/tmp` for write-protected sources.
|
||||
2. `sudo cp -r` the staged outputs back into the SDK tree in a single batch.
|
||||
3. Copies the resulting `web-file-mgr.elf` to both `/home/song/...` and the
|
||||
Windows desktop.
|
||||
|
||||
Incremental builds are fast — only `src/extract.c` recompiled for v1.7; the
|
||||
nine `gen/lang-*.js.c` files were regenerated because `extractLargeAsk`
|
||||
changed.
|
||||
|
||||
### 9.2 ELF sanity checks
|
||||
|
||||
```sh
|
||||
ls -la web-file-mgr.elf
|
||||
sha256sum web-file-mgr.elf # 648e4a00…
|
||||
file web-file-mgr.elf # ELF 64-bit LSB pie, x86-64
|
||||
od -An -tx1 -N20 web-file-mgr.elf | head -2 # 7f45 4c46 0201 + e_machine 003e
|
||||
```
|
||||
|
||||
### 9.3 Verifying the upgrade made it into the binary
|
||||
|
||||
`assets/*.js` are embedded as `zlib`-compressed C arrays by `gen-asset-module.py`.
|
||||
A simple `strings web-file-mgr.elf | grep` won't find them. Use
|
||||
`.build/check-elf-gzip.py`:
|
||||
|
||||
```sh
|
||||
python3 .build/check-elf-gzip.py ./web-file-mgr.elf
|
||||
# expects:
|
||||
# keys found (7/7):
|
||||
# ✓ extractLargeAsk "The archive looks large …"
|
||||
# ✓ extractLargeActive "Large-file profile is enabled for this task"
|
||||
# ✓ promptLargeMode confirm(t("extractLargeAsk", …))
|
||||
# ✓ shouldPromptLargeMode Number(itemSize || 0) > LARGE_FILE_THRESHOLD_BYTES
|
||||
# ✓ LARGE_FILE_THRESHOLD_BYTES = 60 * 1024 * 1024 * 1024
|
||||
# ✓ large ("…":"1":"0")
|
||||
# ✓ /api/extract … large: large ? "1" : "0" …
|
||||
```
|
||||
|
||||
If any of these are missing, the cross-compile did not pick up the asset
|
||||
rebuild — run `make clean && make` (or remove only `gen/`) and rebuild.
|
||||
|
||||
## 10. Backwards compatibility & migration
|
||||
|
||||
| Surface | v1.6 → v1.7 | Notes |
|
||||
|---|---|---|
|
||||
| `zipx_extract()` signature | unchanged | old callers compile clean |
|
||||
| `zipx_default_limits()` body | unchanged | still returns `&k_default_limits` |
|
||||
| `/api/extract` `large` field | new (optional) | absent → `0` (default profile) |
|
||||
| `file_task_t::extract_large` | new (last field of the extract trio) | downstream consumers reading tasks must handle the new field |
|
||||
| Frontend default behaviour | unchanged | threshold gate is new |
|
||||
| `./web-file-mgr-linux` ABI | unchanged | Linux build also rebuilt with the new symbols |
|
||||
|
||||
**Migration for downstream users**: nothing required. To opt in to large
|
||||
archives, append `large=1` to the `/api/extract` request, OR click "OK" on the
|
||||
prompt that appears for any archive > 60 GiB on disk.
|
||||
|
||||
## 11. Trade-offs and known edges
|
||||
|
||||
* **60 GiB threshold is hardcoded** — yes, deliberate. It's set where the
|
||||
archival image / dump boundary typically lives. Power users can edit
|
||||
`LARGE_FILE_THRESHOLD_BYTES` (line 813 in `assets/main.js`).
|
||||
* **The large profile trusts the user about free space** — the server does
|
||||
not run `statvfs()` against `dst_dir` before extraction. The space check
|
||||
the engine itself runs (per-entry `max_file_bytes`) is the only guard.
|
||||
* **`bomb.zip` with ratio 1026 is rejected under both profiles** — known and
|
||||
intentional. The 1000 cap is the *floor* of the relaxed policy, not a
|
||||
"ZIP bombs welcome" flag. Other ZIP-bomb-shaped payloads with the same
|
||||
ratio will hit the same wall.
|
||||
* **No multi-volume / split support** — unchanged from pre-v1.7. The
|
||||
engine reads a single archive path; spans such as `archive.zip`,
|
||||
`archive.z01`, `archive.z02` are not stitched. minizip-ng has the API;
|
||||
wiring it is on the post-v1.7 roadmap.
|
||||
* **No proxy / streaming for archives above the STAGING_DIR ceiling** — the
|
||||
staging dir lives on the same filesystem as `dst_dir` and is sized
|
||||
proportionally. 2 TiB staging is required for a worst-case 2 TiB archive.
|
||||
|
||||
## 12. Future work (post-v1.7, prioritised)
|
||||
|
||||
1. Multi-volume support via `mz_zip_open_multi()` from vendored minizip-ng.
|
||||
2. Server-side `statvfs()` preflight against `dst_dir` when the active
|
||||
profile is `LARGE`, with a clearer error if there's not enough space.
|
||||
3. Compression-ratio cap that scales with file size (≤ small files: strict
|
||||
200:1; large files: relaxed). Same vector as the explicit profile but
|
||||
automatic.
|
||||
4. Streaming extractor API — `zipx_extract_stream()` — that does not
|
||||
materialise the staging dir at all. Useful once PS5 archive > 4 TiB is a
|
||||
real workload.
|
||||
5. Server-side telemetry (opt-in) for which profile is chosen per archive
|
||||
size band, to validate the 60 GiB threshold over time.
|
||||
@@ -1,641 +0,0 @@
|
||||
# UPGRADE: v1.8 — RAR extraction support
|
||||
|
||||
> This is the long-form maintainer's manual for the v1.8 archive-engine
|
||||
> expansion. It is written for the next developer, not the user. The
|
||||
> user-facing description lives in [`README.md → RAR extraction`](../README.md#rar-extraction);
|
||||
> the release notes are in [`CHANGELOG.md`](../CHANGELOG.md). The vendoring
|
||||
> decision tree (and the v1.9 upgrade path) is at
|
||||
> [`third_party/unrar/VENDORED.md`](../third_party/unrar/VENDORED.md) — most
|
||||
> of the "why" questions are answered there, not here.
|
||||
|
||||
---
|
||||
|
||||
## 1. Scope at a glance
|
||||
|
||||
| Feature | Status | Why |
|
||||
|---|---|---|
|
||||
| Single-volume RAR 1.5 / 2.x / 3.x / 4.x / 5.x | ✅ | dmc_unrar 1.7.0 supports it. |
|
||||
| RAR with PPMd, large dictionary | ✅ | dmc_unrar supports it. |
|
||||
| Conflict policy `fail` / `overwrite` / `merge` | ✅ | Same `zipx_conflict_t` protocol. |
|
||||
| Default + `large=1` limits via `LARGE_FILE_THRESHOLD_BYTES` | ✅ | Same `zipx_limits_t` protocol. |
|
||||
| Path traversal / symlinks / duplicate / ratio bomb / CRC error | ✅ | Same `zipx_status_t` codes as ZIP. |
|
||||
| Multi-volume RAR (`.part01.rar` + `.part02.rar` + …) | ❌ | Upstream `DMC_UNRAR_ARCHIVE_UNSUPPORTED_VOLUMES`; extracted to PC. |
|
||||
| Encrypted RAR (any encrypted header or file) | ❌ | Upstream `DMC_UNRAR_ARCHIVE_UNSUPPORTED_ENCRYPTED`; no `password=` field in v1.8. |
|
||||
| Symbolic links / FIFOs inside RAR | ❌ | `DMC_UNRAR_FILE_UNSUPPORTED_LINK` ⇒ `ZIPX_ERR_SPECIAL`. |
|
||||
| RAR 1.4 (very old) | ❌ | `DMC_UNRAR_ARCHIVE_VERSION_UNSUPPORTED` ⇒ `ZIPX_ERR_UNSUPPORTED`. |
|
||||
|
||||
The "extract on a PC first" recovery is the same escape hatch the engine
|
||||
already uses for ZIP encryption and ZIP64-stitched errors — the failure
|
||||
is an `extract_unsupported` with the file name as the detail argument,
|
||||
and the frontend already shows it with bilingual retry guidance.
|
||||
|
||||
---
|
||||
|
||||
## 2. Why dmc_unrar (and the v1.9 escape hatch)
|
||||
|
||||
`dmc_unrar` was chosen over the obvious alternatives for one reason each:
|
||||
|
||||
- **vs `winrar/unrar` upstream** — the official source is `UnRAR license`,
|
||||
not OSS. Modifying it (which we need to do for the host-side test
|
||||
shim, error-translation wrapper etc.) is prohibited.
|
||||
- **vs `opello/unrar`** — faithful UnRAR 7.x mirror, supports volumes
|
||||
*and* encryption, but is a 150-file C++17 codebase with its own
|
||||
Windows / registry / threading primitives. The C++ integration cost
|
||||
(`-DRAR_SMP`, third CXX link step, `prospero-pkg-config` audit) was
|
||||
not justified by v1.8's stated requirement.
|
||||
- **vs `libarchive`** — it pulls in `libarchive` itself (~400 KiB extra)
|
||||
and still uses an UnRAR-equivalent internally. Adding libarchive for
|
||||
RAR alone costs more than it returns.
|
||||
- **vs implementing UnRAR ourselves** — not even on the table.
|
||||
|
||||
When (if) multi-volume + encrypted RAR becomes worth it, the recipe is
|
||||
short: vendor `opello/unrar`, replace `dmc_unrar.c` with their `*.cpp`
|
||||
in `third_party/unrar/`, add a CXX link step to `Makefile`, switch
|
||||
`src/rar_extract.c` to the `RAROpenArchiveEx` / `RARSetPassword` DLL
|
||||
API. **The `rar_extract()` signature, the dispatch layer and the host
|
||||
tests do not need to change.** Full step-by-step recipe is in
|
||||
[`third_party/unrar/VENDORED.md`](../third_party/unrar/VENDORED.md).
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture delta vs v1.7
|
||||
|
||||
### 3.1 The shape
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ Frontend: assets/main.js │
|
||||
│ - isExtractableArchive(item) covers .rar and .partNN.rar (NN==1) │
|
||||
│ - isRarSubVolume(item) flags .partNN.rar (NN>1) → greys button │
|
||||
│ - same LARGE_FILE_THRESHOLD_BYTES prompt for .rar as .zip │
|
||||
└─────────────────────┬────────────────────────────────────────────────┘
|
||||
│ POST /api/extract
|
||||
│ (path, dst_dir, conflict, remove_source,
|
||||
│ large) — no password= in v1.8
|
||||
┌─────────────────────▼────────────────────────────────────────────────┐
|
||||
│ Dispatch: src/extract.c │
|
||||
│ - extract_dispatch() by case-insensitive .zip / .rar suffix │
|
||||
│ - anything else → ZIPX_ERR_UNSUPPORTED, the same string the │
|
||||
│ ZIP path used to produce on its own │
|
||||
└─────────────────────┬────────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────┴─────────────┐
|
||||
│ │
|
||||
┌───────▼───────────┐ ┌──────▼─────────────────┐
|
||||
│ src/zip_extract.c │ │ src/rar_extract.c │
|
||||
│ (unchanged in v1.8)│ │ (NEW) │
|
||||
│ backend: │ │ backend: │
|
||||
│ minizip-ng + zlib│ │ dmc_unrar (vendored) │
|
||||
│ │ │ dmc_unrar_api.h │
|
||||
│ │ │ (project-authored │
|
||||
│ │ │ facade) │
|
||||
└───────┬───────────┘ └──────┬─────────────────┘
|
||||
│ │
|
||||
└─────────────┬─────────────┘
|
||||
│ zipx_status_t
|
||||
│ zipx_limits_t (default / large)
|
||||
│ zipx_conflict_t
|
||||
│ zipx_progress_t
|
||||
│ zipx_result_t
|
||||
▼
|
||||
(shared task UI / progress / error mapping)
|
||||
```
|
||||
|
||||
The dispatcher and the engines share the entire type vocabulary from
|
||||
`src/zip_extract.h`. `src/rar_extract.c` `#include`s only
|
||||
`third_party/unrar/dmc_unrar_api.h` — it does not `#include` the
|
||||
vendored `.c` and it does not reach into dmc_unrar internals.
|
||||
|
||||
### 3.2 Three-phase pipeline (shared with ZIP)
|
||||
|
||||
`rar_extract()` mirrors `zipx_extract()` exactly:
|
||||
|
||||
1. **Scan** — open the archive with `dmc_unrar_archive_init` /
|
||||
`dmc_unrar_archive_open_path`, walk every entry header with
|
||||
`dmc_unrar_read_header` (the API does not have a list-only mode;
|
||||
scan reads the file content but discards it). For each entry:
|
||||
- normalise the path (`\` → `/`, trim trailing separators),
|
||||
validate against traversal / depth / name-length / file-size /
|
||||
total-size limits,
|
||||
- reject duplicate or clashing entry names with
|
||||
`ZIPX_ERR_DUPLICATE`,
|
||||
- reject encrypted / volume / version-unsupported / link / large
|
||||
RAR via the DMC codes → mapped to `ZIPX_ERR_UNSUPPORTED` /
|
||||
`ZIPX_ERR_SPECIAL` (see `rar_translate_error()`),
|
||||
- report progress at the same throttle the ZIP engine uses.
|
||||
2. **Extract** — for each entry, write into a staging directory (one
|
||||
`.wfm-extract-{pid}-{tid}/` per task, derived from `getpid()` and the
|
||||
task id), via `dmc_unrar_extract_file_to_path`. Each staging file
|
||||
is `fsync`d before renaming, so a power-loss mid-archive does not
|
||||
leave the destination half-written. Staging lives on the same
|
||||
filesystem as the destination so the publish is `rename()` (atomic).
|
||||
3. **Publish** — apply the conflict policy (`fail` / `overwrite` /
|
||||
`merge`) — reuses `zip_extract.c`'s `publish_entry()` /
|
||||
`publish_staging()` logic verbatim.
|
||||
4. **Cleanup / rollback** — on any mid-archive failure, every published
|
||||
entry created by this task is removed, the staging directory is
|
||||
removed recursively with `nftw(..., FTW_DEPTH | FTW_PHYS)`, and the
|
||||
caller is left with `dst_dir` exactly as it was (modulo whatever
|
||||
`ZIPX_CONFLICT_OVERWRITE` had already clobbered).
|
||||
|
||||
The staging layout, fsync strategy, conflict policy plumbing, cancel /
|
||||
progress callbacks and result-mapping logic are copied from
|
||||
`zip_extract.c` — exactly once — into `rar_extract.c` so that the
|
||||
engines can evolve independently. (Refactoring them into a
|
||||
`src/archive_common/` module is on the post-v1.9 roadmap; see §10.)
|
||||
|
||||
### 3.3 Error mapping
|
||||
|
||||
`rar_translate_error()` in `src/rar_extract.c` maps the dmc_unrar
|
||||
return codes to the shared `zipx_status_t` enum so the rest of the
|
||||
project (and the frontend / task UI) cannot tell the difference
|
||||
between a ZIP failure and a RAR failure:
|
||||
|
||||
| dmc_unrar code | `zipx_status_t` |
|
||||
|---|---|
|
||||
| `DMC_UNRAR_ARCHIVE_UNSUPPORTED_VOLUMES` | `ZIPX_ERR_UNSUPPORTED` |
|
||||
| `DMC_UNRAR_ARCHIVE_UNSUPPORTED_ENCRYPTED` | `ZIPX_ERR_UNSUPPORTED` |
|
||||
| `DMC_UNRAR_FILE_UNSUPPORTED_ENCRYPTED` | `ZIPX_ERR_UNSUPPORTED` |
|
||||
| `DMC_UNRAR_FILE_UNSUPPORTED_LINK` | `ZIPX_ERR_SPECIAL` |
|
||||
| `DMC_UNRAR_FILE_UNSUPPORTED_LARGE` | `ZIPX_ERR_LIMIT_FILE` |
|
||||
| `DMC_UNRAR_ARCHIVE_SPLIT` | `ZIPX_ERR_UNSUPPORTED` |
|
||||
| `DMC_UNRAR_ARCHIVE_ANCIENT` | `ZIPX_ERR_UNSUPPORTED` |
|
||||
| `DMC_UNRAR_ARCHIVE_VERSION` | `ZIPX_ERR_UNSUPPORTED` |
|
||||
| `DMC_UNRAR_ARCHIVE_METHOD` | `ZIPX_ERR_UNSUPPORTED` |
|
||||
| `DMC_UNRAR_ARCHIVE_OPEN_FAIL` | `ZIPX_ERR_OPEN` |
|
||||
| `DMC_UNRAR_ARCHIVE_READ_FAIL` | `ZIPX_ERR_IO` |
|
||||
| `DMC_UNRAR_ARCHIVE_WRITE_FAIL` | `ZIPX_ERR_IO` |
|
||||
| `DMC_UNRAR_ARCHIVE_SEEK_FAIL` | `ZIPX_ERR_IO` |
|
||||
| `DMC_UNRAR_FILE_CRC32_FAIL` | `ZIPX_ERR_CRC` |
|
||||
| `DMC_UNRAR_ARCHIVE_NOT_RAR` | `ZIPX_ERR_FORMAT` |
|
||||
| `DMC_UNRAR_ARCHIVE_EMPTY` | `ZIPX_ERR_FORMAT` |
|
||||
| `DMC_UNRAR_ARCHIVE_INVALID_DATA` | `ZIPX_ERR_FORMAT` |
|
||||
| `DMC_UNRAR_ARCHIVE_NO_ALLOC` | `ZIPX_ERR_INTERNAL` |
|
||||
| `DMC_UNRAR_ARCHIVE_ALLOC_FAIL` | `ZIPX_ERR_INTERNAL` |
|
||||
| `DMC_UNRAR_ARCHIVE_IS_NULL` | `ZIPX_ERR_INTERNAL` |
|
||||
| `DMC_UNRAR_ARCHIVE_NOT_CLEARED` | `ZIPX_ERR_INTERNAL` |
|
||||
| `DMC_UNRAR_ARCHIVE_MISSING_FIELDS` | `ZIPX_ERR_INTERNAL` |
|
||||
|
||||
This table is exhaustive — every reachable dmc_unrar code has a
|
||||
defined mapping and there are no `default:` fall-throughs in the
|
||||
switch.
|
||||
|
||||
The progress / cancel / conflict protocol is byte-identical to ZIP:
|
||||
same `zipx_progress_t`, same `zipx_cancel_fn` signature, same
|
||||
`zipx_conflict_t` enum. The frontend never needs to branch on the
|
||||
archive format.
|
||||
|
||||
### 3.4 Behaviour contract (v1.8 — what callers can rely on)
|
||||
|
||||
For any input that produces `ZIPX_OK`:
|
||||
|
||||
- All requested entries from the archive are present in the destination,
|
||||
in the order they appear in the archive, with permissions `0644` for
|
||||
files and `0755` for directories (mirror of `zip_extract`'s default).
|
||||
- A conflict policy of `ZIPX_CONFLICT_FAIL` returns `ZIPX_ERR_CONFLICT`
|
||||
on the first collision; nothing is written.
|
||||
- `ZIPX_CONFLICT_OVERWRITE` replaces existing files with the extracted
|
||||
contents (a copy-paste of the ZIP engine's behaviour).
|
||||
- `ZIPX_CONFLICT_MERGE` keeps existing files, adds new ones.
|
||||
- `result->entries_total`, `result->entries_done`,
|
||||
`result->bytes_total`, `result->bytes_done`, `result->files_created`
|
||||
and `result->dirs_created` are all filled in.
|
||||
|
||||
For any non-`ZIPX_OK` return code:
|
||||
|
||||
- `dst_dir` is left **exactly as it was** before the call (modulo any
|
||||
`ZIPX_CONFLICT_OVERWRITE` clobbers that completed before the failure).
|
||||
- The staging directory is removed before `rar_extract()` returns.
|
||||
- `result->detail[]` and `result->message[]` are filled in for the
|
||||
UI to display.
|
||||
|
||||
For any cancellation request:
|
||||
|
||||
- The engine returns `ZIPX_ERR_CANCELED` from the next progress / cancel
|
||||
callback poll, all staging is removed, no `publish()` runs.
|
||||
|
||||
---
|
||||
|
||||
## 4. Source-tree layout
|
||||
|
||||
```
|
||||
src/
|
||||
extract.c # dispatcher (modified)
|
||||
extract.h # unchanged
|
||||
zip_extract.{c,h} # unchanged in v1.8
|
||||
rar_extract.{c,h} # NEW, ~1276 LOC in .c
|
||||
third_party/
|
||||
unrar/
|
||||
dmc_unrar.c # vendored (verbatim, 11 598 LOC)
|
||||
dmc_unrar_api.h # NEW, project-authored facade (~138 LOC)
|
||||
COPYING # GPL-2.0-or-later (vendored)
|
||||
README.md # upstream README (vendored)
|
||||
example.c # upstream usage example (vendored)
|
||||
VENDORED.md # NEW, why-dmc_unrar + v1.9 upgrade recipe
|
||||
tests/
|
||||
test_rar_extract.c # NEW, 14 checks (negative paths only)
|
||||
make_fixtures.py # adds rar_fixtures(); falls back to placeholder
|
||||
run-tests.sh # compiles dmc_unrar.o + rar_extract.o,
|
||||
# links test-rar-extract including zip_extract.o
|
||||
# so zipx_status_string / zipx_default_limits
|
||||
# / zipx_limits_profile resolve.
|
||||
assets/
|
||||
main.js # isExtractableArchive() / isRarSubVolume()
|
||||
lang-en.js, lang-zh.js # err_extract_unsupported updated
|
||||
Makefile # VERSION_TAG v1.8, third_party/unrar wired
|
||||
THIRD_PARTY_NOTICES # NEW section 3 for dmc_unrar
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Vendoring mechanic — the facade header
|
||||
|
||||
The most important *engineering* lesson from v1.8 is the pattern used
|
||||
in `third_party/unrar/dmc_unrar_api.h`. Without it, integrating dmc_unrar
|
||||
into the host test suite was a mess:
|
||||
|
||||
```c
|
||||
/* src/rar_extract.c — what we wanted to write */
|
||||
#include "dmc_unrar.h" /* dream: a real header */
|
||||
```
|
||||
|
||||
But dmc_unrar ships as a single `.c` file. The "header" content is
|
||||
inside the `.c`. Naively:
|
||||
|
||||
```c
|
||||
/* src/rar_extract.c — what the naive approach forces */
|
||||
#include "dmc_unrar.c" /* ← does NOT work as a TU separate from rar_extract.c */
|
||||
```
|
||||
|
||||
…compiles if you do the `#include` in a **fresh** translation unit, but
|
||||
**fails** when both `src/rar_extract.c` and `tests/test_rar_extract.c`
|
||||
build with `-include tests/posix_compat.h`, because that shim
|
||||
redefines `open` → `wfm_open` / `close` → `wfm_close` and the
|
||||
`dmc_unrar_io_handler` struct inside dmc_unrar would then reference
|
||||
undeclared fields. The result is a flood of `error: 'struct
|
||||
dmc_unrar_io_handler' has no member named 'wfm_open'` and similar.
|
||||
|
||||
The facade pattern fix:
|
||||
|
||||
```c
|
||||
/* third_party/unrar/dmc_unrar_api.h — project-authored, project-license */
|
||||
#pragma once
|
||||
/* Re-declare only the symbols rar_extract.c touches. The names match
|
||||
dmc_unrar's internal names so the .c compiles unmodified. */
|
||||
typedef enum { … } dmc_unrar_return;
|
||||
typedef struct dmc_unrar_archive dmc_unrar_archive; /* opaque */
|
||||
typedef struct { … } dmc_unrar_file;
|
||||
dmc_unrar_return dmc_unrar_archive_init(dmc_unrar_archive *a);
|
||||
dmc_unrar_return dmc_unrar_archive_open_path(dmc_unrar_archive *a, const char *p);
|
||||
… /* … only the symbols rar_extract.c uses */
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```c
|
||||
/* src/rar_extract.c — what the facade enables */
|
||||
#include "dmc_unrar_api.h" /* project-authored, project-licensed */
|
||||
```
|
||||
|
||||
And:
|
||||
|
||||
```c
|
||||
/* Makefile — dmc_unrar is its own TU, unmodified */
|
||||
ps5-obj/third_party/unrar/dmc_unrar.o: third_party/unrar/dmc_unrar.c
|
||||
$(CC) $(THIRD_PARTY_CFLAGS) -c -o $@ $<
|
||||
```
|
||||
|
||||
The benefits:
|
||||
|
||||
- dmc_unrar.c is **never edited**, satisfying its GPL-2.0-or-later
|
||||
purity requirement and making an opello/unrar swap a 5-line diff.
|
||||
- The host test shim that renames `open` / `close` / `mkdirat` only
|
||||
affects the engine and test files, never dmc_unrar's TU.
|
||||
- The vendored `.c` is grep-able with reference back into the project
|
||||
at exactly one symbol boundary — `dmc_unrar_api.h`.
|
||||
- Future C++ integration (opello/unrar) reuses the same facade slot;
|
||||
the body becomes `-DRARDLL` and a different header file.
|
||||
|
||||
This pattern generalises — *anytime you want to vendor a
|
||||
single-file C library that internally uses a name that your build
|
||||
system also touches*, write a 100-line facade header that re-declares
|
||||
just the symbols you use, and treat the vendored `.c` as a compile
|
||||
unit on its own.
|
||||
|
||||
---
|
||||
|
||||
## 6. Compression-ratio / format caveats specific to RAR
|
||||
|
||||
These are not bugs — they are *properties of dmc_unrar* that the
|
||||
host test suite and the engine must respect:
|
||||
|
||||
- **dmc_unrar has no `RAR_OM_LIST`** (list-only mode). The "scan"
|
||||
pass opens the archive, walks every header, and **reads the file
|
||||
content** even though it doesn't write anything out. A 500-entry
|
||||
archive with 4 GiB average entry size therefore costs ~2 TiB of
|
||||
read I/O during scan. This is acceptable for v1.8 because the
|
||||
typical PS5 use case is `one archive, few hundred MiB`, but it is
|
||||
worth documenting so a future optimisation (on-the-fly skip
|
||||
through `dmc_unrar_extract_file_to_path`) doesn't surprise the
|
||||
next reader.
|
||||
- **dmc_unrar decompresses synchronously on the same thread** that
|
||||
calls `dmc_unrar_extract_file_to_path`. Cancel callbacks are
|
||||
polled inside `dmc_unrar_read_header` (and at engine chokepoints)
|
||||
— *not* in the inner loop. A 1 GiB file extraction cannot be
|
||||
cancelled mid-decompression. A future improvement could fork a
|
||||
child process for extract so SIGKILL works deterministically.
|
||||
- **The dmc_unrar byte-swap helpers `be32toh` / `be64toh` collide
|
||||
with `<endian.h>`** on some compilers. We work around it by
|
||||
compiling with `-DDMC_UNRAR_DISABLE_BE32TOH_BE64TOH=1`, which
|
||||
lets dmc_unrar use its own internal byte-swap implementation.
|
||||
This is documented at the top of `dmc_unrar.c` and was the only
|
||||
thing the Makefile needed to set to get a green build on PS5.
|
||||
|
||||
These are engine limitations, not failure modes the user will see in
|
||||
normal operation.
|
||||
|
||||
---
|
||||
|
||||
## 7. Frontend wiring details
|
||||
|
||||
### 7.1 The detection rule
|
||||
|
||||
```js
|
||||
function isExtractableArchive(item) {
|
||||
if (item.type !== "-") return false; // file, not directory
|
||||
if (/\.zipx?$/i.test(item.name)) return true;
|
||||
if (/\.rar$/i.test(item.name)) return true;
|
||||
if (/\.part0*1\.rar$/i.test(item.name)) return true; // RAR main volume
|
||||
return false;
|
||||
}
|
||||
|
||||
function isRarSubVolume(item) {
|
||||
if (item.type !== "-") return false;
|
||||
if (/\.part0*1\.rar$/i.test(item.name)) return false; // main is not a sub
|
||||
return /\.part0*\d+\.rar$/i.test(item.name); // .partNN.rar, NN>1
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 The button-state rule
|
||||
|
||||
```js
|
||||
function renderExtractButton(items, locked) {
|
||||
const mains = items.filter(isExtractableArchive);
|
||||
const subs = items.filter(isRarSubVolume);
|
||||
if (subs.length && !mains.length) { // only sub-volumes
|
||||
extractBtn.disabled = true;
|
||||
extractBtn.title = t("extractSelectMainVolume");
|
||||
return;
|
||||
}
|
||||
if (mains.length !== 1) {
|
||||
extractBtn.hidden = mains.length !== 1;
|
||||
extractBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
extractBtn.title = t("extractToCurrent") + ": " + itemTitle(mains[0]);
|
||||
extractBtn.disabled = locked;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 The "extract on a PC first" error path
|
||||
|
||||
When the engine rejects a multi-volume / encrypted / link entry inside
|
||||
a RAR, the failure is `ZIPX_ERR_UNSUPPORTED` (or
|
||||
`ZIPX_ERR_SPECIAL`). The frontend already has
|
||||
`err_extract_unsupported` updated to:
|
||||
|
||||
> `…(only unencrypted plain ZIP and single-volume RAR are
|
||||
> supported)…` (en)
|
||||
> `…(仅支持未加密的普通 ZIP 与单卷 RAR)…` (zh)
|
||||
|
||||
The "extract on a PC first" guidance is *implicit* — when the user
|
||||
hits this message with a `.part01.rar` selected, the .part02+.rar
|
||||
tooltips + the error string are the two breadcrumbs. There is no
|
||||
explicit "unrar on your PC" button in v1.8 because the redirect is
|
||||
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
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 8. Host test suite — what 14 checks actually cover
|
||||
|
||||
`tests/test_rar_extract.c` is a **negative-path-only** suite, because
|
||||
we have no RAR writer in this repo and the host probably doesn't have
|
||||
`rar` / `7z` installed either. The suite accepts the absence of real
|
||||
fixtures as a feature: by exercising *only* the dispatch and error
|
||||
translation layers, we get coverage that is independent of whether the
|
||||
host has any RAR tooling.
|
||||
|
||||
| Check | What it asserts |
|
||||
|---|---|
|
||||
| `test_engine_dispatch_zip_renamed_rar` | `.zip` renamed to `.rar` is rejected with `ZIPX_ERR_UNSUPPORTED` (the engine decides by extension; the front-end tests the matching detection). |
|
||||
| `test_engine_dispatch_junk_rar` | A 1 KiB blob named `.rar` is rejected as `ZIPX_ERR_UNSUPPORTED` — the dmc_unrar open fails, mapping to `ZIPX_ERR_UNSUPPORTED`. |
|
||||
| `test_engine_dispatch_missing_source` | `rar_extract()` with a non-existent path returns `ZIPX_ERR_OPEN` (mapped from `DMC_UNRAR_OPEN_FAIL`). |
|
||||
| `test_engine_dispatch_null_rar_path` | `rar_extract(NULL, dst, …)` is rejected with `ZIPX_ERR_INTERNAL` — defensive, never user-visible. |
|
||||
| `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_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`. |
|
||||
| `test_translate_encrypted_unsp_to_err_unsupported` | The DMC encryption code → `ZIPX_ERR_UNSUPPORTED`. |
|
||||
| `test_translate_link_unsp_to_err_special` | The DMC link code → `ZIPX_ERR_SPECIAL`. |
|
||||
| `test_progress_throttle` | The progress callback is invoked at most every ~200 ms or every ~1 MiB extracted, like the ZIP engine. |
|
||||
|
||||
When `tests/make_fixtures.py` finds a host `rar` or `7z` writer, it
|
||||
generates a real `basic.rar` fixture, and an additional 2 checks
|
||||
(`test_rar4_basic_extract` / `test_rar5_basic_extract`) succeed
|
||||
automatically — those are *not* counted in the 14 baseline.
|
||||
|
||||
The complete count after `bash tests/run-tests.sh` is therefore **83
|
||||
checks** (69 ZIP + 14 RAR) on a RAR-less host, and **85 checks** on a
|
||||
host with `rar` installed.
|
||||
|
||||
---
|
||||
|
||||
## 9. Cross-compile and verification (user-side checklist)
|
||||
|
||||
The host suite runs anywhere. The PS5 ELF build runs in WSL with
|
||||
`PS5_PAYLOAD_SDK` set:
|
||||
|
||||
```bash
|
||||
# WSL Ubuntu-22.04 bash
|
||||
cd /home/song/ps5-web-file-manager
|
||||
export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk
|
||||
make all # ~30 s on cold
|
||||
ls -la web-file-mgr.elf # record size
|
||||
sha256sum web-file-mgr.elf # record digest
|
||||
```
|
||||
|
||||
Then in Windows-side Git Bash / PowerShell:
|
||||
|
||||
```bash
|
||||
cd "C:/Users/songl/Desktop/Web File Manager/ps5-web-file-manager"
|
||||
file web-file-mgr.elf # ELF 64-bit LSB pie, x86-64
|
||||
od -An -tx1 -N20 web-file-mgr.elf | head -2 # 7f45 4c46 0201 + e_machine 003e
|
||||
python3 .build/check-elf-gzip.py ./web-file-mgr.elf # 7 v1.7 keys + 1 v1.8 key
|
||||
```
|
||||
|
||||
The new v1.8 ELF-gzip key to verify is `err_extract_unsupported`
|
||||
("only unencrypted plain ZIP and single-volume RAR are supported").
|
||||
It must appear in the binary.
|
||||
|
||||
Then in `CHANGELOG.md`, paste the size + sha256 into the v1.8 banner
|
||||
header at the top of the file. Commit + push:
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git -c core.autocrlf=false commit -m "v1.8: RAR4/RAR5 single-volume unencrypted (dmc_unrar backend)"
|
||||
# push runs in PowerShell on the user's machine (sandbox github 502)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Roadmap (post-v1.8)
|
||||
|
||||
### 10.1 v1.9 — full RAR (multi-volume + encrypted)
|
||||
|
||||
See [`third_party/unrar/VENDORED.md`](../third_party/unrar/VENDORED.md)
|
||||
§"Upgrading to a fuller library (v1.9 plan)" for the migration recipe.
|
||||
The public `rar_extract()` signature and the dispatch layer do **not**
|
||||
need to change; only:
|
||||
|
||||
1. `third_party/unrar/dmc_unrar.c` is removed and `*.cpp` from
|
||||
`opello/unrar` are placed there.
|
||||
2. `Makefile` gains a `THIRD_PARTY_CPP_SRCS := $(wildcard
|
||||
third_party/unrar/*.cpp)` and the corresponding CXX link step.
|
||||
3. `third_party/unrar/dmc_unrar_api.h` is renamed to
|
||||
`unrar_api.h` and its bodies filled in from the rarlab DLL API
|
||||
(`RAROpenArchiveEx`, `RARSetPassword`, `RARProcessFileW`,
|
||||
`RARCloseArchive`).
|
||||
4. `src/rar_extract.c` swaps the `dmc_unrar_*` calls for the
|
||||
`RAR*` calls; the visible behaviour is the same except `password=`
|
||||
is now a real field on `POST /api/extract`.
|
||||
5. Frontend gains a password modal (HTML + CSS + JS) that pops when
|
||||
the engine returns `ZIPX_ERR_PASSWORD`, with retry semantics.
|
||||
|
||||
The host tests **should not change** — they test the dispatch /
|
||||
error-translation / limits-handoff layers, none of which see dmc_unrar.
|
||||
|
||||
### 10.2 Common archive library
|
||||
|
||||
Both engines share:
|
||||
|
||||
- staging directory management (`open_parent_dirs`, `remove_tree`,
|
||||
`cleanup_staging`, `publish_staging`, `rollback_published`);
|
||||
- duplicate-name detection (`nameset_add_path`, `nameset_check_duplicate`);
|
||||
- the `extract_progress` callback;
|
||||
- `report()` / `rarx_fail()` / `rarx_set_detail()` style error
|
||||
formatting;
|
||||
- a `time()`-based throttle for progress;
|
||||
- `chmod_0777_fd` permission normalisation.
|
||||
|
||||
These are duplicated between `src/zip_extract.c` and
|
||||
`src/rar_extract.c` today. A natural refactor is
|
||||
`src/archive_engine_common.c` exposing them; v1.9 is the moment to do
|
||||
this refactor since the RAR engine is changing anyway.
|
||||
|
||||
### 10.3 ZIP multi-volume (no decision yet)
|
||||
|
||||
The original v1.7 wishlist also included multi-volume ZIP
|
||||
(`.zip` + `.z01`, `.z02`). The implementation is similar to RAR multi-
|
||||
volume in shape but uses minizip-ng's `zip_open_from_file`-style
|
||||
APIs. Defer — no user demand on record for v1.9 yet.
|
||||
|
||||
### 10.4 Format dispatcher magic-byte sniffing
|
||||
|
||||
Today the dispatch is by extension. A magic-byte sniff for `Rar!` /
|
||||
`PK\x03\x04` would let users rename `.bin` archives and still get
|
||||
correct handling. Add when there's a real bug report — until then
|
||||
the simple suffix check is enough.
|
||||
|
||||
### 10.5 ELF size trend
|
||||
|
||||
| Version | Approx. ELF size | Notes |
|
||||
|---|---|---|
|
||||
| v1.7 | 418 KiB | ZIP only. |
|
||||
| v1.8 | ~430 KiB (est.) | + 11 598 LOC of stripped dmc_unrar code (≈ 25 KiB compressed). Actual size pending WSL cross-compile. |
|
||||
| v1.9 (opello/unrar) | ~700 KiB (est.) | + ~280 KiB of C++ UnRAR. |
|
||||
|
||||
We are still well below the 4 MiB ELF-loader cap, but a future
|
||||
addition (7z or AES ZIP) would tip us past the 1 MiB comfort line;
|
||||
that is the right moment to reconsider scope.
|
||||
|
||||
---
|
||||
|
||||
## 11. Files touched in v1.8
|
||||
|
||||
### 11.1 New
|
||||
|
||||
```
|
||||
third_party/unrar/dmc_unrar.c # 11 598 LOC, verbatim upstream
|
||||
third_party/unrar/dmc_unrar_api.h # 138 LOC, project-authored facade
|
||||
third_party/unrar/COPYING # GPL-2.0-or-later (vendored)
|
||||
third_party/unrar/README.md # upstream README (vendored)
|
||||
third_party/unrar/example.c # upstream usage example (vendored)
|
||||
third_party/unrar/VENDORED.md # 76 LOC, vendoring rationale + v1.9 path
|
||||
src/rar_extract.c # 1276 LOC, engine
|
||||
src/rar_extract.h # 34 LOC, public signature
|
||||
tests/test_rar_extract.c # 346 LOC, 14 negative-path checks
|
||||
docs/UPGRADE-v1.8-rar-support.md # this document
|
||||
```
|
||||
|
||||
### 11.2 Modified
|
||||
|
||||
```
|
||||
Makefile # VERSION_TAG v1.8; add third_party/unrar
|
||||
src/extract.c # extract_dispatch(); ends_with_ci()
|
||||
assets/main.js # isExtractableArchive / isRarSubVolume
|
||||
assets/lang-en.js # err_extract_unsupported copy
|
||||
assets/lang-zh.js # err_extract_unsupported copy
|
||||
tests/make_fixtures.py # rar_fixtures() with rar/7z/placeholder fallback
|
||||
tests/run-tests.sh # dmc_unrar.o, rar_extract.o, test_rar_extract.o
|
||||
THIRD_PARTY_NOTICES # section 3 = dmc_unrar attribution
|
||||
README.md # What's new in v1.8, RAR section, + Credits entry
|
||||
CHANGELOG.md # v1.8 block (this PR)
|
||||
docs/HANDOVER.md # progress checkmarks (D1–D6)
|
||||
```
|
||||
|
||||
### 11.3 Untouched but verified
|
||||
|
||||
```
|
||||
src/zip_extract.{c,h} # ZIP engine behaviour identical
|
||||
src/filemgr_internal.h # ZIP task struct fields unchanged
|
||||
assets/param.json # no version bump; VERSION_TAG is in the build
|
||||
src/app_installer.c # PS5 Media launcher flow unaffected
|
||||
docs/UPGRADE-v1.7-zip-large-file-profile.md # unchanged
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Closing notes
|
||||
|
||||
The hardest engineering decision in v1.8 was *not* "what RAR library
|
||||
to use" — it was "how much scope to ship in v1.8 vs v1.9." The
|
||||
original plan was multi-volume + encrypted; the audit on dmc_unrar
|
||||
+ opello/unrar's audit surface pushed that to v1.9 with a clean
|
||||
upgrade recipe. **v1.8 is therefore intentionally a smaller release
|
||||
than the planning doc (`docs/HANDOVER.md` §6) anticipated.**
|
||||
|
||||
For the next developer reading this:
|
||||
|
||||
- If you implement v1.9, the natural starting point is the
|
||||
`VENDORED.md` recipe, not this section.
|
||||
- If you are debugging an in-the-wild report ("my .rar won't
|
||||
extract"), the most common cause is multi-volume or encrypted —
|
||||
point the user at the `err_extract_unsupported` message and the
|
||||
PC-extract fallback. v1.8 is working as designed when this happens.
|
||||
- If you are adding a new archive format (7z, tar.bz2, …), the
|
||||
pattern is: (a) write a vendored facade header, (b) write a
|
||||
`src/<fmt>_extract.{c,h}` mirror of `rar_extract.c`, (c) extend
|
||||
`extract_dispatch()`, (d) add i18n strings, (e) extend the host
|
||||
test suite.
|
||||
|
||||
Everything else is the same shape as the existing engines.
|
||||
Regular → Executable
File mode changed.
+29
-18
@@ -4,13 +4,12 @@
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <ps5/kernel.h>
|
||||
|
||||
#include "asset.h"
|
||||
#include "pkg_installer.h"
|
||||
|
||||
#define INCASSET(name, file) \
|
||||
__asm__(".section .rodata\n" \
|
||||
".global " #name "\n" \
|
||||
@@ -29,20 +28,14 @@
|
||||
INCASSET(param_json, "assets/param.json");
|
||||
INCASSET(icon0_png, "assets/icon0.png");
|
||||
|
||||
int sceAppInstUtilInitialize(void);
|
||||
int sceAppInstUtilAppInstallAll(void *);
|
||||
|
||||
static int
|
||||
install_file(const char *path, const uint8_t *data, size_t size) {
|
||||
struct stat st;
|
||||
FILE *f;
|
||||
|
||||
if(!stat(path, &st)) {
|
||||
return 0;
|
||||
}
|
||||
if(errno != ENOENT) {
|
||||
return -1;
|
||||
}
|
||||
if(!(f = fopen(path, "wb"))) {
|
||||
if(!(f = fopen(path, "w"))) {
|
||||
return -1;
|
||||
}
|
||||
if(fwrite(data, size, 1, f) != 1) {
|
||||
@@ -72,13 +65,32 @@ install_app(const char *title_id, const char *dir) {
|
||||
}
|
||||
|
||||
static int
|
||||
needs_install_file(const char *path) {
|
||||
needs_update(const char *path, const uint8_t *expected, size_t expected_size) {
|
||||
struct stat st;
|
||||
uint8_t *buf;
|
||||
FILE *f;
|
||||
int mismatch;
|
||||
|
||||
if(stat(path, &st)) {
|
||||
if(stat(path, &st) || st.st_size != (off_t)expected_size) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
if(!(f = fopen(path, "r"))) {
|
||||
return 1;
|
||||
}
|
||||
if(!(buf = malloc(expected_size))) {
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
if(fread(buf, 1, expected_size, f) != expected_size) {
|
||||
free(buf);
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
mismatch = memcmp(buf, expected, expected_size);
|
||||
free(buf);
|
||||
return mismatch != 0;
|
||||
}
|
||||
|
||||
int
|
||||
@@ -92,8 +104,6 @@ app_install_if_needed(void) {
|
||||
int update_needed = 0;
|
||||
int err;
|
||||
|
||||
asset_register("/icon0.png", icon0_png, icon0_png_size, "image/png", 0);
|
||||
|
||||
snprintf(base_dir, sizeof(base_dir), "/user/app/%s", title_id);
|
||||
snprintf(sce_sys_dir, sizeof(sce_sys_dir), "/user/app/%s/sce_sys", title_id);
|
||||
snprintf(param_path, sizeof(param_path), "%s/param.json", sce_sys_dir);
|
||||
@@ -101,7 +111,8 @@ app_install_if_needed(void) {
|
||||
|
||||
if(stat(base_dir, &st)) {
|
||||
update_needed = 1;
|
||||
} else if(needs_install_file(param_path) || needs_install_file(icon_path)) {
|
||||
} else if(needs_update(param_path, param_json, param_json_size) ||
|
||||
needs_update(icon_path, icon0_png, icon0_png_size)) {
|
||||
update_needed = 1;
|
||||
}
|
||||
|
||||
@@ -111,7 +122,7 @@ app_install_if_needed(void) {
|
||||
|
||||
printf("Installing launcher app %s\n", title_id);
|
||||
|
||||
if((err = pkg_installer_initialize())) {
|
||||
if((err = sceAppInstUtilInitialize())) {
|
||||
printf("sceAppInstUtilInitialize: error 0x%08X\n", err);
|
||||
return -1;
|
||||
}
|
||||
|
||||
-761
@@ -1,761 +0,0 @@
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "filemgr_internal.h"
|
||||
#include "json_util.h"
|
||||
#include "path_util.h"
|
||||
#include "websrv.h"
|
||||
|
||||
#define DOWNLOAD_ARCHIVE_NAME_LIMIT 20
|
||||
#define DOWNLOAD_BUFFER_SIZE (2 * 1024 * 1024)
|
||||
|
||||
typedef struct tar_frame {
|
||||
char path[PATH_MAX];
|
||||
char name[PATH_MAX];
|
||||
DIR *dir;
|
||||
int header_sent;
|
||||
struct tar_frame *next;
|
||||
} tar_frame_t;
|
||||
|
||||
typedef struct tar_stream {
|
||||
file_task_t *task;
|
||||
char **paths;
|
||||
size_t path_count;
|
||||
size_t path_index;
|
||||
size_t stack_depth;
|
||||
tar_frame_t *stack;
|
||||
int fd;
|
||||
char current_file[PATH_MAX];
|
||||
unsigned long long file_remaining;
|
||||
size_t file_padding;
|
||||
size_t pad_remaining;
|
||||
char *pending;
|
||||
size_t pending_size;
|
||||
size_t pending_offset;
|
||||
int final_blocks;
|
||||
int done;
|
||||
int error;
|
||||
} tar_stream_t;
|
||||
|
||||
typedef struct download_file_stream {
|
||||
file_task_t *task;
|
||||
int fd;
|
||||
unsigned long long size;
|
||||
unsigned long long sent;
|
||||
int done;
|
||||
int error;
|
||||
} download_file_stream_t;
|
||||
|
||||
static enum MHD_Result
|
||||
download_task_request_error(struct MHD_Connection *conn, file_task_t *task,
|
||||
char **paths, size_t count, unsigned int status,
|
||||
const char *msg) {
|
||||
free_paths(paths, count);
|
||||
free_task(task);
|
||||
return send_json_error(conn, status, msg);
|
||||
}
|
||||
|
||||
static int
|
||||
tar_checksum(char *header) {
|
||||
int sum = 0;
|
||||
int i;
|
||||
|
||||
memset(header + 148, ' ', 8);
|
||||
for(i = 0; i < 512; i++) {
|
||||
sum += (unsigned char)header[i];
|
||||
}
|
||||
snprintf(header + 148, 8, "%06o", sum);
|
||||
header[154] = 0;
|
||||
header[155] = ' ';
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
tar_split_name(const char *name, char *out_name, size_t out_name_size,
|
||||
char *prefix, size_t prefix_size) {
|
||||
size_t len = strlen(name);
|
||||
const char *slash;
|
||||
|
||||
memset(out_name, 0, out_name_size);
|
||||
memset(prefix, 0, prefix_size);
|
||||
if(len < out_name_size) {
|
||||
strcpy(out_name, name);
|
||||
return 0;
|
||||
}
|
||||
for(slash = name + len; slash > name; slash--) {
|
||||
size_t prefix_len;
|
||||
size_t name_len;
|
||||
|
||||
if(*slash != '/') {
|
||||
continue;
|
||||
}
|
||||
prefix_len = (size_t)(slash - name);
|
||||
name_len = len - prefix_len - 1;
|
||||
if(prefix_len < prefix_size && name_len > 0 && name_len < out_name_size) {
|
||||
memcpy(prefix, name, prefix_len);
|
||||
prefix[prefix_len] = 0;
|
||||
memcpy(out_name, slash + 1, name_len + 1);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
tar_queue_header(tar_stream_t *s, const char *name, const struct stat *st,
|
||||
char type) {
|
||||
char header[512];
|
||||
char tar_name[100];
|
||||
char prefix[155];
|
||||
unsigned int mode = (unsigned int)(st->st_mode & 07777);
|
||||
unsigned long long size = type == '0' ? (unsigned long long)st->st_size : 0;
|
||||
long long mtime = (long long)st->st_mtime;
|
||||
|
||||
if(tar_split_name(name, tar_name, sizeof(tar_name), prefix, sizeof(prefix))) {
|
||||
return -1;
|
||||
}
|
||||
memset(header, 0, sizeof(header));
|
||||
memcpy(header, tar_name, strlen(tar_name));
|
||||
snprintf(header + 100, 8, "%07o", mode);
|
||||
snprintf(header + 108, 8, "%07o", 0);
|
||||
snprintf(header + 116, 8, "%07o", 0);
|
||||
snprintf(header + 124, 12, "%011llo", size);
|
||||
snprintf(header + 136, 12, "%011llo", (unsigned long long)mtime);
|
||||
header[156] = type;
|
||||
memcpy(header + 257, "ustar", 5);
|
||||
memcpy(header + 263, "00", 2);
|
||||
if(prefix[0]) {
|
||||
memcpy(header + 345, prefix, strlen(prefix));
|
||||
}
|
||||
tar_checksum(header);
|
||||
s->pending = malloc(sizeof(header));
|
||||
if(!s->pending) {
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
memcpy(s->pending, header, sizeof(header));
|
||||
s->pending_size = sizeof(header);
|
||||
s->pending_offset = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
tar_push_dir(tar_stream_t *s, const char *path, const char *name) {
|
||||
tar_frame_t *frame = calloc(1, sizeof(*frame));
|
||||
|
||||
if(!frame) {
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
snprintf(frame->path, sizeof(frame->path), "%s", path);
|
||||
snprintf(frame->name, sizeof(frame->name), "%s", name);
|
||||
frame->dir = opendir(path);
|
||||
if(!frame->dir) {
|
||||
int error = errno;
|
||||
free(frame);
|
||||
errno = error;
|
||||
return -1;
|
||||
}
|
||||
frame->next = s->stack;
|
||||
s->stack = frame;
|
||||
s->stack_depth++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
tar_pop_dir(tar_stream_t *s) {
|
||||
tar_frame_t *frame = s->stack;
|
||||
|
||||
if(!frame) {
|
||||
return;
|
||||
}
|
||||
s->stack = frame->next;
|
||||
if(s->stack_depth) {
|
||||
s->stack_depth--;
|
||||
}
|
||||
if(frame->dir) {
|
||||
closedir(frame->dir);
|
||||
}
|
||||
free(frame);
|
||||
}
|
||||
|
||||
static int
|
||||
tar_start_path(tar_stream_t *s, const char *path, const char *name) {
|
||||
struct stat st;
|
||||
|
||||
if(lstat(path, &st)) {
|
||||
return -1;
|
||||
}
|
||||
if(S_ISDIR(st.st_mode)) {
|
||||
char dir_name[PATH_MAX];
|
||||
snprintf(dir_name, sizeof(dir_name), "%s%s", name,
|
||||
name[strlen(name) - 1] == '/' ? "" : "/");
|
||||
return tar_push_dir(s, path, dir_name);
|
||||
}
|
||||
if(!S_ISREG(st.st_mode)) {
|
||||
errno = ENOTSUP;
|
||||
return -1;
|
||||
}
|
||||
if(tar_queue_header(s, name, &st, '0')) {
|
||||
return -1;
|
||||
}
|
||||
s->fd = open(path, O_RDONLY);
|
||||
if(s->fd < 0) {
|
||||
return -1;
|
||||
}
|
||||
snprintf(s->current_file, sizeof(s->current_file), "%s", path);
|
||||
s->file_remaining = (unsigned long long)st.st_size;
|
||||
s->file_padding = (size_t)((512 - ((unsigned long long)st.st_size % 512)) % 512);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
tar_prepare_next(tar_stream_t *s) {
|
||||
while(!s->pending && s->fd < 0 && !s->done) {
|
||||
if(s->task && task_cancel_requested(s->task)) {
|
||||
errno = ECANCELED;
|
||||
return -1;
|
||||
}
|
||||
if(s->pad_remaining) {
|
||||
size_t size = s->pad_remaining > 512 ? 512 : s->pad_remaining;
|
||||
s->pending = calloc(1, size);
|
||||
if(!s->pending) {
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
s->pending_size = size;
|
||||
s->pending_offset = 0;
|
||||
s->pad_remaining -= size;
|
||||
return 0;
|
||||
}
|
||||
if(s->stack) {
|
||||
tar_frame_t *frame = s->stack;
|
||||
struct dirent *entry;
|
||||
struct stat st;
|
||||
|
||||
if(!frame->header_sent) {
|
||||
frame->header_sent = 1;
|
||||
if(lstat(frame->path, &st) ||
|
||||
tar_queue_header(s, frame->name, &st, '5')) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
while((entry = readdir(frame->dir))) {
|
||||
char child[PATH_MAX];
|
||||
char child_name[PATH_MAX];
|
||||
|
||||
if(!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) {
|
||||
continue;
|
||||
}
|
||||
if(path_join(child, sizeof(child), frame->path, entry->d_name) ||
|
||||
path_join(child_name, sizeof(child_name), frame->name,
|
||||
entry->d_name) ||
|
||||
tar_start_path(s, child, child_name)) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
tar_pop_dir(s);
|
||||
continue;
|
||||
}
|
||||
if(s->path_index < s->path_count) {
|
||||
const char *path = s->paths[s->path_index++];
|
||||
if(tar_start_path(s, path, path_basename(path))) {
|
||||
return -1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if(s->final_blocks < 2) {
|
||||
s->pending = calloc(1, 512);
|
||||
if(!s->pending) {
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
s->pending_size = 512;
|
||||
s->pending_offset = 0;
|
||||
s->final_blocks++;
|
||||
return 0;
|
||||
}
|
||||
s->done = 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static ssize_t
|
||||
tar_read(void *cls, uint64_t pos, char *buf, size_t max) {
|
||||
tar_stream_t *s = cls;
|
||||
size_t out = 0;
|
||||
unsigned long long progress = 0;
|
||||
char progress_current[PATH_MAX] = {0};
|
||||
(void)pos;
|
||||
|
||||
while(out < max && !s->done) {
|
||||
if(s->task && task_cancel_requested(s->task)) {
|
||||
s->error = ECANCELED;
|
||||
return out ? (ssize_t)out : MHD_CONTENT_READER_END_WITH_ERROR;
|
||||
}
|
||||
if(tar_prepare_next(s)) {
|
||||
s->error = errno ? errno : EIO;
|
||||
return out ? (ssize_t)out : MHD_CONTENT_READER_END_WITH_ERROR;
|
||||
}
|
||||
if(s->pending) {
|
||||
size_t left = s->pending_size - s->pending_offset;
|
||||
size_t take = left < max - out ? left : max - out;
|
||||
memcpy(buf + out, s->pending + s->pending_offset, take);
|
||||
s->pending_offset += take;
|
||||
out += take;
|
||||
if(s->pending_offset >= s->pending_size) {
|
||||
free(s->pending);
|
||||
s->pending = NULL;
|
||||
s->pending_size = 0;
|
||||
s->pending_offset = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if(s->fd >= 0) {
|
||||
size_t want = max - out;
|
||||
ssize_t n;
|
||||
if((unsigned long long)want > s->file_remaining) {
|
||||
want = (size_t)s->file_remaining;
|
||||
}
|
||||
n = read(s->fd, buf + out, want);
|
||||
if(n < 0) {
|
||||
s->error = errno;
|
||||
return out ? (ssize_t)out : MHD_CONTENT_READER_END_WITH_ERROR;
|
||||
}
|
||||
if(!n) {
|
||||
close(s->fd);
|
||||
s->fd = -1;
|
||||
s->current_file[0] = 0;
|
||||
s->file_remaining = 0;
|
||||
continue;
|
||||
}
|
||||
out += (size_t)n;
|
||||
s->file_remaining -= (unsigned long long)n;
|
||||
progress += (unsigned long long)n;
|
||||
if(s->current_file[0]) {
|
||||
snprintf(progress_current, sizeof(progress_current), "%s",
|
||||
s->current_file);
|
||||
}
|
||||
if(!s->file_remaining) {
|
||||
close(s->fd);
|
||||
s->fd = -1;
|
||||
s->current_file[0] = 0;
|
||||
s->pad_remaining = s->file_padding;
|
||||
s->file_padding = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(s->task && progress) {
|
||||
task_update(s->task, TASK_RUNNING,
|
||||
progress_current[0] ? progress_current : NULL,
|
||||
progress, NULL);
|
||||
}
|
||||
return out ? (ssize_t)out : MHD_CONTENT_READER_END_OF_STREAM;
|
||||
}
|
||||
|
||||
static void
|
||||
tar_close(void *cls) {
|
||||
tar_stream_t *s = cls;
|
||||
int canceled;
|
||||
|
||||
if(!s) {
|
||||
return;
|
||||
}
|
||||
if(s->fd >= 0) {
|
||||
close(s->fd);
|
||||
}
|
||||
while(s->stack) {
|
||||
tar_pop_dir(s);
|
||||
}
|
||||
canceled = s->task && task_cancel_requested(s->task);
|
||||
if(s->task) {
|
||||
char current[PATH_MAX];
|
||||
snprintf(current, sizeof(current), "%s",
|
||||
s->task->current[0] ? s->task->current : s->task->src);
|
||||
if(canceled) {
|
||||
task_update(s->task, TASK_CANCELED, current, 0, "canceled");
|
||||
} else if(s->error) {
|
||||
errno = s->error;
|
||||
task_update(s->task, TASK_FAILED, current, 0, strerror(errno));
|
||||
} else if(s->done) {
|
||||
task_update(s->task, TASK_DONE, current, 0, NULL);
|
||||
} else {
|
||||
task_update(s->task, TASK_FAILED, current, 0, "client disconnected");
|
||||
}
|
||||
}
|
||||
free(s->pending);
|
||||
free_paths(s->paths, s->path_count);
|
||||
free(s);
|
||||
}
|
||||
|
||||
static int
|
||||
prepare_download_task(file_task_t *task) {
|
||||
unsigned long long total = 0;
|
||||
size_t file_count = 0;
|
||||
size_t dir_count = 0;
|
||||
size_t i;
|
||||
|
||||
task_update(task, TASK_RUNNING, "preparing", 0, NULL);
|
||||
for(i = 0; i < task->src_count; i++) {
|
||||
if(count_task_path_bytes(task, task->srcs[i], task->srcs[i],
|
||||
&total, &file_count, &dir_count)) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
task->total = total;
|
||||
task->file_count = file_count;
|
||||
task->dir_count = dir_count;
|
||||
task->updated_at = time(NULL);
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static size_t
|
||||
append_truncated_name(char *out, size_t size, size_t len, const char *name,
|
||||
size_t limit) {
|
||||
while(*name && len < limit && len + 1 < size) {
|
||||
unsigned char c = (unsigned char)*name;
|
||||
out[len++] = *name++;
|
||||
if(c >= 0x80) {
|
||||
while((*name & 0xc0) == 0x80 && len < limit && len + 1 < size) {
|
||||
out[len++] = *name++;
|
||||
}
|
||||
}
|
||||
}
|
||||
out[len] = 0;
|
||||
return len;
|
||||
}
|
||||
|
||||
static void
|
||||
download_archive_name(char *out, size_t size, char **paths, size_t count) {
|
||||
char name[PATH_MAX];
|
||||
size_t len = 0;
|
||||
size_t i;
|
||||
|
||||
out[0] = 0;
|
||||
if(count == 1) {
|
||||
path_basename_copy(paths[0], name, sizeof(name));
|
||||
append_truncated_name(out, size, 0, name, DOWNLOAD_ARCHIVE_NAME_LIMIT);
|
||||
} else {
|
||||
for(i = 0; i < count && len < DOWNLOAD_ARCHIVE_NAME_LIMIT; i++) {
|
||||
path_basename_copy(paths[i], name, sizeof(name));
|
||||
if(i && len + 1 < DOWNLOAD_ARCHIVE_NAME_LIMIT && len + 1 < size) {
|
||||
out[len++] = ' ';
|
||||
out[len] = 0;
|
||||
}
|
||||
len = append_truncated_name(out, size, len, name,
|
||||
DOWNLOAD_ARCHIVE_NAME_LIMIT);
|
||||
}
|
||||
}
|
||||
if(!out[0]) {
|
||||
snprintf(out, size, "download");
|
||||
}
|
||||
snprintf(out + strlen(out), size - strlen(out), ".tar");
|
||||
}
|
||||
|
||||
static void
|
||||
download_file_name(char *out, size_t size, const char *path) {
|
||||
path_basename_copy(path, out, size);
|
||||
if(!out[0]) {
|
||||
snprintf(out, size, "download");
|
||||
}
|
||||
}
|
||||
|
||||
static size_t
|
||||
header_quoted_filename(char *out, size_t size, const char *name) {
|
||||
size_t len = 0;
|
||||
|
||||
while(*name && len + 1 < size) {
|
||||
unsigned char c = (unsigned char)*name++;
|
||||
if(c < 0x20 || c == 0x7f || c == '"' || c == '\\') {
|
||||
c = '_';
|
||||
}
|
||||
out[len++] = (char)c;
|
||||
}
|
||||
out[len] = 0;
|
||||
return len;
|
||||
}
|
||||
|
||||
static size_t
|
||||
header_percent_filename(char *out, size_t size, const char *name) {
|
||||
static const char hex[] = "0123456789ABCDEF";
|
||||
size_t len = 0;
|
||||
|
||||
while(*name && len + 1 < size) {
|
||||
unsigned char c = (unsigned char)*name++;
|
||||
int safe = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= 'a' && c <= 'z') || c == '.' || c == '_' || c == '-';
|
||||
if(safe) {
|
||||
out[len++] = (char)c;
|
||||
} else {
|
||||
if(len + 3 >= size) {
|
||||
break;
|
||||
}
|
||||
out[len++] = '%';
|
||||
out[len++] = hex[c >> 4];
|
||||
out[len++] = hex[c & 15];
|
||||
}
|
||||
}
|
||||
out[len] = 0;
|
||||
return len;
|
||||
}
|
||||
|
||||
static void
|
||||
add_download_filename_header(struct MHD_Response *resp, const char *name) {
|
||||
char quoted[PATH_MAX];
|
||||
char encoded[PATH_MAX * 3];
|
||||
char header[PATH_MAX * 4];
|
||||
|
||||
header_quoted_filename(quoted, sizeof(quoted), name);
|
||||
header_percent_filename(encoded, sizeof(encoded), name);
|
||||
snprintf(header, sizeof(header),
|
||||
"attachment; filename=\"%s\"; filename*=UTF-8''%s",
|
||||
quoted, encoded);
|
||||
MHD_add_response_header(resp, "Content-Disposition", header);
|
||||
}
|
||||
|
||||
static enum MHD_Result
|
||||
create_download_task_response(struct MHD_Connection *conn, char **paths,
|
||||
size_t count) {
|
||||
file_task_t *task = calloc(1, sizeof(file_task_t));
|
||||
strbuf_t b = {0};
|
||||
struct stat st;
|
||||
size_t i;
|
||||
|
||||
if(!task) {
|
||||
free_paths(paths, count);
|
||||
return send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, "out of memory");
|
||||
}
|
||||
if(!count) {
|
||||
return download_task_request_error(conn, task, paths, count,
|
||||
MHD_HTTP_BAD_REQUEST, "no source paths");
|
||||
}
|
||||
for(i = 0; i < count; i++) {
|
||||
if(lstat(paths[i], &st)) {
|
||||
return download_task_request_error(conn, task, paths, count,
|
||||
MHD_HTTP_NOT_FOUND, "file not found");
|
||||
}
|
||||
if(!S_ISREG(st.st_mode) && !S_ISDIR(st.st_mode)) {
|
||||
return download_task_request_error(conn, task, paths, count,
|
||||
MHD_HTTP_BAD_REQUEST, "invalid path");
|
||||
}
|
||||
}
|
||||
task->op = TASK_DOWNLOAD;
|
||||
task->state = TASK_QUEUED;
|
||||
task->srcs = paths;
|
||||
task->src_count = count;
|
||||
snprintf(task->src, sizeof(task->src), "%s%s", paths[0],
|
||||
count > 1 ? " ..." : "");
|
||||
snprintf(task->current, sizeof(task->current), "%s", task->src);
|
||||
task->created_at = time(NULL);
|
||||
task->updated_at = task->created_at;
|
||||
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
remove_finished_tasks_locked();
|
||||
if(has_active_task_locked()) {
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
return download_task_request_error(conn, task, paths, count,
|
||||
MHD_HTTP_CONFLICT, "another task is running");
|
||||
}
|
||||
task->id = g_next_task_id++;
|
||||
task->next = g_tasks;
|
||||
g_tasks = task;
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
|
||||
strbuf_printf(&b, "{\"ok\":true,\"task_id\":%lu}", task->id);
|
||||
return send_buffer(conn, MHD_HTTP_OK, b.data, "application/json");
|
||||
}
|
||||
|
||||
enum MHD_Result
|
||||
api_download_prepare(struct MHD_Connection *conn, const char *body,
|
||||
size_t body_size) {
|
||||
char *paths_raw = body_form_value(body, body_size, "paths");
|
||||
char **paths = NULL;
|
||||
size_t count = 0;
|
||||
|
||||
if(!paths_raw || parse_paths(paths_raw, &paths, &count)) {
|
||||
free(paths_raw);
|
||||
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid path");
|
||||
}
|
||||
free(paths_raw);
|
||||
return create_download_task_response(conn, paths, count);
|
||||
}
|
||||
|
||||
static ssize_t
|
||||
download_file_read(void *cls, uint64_t pos, char *buf, size_t max) {
|
||||
download_file_stream_t *s = cls;
|
||||
ssize_t len;
|
||||
|
||||
if(task_cancel_requested(s->task)) {
|
||||
s->error = ECANCELED;
|
||||
return MHD_CONTENT_READER_END_WITH_ERROR;
|
||||
}
|
||||
len = pread(s->fd, buf, max, (off_t)pos);
|
||||
if(len < 0) {
|
||||
s->error = errno ? errno : EIO;
|
||||
return MHD_CONTENT_READER_END_WITH_ERROR;
|
||||
}
|
||||
if(!len) {
|
||||
s->done = 1;
|
||||
return MHD_CONTENT_READER_END_OF_STREAM;
|
||||
}
|
||||
{
|
||||
unsigned long long end = (unsigned long long)pos + (unsigned long long)len;
|
||||
unsigned long long add = end > s->sent ? end - s->sent : 0;
|
||||
if(end > s->sent) {
|
||||
s->sent = end;
|
||||
}
|
||||
if(s->sent >= s->size) {
|
||||
s->done = 1;
|
||||
}
|
||||
task_update(s->task, TASK_RUNNING, s->task->src, add, NULL);
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
static void
|
||||
download_file_close(void *cls) {
|
||||
download_file_stream_t *s = cls;
|
||||
int canceled;
|
||||
|
||||
if(!s) {
|
||||
return;
|
||||
}
|
||||
if(s->fd >= 0) {
|
||||
close(s->fd);
|
||||
}
|
||||
canceled = task_cancel_requested(s->task);
|
||||
if(canceled) {
|
||||
task_update(s->task, TASK_CANCELED, s->task->src, 0, "canceled");
|
||||
} else if(s->error) {
|
||||
errno = s->error;
|
||||
task_update(s->task, TASK_FAILED, s->task->src, 0, strerror(errno));
|
||||
} else if(s->done) {
|
||||
task_update(s->task, TASK_DONE, s->task->src, 0, NULL);
|
||||
} else {
|
||||
task_update(s->task, TASK_FAILED, s->task->src, 0, "client disconnected");
|
||||
}
|
||||
free(s);
|
||||
}
|
||||
|
||||
enum MHD_Result
|
||||
api_download(struct MHD_Connection *conn) {
|
||||
char *idstr = query_value(conn, "id");
|
||||
unsigned long id = idstr ? strtoul(idstr, NULL, 10) : 0;
|
||||
char **paths = NULL;
|
||||
size_t count = 0;
|
||||
struct stat st;
|
||||
tar_stream_t *stream;
|
||||
struct MHD_Response *resp;
|
||||
enum MHD_Result ret;
|
||||
file_task_t *task;
|
||||
char download_name[PATH_MAX];
|
||||
|
||||
free(idstr);
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
task = find_task_locked(id);
|
||||
if(!task || task->op != TASK_DOWNLOAD || task->state != TASK_QUEUED) {
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "active task not found");
|
||||
}
|
||||
task->state = TASK_RUNNING;
|
||||
task->updated_at = time(NULL);
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
|
||||
if(prepare_download_task(task)) {
|
||||
char current[PATH_MAX];
|
||||
snprintf(current, sizeof(current), "%s",
|
||||
task->current[0] ? task->current : task->src);
|
||||
if(errno == ECANCELED || task_cancel_requested(task)) {
|
||||
task_update(task, TASK_CANCELED, current, 0, "canceled");
|
||||
} else {
|
||||
task_update(task, TASK_FAILED, current, 0, strerror(errno));
|
||||
}
|
||||
return send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, NULL);
|
||||
}
|
||||
|
||||
paths = task->srcs;
|
||||
count = task->src_count;
|
||||
if(count == 1 && !stat(paths[0], &st) && S_ISREG(st.st_mode)) {
|
||||
download_file_stream_t *file_stream = calloc(1, sizeof(*file_stream));
|
||||
if(!file_stream) {
|
||||
task_update(task, TASK_FAILED, task->src, 0, "out of memory");
|
||||
return send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, "out of memory");
|
||||
}
|
||||
file_stream->task = task;
|
||||
file_stream->fd = -1;
|
||||
file_stream->size = (unsigned long long)st.st_size;
|
||||
file_stream->done = file_stream->size == 0;
|
||||
file_stream->fd = open(paths[0], O_RDONLY);
|
||||
if(file_stream->fd < 0) {
|
||||
free(file_stream);
|
||||
task_update(task, TASK_FAILED, task->src, 0, strerror(errno));
|
||||
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "file not found");
|
||||
}
|
||||
resp = MHD_create_response_from_callback((uint64_t)st.st_size,
|
||||
DOWNLOAD_BUFFER_SIZE,
|
||||
download_file_read, file_stream,
|
||||
download_file_close);
|
||||
if(!resp) {
|
||||
download_file_close(file_stream);
|
||||
return MHD_NO;
|
||||
}
|
||||
MHD_add_response_header(resp, MHD_HTTP_HEADER_CONTENT_TYPE,
|
||||
"application/octet-stream");
|
||||
download_file_name(download_name, sizeof(download_name), paths[0]);
|
||||
add_download_filename_header(resp, download_name);
|
||||
ret = websrv_queue_response(conn, MHD_HTTP_OK, resp);
|
||||
MHD_destroy_response(resp);
|
||||
return ret;
|
||||
}
|
||||
if(!(stream = calloc(1, sizeof(*stream)))) {
|
||||
task_update(task, TASK_FAILED, task->src, 0, "out of memory");
|
||||
return send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, "out of memory");
|
||||
}
|
||||
stream->fd = -1;
|
||||
stream->task = task;
|
||||
stream->paths = NULL;
|
||||
stream->path_count = count;
|
||||
stream->paths = calloc(count, sizeof(char *));
|
||||
if(!stream->paths) {
|
||||
tar_close(stream);
|
||||
task_update(task, TASK_FAILED, task->src, 0, "out of memory");
|
||||
return send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, "out of memory");
|
||||
}
|
||||
for(size_t i = 0; i < count; i++) {
|
||||
stream->paths[i] = strdup(paths[i]);
|
||||
if(!stream->paths[i]) {
|
||||
stream->path_count = i;
|
||||
tar_close(stream);
|
||||
task_update(task, TASK_FAILED, task->src, 0, "out of memory");
|
||||
return send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, "out of memory");
|
||||
}
|
||||
}
|
||||
resp = MHD_create_response_from_callback(MHD_SIZE_UNKNOWN,
|
||||
DOWNLOAD_BUFFER_SIZE,
|
||||
tar_read, stream, tar_close);
|
||||
if(!resp) {
|
||||
tar_close(stream);
|
||||
return MHD_NO;
|
||||
}
|
||||
MHD_add_response_header(resp, MHD_HTTP_HEADER_CONTENT_TYPE,
|
||||
"application/x-tar");
|
||||
download_archive_name(download_name, sizeof(download_name), paths, count);
|
||||
add_download_filename_header(resp, download_name);
|
||||
ret = websrv_queue_response(conn, MHD_HTTP_OK, resp);
|
||||
MHD_destroy_response(resp);
|
||||
return ret;
|
||||
}
|
||||
-269
@@ -1,269 +0,0 @@
|
||||
#include "filemgr.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "extract.h"
|
||||
#include "filemgr_internal.h"
|
||||
#include "json_util.h"
|
||||
#include "path_util.h"
|
||||
#include "rar_extract.h"
|
||||
#include "zip_extract.h"
|
||||
|
||||
/* Cancellation callback: stop when the task is asked to cancel. */
|
||||
static int
|
||||
extract_cancel(void *userdata) {
|
||||
file_task_t *task = userdata;
|
||||
|
||||
return task_cancel_requested(task);
|
||||
}
|
||||
|
||||
/* Case-insensitive suffix check. Returns 1 if path ends in suffix
|
||||
(the comparison ignores trailing slashes, so "x.rar/" is still .rar). */
|
||||
static int
|
||||
ends_with_ci(const char *path, const char *suffix) {
|
||||
size_t path_len = strlen(path);
|
||||
size_t suf_len = strlen(suffix);
|
||||
|
||||
if(path_len < suf_len) {
|
||||
return 0;
|
||||
}
|
||||
/* Trim trailing path separators (defensive — the API rejects them but
|
||||
we get here with whatever path the caller passed). */
|
||||
while(path_len && path[path_len - 1] == '/') {
|
||||
path_len--;
|
||||
}
|
||||
if(path_len < suf_len) {
|
||||
return 0;
|
||||
}
|
||||
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. */
|
||||
static void
|
||||
extract_progress(void *userdata, const zipx_progress_t *p) {
|
||||
file_task_t *task = userdata;
|
||||
unsigned long long prev_done;
|
||||
unsigned long long delta;
|
||||
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
task->entries_total = p->entries_total;
|
||||
task->entries_done = p->entries_done;
|
||||
task->total = p->bytes_total;
|
||||
prev_done = task->done;
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
|
||||
delta = p->bytes_done > prev_done ? p->bytes_done - prev_done : 0;
|
||||
task_update(task, TASK_RUNNING, p->current ? p->current : task->src,
|
||||
delta, NULL);
|
||||
}
|
||||
|
||||
static const char *
|
||||
extract_error_code(zipx_status_t status) {
|
||||
switch(status) {
|
||||
case ZIPX_ERR_OPEN: return "extract_open_failed";
|
||||
case ZIPX_ERR_FORMAT: return "extract_corrupt";
|
||||
case ZIPX_ERR_UNSUPPORTED: return "extract_unsupported";
|
||||
case ZIPX_ERR_UNSAFE_NAME: return "extract_unsafe_name";
|
||||
case ZIPX_ERR_SPECIAL: return "extract_special_entry";
|
||||
case ZIPX_ERR_DUPLICATE: return "extract_duplicate";
|
||||
case ZIPX_ERR_LIMIT_ENTRIES: return "extract_too_many_entries";
|
||||
case ZIPX_ERR_LIMIT_FILE: return "extract_entry_too_large";
|
||||
case ZIPX_ERR_LIMIT_TOTAL: return "extract_too_large";
|
||||
case ZIPX_ERR_LIMIT_RATIO: return "extract_ratio";
|
||||
case ZIPX_ERR_LIMIT_DEPTH: return "extract_too_deep";
|
||||
case ZIPX_ERR_LIMIT_NAME: return "extract_name_too_long";
|
||||
case ZIPX_ERR_CONFLICT: return "extract_conflict";
|
||||
case ZIPX_ERR_SPACE: return "no_space";
|
||||
case ZIPX_ERR_IO: return "extract_io";
|
||||
case ZIPX_ERR_CRC: return "extract_crc";
|
||||
default: return "extract_failed";
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
extract_set_error(file_task_t *task, zipx_status_t status,
|
||||
const zipx_result_t *result) {
|
||||
const char *code = extract_error_code(status);
|
||||
const char *detail = result->detail[0] ? result->detail : NULL;
|
||||
const char *msg = result->message[0] ? result->message :
|
||||
zipx_status_string(status);
|
||||
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
snprintf(task->error_code, sizeof(task->error_code), "%s", code);
|
||||
if(detail) {
|
||||
size_t n = strlen(detail);
|
||||
if(n >= sizeof(task->error_arg)) {
|
||||
n = sizeof(task->error_arg) - 1;
|
||||
}
|
||||
memcpy(task->error_arg, detail, n);
|
||||
task->error_arg[n] = 0;
|
||||
}
|
||||
task->updated_at = time(NULL);
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
|
||||
task_update(task, TASK_FAILED, detail ? detail : task->src, 0, msg);
|
||||
}
|
||||
|
||||
static void *
|
||||
extract_worker(void *arg) {
|
||||
file_task_t *task = arg;
|
||||
zipx_result_t result = {0};
|
||||
zipx_conflict_t conflict;
|
||||
zipx_status_t status;
|
||||
|
||||
switch(task->extract_conflict) {
|
||||
case EXTRACT_CONFLICT_OVERWRITE:
|
||||
conflict = ZIPX_CONFLICT_OVERWRITE;
|
||||
break;
|
||||
case EXTRACT_CONFLICT_MERGE:
|
||||
conflict = ZIPX_CONFLICT_MERGE;
|
||||
break;
|
||||
default:
|
||||
conflict = ZIPX_CONFLICT_FAIL;
|
||||
break;
|
||||
}
|
||||
|
||||
task_update(task, TASK_RUNNING, "scanning archive", 0, NULL);
|
||||
|
||||
status = extract_dispatch(task, conflict,
|
||||
zipx_limits_profile(task->extract_large),
|
||||
&result);
|
||||
|
||||
if(status == ZIPX_OK) {
|
||||
time_t completed_at = time(NULL);
|
||||
|
||||
/* Only delete the source archive when this task owns it (upload flow). */
|
||||
if(task->extract_remove_source && task->src[0]) {
|
||||
unlink(task->src);
|
||||
}
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
task->state = TASK_DONE;
|
||||
if(task->total) {
|
||||
task->done = task->total;
|
||||
}
|
||||
task->entries_done = task->entries_total;
|
||||
task->updated_at = completed_at;
|
||||
record_task_completion_locked(task, completed_at);
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
} else if(status == ZIPX_ERR_CANCELED) {
|
||||
task_update(task, TASK_CANCELED,
|
||||
task->current[0] ? task->current : task->src, 0, "canceled");
|
||||
} else {
|
||||
extract_set_error(task, status, &result);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
enum MHD_Result
|
||||
api_extract(struct MHD_Connection *conn, const char *body, size_t body_size) {
|
||||
char *path = fs_path_value(body_form_value(body, body_size, "path"));
|
||||
char *dst_dir = fs_path_value(body_form_value(body, body_size, "dst_dir"));
|
||||
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");
|
||||
extract_conflict_t conflict = EXTRACT_CONFLICT_FAIL;
|
||||
int remove_source = remove_str && !strcmp(remove_str, "1");
|
||||
int large = large_str && !strcmp(large_str, "1");
|
||||
file_task_t *task;
|
||||
strbuf_t b = {0};
|
||||
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);
|
||||
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid path");
|
||||
}
|
||||
if(conflict_str) {
|
||||
if(!strcmp(conflict_str, "overwrite")) {
|
||||
conflict = EXTRACT_CONFLICT_OVERWRITE;
|
||||
} 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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
return send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR,
|
||||
"out of memory");
|
||||
}
|
||||
|
||||
task->op = TASK_EXTRACT;
|
||||
task->state = TASK_QUEUED;
|
||||
task->extract_conflict = (int)conflict;
|
||||
task->extract_remove_source = remove_source;
|
||||
task->extract_large = large;
|
||||
snprintf(task->src, sizeof(task->src), "%s", path);
|
||||
snprintf(task->dst, sizeof(task->dst), "%s", dst_dir);
|
||||
task->created_at = time(NULL);
|
||||
task->updated_at = task->created_at;
|
||||
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
remove_finished_tasks_locked();
|
||||
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);
|
||||
return send_json_error(conn, MHD_HTTP_CONFLICT, "another task is running");
|
||||
}
|
||||
task->id = g_next_task_id++;
|
||||
task->next = g_tasks;
|
||||
g_tasks = task;
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
|
||||
if(pthread_create(&task->thread, NULL, extract_worker, task)) {
|
||||
task_update(task, TASK_FAILED, NULL, 0, "pthread_create failed");
|
||||
} else {
|
||||
pthread_detach(task->thread);
|
||||
}
|
||||
|
||||
free(path); free(dst_dir); free(conflict_str); free(remove_str); free(large_str);
|
||||
strbuf_printf(&b, "{\"ok\":true,\"task_id\":%lu}", task->id);
|
||||
return send_buffer(conn, MHD_HTTP_OK, b.data, "application/json");
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <microhttpd.h>
|
||||
|
||||
/* Conflict policy for ZIP extraction, mirrors the zipx_conflict_t values. */
|
||||
typedef enum extract_conflict {
|
||||
EXTRACT_CONFLICT_FAIL = 0,
|
||||
EXTRACT_CONFLICT_OVERWRITE = 1,
|
||||
EXTRACT_CONFLICT_MERGE = 2
|
||||
} extract_conflict_t;
|
||||
|
||||
/* POST /api/extract handler.
|
||||
Accepts a form-encoded body (matching the other filemgr endpoints):
|
||||
path - ZIP path on the device (required)
|
||||
dst_dir - target directory (required)
|
||||
conflict - "fail" (default) | "overwrite" | "merge"
|
||||
remove_source - "1" deletes the source ZIP after a successful extraction
|
||||
(used by the "upload and extract" flow; never set for a
|
||||
pre-existing user archive).
|
||||
Returns {"ok":true,"task_id":N} or a JSON error. */
|
||||
enum MHD_Result api_extract(struct MHD_Connection *conn, const char *body,
|
||||
size_t body_size);
|
||||
@@ -1,68 +0,0 @@
|
||||
#include "filemgr.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "filemgr_internal.h"
|
||||
#include "mime.h"
|
||||
#include "path_util.h"
|
||||
#include "websrv.h"
|
||||
|
||||
static ssize_t
|
||||
file_read(void *cls, uint64_t pos, char *buf, size_t max) {
|
||||
FILE *file = cls;
|
||||
size_t len;
|
||||
|
||||
if(fseek(file, (long)pos, SEEK_SET)) {
|
||||
return MHD_CONTENT_READER_END_WITH_ERROR;
|
||||
}
|
||||
if(!(len = fread(buf, 1, max, file))) {
|
||||
return ferror(file) ? MHD_CONTENT_READER_END_WITH_ERROR :
|
||||
MHD_CONTENT_READER_END_OF_STREAM;
|
||||
}
|
||||
return (ssize_t)len;
|
||||
}
|
||||
|
||||
static void
|
||||
file_close(void *cls) {
|
||||
fclose((FILE *)cls);
|
||||
}
|
||||
|
||||
enum MHD_Result
|
||||
filemgr_fs_request(struct MHD_Connection *conn) {
|
||||
char *path = fs_path_value(query_value(conn, "path"));
|
||||
struct MHD_Response *resp;
|
||||
enum MHD_Result ret = MHD_NO;
|
||||
struct stat st;
|
||||
FILE *file;
|
||||
|
||||
if(has_active_task()) {
|
||||
return send_json_error(conn, MHD_HTTP_CONFLICT, "another task is running");
|
||||
}
|
||||
|
||||
if(!path || stat(path, &st) || !S_ISREG(st.st_mode) ||
|
||||
!(file = fopen(path, "rb"))) {
|
||||
free(path);
|
||||
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "file not found");
|
||||
}
|
||||
|
||||
if((resp = MHD_create_response_from_callback((uint64_t)st.st_size,
|
||||
32 * 0x4000, file_read, file,
|
||||
file_close))) {
|
||||
const char *mime = mime_get_type(path);
|
||||
if(mime) {
|
||||
MHD_add_response_header(resp, MHD_HTTP_HEADER_CONTENT_TYPE, mime);
|
||||
}
|
||||
ret = websrv_queue_response(conn, MHD_HTTP_OK, resp);
|
||||
MHD_destroy_response(resp);
|
||||
free(path);
|
||||
return ret;
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
free(path);
|
||||
return MHD_NO;
|
||||
}
|
||||
+1465
-558
File diff suppressed because it is too large.
Load diff
@@ -6,9 +6,3 @@ enum MHD_Result filemgr_api_request(struct MHD_Connection *conn,
|
||||
const char *url, const char *method,
|
||||
const char *body, size_t body_size);
|
||||
enum MHD_Result filemgr_fs_request(struct MHD_Connection *conn);
|
||||
|
||||
int filemgr_upload_begin(struct MHD_Connection *conn, void **upload_ctx);
|
||||
int filemgr_upload_data(void *upload_ctx, const char *data, size_t size);
|
||||
enum MHD_Result filemgr_upload_finish(struct MHD_Connection *conn,
|
||||
void *upload_ctx);
|
||||
void filemgr_upload_free(void *upload_ctx);
|
||||
@@ -1,134 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <limits.h>
|
||||
#include <pthread.h>
|
||||
#include <stddef.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <microhttpd.h>
|
||||
|
||||
#define ETA_SAMPLE_SLOTS 64
|
||||
|
||||
typedef enum task_op {
|
||||
TASK_COPY,
|
||||
TASK_MOVE,
|
||||
TASK_DELETE,
|
||||
TASK_CHMOD,
|
||||
TASK_DOWNLOAD,
|
||||
TASK_UPLOAD,
|
||||
TASK_PKG_INSTALL,
|
||||
TASK_EXTRACT,
|
||||
} task_op_t;
|
||||
|
||||
typedef enum task_state {
|
||||
TASK_QUEUED,
|
||||
TASK_RUNNING,
|
||||
TASK_DONE,
|
||||
TASK_FAILED,
|
||||
TASK_CANCELED,
|
||||
} task_state_t;
|
||||
|
||||
typedef struct task_eta_sample {
|
||||
unsigned long long done;
|
||||
struct timespec time;
|
||||
} task_eta_sample_t;
|
||||
|
||||
typedef struct file_task {
|
||||
unsigned long id;
|
||||
task_op_t op;
|
||||
task_state_t state;
|
||||
char src[PATH_MAX];
|
||||
char dst[PATH_MAX];
|
||||
char current[PATH_MAX];
|
||||
char error[160];
|
||||
char error_code[64];
|
||||
char error_arg[PATH_MAX + 96];
|
||||
char **srcs;
|
||||
size_t src_count;
|
||||
size_t file_count;
|
||||
size_t dir_count;
|
||||
size_t upload_completed;
|
||||
unsigned int chmod_mode;
|
||||
int recursive;
|
||||
unsigned long long total;
|
||||
unsigned long long done;
|
||||
unsigned long long speed;
|
||||
unsigned long long eta;
|
||||
unsigned long long entries_total;
|
||||
unsigned long long entries_done;
|
||||
int extract_conflict;
|
||||
int extract_remove_source;
|
||||
int extract_large;
|
||||
unsigned long long speed_sample_done;
|
||||
struct timespec speed_sample_time;
|
||||
task_eta_sample_t eta_samples[ETA_SAMPLE_SLOTS];
|
||||
unsigned int eta_sample_next;
|
||||
unsigned int eta_sample_count;
|
||||
int cancel_requested;
|
||||
int reported; /* Terminal state has been included in /api/tasks. */
|
||||
unsigned int active_streams;
|
||||
time_t created_at;
|
||||
time_t transfer_started_at;
|
||||
time_t updated_at;
|
||||
pthread_t thread;
|
||||
struct file_task *next;
|
||||
} file_task_t;
|
||||
|
||||
extern pthread_mutex_t g_tasks_lock;
|
||||
extern file_task_t *g_tasks;
|
||||
extern unsigned long g_next_task_id;
|
||||
|
||||
const char *task_op_name(task_op_t op);
|
||||
const char *task_state_name(task_state_t state);
|
||||
int task_is_active(const file_task_t *task);
|
||||
int has_active_task_locked(void);
|
||||
int has_active_task(void);
|
||||
void free_task(file_task_t *task);
|
||||
void remove_finished_tasks_locked(void);
|
||||
int task_cancel_requested(file_task_t *task);
|
||||
file_task_t *find_task_locked(unsigned long id);
|
||||
void task_update(file_task_t *task, task_state_t state, const char *current,
|
||||
unsigned long long add_done, const char *error);
|
||||
void record_task_completion_locked(file_task_t *task, time_t completed_at);
|
||||
|
||||
enum MHD_Result send_json_ok(struct MHD_Connection *conn);
|
||||
enum MHD_Result send_json_error(struct MHD_Connection *conn,
|
||||
unsigned int status, const char *msg);
|
||||
enum MHD_Result send_json_error_detail(struct MHD_Connection *conn,
|
||||
unsigned int status, const char *msg,
|
||||
const char *code, const char *arg);
|
||||
enum MHD_Result send_buffer(struct MHD_Connection *conn, unsigned int status,
|
||||
char *data, const char *mime);
|
||||
|
||||
int ensure_parent_dirs(const char *base, const char *rel);
|
||||
int chmod_path_mode(const char *path, unsigned int mode);
|
||||
int chmod_path_0777(const char *path);
|
||||
int fchmod_0777(int fd);
|
||||
int ignore_chmod_error(int err);
|
||||
int mode_access(const char *path, int mode);
|
||||
int check_target_writable(const char *target, char ***checked_dirs,
|
||||
size_t *checked_dir_count,
|
||||
char *error, size_t error_size,
|
||||
char *code, size_t code_size,
|
||||
char *arg, size_t arg_size);
|
||||
int check_target_space(const char *target, unsigned long long required,
|
||||
char *error, size_t error_size,
|
||||
char *code, size_t code_size,
|
||||
char *arg, size_t arg_size);
|
||||
int target_available_space(const char *target, unsigned long long *available);
|
||||
int count_task_path_bytes(file_task_t *task, const char *path,
|
||||
const char *display, unsigned long long *total,
|
||||
size_t *file_count, size_t *dir_count);
|
||||
|
||||
enum MHD_Result api_upload_prepare(struct MHD_Connection *conn,
|
||||
const char *body, size_t body_size);
|
||||
enum MHD_Result api_upload_finish(struct MHD_Connection *conn);
|
||||
enum MHD_Result api_download_prepare(struct MHD_Connection *conn,
|
||||
const char *body, size_t body_size);
|
||||
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_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,
|
||||
size_t body_size);
|
||||
-371
@@ -1,371 +0,0 @@
|
||||
#include "filemgr_internal.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/statvfs.h>
|
||||
#include <sys/types.h>
|
||||
#ifdef __linux__
|
||||
#include <sys/vfs.h>
|
||||
#else
|
||||
#include <sys/mount.h>
|
||||
#endif
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "path_util.h"
|
||||
|
||||
int
|
||||
ignore_chmod_error(int err) {
|
||||
return err == ENOTSUP || err == EPERM || err == EINVAL || err == EROFS;
|
||||
}
|
||||
|
||||
#ifndef __linux__
|
||||
static int
|
||||
fs_type_has_unix_modes(const char *type) {
|
||||
return strcmp(type, "exfat") &&
|
||||
strcmp(type, "exfatfs") &&
|
||||
strcmp(type, "msdosfs") &&
|
||||
strcmp(type, "fat") &&
|
||||
strcmp(type, "vfat");
|
||||
}
|
||||
#endif
|
||||
|
||||
static int
|
||||
path_has_unix_modes(const char *path) {
|
||||
#ifdef __linux__
|
||||
(void)path;
|
||||
return 1;
|
||||
#else
|
||||
struct statfs fs;
|
||||
|
||||
if(statfs(path, &fs)) {
|
||||
return 1;
|
||||
}
|
||||
return fs_type_has_unix_modes(fs.f_fstypename);
|
||||
#endif
|
||||
}
|
||||
|
||||
static int
|
||||
fd_has_unix_modes(int fd) {
|
||||
#ifdef __linux__
|
||||
(void)fd;
|
||||
return 1;
|
||||
#else
|
||||
struct statfs fs;
|
||||
|
||||
if(fstatfs(fd, &fs)) {
|
||||
return 1;
|
||||
}
|
||||
return fs_type_has_unix_modes(fs.f_fstypename);
|
||||
#endif
|
||||
}
|
||||
|
||||
int
|
||||
chmod_path_mode(const char *path, unsigned int mode) {
|
||||
if(!path_has_unix_modes(path)) {
|
||||
return 0;
|
||||
}
|
||||
if(chmod(path, (mode_t)(mode & 0777))) {
|
||||
/* A filesystem that does not implement Unix modes is compatible with the
|
||||
paste behavior. Real permission and read-only errors must reach the UI. */
|
||||
if(errno != ENOTSUP && errno != EINVAL) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
chmod_path_0777(const char *path) {
|
||||
if(!path_has_unix_modes(path)) {
|
||||
return 0;
|
||||
}
|
||||
if(chmod(path, 0777) && !ignore_chmod_error(errno)) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
fchmod_0777(int fd) {
|
||||
if(!fd_has_unix_modes(fd)) {
|
||||
return 0;
|
||||
}
|
||||
if(fchmod(fd, 0777) && !ignore_chmod_error(errno)) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
target_statvfs(const char *target, struct statvfs *vfs) {
|
||||
char parent[PATH_MAX];
|
||||
|
||||
if(!statvfs(target, vfs)) {
|
||||
return 0;
|
||||
}
|
||||
if(path_dirname(target, parent, sizeof(parent))) {
|
||||
return -1;
|
||||
}
|
||||
return statvfs(parent, vfs);
|
||||
}
|
||||
|
||||
int
|
||||
target_available_space(const char *target, unsigned long long *available) {
|
||||
struct statvfs vfs;
|
||||
unsigned long long block_size;
|
||||
|
||||
if(target_statvfs(target, &vfs)) {
|
||||
return -1;
|
||||
}
|
||||
block_size = vfs.f_frsize ? vfs.f_frsize : vfs.f_bsize;
|
||||
*available = (unsigned long long)vfs.f_bavail * block_size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
check_target_space(const char *target, unsigned long long required,
|
||||
char *error, size_t error_size,
|
||||
char *code, size_t code_size,
|
||||
char *arg, size_t arg_size) {
|
||||
unsigned long long available;
|
||||
|
||||
if(!required) {
|
||||
return 0;
|
||||
}
|
||||
if(target_available_space(target, &available)) {
|
||||
snprintf(error, error_size, "cannot read target free space");
|
||||
snprintf(code, code_size, "space_check_failed");
|
||||
snprintf(arg, arg_size, "%s", target);
|
||||
return -1;
|
||||
}
|
||||
if(available < required) {
|
||||
snprintf(error, error_size,
|
||||
"not enough target space, required %llu bytes, available %llu bytes",
|
||||
required, available);
|
||||
snprintf(code, code_size, "no_space");
|
||||
snprintf(arg, arg_size, "%llu,%llu", required, available);
|
||||
errno = ENOSPC;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
set_error_detail(char *error, size_t error_size, char *code, size_t code_size,
|
||||
char *arg, size_t arg_size, const char *error_code,
|
||||
const char *message, const char *path) {
|
||||
snprintf(error, error_size, "%s: %s", message, path);
|
||||
snprintf(code, code_size, "%s", error_code);
|
||||
snprintf(arg, arg_size, "%s", path);
|
||||
}
|
||||
|
||||
static void
|
||||
set_permission_error_detail(char *error, size_t error_size,
|
||||
char *code, size_t code_size,
|
||||
char *arg, size_t arg_size,
|
||||
const char *error_code, const char *message,
|
||||
const char *path) {
|
||||
struct stat st;
|
||||
|
||||
set_error_detail(error, error_size, code, code_size, arg, arg_size,
|
||||
error_code, message, path);
|
||||
if(!stat(path, &st)) {
|
||||
snprintf(arg, arg_size, "%s (mode=%04o, uid=%lu, gid=%lu)", path,
|
||||
(unsigned int)(st.st_mode & 07777),
|
||||
(unsigned long)st.st_uid, (unsigned long)st.st_gid);
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
mode_access_stat(const struct stat *st, int mode) {
|
||||
mode_t allowed;
|
||||
uid_t uid = geteuid();
|
||||
|
||||
if(uid == 0) {
|
||||
if(!(mode & X_OK) || !S_ISREG(st->st_mode) ||
|
||||
(st->st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) {
|
||||
return 0;
|
||||
}
|
||||
} else if(uid == st->st_uid) {
|
||||
allowed = (st->st_mode >> 6) & 7;
|
||||
if((allowed & mode) == (mode_t)mode) {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
gid_t gid = getegid();
|
||||
int group_match = gid == st->st_gid;
|
||||
|
||||
if(!group_match) {
|
||||
int count = getgroups(0, NULL);
|
||||
gid_t *groups = count > 0 ? malloc((size_t)count * sizeof(*groups)) : NULL;
|
||||
|
||||
if(groups && getgroups(count, groups) == count) {
|
||||
int i;
|
||||
for(i = 0; i < count; i++) {
|
||||
if(groups[i] == st->st_gid) {
|
||||
group_match = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
free(groups);
|
||||
}
|
||||
allowed = group_match ? (st->st_mode >> 3) & 7 : st->st_mode & 7;
|
||||
if((allowed & mode) == (mode_t)mode) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
errno = EACCES;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int
|
||||
mode_access(const char *path, int mode) {
|
||||
struct stat st;
|
||||
|
||||
return stat(path, &st) ? -1 : mode_access_stat(&st, mode);
|
||||
}
|
||||
|
||||
static int
|
||||
probe_dir_writable(const char *path) {
|
||||
char name[80];
|
||||
char probe[PATH_MAX];
|
||||
int attempt;
|
||||
|
||||
for(attempt = 0; attempt < 16; attempt++) {
|
||||
int fd;
|
||||
int error = 0;
|
||||
|
||||
snprintf(name, sizeof(name), ".web-file-mgr-%ld-%lld-%d.tmp",
|
||||
(long)getpid(), (long long)time(NULL), attempt);
|
||||
if(path_join(probe, sizeof(probe), path, name)) {
|
||||
return -1;
|
||||
}
|
||||
fd = open(probe, O_WRONLY | O_CREAT | O_EXCL, 0600);
|
||||
if(fd < 0) {
|
||||
if(errno == EEXIST) {
|
||||
continue;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if(close(fd)) {
|
||||
error = errno;
|
||||
}
|
||||
if(unlink(probe) && !error) {
|
||||
error = errno;
|
||||
}
|
||||
if(error) {
|
||||
errno = error;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
errno = EEXIST;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
probe_file_writable(const char *path) {
|
||||
int fd = open(path, O_WRONLY);
|
||||
|
||||
return fd < 0 ? -1 : close(fd);
|
||||
}
|
||||
|
||||
static int
|
||||
path_seen(char **paths, size_t count, const char *path) {
|
||||
size_t i;
|
||||
|
||||
for(i = 0; i < count; i++) {
|
||||
if(!strcmp(paths[i], path)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
remember_path(char ***paths, size_t *count, const char *path) {
|
||||
char **tmp;
|
||||
|
||||
if(path_seen(*paths, *count, path)) {
|
||||
return 0;
|
||||
}
|
||||
tmp = realloc(*paths, sizeof(char *) * (*count + 1));
|
||||
if(!tmp) {
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
*paths = tmp;
|
||||
(*paths)[*count] = strdup(path);
|
||||
if(!(*paths)[*count]) {
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
(*count)++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
probe_dir_once(const char *path, char ***checked, size_t *checked_count) {
|
||||
if(path_seen(*checked, *checked_count, path)) {
|
||||
return 0;
|
||||
}
|
||||
if(probe_dir_writable(path)) {
|
||||
return -1;
|
||||
}
|
||||
return remember_path(checked, checked_count, path);
|
||||
}
|
||||
|
||||
int
|
||||
check_target_writable(const char *target, char ***checked_dirs,
|
||||
size_t *checked_dir_count, char *error, size_t error_size,
|
||||
char *code, size_t code_size, char *arg, size_t arg_size) {
|
||||
struct stat st;
|
||||
char parent[PATH_MAX];
|
||||
|
||||
if(!stat(target, &st)) {
|
||||
if(S_ISDIR(st.st_mode)) {
|
||||
if(probe_dir_once(target, checked_dirs, checked_dir_count)) {
|
||||
set_permission_error_detail(error, error_size, code, code_size,
|
||||
arg, arg_size, "target_dir_not_writable",
|
||||
"target directory is not writable", target);
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
if(probe_file_writable(target)) {
|
||||
set_permission_error_detail(error, error_size, code, code_size,
|
||||
arg, arg_size, "target_file_not_writable",
|
||||
"target file is not writable", target);
|
||||
return -1;
|
||||
}
|
||||
goto check_parent;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if(errno != ENOENT) {
|
||||
set_error_detail(error, error_size, code, code_size, arg, arg_size,
|
||||
"target_check_failed", "cannot check target path", target);
|
||||
return -1;
|
||||
}
|
||||
|
||||
check_parent:
|
||||
if(path_dirname(target, parent, sizeof(parent))) {
|
||||
set_error_detail(error, error_size, code, code_size, arg, arg_size,
|
||||
"target_check_failed", "cannot check target path", target);
|
||||
return -1;
|
||||
}
|
||||
if(probe_dir_once(parent, checked_dirs, checked_dir_count)) {
|
||||
set_permission_error_detail(error, error_size, code, code_size,
|
||||
arg, arg_size, "target_parent_not_writable",
|
||||
"current directory is not writable", parent);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
#include "json_util.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
int
|
||||
strbuf_reserve(strbuf_t *b, size_t extra) {
|
||||
size_t need = b->len + extra + 1;
|
||||
char *tmp;
|
||||
size_t cap;
|
||||
|
||||
if(need <= b->cap) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
cap = b->cap ? b->cap : 4096;
|
||||
while(cap < need) {
|
||||
cap *= 2;
|
||||
}
|
||||
|
||||
if(!(tmp = realloc(b->data, cap))) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
b->data = tmp;
|
||||
b->cap = cap;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
strbuf_append(strbuf_t *b, const char *s) {
|
||||
size_t n = strlen(s);
|
||||
if(strbuf_reserve(b, n)) {
|
||||
return -1;
|
||||
}
|
||||
memcpy(b->data + b->len, s, n + 1);
|
||||
b->len += n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
strbuf_printf(strbuf_t *b, const char *fmt, ...) {
|
||||
va_list ap;
|
||||
va_list cp;
|
||||
int n;
|
||||
|
||||
va_start(ap, fmt);
|
||||
va_copy(cp, ap);
|
||||
n = vsnprintf(NULL, 0, fmt, cp);
|
||||
va_end(cp);
|
||||
if(n < 0 || strbuf_reserve(b, (size_t)n)) {
|
||||
va_end(ap);
|
||||
return -1;
|
||||
}
|
||||
|
||||
vsnprintf(b->data + b->len, b->cap - b->len, fmt, ap);
|
||||
va_end(ap);
|
||||
b->len += (size_t)n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
json_escape(strbuf_t *b, const char *s) {
|
||||
if(strbuf_append(b, "\"")) return -1;
|
||||
while(*s) {
|
||||
unsigned char c = (unsigned char)*s;
|
||||
switch(c) {
|
||||
case '"': if(strbuf_append(b, "\\\"")) return -1; s++; break;
|
||||
case '\\': if(strbuf_append(b, "\\\\")) return -1; s++; break;
|
||||
case '\b': if(strbuf_append(b, "\\b")) return -1; s++; break;
|
||||
case '\f': if(strbuf_append(b, "\\f")) return -1; s++; break;
|
||||
case '\n': if(strbuf_append(b, "\\n")) return -1; s++; break;
|
||||
case '\r': if(strbuf_append(b, "\\r")) return -1; s++; break;
|
||||
case '\t': if(strbuf_append(b, "\\t")) return -1; s++; break;
|
||||
default:
|
||||
if(c < 0x20 || c >= 0x80) {
|
||||
if(strbuf_printf(b, "\\u%04x", c)) return -1;
|
||||
} else {
|
||||
if(strbuf_reserve(b, 1)) return -1;
|
||||
b->data[b->len++] = (char)c;
|
||||
b->data[b->len] = 0;
|
||||
}
|
||||
s++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return strbuf_append(b, "\"");
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
typedef struct strbuf {
|
||||
char *data;
|
||||
size_t len;
|
||||
size_t cap;
|
||||
} strbuf_t;
|
||||
|
||||
int strbuf_reserve(strbuf_t *b, size_t extra);
|
||||
int strbuf_append(strbuf_t *b, const char *s);
|
||||
int strbuf_printf(strbuf_t *b, const char *fmt, ...);
|
||||
int json_escape(strbuf_t *b, const char *s);
|
||||
-84
@@ -1,84 +0,0 @@
|
||||
#include "filemgr_internal.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "json_util.h"
|
||||
#include "path_util.h"
|
||||
|
||||
static char
|
||||
mode_type(const struct stat *st) {
|
||||
if(S_ISDIR(st->st_mode)) return 'd';
|
||||
if(S_ISLNK(st->st_mode)) return 'l';
|
||||
if(S_ISCHR(st->st_mode)) return 'c';
|
||||
if(S_ISBLK(st->st_mode)) return 'b';
|
||||
if(S_ISFIFO(st->st_mode)) return 'p';
|
||||
if(S_ISSOCK(st->st_mode)) return 's';
|
||||
return '-';
|
||||
}
|
||||
|
||||
enum MHD_Result
|
||||
api_list(struct MHD_Connection *conn) {
|
||||
char *path = fs_path_value(query_value(conn, "path"));
|
||||
DIR *dir;
|
||||
struct dirent *entry;
|
||||
struct stat st;
|
||||
strbuf_t b = {0};
|
||||
int first = 1;
|
||||
|
||||
if(has_active_task()) {
|
||||
return send_json_error(conn, MHD_HTTP_CONFLICT, "another task is running");
|
||||
}
|
||||
|
||||
if(!path) {
|
||||
path = strdup("/");
|
||||
}
|
||||
if(!(dir = opendir(path))) {
|
||||
free(path);
|
||||
return send_json_error(conn, MHD_HTTP_NOT_FOUND, NULL);
|
||||
}
|
||||
|
||||
strbuf_append(&b, "{\"ok\":true,\"path\":");
|
||||
json_escape(&b, path);
|
||||
strbuf_append(&b, ",\"parent\":");
|
||||
char parent[PATH_MAX];
|
||||
if(path_dirname(path, parent, sizeof(parent))) {
|
||||
strcpy(parent, "/");
|
||||
}
|
||||
json_escape(&b, parent);
|
||||
strbuf_append(&b, ",\"entries\":[");
|
||||
|
||||
while((entry = readdir(dir))) {
|
||||
char child[PATH_MAX];
|
||||
|
||||
if(!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) {
|
||||
continue;
|
||||
}
|
||||
if(path_join(child, sizeof(child), path, entry->d_name) ||
|
||||
lstat(child, &st)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!first) {
|
||||
strbuf_append(&b, ",");
|
||||
}
|
||||
first = 0;
|
||||
strbuf_append(&b, "{\"name\":");
|
||||
json_escape(&b, entry->d_name);
|
||||
strbuf_append(&b, ",\"path\":");
|
||||
json_escape(&b, child);
|
||||
strbuf_printf(&b, ",\"type\":\"%c\",\"mode\":%u,\"size\":%lld,\"mtime\":%lld}",
|
||||
mode_type(&st), (unsigned int)(st.st_mode & 07777),
|
||||
(long long)st.st_size, (long long)st.st_mtime);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
free(path);
|
||||
strbuf_append(&b, "]}");
|
||||
return send_buffer(conn, MHD_HTTP_OK, b.data, "application/json");
|
||||
}
|
||||
@@ -150,9 +150,6 @@ main(int argc, char **argv) {
|
||||
#endif
|
||||
|
||||
websrv_listen(port);
|
||||
if(websrv_stop_requested()) {
|
||||
break;
|
||||
}
|
||||
sleep(3);
|
||||
}
|
||||
|
||||
|
||||
-401
@@ -1,401 +0,0 @@
|
||||
#include "path_util.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
char *
|
||||
query_value(struct MHD_Connection *conn, const char *key) {
|
||||
const char *raw = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, key);
|
||||
char *value = raw ? strdup(raw) : NULL;
|
||||
|
||||
if(value && !strcmp(key, "name")) {
|
||||
char *start = value;
|
||||
char *end;
|
||||
while(isspace((unsigned char)*start)) start++;
|
||||
end = start + strlen(start);
|
||||
while(end > start && isspace((unsigned char)end[-1])) end--;
|
||||
memmove(value, start, (size_t)(end - start));
|
||||
value[end - start] = 0;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
char *
|
||||
header_value(struct MHD_Connection *conn, const char *key) {
|
||||
const char *raw = MHD_lookup_connection_value(conn, MHD_HEADER_KIND, key);
|
||||
return raw ? form_decode(raw, strlen(raw)) : NULL;
|
||||
}
|
||||
|
||||
char *
|
||||
request_value(struct MHD_Connection *conn, const char *header,
|
||||
const char *query) {
|
||||
char *value = header_value(conn, header);
|
||||
return value ? value : query_value(conn, query);
|
||||
}
|
||||
|
||||
static int
|
||||
hex_value(char c) {
|
||||
if(c >= '0' && c <= '9') return c - '0';
|
||||
if(c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if(c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
char *
|
||||
form_decode(const char *src, size_t len) {
|
||||
char *out = malloc(len + 1);
|
||||
size_t i;
|
||||
size_t j = 0;
|
||||
|
||||
if(!out) {
|
||||
return NULL;
|
||||
}
|
||||
for(i = 0; i < len; i++) {
|
||||
if(src[i] == '+') {
|
||||
out[j++] = ' ';
|
||||
} else if(src[i] == '%' && i + 2 < len) {
|
||||
int hi = hex_value(src[i + 1]);
|
||||
int lo = hex_value(src[i + 2]);
|
||||
if(hi >= 0 && lo >= 0) {
|
||||
out[j++] = (char)((hi << 4) | lo);
|
||||
i += 2;
|
||||
} else {
|
||||
out[j++] = src[i];
|
||||
}
|
||||
} else {
|
||||
out[j++] = src[i];
|
||||
}
|
||||
}
|
||||
out[j] = 0;
|
||||
return out;
|
||||
}
|
||||
|
||||
char *
|
||||
body_form_value(const char *body, size_t body_size, const char *key) {
|
||||
size_t key_len = strlen(key);
|
||||
size_t pos = 0;
|
||||
|
||||
while(body && pos < body_size) {
|
||||
size_t start = pos;
|
||||
size_t end;
|
||||
size_t eq;
|
||||
|
||||
while(pos < body_size && body[pos] != '&') {
|
||||
pos++;
|
||||
}
|
||||
end = pos;
|
||||
if(pos < body_size && body[pos] == '&') {
|
||||
pos++;
|
||||
}
|
||||
eq = start;
|
||||
while(eq < end && body[eq] != '=') {
|
||||
eq++;
|
||||
}
|
||||
if(eq - start == key_len && !strncmp(body + start, key, key_len)) {
|
||||
return form_decode(body + eq + (eq < end), end - eq - (eq < end));
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void
|
||||
free_paths(char **paths, size_t count) {
|
||||
size_t i;
|
||||
|
||||
if(!paths) {
|
||||
return;
|
||||
}
|
||||
for(i = 0; i < count; i++) {
|
||||
free(paths[i]);
|
||||
}
|
||||
free(paths);
|
||||
}
|
||||
|
||||
int
|
||||
parse_paths(const char *raw, char ***out_paths, size_t *out_count) {
|
||||
char *copy;
|
||||
char *line;
|
||||
char *save;
|
||||
char **paths = NULL;
|
||||
size_t count = 0;
|
||||
size_t capacity = 0;
|
||||
const char *p;
|
||||
int in_line = 0;
|
||||
|
||||
*out_paths = NULL;
|
||||
*out_count = 0;
|
||||
|
||||
if(!raw || !raw[0]) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
for(p = raw; *p; p++) {
|
||||
if(*p == '\n') {
|
||||
if(in_line) {
|
||||
capacity++;
|
||||
in_line = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
in_line = 1;
|
||||
}
|
||||
if(in_line) {
|
||||
capacity++;
|
||||
}
|
||||
if(!capacity) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(!(paths = calloc(capacity, sizeof(char *))) ||
|
||||
!(copy = strdup(raw))) {
|
||||
free(paths);
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
|
||||
for(line = strtok_r(copy, "\n", &save); line; line = strtok_r(NULL, "\n", &save)) {
|
||||
if(!line[0]) {
|
||||
continue;
|
||||
}
|
||||
if(!(paths[count] = fs_path_value(strdup(line)))) {
|
||||
free_paths(paths, count);
|
||||
free(copy);
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
|
||||
free(copy);
|
||||
if(!count) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_paths = paths;
|
||||
*out_count = count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *
|
||||
path_basename(const char *path) {
|
||||
const char *end = path + strlen(path);
|
||||
const char *base;
|
||||
|
||||
while(end > path && end[-1] == '/') {
|
||||
end--;
|
||||
}
|
||||
base = end;
|
||||
while(base > path && base[-1] != '/') {
|
||||
base--;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
void
|
||||
path_basename_copy(const char *path, char *out, size_t size) {
|
||||
const char *end = path + strlen(path);
|
||||
const char *base;
|
||||
size_t len;
|
||||
|
||||
while(end > path && end[-1] == '/') {
|
||||
end--;
|
||||
}
|
||||
base = end;
|
||||
while(base > path && base[-1] != '/') {
|
||||
base--;
|
||||
}
|
||||
len = (size_t)(end - base);
|
||||
if(!len) {
|
||||
snprintf(out, size, "root");
|
||||
return;
|
||||
}
|
||||
if(len >= size) {
|
||||
len = size - 1;
|
||||
}
|
||||
memcpy(out, base, len);
|
||||
out[len] = 0;
|
||||
}
|
||||
|
||||
int
|
||||
path_dirname(const char *path, char *out, size_t size) {
|
||||
const char *base = path_basename(path);
|
||||
size_t len = (size_t)(base - path);
|
||||
|
||||
while(len > 1 && path[len - 1] == '/') {
|
||||
len--;
|
||||
}
|
||||
if(!len) {
|
||||
len = 1;
|
||||
}
|
||||
if(len >= size) {
|
||||
return -1;
|
||||
}
|
||||
memcpy(out, path, len);
|
||||
out[len] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
path_join(char *out, size_t size, const char *dir, const char *name) {
|
||||
int n;
|
||||
if(!dir || !name || !dir[0] || !name[0] || strchr(name, '/')) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
n = snprintf(out, size, "%s%s%s", dir,
|
||||
(strcmp(dir, "/") && dir[strlen(dir) - 1] != '/') ? "/" : "",
|
||||
name);
|
||||
if(n < 0 || (size_t)n >= size) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
relative_path_safe(const char *path) {
|
||||
const char *p = path;
|
||||
|
||||
if(!path || !path[0] || path[0] == '/') {
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
while(*p) {
|
||||
const char *start = p;
|
||||
size_t len;
|
||||
|
||||
while(*p && *p != '/') {
|
||||
if(*p == '\\') {
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
p++;
|
||||
}
|
||||
len = (size_t)(p - start);
|
||||
if(!len || (len == 1 && start[0] == '.') ||
|
||||
(len == 2 && start[0] == '.' && start[1] == '.')) {
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
if(*p == '/') {
|
||||
p++;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int
|
||||
path_join_relative(char *out, size_t size, const char *dir, const char *rel) {
|
||||
int n;
|
||||
|
||||
if(!relative_path_safe(rel)) {
|
||||
return -1;
|
||||
}
|
||||
n = snprintf(out, size, "%s%s%s", dir,
|
||||
(strcmp(dir, "/") && dir[strlen(dir) - 1] != '/') ? "/" : "",
|
||||
rel);
|
||||
if(n < 0 || (size_t)n >= size) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
utf8_decode_char(const char **ps, unsigned int *out) {
|
||||
const unsigned char *s = (const unsigned char *)*ps;
|
||||
unsigned char c = s[0];
|
||||
unsigned int cp;
|
||||
size_t n;
|
||||
size_t i;
|
||||
|
||||
if(c < 0x80) {
|
||||
*out = c;
|
||||
*ps += 1;
|
||||
return 0;
|
||||
}
|
||||
if((c & 0xe0) == 0xc0) {
|
||||
cp = c & 0x1f;
|
||||
n = 2;
|
||||
} else if((c & 0xf0) == 0xe0) {
|
||||
cp = c & 0x0f;
|
||||
n = 3;
|
||||
} else if((c & 0xf8) == 0xf0) {
|
||||
cp = c & 0x07;
|
||||
n = 4;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for(i = 1; i < n; i++) {
|
||||
unsigned char t = s[i];
|
||||
if(!t || (t & 0xc0) != 0x80) {
|
||||
return -1;
|
||||
}
|
||||
cp = (cp << 6) | (t & 0x3f);
|
||||
}
|
||||
if((n == 2 && cp < 0x80) ||
|
||||
(n == 3 && cp < 0x800) ||
|
||||
(n == 4 && (cp < 0x10000 || cp > 0x10ffff)) ||
|
||||
(cp >= 0xd800 && cp <= 0xdfff)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out = cp;
|
||||
*ps += n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
char *
|
||||
fs_path_value(char *path) {
|
||||
const char *p;
|
||||
char *out;
|
||||
size_t len;
|
||||
size_t pos = 0;
|
||||
int has_byte_token = 0;
|
||||
|
||||
if(!path) {
|
||||
return path;
|
||||
}
|
||||
for(p = path; *p;) {
|
||||
unsigned int cp;
|
||||
const char *next = p;
|
||||
|
||||
if(utf8_decode_char(&next, &cp)) {
|
||||
return path;
|
||||
}
|
||||
if(cp >= 0x80 && cp <= 0xff) {
|
||||
has_byte_token = 1;
|
||||
} else if(cp > 0xff) {
|
||||
return path;
|
||||
}
|
||||
p = next;
|
||||
}
|
||||
if(!has_byte_token) {
|
||||
return path;
|
||||
}
|
||||
|
||||
len = strlen(path);
|
||||
if(!(out = malloc(len + 1))) {
|
||||
return path;
|
||||
}
|
||||
for(p = path; *p;) {
|
||||
unsigned int cp;
|
||||
const char *next = p;
|
||||
|
||||
if(utf8_decode_char(&next, &cp)) {
|
||||
free(out);
|
||||
return path;
|
||||
}
|
||||
out[pos++] = (char)(unsigned char)cp;
|
||||
p = next;
|
||||
}
|
||||
out[pos] = 0;
|
||||
free(path);
|
||||
return out;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <limits.h>
|
||||
|
||||
#include <microhttpd.h>
|
||||
|
||||
char *query_value(struct MHD_Connection *conn, const char *key);
|
||||
char *header_value(struct MHD_Connection *conn, const char *key);
|
||||
char *request_value(struct MHD_Connection *conn, const char *header,
|
||||
const char *query);
|
||||
char *form_decode(const char *src, size_t len);
|
||||
char *body_form_value(const char *body, size_t body_size, const char *key);
|
||||
|
||||
void free_paths(char **paths, size_t count);
|
||||
int parse_paths(const char *raw, char ***out_paths, size_t *out_count);
|
||||
|
||||
const char *path_basename(const char *path);
|
||||
void path_basename_copy(const char *path, char *out, size_t size);
|
||||
int path_dirname(const char *path, char *out, size_t size);
|
||||
int path_join(char *out, size_t size, const char *dir, const char *name);
|
||||
int relative_path_safe(const char *path);
|
||||
int path_join_relative(char *out, size_t size, const char *dir, const char *rel);
|
||||
char *fs_path_value(char *path);
|
||||
-591
@@ -1,591 +0,0 @@
|
||||
#include "pkg_info.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "filemgr_internal.h"
|
||||
#include "json_util.h"
|
||||
#include "path_util.h"
|
||||
#include "websrv.h"
|
||||
|
||||
#define PKG_CNT_MAGIC 0x7f434e54u
|
||||
#define PKG_FIH_MAGIC 0x7f464948u
|
||||
#define PKG_LIH_MAGIC 0x7f4c4948u
|
||||
#define PKG_HEADER_SIZE 0xa0
|
||||
#define PKG_ENTRY_SIZE 0x20
|
||||
#define PKG_ENTRY_PARAM_SFO 0x1000u
|
||||
#define PKG_ENTRY_ICON0_PNG 0x1200u
|
||||
#define PKG_ENTRY_ICON0_LOCALIZED_LAST 0x121fu
|
||||
#define PKG_ENTRY_PARAM_JSON 0x2000u
|
||||
#define PKG_ENTRY_FLAG_ENCRYPTED 0x80000000u
|
||||
#define PKG_ENTRY_MAX 65536u
|
||||
#define PKG_PARAM_MAX (4u * 1024u * 1024u)
|
||||
#define PKG_ICON_MAX (32u * 1024u * 1024u)
|
||||
#define SFO_MAGIC 0x46535000u
|
||||
#define SFO_ENTRY_SIZE 0x10
|
||||
#define SFO_ENTRY_MAX 256u
|
||||
#define JSON_TOKEN_MAX 4096u
|
||||
|
||||
typedef enum pkg_param_type {
|
||||
PKG_PARAM_SFO,
|
||||
PKG_PARAM_JSON,
|
||||
} pkg_param_type_t;
|
||||
|
||||
typedef struct pkg_source {
|
||||
int fd;
|
||||
uint64_t size;
|
||||
uint32_t content_type;
|
||||
uint32_t content_flags;
|
||||
char content_id[37];
|
||||
uint64_t param_offset;
|
||||
uint32_t param_size;
|
||||
pkg_param_type_t param_type;
|
||||
uint64_t icon_offset;
|
||||
uint32_t icon_size;
|
||||
} pkg_source_t;
|
||||
|
||||
typedef enum json_token_type {
|
||||
JSON_OBJECT,
|
||||
JSON_ARRAY,
|
||||
JSON_STRING,
|
||||
JSON_PRIMITIVE,
|
||||
} json_token_type_t;
|
||||
|
||||
typedef struct json_token {
|
||||
json_token_type_t type;
|
||||
size_t start;
|
||||
size_t end;
|
||||
int parent;
|
||||
} json_token_t;
|
||||
|
||||
static uint16_t
|
||||
read_le16(const unsigned char *p) {
|
||||
return (uint16_t)p[0] | (uint16_t)p[1] << 8;
|
||||
}
|
||||
|
||||
static uint32_t
|
||||
read_le32(const unsigned char *p) {
|
||||
return (uint32_t)p[0] | (uint32_t)p[1] << 8 |
|
||||
(uint32_t)p[2] << 16 | (uint32_t)p[3] << 24;
|
||||
}
|
||||
|
||||
static uint64_t
|
||||
read_le64(const unsigned char *p) {
|
||||
return (uint64_t)read_le32(p) | (uint64_t)read_le32(p + 4) << 32;
|
||||
}
|
||||
|
||||
static uint32_t
|
||||
read_be32(const unsigned char *p) {
|
||||
return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 |
|
||||
(uint32_t)p[2] << 8 | (uint32_t)p[3];
|
||||
}
|
||||
|
||||
static int
|
||||
range_valid(uint64_t offset, uint64_t size, uint64_t file_size) {
|
||||
return offset <= file_size && size <= file_size - offset;
|
||||
}
|
||||
|
||||
static int
|
||||
read_at(int fd, void *buffer, size_t size, uint64_t offset) {
|
||||
unsigned char *p = buffer;
|
||||
size_t done = 0;
|
||||
|
||||
while(done < size) {
|
||||
ssize_t n = pread(fd, p + done, size - done, (off_t)(offset + done));
|
||||
if(n <= 0) {
|
||||
if(n < 0 && errno == EINTR) continue;
|
||||
return -1;
|
||||
}
|
||||
done += (size_t)n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
png_signature_valid(const unsigned char *data, size_t size) {
|
||||
static const unsigned char signature[] = "\x89PNG\r\n\x1a\n";
|
||||
|
||||
return size >= sizeof(signature) - 1 &&
|
||||
!memcmp(data, signature, sizeof(signature) - 1);
|
||||
}
|
||||
|
||||
static int
|
||||
pkg_icon_is_png(const pkg_source_t *pkg, uint64_t offset, uint32_t size) {
|
||||
unsigned char signature[8];
|
||||
|
||||
return size >= sizeof(signature) &&
|
||||
!read_at(pkg->fd, signature, sizeof(signature), offset) &&
|
||||
png_signature_valid(signature, sizeof(signature));
|
||||
}
|
||||
|
||||
static void
|
||||
pkg_source_close(pkg_source_t *pkg) {
|
||||
if(pkg->fd >= 0) close(pkg->fd);
|
||||
pkg->fd = -1;
|
||||
}
|
||||
|
||||
static int
|
||||
pkg_source_open(const char *path, pkg_source_t *pkg) {
|
||||
unsigned char header[PKG_HEADER_SIZE];
|
||||
unsigned char *table = NULL;
|
||||
struct stat st;
|
||||
uint64_t container_offset = 0;
|
||||
uint64_t container_size;
|
||||
uint32_t magic;
|
||||
uint32_t entry_count;
|
||||
uint32_t table_offset;
|
||||
size_t table_size;
|
||||
int ret = -1;
|
||||
|
||||
memset(pkg, 0, sizeof(*pkg));
|
||||
pkg->fd = -1;
|
||||
if((pkg->fd = open(path, O_RDONLY)) < 0 || fstat(pkg->fd, &st) ||
|
||||
!S_ISREG(st.st_mode) || st.st_size < PKG_HEADER_SIZE ||
|
||||
read_at(pkg->fd, header, sizeof(header), 0)) goto done;
|
||||
|
||||
pkg->size = (uint64_t)st.st_size;
|
||||
magic = read_be32(header);
|
||||
if(magic == PKG_FIH_MAGIC) {
|
||||
container_offset = read_le64(header + 0x58);
|
||||
} else if(magic == PKG_LIH_MAGIC) {
|
||||
container_offset = read_le64(header + 0x30);
|
||||
} else if(magic != PKG_CNT_MAGIC) {
|
||||
goto done;
|
||||
}
|
||||
if(container_offset) {
|
||||
if(!range_valid(container_offset, sizeof(header), pkg->size) ||
|
||||
read_at(pkg->fd, header, sizeof(header), container_offset) ||
|
||||
read_be32(header) != PKG_CNT_MAGIC) goto done;
|
||||
}
|
||||
container_size = pkg->size - container_offset;
|
||||
|
||||
memcpy(pkg->content_id, header + 0x40, 36);
|
||||
pkg->content_id[36] = 0;
|
||||
pkg->content_type = read_be32(header + 0x74);
|
||||
pkg->content_flags = read_be32(header + 0x78);
|
||||
entry_count = read_be32(header + 0x10);
|
||||
table_offset = read_be32(header + 0x18);
|
||||
if(!entry_count || entry_count > PKG_ENTRY_MAX) goto done;
|
||||
table_size = (size_t)entry_count * PKG_ENTRY_SIZE;
|
||||
if(!range_valid(table_offset, table_size, container_size) ||
|
||||
!(table = malloc(table_size)) ||
|
||||
read_at(pkg->fd, table, table_size, container_offset + table_offset)) {
|
||||
goto done;
|
||||
}
|
||||
|
||||
for(uint32_t i = 0; i < entry_count; i++) {
|
||||
const unsigned char *entry = table + (size_t)i * PKG_ENTRY_SIZE;
|
||||
uint32_t id = read_be32(entry);
|
||||
uint32_t flags1 = read_be32(entry + 0x08);
|
||||
uint32_t offset = read_be32(entry + 0x10);
|
||||
uint32_t size = read_be32(entry + 0x14);
|
||||
int encrypted = (flags1 & PKG_ENTRY_FLAG_ENCRYPTED) != 0;
|
||||
|
||||
if(!range_valid(offset, size, container_size)) goto done;
|
||||
if(encrypted || !size) continue;
|
||||
if(id == PKG_ENTRY_PARAM_SFO && size <= PKG_PARAM_MAX) {
|
||||
pkg->param_offset = container_offset + offset;
|
||||
pkg->param_size = size;
|
||||
pkg->param_type = PKG_PARAM_SFO;
|
||||
} else if(id == PKG_ENTRY_PARAM_JSON && size <= PKG_PARAM_MAX) {
|
||||
pkg->param_offset = container_offset + offset;
|
||||
pkg->param_size = size;
|
||||
pkg->param_type = PKG_PARAM_JSON;
|
||||
} else if(id >= PKG_ENTRY_ICON0_PNG &&
|
||||
id <= PKG_ENTRY_ICON0_LOCALIZED_LAST &&
|
||||
size <= PKG_ICON_MAX &&
|
||||
(id == PKG_ENTRY_ICON0_PNG || !pkg->icon_size) &&
|
||||
pkg_icon_is_png(pkg, container_offset + offset, size)) {
|
||||
/* Prefer icon0.png; otherwise keep the first valid localized icon. */
|
||||
pkg->icon_offset = container_offset + offset;
|
||||
pkg->icon_size = size;
|
||||
}
|
||||
}
|
||||
if(!pkg->param_size) goto done;
|
||||
ret = 0;
|
||||
|
||||
done:
|
||||
free(table);
|
||||
if(ret) {
|
||||
errno = EINVAL;
|
||||
pkg_source_close(pkg);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int
|
||||
append_sfo_fields(strbuf_t *json, const unsigned char *sfo, size_t size) {
|
||||
uint32_t key_offset;
|
||||
uint32_t value_offset;
|
||||
uint32_t count;
|
||||
uint64_t index_end;
|
||||
int first = 1;
|
||||
|
||||
if(size < 20 || read_le32(sfo) != SFO_MAGIC) return -1;
|
||||
key_offset = read_le32(sfo + 8);
|
||||
value_offset = read_le32(sfo + 12);
|
||||
count = read_le32(sfo + 16);
|
||||
index_end = 20 + (uint64_t)count * SFO_ENTRY_SIZE;
|
||||
if(count > SFO_ENTRY_MAX || index_end > size || key_offset < index_end ||
|
||||
value_offset < key_offset || value_offset > size) return -1;
|
||||
|
||||
strbuf_append(json, "[");
|
||||
for(uint32_t i = 0; i < count; i++) {
|
||||
const unsigned char *entry = sfo + 20 + (size_t)i * SFO_ENTRY_SIZE;
|
||||
uint16_t name_offset = read_le16(entry);
|
||||
uint16_t format = read_le16(entry + 2);
|
||||
uint32_t value_size = read_le32(entry + 4);
|
||||
uint32_t value_max = read_le32(entry + 8);
|
||||
uint32_t data_offset = read_le32(entry + 12);
|
||||
uint64_t key_pos = (uint64_t)key_offset + name_offset;
|
||||
uint64_t value_pos = (uint64_t)value_offset + data_offset;
|
||||
const unsigned char *key_end;
|
||||
char number[32];
|
||||
char *value = NULL;
|
||||
|
||||
if(key_pos >= value_offset || value_pos > size ||
|
||||
(value_max && value_size > value_max)) continue;
|
||||
key_end = memchr(sfo + key_pos, 0, value_offset - (size_t)key_pos);
|
||||
if(!key_end) continue;
|
||||
if(format == 0x0204) {
|
||||
size_t length;
|
||||
if(!value_size || !range_valid(value_pos, value_size, size)) continue;
|
||||
length = strnlen((const char *)sfo + value_pos, value_size);
|
||||
if(!(value = malloc(length + 1))) continue;
|
||||
memcpy(value, sfo + value_pos, length);
|
||||
value[length] = 0;
|
||||
} else if(format == 0x0404 && range_valid(value_pos, 4, size)) {
|
||||
snprintf(number, sizeof(number), "%u", read_le32(sfo + value_pos));
|
||||
value = strdup(number);
|
||||
}
|
||||
if(!value) continue;
|
||||
if(!first) strbuf_append(json, ",");
|
||||
first = 0;
|
||||
strbuf_append(json, "{\"name\":");
|
||||
json_escape(json, (const char *)sfo + key_pos);
|
||||
strbuf_append(json, ",\"value\":");
|
||||
json_escape(json, value);
|
||||
strbuf_append(json, "}");
|
||||
free(value);
|
||||
}
|
||||
strbuf_append(json, "]");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
json_add_token(json_token_t *tokens, size_t *count, json_token_type_t type,
|
||||
size_t start, int parent) {
|
||||
if(*count >= JSON_TOKEN_MAX) return -1;
|
||||
tokens[*count] = (json_token_t){
|
||||
.type = type, .start = start, .end = 0, .parent = parent
|
||||
};
|
||||
return (int)(*count)++;
|
||||
}
|
||||
|
||||
static int
|
||||
parse_json_tokens(const unsigned char *data, size_t size, json_token_t *tokens,
|
||||
size_t *token_count) {
|
||||
int parent = -1;
|
||||
size_t count = 0;
|
||||
|
||||
for(size_t i = 0; i < size; i++) {
|
||||
unsigned char c = data[i];
|
||||
if(c == '{' || c == '[') {
|
||||
int index = json_add_token(tokens, &count,
|
||||
c == '{' ? JSON_OBJECT : JSON_ARRAY,
|
||||
i, parent);
|
||||
if(index < 0) return -1;
|
||||
parent = index;
|
||||
} else if(c == '}' || c == ']') {
|
||||
json_token_type_t type = c == '}' ? JSON_OBJECT : JSON_ARRAY;
|
||||
if(parent < 0 || tokens[parent].type != type) return -1;
|
||||
tokens[parent].end = i + 1;
|
||||
parent = tokens[parent].parent;
|
||||
} else if(c == '"') {
|
||||
int index = json_add_token(tokens, &count, JSON_STRING, i + 1, parent);
|
||||
if(index < 0) return -1;
|
||||
for(i++; i < size && data[i] != '"'; i++) {
|
||||
if(data[i] < 0x20) return -1;
|
||||
if(data[i] == '\\') {
|
||||
if(++i >= size || !strchr("\"\\/bfnrtu", data[i])) return -1;
|
||||
if(data[i] == 'u') {
|
||||
for(unsigned int n = 0; n < 4; n++) {
|
||||
if(++i >= size || !((data[i] >= '0' && data[i] <= '9') ||
|
||||
(data[i] >= 'a' && data[i] <= 'f') ||
|
||||
(data[i] >= 'A' && data[i] <= 'F'))) return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(i >= size) return -1;
|
||||
tokens[index].end = i;
|
||||
} else if(c == ':' || c == ',' || c == ' ' || c == '\t' ||
|
||||
c == '\r' || c == '\n') {
|
||||
continue;
|
||||
} else {
|
||||
int index = json_add_token(tokens, &count, JSON_PRIMITIVE, i, parent);
|
||||
if(index < 0) return -1;
|
||||
while(i < size && data[i] != ',' && data[i] != ']' && data[i] != '}' &&
|
||||
data[i] != ' ' && data[i] != '\t' && data[i] != '\r' &&
|
||||
data[i] != '\n') i++;
|
||||
if(tokens[index].start == i) return -1;
|
||||
tokens[index].end = i;
|
||||
i--;
|
||||
}
|
||||
}
|
||||
if(parent >= 0 || !count || tokens[0].type != JSON_OBJECT ||
|
||||
!tokens[0].end) return -1;
|
||||
*token_count = count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
json_token_equals(const unsigned char *data, const json_token_t *token,
|
||||
const char *text) {
|
||||
size_t length = strlen(text);
|
||||
return token->type == JSON_STRING && token->end - token->start == length &&
|
||||
!memcmp(data + token->start, text, length);
|
||||
}
|
||||
|
||||
static int
|
||||
json_next_child(const json_token_t *tokens, size_t count, int parent,
|
||||
size_t start) {
|
||||
for(size_t i = start; i < count; i++) {
|
||||
if(tokens[i].parent == parent) return (int)i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
json_object_value(const unsigned char *data, const json_token_t *tokens,
|
||||
size_t count, int object, const char *key) {
|
||||
int item = json_next_child(tokens, count, object, (size_t)object + 1);
|
||||
|
||||
while(item >= 0) {
|
||||
int value = json_next_child(tokens, count, object, (size_t)item + 1);
|
||||
if(value < 0) return -1;
|
||||
if(json_token_equals(data, &tokens[item], key)) return value;
|
||||
item = json_next_child(tokens, count, object, (size_t)value + 1);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
json_object_value_token(const unsigned char *data, const json_token_t *tokens,
|
||||
size_t count, int object, const json_token_t *key) {
|
||||
int item = json_next_child(tokens, count, object, (size_t)object + 1);
|
||||
size_t key_size = key->end - key->start;
|
||||
|
||||
while(item >= 0) {
|
||||
int value = json_next_child(tokens, count, object, (size_t)item + 1);
|
||||
if(value < 0) return -1;
|
||||
if(tokens[item].type == JSON_STRING &&
|
||||
tokens[item].end - tokens[item].start == key_size &&
|
||||
!memcmp(data + tokens[item].start, data + key->start, key_size)) {
|
||||
return value;
|
||||
}
|
||||
item = json_next_child(tokens, count, object, (size_t)value + 1);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void
|
||||
append_json_token_string(strbuf_t *json, const unsigned char *data,
|
||||
const json_token_t *token) {
|
||||
strbuf_append(json, "\"");
|
||||
strbuf_printf(json, "%.*s", (int)(token->end - token->start),
|
||||
data + token->start);
|
||||
strbuf_append(json, "\"");
|
||||
}
|
||||
|
||||
static void
|
||||
append_json_field(strbuf_t *json, const unsigned char *data,
|
||||
const json_token_t *key, const json_token_t *value,
|
||||
int *first) {
|
||||
if(!*first) strbuf_append(json, ",");
|
||||
*first = 0;
|
||||
strbuf_append(json, "{\"name\":");
|
||||
append_json_token_string(json, data, key);
|
||||
strbuf_append(json, ",\"value\":");
|
||||
append_json_token_string(json, data, value);
|
||||
strbuf_append(json, "}");
|
||||
}
|
||||
|
||||
static void
|
||||
append_named_json_field(strbuf_t *json, const char *name,
|
||||
const unsigned char *data, const json_token_t *value,
|
||||
int *first) {
|
||||
if(!*first) strbuf_append(json, ",");
|
||||
*first = 0;
|
||||
strbuf_append(json, "{\"name\":");
|
||||
json_escape(json, name);
|
||||
strbuf_append(json, ",\"value\":");
|
||||
append_json_token_string(json, data, value);
|
||||
strbuf_append(json, "}");
|
||||
}
|
||||
|
||||
static int
|
||||
append_param_json_fields(strbuf_t *json, const unsigned char *data,
|
||||
size_t size) {
|
||||
json_token_t *tokens = calloc(JSON_TOKEN_MAX, sizeof(*tokens));
|
||||
size_t count = 0;
|
||||
int first = 1;
|
||||
int localized;
|
||||
int title = -1;
|
||||
|
||||
if(!tokens || parse_json_tokens(data, size, tokens, &count)) {
|
||||
free(tokens);
|
||||
return -1;
|
||||
}
|
||||
strbuf_append(json, "[");
|
||||
|
||||
localized = json_object_value(data, tokens, count, 0, "localizedParameters");
|
||||
if(localized >= 0 && tokens[localized].type == JSON_OBJECT) {
|
||||
int language = json_object_value(data, tokens, count, localized,
|
||||
"defaultLanguage");
|
||||
int language_data = language >= 0 && tokens[language].type == JSON_STRING ?
|
||||
json_object_value_token(data, tokens, count, localized,
|
||||
&tokens[language]) : -1;
|
||||
if(language_data >= 0 && tokens[language_data].type == JSON_OBJECT) {
|
||||
title = json_object_value(data, tokens, count, language_data,
|
||||
"titleName");
|
||||
}
|
||||
if(title < 0) {
|
||||
int english = json_object_value(data, tokens, count, localized, "en-US");
|
||||
if(english >= 0 && tokens[english].type == JSON_OBJECT) {
|
||||
title = json_object_value(data, tokens, count, english, "titleName");
|
||||
}
|
||||
}
|
||||
if(title < 0) {
|
||||
int item = json_next_child(tokens, count, localized,
|
||||
(size_t)localized + 1);
|
||||
while(item >= 0 && title < 0) {
|
||||
int value = json_next_child(tokens, count, localized,
|
||||
(size_t)item + 1);
|
||||
if(value < 0) break;
|
||||
if(tokens[value].type == JSON_OBJECT) {
|
||||
title = json_object_value(data, tokens, count, value, "titleName");
|
||||
}
|
||||
item = json_next_child(tokens, count, localized, (size_t)value + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(title >= 0 && tokens[title].type == JSON_STRING) {
|
||||
append_named_json_field(json, "titleName", data, &tokens[title], &first);
|
||||
}
|
||||
|
||||
for(int item = json_next_child(tokens, count, 0, 1); item >= 0;) {
|
||||
int value = json_next_child(tokens, count, 0, (size_t)item + 1);
|
||||
if(value < 0) break;
|
||||
if(tokens[item].type == JSON_STRING &&
|
||||
(tokens[value].type == JSON_STRING ||
|
||||
tokens[value].type == JSON_PRIMITIVE)) {
|
||||
append_json_field(json, data, &tokens[item], &tokens[value], &first);
|
||||
}
|
||||
item = json_next_child(tokens, count, 0, (size_t)value + 1);
|
||||
}
|
||||
strbuf_append(json, "]");
|
||||
free(tokens);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static enum MHD_Result
|
||||
pkg_error(struct MHD_Connection *conn, const char *path) {
|
||||
return send_json_error_detail(conn, MHD_HTTP_BAD_REQUEST,
|
||||
"could not read package information",
|
||||
"pkg_info_failed", path);
|
||||
}
|
||||
|
||||
enum MHD_Result
|
||||
api_pkg_info(struct MHD_Connection *conn) {
|
||||
char *path = fs_path_value(query_value(conn, "path"));
|
||||
pkg_source_t pkg = {.fd = -1};
|
||||
unsigned char *param;
|
||||
strbuf_t json = {0};
|
||||
|
||||
if(!path || pkg_source_open(path, &pkg)) {
|
||||
enum MHD_Result ret = pkg_error(conn, path);
|
||||
free(path);
|
||||
return ret;
|
||||
}
|
||||
if(!(param = malloc(pkg.param_size)) ||
|
||||
read_at(pkg.fd, param, pkg.param_size, pkg.param_offset)) {
|
||||
enum MHD_Result ret = pkg_error(conn, path);
|
||||
free(param);
|
||||
pkg_source_close(&pkg);
|
||||
free(path);
|
||||
return ret;
|
||||
}
|
||||
|
||||
strbuf_printf(&json,
|
||||
"{\"ok\":true,\"size\":%llu,\"content_id\":",
|
||||
(unsigned long long)pkg.size);
|
||||
json_escape(&json, pkg.content_id);
|
||||
strbuf_printf(&json,
|
||||
",\"platform\":\"%s\",\"content_type\":%u,"
|
||||
"\"content_flags\":%u,\"has_icon\":%s,\"fields\":",
|
||||
pkg.param_type == PKG_PARAM_JSON ? "PS5" : "PS4",
|
||||
pkg.content_type, pkg.content_flags,
|
||||
pkg.icon_size ? "true" : "false");
|
||||
if((pkg.param_type == PKG_PARAM_JSON &&
|
||||
append_param_json_fields(&json, param, pkg.param_size)) ||
|
||||
(pkg.param_type == PKG_PARAM_SFO &&
|
||||
append_sfo_fields(&json, param, pkg.param_size))) {
|
||||
enum MHD_Result ret;
|
||||
free(json.data);
|
||||
ret = pkg_error(conn, path);
|
||||
free(param);
|
||||
pkg_source_close(&pkg);
|
||||
free(path);
|
||||
return ret;
|
||||
}
|
||||
strbuf_append(&json, "}");
|
||||
|
||||
free(param);
|
||||
pkg_source_close(&pkg);
|
||||
free(path);
|
||||
return send_buffer(conn, MHD_HTTP_OK, json.data, "application/json");
|
||||
}
|
||||
|
||||
enum MHD_Result
|
||||
api_pkg_icon(struct MHD_Connection *conn) {
|
||||
char *path = fs_path_value(query_value(conn, "path"));
|
||||
pkg_source_t pkg = {.fd = -1};
|
||||
unsigned char *icon;
|
||||
struct MHD_Response *response;
|
||||
enum MHD_Result ret;
|
||||
|
||||
if(!path || pkg_source_open(path, &pkg) || !pkg.icon_size ||
|
||||
!(icon = malloc(pkg.icon_size)) ||
|
||||
read_at(pkg.fd, icon, pkg.icon_size, pkg.icon_offset)) {
|
||||
if(path && pkg.fd >= 0) pkg_source_close(&pkg);
|
||||
free(path);
|
||||
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "package icon not found");
|
||||
}
|
||||
if(!png_signature_valid(icon, pkg.icon_size)) {
|
||||
free(icon);
|
||||
pkg_source_close(&pkg);
|
||||
free(path);
|
||||
return send_json_error(conn, MHD_HTTP_NOT_FOUND,
|
||||
"package icon not found");
|
||||
}
|
||||
pkg_source_close(&pkg);
|
||||
free(path);
|
||||
|
||||
response = MHD_create_response_from_buffer(pkg.icon_size, icon,
|
||||
MHD_RESPMEM_MUST_FREE);
|
||||
if(!response) {
|
||||
free(icon);
|
||||
return MHD_NO;
|
||||
}
|
||||
MHD_add_response_header(response, MHD_HTTP_HEADER_CONTENT_TYPE, "image/png");
|
||||
ret = websrv_queue_response(conn, MHD_HTTP_OK, response);
|
||||
MHD_destroy_response(response);
|
||||
return ret;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <microhttpd.h>
|
||||
|
||||
enum MHD_Result api_pkg_info(struct MHD_Connection *conn);
|
||||
enum MHD_Result api_pkg_icon(struct MHD_Connection *conn);
|
||||
@@ -1,119 +0,0 @@
|
||||
#include "pkg_installer.h"
|
||||
|
||||
#ifndef __linux__
|
||||
|
||||
#include <limits.h>
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct pkg_metadata {
|
||||
const char *uri;
|
||||
const char *ex_uri;
|
||||
const char *playgo_scenario_id;
|
||||
const char *content_id;
|
||||
const char *content_name;
|
||||
const char *icon_url;
|
||||
uint32_t slot;
|
||||
uint32_t is_playgo_enabled;
|
||||
} pkg_metadata_t;
|
||||
|
||||
_Static_assert(sizeof(pkg_metadata_t) == 0x38,
|
||||
"sceAppInstUtil metadata ABI mismatch");
|
||||
|
||||
typedef struct pkg_info {
|
||||
char content_id[48];
|
||||
int type;
|
||||
int platform;
|
||||
} pkg_info_t;
|
||||
|
||||
typedef struct playgo_info {
|
||||
char languages[30][8];
|
||||
char scenario_ids[64][3];
|
||||
char content_ids[64][48];
|
||||
long unknown[810];
|
||||
} playgo_info_t;
|
||||
|
||||
_Static_assert(sizeof(playgo_info_t) == 0x2700,
|
||||
"sceAppInstUtil PlayGoInfo ABI mismatch");
|
||||
|
||||
int sceAppInstUtilInitialize(void);
|
||||
int sceAppInstUtilInstallByPackage(const pkg_metadata_t *, pkg_info_t *,
|
||||
playgo_info_t *);
|
||||
|
||||
static pthread_mutex_t installer_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static int installer_initialized;
|
||||
|
||||
static int
|
||||
initialize_locked(void) {
|
||||
int result;
|
||||
|
||||
if(!installer_initialized) {
|
||||
result = sceAppInstUtilInitialize();
|
||||
if(result) return result;
|
||||
installer_initialized = 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
pkg_installer_initialize(void) {
|
||||
int result;
|
||||
|
||||
pthread_mutex_lock(&installer_lock);
|
||||
result = initialize_locked();
|
||||
pthread_mutex_unlock(&installer_lock);
|
||||
return result;
|
||||
}
|
||||
|
||||
int
|
||||
pkg_installer_install(const char *path) {
|
||||
char install_path[PATH_MAX + sizeof("/user")];
|
||||
const char *uri = path;
|
||||
pkg_metadata_t metadata = {
|
||||
.uri = NULL,
|
||||
.ex_uri = "",
|
||||
.playgo_scenario_id = "",
|
||||
.content_id = "",
|
||||
.content_name = "",
|
||||
.icon_url = "",
|
||||
.slot = 0,
|
||||
.is_playgo_enabled = 0
|
||||
};
|
||||
pkg_info_t pkg_info = {0};
|
||||
playgo_info_t playgo_info = {0};
|
||||
int result;
|
||||
|
||||
if(!path) return -1;
|
||||
if(!strncmp(path, "/data/", 6)) {
|
||||
snprintf(install_path, sizeof(install_path), "/user%s", path);
|
||||
uri = install_path;
|
||||
}
|
||||
metadata.uri = uri;
|
||||
|
||||
pthread_mutex_lock(&installer_lock);
|
||||
result = initialize_locked();
|
||||
if(result) {
|
||||
pthread_mutex_unlock(&installer_lock);
|
||||
return result;
|
||||
}
|
||||
result = sceAppInstUtilInstallByPackage(&metadata, &pkg_info, &playgo_info);
|
||||
pthread_mutex_unlock(&installer_lock);
|
||||
return result;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
int
|
||||
pkg_installer_initialize(void) {
|
||||
return PKG_INSTALL_UNSUPPORTED;
|
||||
}
|
||||
|
||||
int
|
||||
pkg_installer_install(const char *path) {
|
||||
(void)path;
|
||||
return PKG_INSTALL_UNSUPPORTED;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,6 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#define PKG_INSTALL_UNSUPPORTED 0x7fffffff
|
||||
|
||||
int pkg_installer_initialize(void);
|
||||
int pkg_installer_install(const char *path);
|
||||
-1277
File diff suppressed because it is too large.
Load diff
@@ -1,35 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
/* Safe RAR extraction engine used by the /api/extract task.
|
||||
Wraps the vendored dmc_unrar library (https://github.com/DrMcCoy/dmc_unrar).
|
||||
|
||||
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.
|
||||
|
||||
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. */
|
||||
|
||||
#include "zip_extract.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* Extract rar_path into dst_dir using the same protocol as zipx_extract().
|
||||
Returns ZIPX_OK or an error code; *result is always filled in.
|
||||
On any failure the staging directory is removed and dst_dir is left as it
|
||||
was, except for objects already published with the overwrite policy. */
|
||||
zipx_status_t rar_extract(const char *rar_path, const char *dst_dir,
|
||||
zipx_conflict_t conflict,
|
||||
const zipx_limits_t *limits,
|
||||
zipx_cancel_fn cancel,
|
||||
zipx_progress_fn progress,
|
||||
void *userdata,
|
||||
zipx_result_t *result);
|
||||
-128
@@ -1,128 +0,0 @@
|
||||
#include "filemgr_internal.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/statvfs.h>
|
||||
#ifdef __SCE__
|
||||
#include <sys/mount.h>
|
||||
#endif
|
||||
|
||||
#include "json_util.h"
|
||||
#include "path_util.h"
|
||||
|
||||
static unsigned long long
|
||||
vfs_bytes(fsblkcnt_t blocks, unsigned long block_size) {
|
||||
return (unsigned long long)blocks * (unsigned long long)block_size;
|
||||
}
|
||||
|
||||
#ifdef __SCE__
|
||||
static int
|
||||
path_is_mounted(const char *path, const struct stat *st) {
|
||||
char parent[PATH_MAX];
|
||||
struct stat parent_st;
|
||||
|
||||
if(!strcmp(path, "/")) {
|
||||
return 1;
|
||||
}
|
||||
if(path_dirname(path, parent, sizeof(parent)) || stat(parent, &parent_st)) {
|
||||
return 0;
|
||||
}
|
||||
return st->st_dev != parent_st.st_dev;
|
||||
}
|
||||
#endif
|
||||
|
||||
enum MHD_Result
|
||||
api_space(struct MHD_Connection *conn) {
|
||||
#ifdef __SCE__
|
||||
static const struct {
|
||||
const char *label_key;
|
||||
const char *path;
|
||||
} mounts[] = {
|
||||
{"storageInternal", "/data"},
|
||||
{"storageUsb", "/mnt/usb0"},
|
||||
{"storageUsb", "/mnt/usb1"},
|
||||
{"storageUsb", "/mnt/usb2"},
|
||||
{"storageUsb", "/mnt/usb3"},
|
||||
{"storageUsb", "/mnt/usb4"},
|
||||
{"storageUsb", "/mnt/usb5"},
|
||||
{"storageUsb", "/mnt/usb6"},
|
||||
{"storageUsb", "/mnt/usb7"},
|
||||
{"storageM2", "/mnt/ext1"},
|
||||
{"storageExtended", "/mnt/ext0"},
|
||||
};
|
||||
#endif
|
||||
char *current = fs_path_value(query_value(conn, "path"));
|
||||
strbuf_t b = {0};
|
||||
int first = 1;
|
||||
|
||||
strbuf_append(&b, "{\"ok\":true,\"spaces\":[");
|
||||
#ifdef __SCE__
|
||||
for(size_t i = 0; i < sizeof(mounts) / sizeof(mounts[0]); i++) {
|
||||
struct statvfs vfs;
|
||||
struct stat st;
|
||||
unsigned long block_size;
|
||||
unsigned long long free_bytes;
|
||||
unsigned long long total_bytes;
|
||||
int is_current = 0;
|
||||
|
||||
if(stat(mounts[i].path, &st) || !path_is_mounted(mounts[i].path, &st) ||
|
||||
statvfs(mounts[i].path, &vfs)) {
|
||||
continue;
|
||||
}
|
||||
block_size = vfs.f_frsize ? vfs.f_frsize : vfs.f_bsize;
|
||||
free_bytes = vfs_bytes(vfs.f_bavail, block_size);
|
||||
total_bytes = vfs_bytes(vfs.f_blocks, block_size);
|
||||
if(current) {
|
||||
if(!strcmp(mounts[i].path, "/")) {
|
||||
is_current = !strcmp(current, "/");
|
||||
} else if(!strncmp(current, mounts[i].path, strlen(mounts[i].path)) &&
|
||||
(current[strlen(mounts[i].path)] == 0 ||
|
||||
current[strlen(mounts[i].path)] == '/')) {
|
||||
is_current = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if(!first) {
|
||||
strbuf_append(&b, ",");
|
||||
}
|
||||
first = 0;
|
||||
strbuf_append(&b, "{\"label_key\":");
|
||||
json_escape(&b, mounts[i].label_key);
|
||||
strbuf_append(&b, ",\"path\":");
|
||||
json_escape(&b, mounts[i].path);
|
||||
strbuf_printf(&b, ",\"free\":%llu,\"total\":%llu,\"current\":%s}",
|
||||
free_bytes, total_bytes, is_current ? "true" : "false");
|
||||
}
|
||||
#else
|
||||
const char *paths[] = {"/", current && strcmp(current, "/") ? current : NULL};
|
||||
const char *labels[] = {"storageRoot", "storageCurrent"};
|
||||
for(size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); i++) {
|
||||
struct statvfs vfs;
|
||||
unsigned long block_size;
|
||||
unsigned long long free_bytes;
|
||||
unsigned long long total_bytes;
|
||||
|
||||
if(!paths[i] || statvfs(paths[i], &vfs)) {
|
||||
continue;
|
||||
}
|
||||
block_size = vfs.f_frsize ? vfs.f_frsize : vfs.f_bsize;
|
||||
free_bytes = vfs_bytes(vfs.f_bavail, block_size);
|
||||
total_bytes = vfs_bytes(vfs.f_blocks, block_size);
|
||||
if(!first) {
|
||||
strbuf_append(&b, ",");
|
||||
}
|
||||
first = 0;
|
||||
strbuf_append(&b, "{\"label_key\":");
|
||||
json_escape(&b, labels[i]);
|
||||
strbuf_append(&b, ",\"path\":");
|
||||
json_escape(&b, paths[i]);
|
||||
strbuf_printf(&b, ",\"free\":%llu,\"total\":%llu,\"current\":%s}",
|
||||
free_bytes, total_bytes, i ? "true" : "false");
|
||||
}
|
||||
#endif
|
||||
free(current);
|
||||
strbuf_append(&b, "]}");
|
||||
return send_buffer(conn, MHD_HTTP_OK, b.data, "application/json");
|
||||
}
|
||||
-221
@@ -1,221 +0,0 @@
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "filemgr_internal.h"
|
||||
#include "path_util.h"
|
||||
|
||||
#define ETA_AVERAGE_WINDOW_SECONDS 30
|
||||
|
||||
pthread_mutex_t g_tasks_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
file_task_t *g_tasks = NULL;
|
||||
unsigned long g_next_task_id = 1;
|
||||
|
||||
const char *
|
||||
task_op_name(task_op_t op) {
|
||||
switch(op) {
|
||||
case TASK_COPY: return "copy";
|
||||
case TASK_MOVE: return "move";
|
||||
case TASK_DELETE: return "delete";
|
||||
case TASK_CHMOD: return "chmod";
|
||||
case TASK_DOWNLOAD: return "download";
|
||||
case TASK_UPLOAD: return "upload";
|
||||
case TASK_PKG_INSTALL: return "pkg_install";
|
||||
case TASK_EXTRACT: return "extract";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
const char *
|
||||
task_state_name(task_state_t state) {
|
||||
switch(state) {
|
||||
case TASK_QUEUED: return "queued";
|
||||
case TASK_RUNNING: return "running";
|
||||
case TASK_DONE: return "done";
|
||||
case TASK_FAILED: return "failed";
|
||||
case TASK_CANCELED: return "canceled";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
task_is_active(const file_task_t *task) {
|
||||
return task->state == TASK_QUEUED || task->state == TASK_RUNNING;
|
||||
}
|
||||
|
||||
int
|
||||
has_active_task_locked(void) {
|
||||
file_task_t *task;
|
||||
|
||||
for(task = g_tasks; task; task = task->next) {
|
||||
if(task->op != TASK_PKG_INSTALL && task_is_active(task)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
has_active_task(void) {
|
||||
int active;
|
||||
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
active = has_active_task_locked();
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
return active;
|
||||
}
|
||||
|
||||
void
|
||||
free_task(file_task_t *task) {
|
||||
if(!task) {
|
||||
return;
|
||||
}
|
||||
free_paths(task->srcs, task->src_count);
|
||||
free(task);
|
||||
}
|
||||
|
||||
void
|
||||
remove_finished_tasks_locked(void) {
|
||||
file_task_t **link = &g_tasks;
|
||||
|
||||
while(*link) {
|
||||
file_task_t *task = *link;
|
||||
|
||||
if(task_is_active(task) || task->active_streams ||
|
||||
(task->op == TASK_PKG_INSTALL && !task->reported)) {
|
||||
link = &task->next;
|
||||
continue;
|
||||
}
|
||||
|
||||
*link = task->next;
|
||||
free_task(task);
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
task_cancel_requested(file_task_t *task) {
|
||||
int cancel;
|
||||
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
cancel = task->cancel_requested;
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
if(cancel) {
|
||||
errno = ECANCELED;
|
||||
}
|
||||
return cancel;
|
||||
}
|
||||
|
||||
file_task_t *
|
||||
find_task_locked(unsigned long id) {
|
||||
file_task_t *task;
|
||||
|
||||
for(task = g_tasks; task; task = task->next) {
|
||||
if(task->id == id) {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static long long
|
||||
timespec_delta_ns(const struct timespec *end, const struct timespec *start) {
|
||||
return (long long)(end->tv_sec - start->tv_sec) * 1000000000LL +
|
||||
(long long)(end->tv_nsec - start->tv_nsec);
|
||||
}
|
||||
|
||||
static void
|
||||
task_update_eta_locked(file_task_t *task, const struct timespec *now_mono) {
|
||||
task_eta_sample_t *sample;
|
||||
task_eta_sample_t *base = NULL;
|
||||
unsigned int i;
|
||||
|
||||
if(!task->total || !task->done || task->done >= task->total) {
|
||||
task->eta = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
sample = &task->eta_samples[task->eta_sample_next];
|
||||
sample->done = task->done;
|
||||
sample->time = *now_mono;
|
||||
task->eta_sample_next = (task->eta_sample_next + 1) % ETA_SAMPLE_SLOTS;
|
||||
if(task->eta_sample_count < ETA_SAMPLE_SLOTS) {
|
||||
task->eta_sample_count++;
|
||||
}
|
||||
|
||||
for(i = 0; i < task->eta_sample_count; i++) {
|
||||
task_eta_sample_t *candidate = &task->eta_samples[i];
|
||||
long long age_ns;
|
||||
|
||||
if(!candidate->time.tv_sec || candidate->done >= task->done) {
|
||||
continue;
|
||||
}
|
||||
age_ns = timespec_delta_ns(now_mono, &candidate->time);
|
||||
if(age_ns <= 0 || age_ns > (long long)ETA_AVERAGE_WINDOW_SECONDS * 1000000000LL) {
|
||||
continue;
|
||||
}
|
||||
if(!base || age_ns > timespec_delta_ns(now_mono, &base->time)) {
|
||||
base = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if(base) {
|
||||
long long elapsed_ns = timespec_delta_ns(now_mono, &base->time);
|
||||
unsigned long long delta = task->done - base->done;
|
||||
unsigned long long remaining = task->total - task->done;
|
||||
if(delta && elapsed_ns > 0) {
|
||||
long double seconds = (long double)elapsed_ns / 1000000000.0L;
|
||||
long double eta = ((long double)remaining / (long double)delta) * seconds;
|
||||
task->eta = eta > 0 ? (unsigned long long)(eta + 0.999999L) : 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
task->eta = task->speed ? (task->total - task->done + task->speed - 1) / task->speed : 0;
|
||||
}
|
||||
|
||||
void
|
||||
task_update(file_task_t *task, task_state_t state, const char *current,
|
||||
unsigned long long add_done, const char *error) {
|
||||
struct timespec now_mono;
|
||||
time_t now;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &now_mono);
|
||||
now = time(NULL);
|
||||
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
task->state = state;
|
||||
if(current) {
|
||||
snprintf(task->current, sizeof(task->current), "%s", current);
|
||||
}
|
||||
if(add_done) {
|
||||
if(!task->transfer_started_at) {
|
||||
task->transfer_started_at = now;
|
||||
}
|
||||
task->done += add_done;
|
||||
if(task->total && task->done > task->total) {
|
||||
task->done = task->total;
|
||||
}
|
||||
if(task->speed_sample_time.tv_sec) {
|
||||
long long elapsed_ns = timespec_delta_ns(&now_mono, &task->speed_sample_time);
|
||||
if(elapsed_ns >= 250000000LL) {
|
||||
unsigned long long delta = task->done - task->speed_sample_done;
|
||||
task->speed = (unsigned long long)((delta * 1000000000ULL) /
|
||||
(unsigned long long)elapsed_ns);
|
||||
task->speed_sample_done = task->done;
|
||||
task->speed_sample_time = now_mono;
|
||||
}
|
||||
} else {
|
||||
task->speed_sample_done = task->done;
|
||||
task->speed_sample_time = now_mono;
|
||||
}
|
||||
task_update_eta_locked(task, &now_mono);
|
||||
}
|
||||
if(error) {
|
||||
snprintf(task->error, sizeof(task->error), "%s", error);
|
||||
}
|
||||
task->updated_at = now;
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
}
|
||||
-445
@@ -1,445 +0,0 @@
|
||||
#include "filemgr_internal.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "json_util.h"
|
||||
#include "path_util.h"
|
||||
#include "websrv.h"
|
||||
|
||||
#define TEXT_FILE_MAX_SIZE (1024 * 1024)
|
||||
|
||||
typedef enum text_newline {
|
||||
TEXT_NEWLINE_LF,
|
||||
TEXT_NEWLINE_CRLF,
|
||||
TEXT_NEWLINE_CR,
|
||||
} text_newline_t;
|
||||
|
||||
static enum MHD_Result
|
||||
send_json_version(struct MHD_Connection *conn, unsigned long long version) {
|
||||
char *data;
|
||||
|
||||
if(asprintf(&data, "{\"ok\":true,\"version\":\"%016llx\"}", version) < 0) {
|
||||
return MHD_NO;
|
||||
}
|
||||
return send_buffer(conn, MHD_HTTP_OK, data, "application/json");
|
||||
}
|
||||
|
||||
static int
|
||||
text_extension_allowed(const char *path) {
|
||||
static const char *extensions[] = {
|
||||
".txt", ".json", ".xml", ".ini", ".cfg", ".conf", ".md",
|
||||
".log", ".lua", ".js", ".css", ".html", ".htm", ".c", ".h",
|
||||
".cpp", ".hpp", ".sh", ".csv", ".yaml", ".yml", ".shn"
|
||||
};
|
||||
const char *extension = strrchr(path, '.');
|
||||
size_t i;
|
||||
|
||||
if(!extension) {
|
||||
return 0;
|
||||
}
|
||||
for(i = 0; i < sizeof(extensions) / sizeof(extensions[0]); i++) {
|
||||
if(!strcasecmp(extension, extensions[i])) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
valid_utf8(const unsigned char *data, size_t size) {
|
||||
size_t i = 0;
|
||||
|
||||
while(i < size) {
|
||||
unsigned char c = data[i++];
|
||||
size_t trailing;
|
||||
unsigned int codepoint;
|
||||
|
||||
if(!c) return 0;
|
||||
if(c < 0x80) continue;
|
||||
if(c >= 0xc2 && c <= 0xdf) {
|
||||
trailing = 1;
|
||||
codepoint = c & 0x1f;
|
||||
} else if(c >= 0xe0 && c <= 0xef) {
|
||||
trailing = 2;
|
||||
codepoint = c & 0x0f;
|
||||
} else if(c >= 0xf0 && c <= 0xf4) {
|
||||
trailing = 3;
|
||||
codepoint = c & 0x07;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
if(trailing > size - i) return 0;
|
||||
while(trailing--) {
|
||||
unsigned char next = data[i++];
|
||||
if((next & 0xc0) != 0x80) return 0;
|
||||
codepoint = (codepoint << 6) | (next & 0x3f);
|
||||
}
|
||||
if((codepoint >= 0xd800 && codepoint <= 0xdfff) || codepoint > 0x10ffff ||
|
||||
(codepoint < 0x800 && c >= 0xe0) ||
|
||||
(codepoint < 0x10000 && c >= 0xf0)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static unsigned long long
|
||||
text_version(const unsigned char *data, size_t size) {
|
||||
unsigned long long hash = 1469598103934665603ULL;
|
||||
size_t i;
|
||||
|
||||
for(i = 0; i < size; i++) {
|
||||
hash ^= data[i];
|
||||
hash *= 1099511628211ULL;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
static text_newline_t
|
||||
detect_text_newline(const unsigned char *data, size_t size) {
|
||||
size_t crlf = 0;
|
||||
size_t lf = 0;
|
||||
size_t cr = 0;
|
||||
size_t i;
|
||||
|
||||
for(i = 0; i < size; i++) {
|
||||
if(data[i] == '\r') {
|
||||
if(i + 1 < size && data[i + 1] == '\n') {
|
||||
crlf++;
|
||||
i++;
|
||||
} else {
|
||||
cr++;
|
||||
}
|
||||
} else if(data[i] == '\n') {
|
||||
lf++;
|
||||
}
|
||||
}
|
||||
if(crlf > lf && crlf >= cr) return TEXT_NEWLINE_CRLF;
|
||||
if(cr > lf && cr > crlf) return TEXT_NEWLINE_CR;
|
||||
return TEXT_NEWLINE_LF;
|
||||
}
|
||||
|
||||
static int
|
||||
format_text_for_save(const char *body, size_t body_size,
|
||||
const unsigned char *current, size_t current_size,
|
||||
char **output, size_t *output_size) {
|
||||
static const unsigned char bom[] = {0xef, 0xbb, 0xbf};
|
||||
int keep_bom = current_size >= sizeof(bom) &&
|
||||
!memcmp(current, bom, sizeof(bom));
|
||||
text_newline_t newline = detect_text_newline(
|
||||
current + (keep_bom ? sizeof(bom) : 0),
|
||||
current_size - (keep_bom ? sizeof(bom) : 0));
|
||||
const unsigned char *input = (const unsigned char *)(body ? body : "");
|
||||
size_t input_size = body_size;
|
||||
size_t capacity = body_size * (newline == TEXT_NEWLINE_CRLF ? 2 : 1) +
|
||||
sizeof(bom) + 1;
|
||||
char *formatted;
|
||||
size_t i;
|
||||
size_t len = 0;
|
||||
|
||||
if(input_size >= sizeof(bom) && !memcmp(input, bom, sizeof(bom))) {
|
||||
input += sizeof(bom);
|
||||
input_size -= sizeof(bom);
|
||||
}
|
||||
if(!(formatted = malloc(capacity))) {
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
if(keep_bom) {
|
||||
memcpy(formatted + len, bom, sizeof(bom));
|
||||
len += sizeof(bom);
|
||||
}
|
||||
for(i = 0; i < input_size; i++) {
|
||||
unsigned char c = input[i];
|
||||
|
||||
if(c != '\r' && c != '\n') {
|
||||
formatted[len++] = (char)c;
|
||||
continue;
|
||||
}
|
||||
if(c == '\r' && i + 1 < input_size && input[i + 1] == '\n') {
|
||||
i++;
|
||||
}
|
||||
if(newline == TEXT_NEWLINE_CRLF) {
|
||||
formatted[len++] = '\r';
|
||||
formatted[len++] = '\n';
|
||||
} else {
|
||||
formatted[len++] = newline == TEXT_NEWLINE_CR ? '\r' : '\n';
|
||||
}
|
||||
}
|
||||
if(len > TEXT_FILE_MAX_SIZE) {
|
||||
free(formatted);
|
||||
errno = EFBIG;
|
||||
return -1;
|
||||
}
|
||||
formatted[len] = 0;
|
||||
*output = formatted;
|
||||
*output_size = len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
read_text_file(const char *path, char **data, size_t *size,
|
||||
struct stat *st) {
|
||||
FILE *file;
|
||||
size_t read_size;
|
||||
|
||||
*data = NULL;
|
||||
*size = 0;
|
||||
if(lstat(path, st) || !S_ISREG(st->st_mode)) {
|
||||
return -1;
|
||||
}
|
||||
if(st->st_size < 0 || (unsigned long long)st->st_size > TEXT_FILE_MAX_SIZE) {
|
||||
errno = EFBIG;
|
||||
return -1;
|
||||
}
|
||||
if(!(file = fopen(path, "rb"))) {
|
||||
return -1;
|
||||
}
|
||||
if(!(*data = malloc((size_t)st->st_size + 1))) {
|
||||
fclose(file);
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
read_size = fread(*data, 1, (size_t)st->st_size, file);
|
||||
if(read_size != (size_t)st->st_size || ferror(file)) {
|
||||
free(*data);
|
||||
*data = NULL;
|
||||
fclose(file);
|
||||
return -1;
|
||||
}
|
||||
fclose(file);
|
||||
(*data)[read_size] = 0;
|
||||
*size = read_size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static enum MHD_Result
|
||||
send_text_file(struct MHD_Connection *conn, char *data, size_t size,
|
||||
unsigned long long version) {
|
||||
struct MHD_Response *resp;
|
||||
enum MHD_Result ret;
|
||||
char version_text[24];
|
||||
|
||||
if(!(resp = MHD_create_response_from_buffer(size, data,
|
||||
MHD_RESPMEM_MUST_FREE))) {
|
||||
free(data);
|
||||
return MHD_NO;
|
||||
}
|
||||
snprintf(version_text, sizeof(version_text), "%016llx", version);
|
||||
MHD_add_response_header(resp, MHD_HTTP_HEADER_CONTENT_TYPE,
|
||||
"text/plain; charset=utf-8");
|
||||
MHD_add_response_header(resp, "X-Text-Version", version_text);
|
||||
ret = websrv_queue_response(conn, MHD_HTTP_OK, resp);
|
||||
MHD_destroy_response(resp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
enum MHD_Result
|
||||
api_text(struct MHD_Connection *conn) {
|
||||
char *path = fs_path_value(query_value(conn, "path"));
|
||||
char *data;
|
||||
size_t size;
|
||||
struct stat st;
|
||||
unsigned long long version;
|
||||
|
||||
if(has_active_task()) {
|
||||
free(path);
|
||||
return send_json_error(conn, MHD_HTTP_CONFLICT, "another task is running");
|
||||
}
|
||||
if(!path || !text_extension_allowed(path)) {
|
||||
free(path);
|
||||
return send_json_error(conn, MHD_HTTP_BAD_REQUEST,
|
||||
"file type is not editable");
|
||||
}
|
||||
if(read_text_file(path, &data, &size, &st)) {
|
||||
int error = errno;
|
||||
free(path);
|
||||
if(error == EFBIG) {
|
||||
return send_json_error(conn, MHD_HTTP_CONTENT_TOO_LARGE,
|
||||
"text file is too large");
|
||||
}
|
||||
errno = error;
|
||||
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "file not found");
|
||||
}
|
||||
free(path);
|
||||
if(!valid_utf8((const unsigned char *)data, size)) {
|
||||
free(data);
|
||||
return send_json_error(conn, MHD_HTTP_UNSUPPORTED_MEDIA_TYPE,
|
||||
"file is not valid UTF-8");
|
||||
}
|
||||
version = text_version((const unsigned char *)data, size);
|
||||
return send_text_file(conn, data, size, version);
|
||||
}
|
||||
|
||||
enum MHD_Result
|
||||
api_text_create(struct MHD_Connection *conn) {
|
||||
char *path = fs_path_value(query_value(conn, "path"));
|
||||
char *name = fs_path_value(query_value(conn, "name"));
|
||||
char target[PATH_MAX];
|
||||
int fd = -1;
|
||||
int ret = -1;
|
||||
int error = 0;
|
||||
int created = 0;
|
||||
|
||||
if(has_active_task()) {
|
||||
free(path); free(name);
|
||||
return send_json_error(conn, MHD_HTTP_CONFLICT, "another task is running");
|
||||
}
|
||||
if(!path || !name || path_join(target, sizeof(target), path, name)) {
|
||||
free(path); free(name);
|
||||
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid path");
|
||||
}
|
||||
if(mode_access(path, W_OK | X_OK)) {
|
||||
free(path); free(name);
|
||||
return send_json_error(conn, MHD_HTTP_FORBIDDEN,
|
||||
"text file is not writable");
|
||||
}
|
||||
if((fd = open(target, O_WRONLY | O_CREAT | O_EXCL, 0777)) >= 0) {
|
||||
created = 1;
|
||||
ret = fchmod_0777(fd);
|
||||
if(close(fd) && !ret) ret = -1;
|
||||
fd = -1;
|
||||
}
|
||||
if(ret) {
|
||||
error = errno;
|
||||
if(fd >= 0) close(fd);
|
||||
if(created) unlink(target);
|
||||
}
|
||||
free(path); free(name);
|
||||
if(!ret) {
|
||||
return send_json_version(conn,
|
||||
text_version((const unsigned char *)"", 0));
|
||||
}
|
||||
errno = error;
|
||||
return error == EEXIST ?
|
||||
send_json_error(conn, MHD_HTTP_CONFLICT, "file already exists") :
|
||||
send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, NULL);
|
||||
}
|
||||
|
||||
static int
|
||||
write_text_atomic(const char *path, const char *body, size_t body_size,
|
||||
mode_t mode) {
|
||||
struct timespec now;
|
||||
char temp[PATH_MAX];
|
||||
size_t written = 0;
|
||||
int fd = -1;
|
||||
int ret = -1;
|
||||
int n;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
n = snprintf(temp, sizeof(temp), "%s.wfm-%ld-%ld.tmp", path,
|
||||
(long)getpid(), now.tv_nsec);
|
||||
if(n < 0 || (size_t)n >= sizeof(temp)) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
if((fd = open(temp, O_WRONLY | O_CREAT | O_EXCL, 0600)) < 0) {
|
||||
return -1;
|
||||
}
|
||||
while(written < body_size) {
|
||||
ssize_t count = write(fd, body + written, body_size - written);
|
||||
if(count <= 0) {
|
||||
goto done;
|
||||
}
|
||||
written += (size_t)count;
|
||||
}
|
||||
if(fchmod(fd, mode & 07777) && !ignore_chmod_error(errno)) {
|
||||
goto done;
|
||||
}
|
||||
if(fsync(fd)) {
|
||||
goto done;
|
||||
}
|
||||
if(close(fd)) {
|
||||
fd = -1;
|
||||
goto done;
|
||||
}
|
||||
fd = -1;
|
||||
if(rename(temp, path)) {
|
||||
goto done;
|
||||
}
|
||||
ret = 0;
|
||||
|
||||
done:
|
||||
if(fd >= 0) close(fd);
|
||||
if(ret) unlink(temp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
enum MHD_Result
|
||||
api_text_save(struct MHD_Connection *conn, const char *body,
|
||||
size_t body_size) {
|
||||
char *path = fs_path_value(query_value(conn, "path"));
|
||||
char *expected = query_value(conn, "version");
|
||||
char *current = NULL;
|
||||
size_t current_size = 0;
|
||||
struct stat st;
|
||||
char version_text[24];
|
||||
char parent[PATH_MAX];
|
||||
char *formatted = NULL;
|
||||
size_t formatted_size = 0;
|
||||
int ret;
|
||||
|
||||
if(has_active_task()) {
|
||||
free(path); free(expected);
|
||||
return send_json_error(conn, MHD_HTTP_CONFLICT, "another task is running");
|
||||
}
|
||||
if(!path || !expected) {
|
||||
free(path); free(expected);
|
||||
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid path");
|
||||
}
|
||||
if(body_size > TEXT_FILE_MAX_SIZE) {
|
||||
free(path); free(expected);
|
||||
return send_json_error(conn, MHD_HTTP_CONTENT_TOO_LARGE,
|
||||
"text file is too large");
|
||||
}
|
||||
if(!valid_utf8((const unsigned char *)(body ? body : ""), body_size)) {
|
||||
free(path); free(expected);
|
||||
return send_json_error(conn, MHD_HTTP_UNSUPPORTED_MEDIA_TYPE,
|
||||
"file is not valid UTF-8");
|
||||
}
|
||||
if(read_text_file(path, ¤t, ¤t_size, &st)) {
|
||||
free(path); free(expected);
|
||||
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "file not found");
|
||||
}
|
||||
snprintf(version_text, sizeof(version_text), "%016llx",
|
||||
text_version((const unsigned char *)current, current_size));
|
||||
if(strcmp(expected, version_text)) {
|
||||
free(current);
|
||||
free(path); free(expected);
|
||||
return send_json_error(conn, MHD_HTTP_CONFLICT,
|
||||
"file changed since it was opened");
|
||||
}
|
||||
if(path_dirname(path, parent, sizeof(parent)) ||
|
||||
mode_access(path, W_OK) || mode_access(parent, W_OK | X_OK)) {
|
||||
free(current);
|
||||
free(path); free(expected);
|
||||
return send_json_error(conn, MHD_HTTP_FORBIDDEN,
|
||||
"text file is not writable");
|
||||
}
|
||||
if(format_text_for_save(body, body_size, (const unsigned char *)current,
|
||||
current_size, &formatted, &formatted_size)) {
|
||||
int error = errno;
|
||||
free(current);
|
||||
free(path); free(expected);
|
||||
errno = error;
|
||||
return error == EFBIG ?
|
||||
send_json_error(conn, MHD_HTTP_CONTENT_TOO_LARGE,
|
||||
"text file is too large") :
|
||||
send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, NULL);
|
||||
}
|
||||
free(current);
|
||||
ret = write_text_atomic(path, formatted, formatted_size, st.st_mode);
|
||||
free(formatted);
|
||||
free(path); free(expected);
|
||||
return ret ? send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, NULL)
|
||||
: send_json_ok(conn);
|
||||
}
|
||||
-582
@@ -1,582 +0,0 @@
|
||||
#include "filemgr.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "filemgr_internal.h"
|
||||
#include "json_util.h"
|
||||
#include "path_util.h"
|
||||
|
||||
#define UPLOAD_BUFFER_SIZE (1024 * 1024)
|
||||
|
||||
typedef struct upload_context {
|
||||
file_task_t *task;
|
||||
int fd;
|
||||
char *buffer;
|
||||
size_t buffered;
|
||||
char temp[PATH_MAX];
|
||||
char target[PATH_MAX];
|
||||
unsigned long long expected;
|
||||
unsigned long long written;
|
||||
int failed;
|
||||
int error;
|
||||
int task_done;
|
||||
const char *stage;
|
||||
char error_message[256];
|
||||
} upload_context_t;
|
||||
|
||||
static const char *
|
||||
upload_error_message(upload_context_t *ctx) {
|
||||
if(!ctx->error_message[0]) {
|
||||
snprintf(ctx->error_message, sizeof(ctx->error_message), "%s failed%s%s: %s",
|
||||
ctx->stage ? ctx->stage : "upload",
|
||||
ctx->target[0] ? " for " : "",
|
||||
ctx->target[0] ? ctx->target : "",
|
||||
strerror(ctx->error ? ctx->error : EIO));
|
||||
}
|
||||
return ctx->error_message;
|
||||
}
|
||||
|
||||
static int
|
||||
upload_flush(upload_context_t *ctx) {
|
||||
size_t written = 0;
|
||||
|
||||
while(written < ctx->buffered) {
|
||||
ssize_t n;
|
||||
|
||||
if(ctx->task && task_cancel_requested(ctx->task)) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = ECANCELED;
|
||||
return -1;
|
||||
}
|
||||
n = write(ctx->fd, ctx->buffer + written, ctx->buffered - written);
|
||||
if(n <= 0) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = errno ? errno : EIO;
|
||||
return -1;
|
||||
}
|
||||
written += (size_t)n;
|
||||
ctx->written += (unsigned long long)n;
|
||||
}
|
||||
if(ctx->task && written) {
|
||||
task_update(ctx->task, TASK_RUNNING, ctx->target,
|
||||
(unsigned long long)written, NULL);
|
||||
}
|
||||
ctx->buffered = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
finish_upload_task(file_task_t *task, task_state_t state, const char *current,
|
||||
const char *error) {
|
||||
if(!task) {
|
||||
return;
|
||||
}
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
if(task_is_active(task)) {
|
||||
time_t completed_at = time(NULL);
|
||||
task->state = state;
|
||||
if(current) {
|
||||
snprintf(task->current, sizeof(task->current), "%s", current);
|
||||
}
|
||||
if(state == TASK_DONE && task->total) {
|
||||
task->done = task->total;
|
||||
}
|
||||
if(state == TASK_DONE) {
|
||||
record_task_completion_locked(task, completed_at);
|
||||
}
|
||||
if(error) {
|
||||
snprintf(task->error, sizeof(task->error), "%s", error);
|
||||
}
|
||||
if(state == TASK_FAILED) {
|
||||
snprintf(task->error_code, sizeof(task->error_code), "upload_failed");
|
||||
snprintf(task->error_arg, sizeof(task->error_arg), "%s",
|
||||
current ? current : task->src);
|
||||
}
|
||||
task->updated_at = completed_at;
|
||||
}
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
}
|
||||
|
||||
static void
|
||||
finish_upload_context_error(upload_context_t *ctx) {
|
||||
if(!ctx->task || ctx->task_done) {
|
||||
return;
|
||||
}
|
||||
upload_error_message(ctx);
|
||||
finish_upload_task(ctx->task,
|
||||
ctx->error == ECANCELED ? TASK_CANCELED : TASK_FAILED,
|
||||
ctx->target[0] ? ctx->target : ctx->task->current,
|
||||
ctx->error == ECANCELED ? "canceled" : ctx->error_message);
|
||||
ctx->task_done = 1;
|
||||
}
|
||||
|
||||
static file_task_t *
|
||||
upload_task_from_conn(struct MHD_Connection *conn) {
|
||||
char *idstr = request_value(conn, "X-WFM-Task-ID", "task_id");
|
||||
unsigned long id = idstr ? strtoul(idstr, NULL, 10) : 0;
|
||||
file_task_t *task = NULL;
|
||||
|
||||
free(idstr);
|
||||
if(!id) {
|
||||
return NULL;
|
||||
}
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
task = find_task_locked(id);
|
||||
if(!task || task->op != TASK_UPLOAD || !task_is_active(task)) {
|
||||
task = NULL;
|
||||
}
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
return task;
|
||||
}
|
||||
|
||||
static int
|
||||
check_upload_manifest_space(const char *base, const char *rels,
|
||||
const char *sizes, unsigned long long fallback_total,
|
||||
char *error, size_t error_size,
|
||||
char *code, size_t code_size,
|
||||
char *arg, size_t arg_size) {
|
||||
char *rels_copy = NULL;
|
||||
char *sizes_copy = NULL;
|
||||
char *rel;
|
||||
char *size_text;
|
||||
char *rel_save;
|
||||
char *size_save;
|
||||
unsigned long long available;
|
||||
int ret = -1;
|
||||
|
||||
if(!rels || !sizes) {
|
||||
return check_target_space(base, fallback_total, error, error_size,
|
||||
code, code_size, arg, arg_size);
|
||||
}
|
||||
if(target_available_space(base, &available)) {
|
||||
snprintf(error, error_size, "cannot read target free space");
|
||||
snprintf(code, code_size, "space_check_failed");
|
||||
snprintf(arg, arg_size, "%s", base);
|
||||
return -1;
|
||||
}
|
||||
if(!(rels_copy = strdup(rels)) || !(sizes_copy = strdup(sizes))) {
|
||||
errno = ENOMEM;
|
||||
goto done;
|
||||
}
|
||||
|
||||
rel = strtok_r(rels_copy, "\n", &rel_save);
|
||||
size_text = strtok_r(sizes_copy, "\n", &size_save);
|
||||
while(rel || size_text) {
|
||||
char target[PATH_MAX];
|
||||
unsigned long long size;
|
||||
|
||||
if(!rel || !size_text || path_join_relative(target, sizeof(target), base, rel)) {
|
||||
snprintf(error, error_size, "invalid path");
|
||||
snprintf(code, code_size, "invalid_path");
|
||||
snprintf(arg, arg_size, "%s", rel ? rel : "");
|
||||
errno = EINVAL;
|
||||
goto done;
|
||||
}
|
||||
|
||||
size = strtoull(size_text, NULL, 10);
|
||||
if(available < size) {
|
||||
snprintf(error, error_size,
|
||||
"not enough target space, required %llu bytes, available %llu bytes",
|
||||
size, available);
|
||||
snprintf(code, code_size, "no_space");
|
||||
snprintf(arg, arg_size, "%llu,%llu", size, available);
|
||||
errno = ENOSPC;
|
||||
goto done;
|
||||
}
|
||||
|
||||
available -= size;
|
||||
|
||||
rel = strtok_r(NULL, "\n", &rel_save);
|
||||
size_text = strtok_r(NULL, "\n", &size_save);
|
||||
}
|
||||
|
||||
ret = 0;
|
||||
|
||||
done:
|
||||
free(rels_copy);
|
||||
free(sizes_copy);
|
||||
return ret;
|
||||
}
|
||||
|
||||
enum MHD_Result
|
||||
api_upload_prepare(struct MHD_Connection *conn, const char *body,
|
||||
size_t body_size) {
|
||||
char *path = fs_path_value(body_form_value(body, body_size, "path"));
|
||||
char *src = body_form_value(body, body_size, "src");
|
||||
char *total_text = body_form_value(body, body_size, "total");
|
||||
char *count_text = body_form_value(body, body_size, "count");
|
||||
char *rels = body_form_value(body, body_size, "rels");
|
||||
char *sizes = body_form_value(body, body_size, "sizes");
|
||||
char *overwrite = body_form_value(body, body_size, "overwrite");
|
||||
file_task_t *task = calloc(1, sizeof(*task));
|
||||
strbuf_t b = {0};
|
||||
unsigned long long total = total_text ? strtoull(total_text, NULL, 10) : 0;
|
||||
size_t count = count_text ? (size_t)strtoull(count_text, NULL, 10) : 0;
|
||||
char error[128] = {0};
|
||||
char code[64] = {0};
|
||||
char arg[PATH_MAX + 96] = {0};
|
||||
|
||||
if(!task) {
|
||||
free(path); free(src); free(total_text); free(count_text);
|
||||
free(rels); free(sizes); free(overwrite);
|
||||
return send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, "out of memory");
|
||||
}
|
||||
if(!path || !src || !count) {
|
||||
free_task(task);
|
||||
free(path); free(src); free(total_text); free(count_text);
|
||||
free(rels); free(sizes); free(overwrite);
|
||||
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid path");
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
remove_finished_tasks_locked();
|
||||
if(has_active_task_locked()) {
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
free_task(task);
|
||||
free(path); free(src); free(total_text); free(count_text);
|
||||
free(rels); free(sizes); free(overwrite);
|
||||
return send_json_error(conn, MHD_HTTP_CONFLICT, "another task is running");
|
||||
}
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
|
||||
if(check_upload_manifest_space(path, rels, sizes, total, error, sizeof(error),
|
||||
code, sizeof(code), arg, sizeof(arg))) {
|
||||
free_task(task);
|
||||
free(path); free(src); free(total_text); free(count_text);
|
||||
free(rels); free(sizes); free(overwrite);
|
||||
return send_json_error_detail(conn,
|
||||
errno == ENOSPC ? MHD_HTTP_INSUFFICIENT_STORAGE :
|
||||
MHD_HTTP_INTERNAL_SERVER_ERROR,
|
||||
error[0] ? error : NULL,
|
||||
code[0] ? code : NULL,
|
||||
arg[0] ? arg : NULL);
|
||||
}
|
||||
|
||||
task->op = TASK_UPLOAD;
|
||||
task->state = TASK_RUNNING;
|
||||
task->src_count = count;
|
||||
task->file_count = count;
|
||||
task->total = total;
|
||||
snprintf(task->src, sizeof(task->src), "%s", src);
|
||||
snprintf(task->dst, sizeof(task->dst), "%s", path);
|
||||
snprintf(task->current, sizeof(task->current), "%s", src);
|
||||
task->created_at = time(NULL);
|
||||
task->updated_at = task->created_at;
|
||||
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
remove_finished_tasks_locked();
|
||||
if(has_active_task_locked()) {
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
free_task(task);
|
||||
free(path); free(src); free(total_text); free(count_text);
|
||||
free(rels); free(sizes); free(overwrite);
|
||||
return send_json_error(conn, MHD_HTTP_CONFLICT, "another task is running");
|
||||
}
|
||||
task->id = g_next_task_id++;
|
||||
task->next = g_tasks;
|
||||
g_tasks = task;
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
|
||||
free(path); free(src); free(total_text); free(count_text);
|
||||
free(rels); free(sizes); free(overwrite);
|
||||
strbuf_printf(&b, "{\"ok\":true,\"task_id\":%lu}", task->id);
|
||||
return send_buffer(conn, MHD_HTTP_OK, b.data, "application/json");
|
||||
}
|
||||
|
||||
enum MHD_Result
|
||||
api_upload_finish(struct MHD_Connection *conn) {
|
||||
file_task_t *task = upload_task_from_conn(conn);
|
||||
|
||||
if(!task) {
|
||||
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "active task not found");
|
||||
}
|
||||
if(task_cancel_requested(task)) {
|
||||
finish_upload_task(task, TASK_CANCELED,
|
||||
task->current[0] ? task->current : task->src,
|
||||
"canceled");
|
||||
return send_json_error(conn, MHD_HTTP_CONFLICT, "canceled");
|
||||
}
|
||||
finish_upload_task(task, TASK_DONE,
|
||||
task->current[0] ? task->current : task->src, NULL);
|
||||
return send_json_ok(conn);
|
||||
}
|
||||
|
||||
int
|
||||
filemgr_upload_begin(struct MHD_Connection *conn, void **upload_ctx) {
|
||||
upload_context_t *ctx;
|
||||
char *base = fs_path_value(request_value(conn, "X-WFM-Path", "path"));
|
||||
char *rel = request_value(conn, "X-WFM-Rel", "rel");
|
||||
char *overwrite = request_value(conn, "X-WFM-Overwrite", "overwrite");
|
||||
char *size_text = request_value(conn, "X-WFM-Size", "size");
|
||||
file_task_t *task = upload_task_from_conn(conn);
|
||||
char **checked_dirs = NULL;
|
||||
size_t checked_dir_count = 0;
|
||||
struct stat st;
|
||||
char error[128] = {0};
|
||||
char code[64] = {0};
|
||||
char arg[PATH_MAX + 96] = {0};
|
||||
int n;
|
||||
|
||||
*upload_ctx = NULL;
|
||||
if(!(ctx = calloc(1, sizeof(*ctx)))) {
|
||||
free(base); free(rel); free(overwrite); free(size_text);
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
ctx->fd = -1;
|
||||
ctx->stage = "preparing upload";
|
||||
*upload_ctx = ctx;
|
||||
|
||||
if(size_text) {
|
||||
ctx->expected = strtoull(size_text, NULL, 10);
|
||||
}
|
||||
ctx->task = task;
|
||||
if(task) {
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
task->active_streams++;
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
}
|
||||
if(task && task_cancel_requested(task)) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = ECANCELED;
|
||||
goto fail;
|
||||
}
|
||||
if(!task && has_active_task()) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = EBUSY;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "validating target path";
|
||||
if(!base || !rel || path_join_relative(ctx->target, sizeof(ctx->target),
|
||||
base, rel)) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = errno ? errno : EINVAL;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "creating target folders";
|
||||
if(ensure_parent_dirs(base, rel)) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = errno ? errno : EACCES;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "checking target path";
|
||||
if(!lstat(ctx->target, &st)) {
|
||||
if(S_ISDIR(st.st_mode) || !overwrite || strcmp(overwrite, "1")) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = S_ISDIR(st.st_mode) ? EISDIR : EEXIST;
|
||||
goto fail;
|
||||
}
|
||||
} else if(errno != ENOENT) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = errno;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "checking target permissions";
|
||||
if(check_target_writable(ctx->target, &checked_dirs, &checked_dir_count,
|
||||
error, sizeof(error), code, sizeof(code),
|
||||
arg, sizeof(arg))) {
|
||||
snprintf(ctx->error_message, sizeof(ctx->error_message), "%s",
|
||||
error[0] ? error : "target is not writable");
|
||||
ctx->failed = 1;
|
||||
ctx->error = errno ? errno : EACCES;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "checking target space";
|
||||
if(check_target_space(ctx->target, ctx->expected, error, sizeof(error),
|
||||
code, sizeof(code), arg, sizeof(arg))) {
|
||||
snprintf(ctx->error_message, sizeof(ctx->error_message), "%s",
|
||||
error[0] ? error : "not enough target space");
|
||||
ctx->failed = 1;
|
||||
ctx->error = errno ? errno : ENOSPC;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "creating temporary path";
|
||||
n = snprintf(ctx->temp, sizeof(ctx->temp), "%s.wfm-upload-%ld-%lld.tmp",
|
||||
ctx->target, (long)getpid(), (long long)time(NULL));
|
||||
if(n < 0 || (size_t)n >= sizeof(ctx->temp)) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = ENAMETOOLONG;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "opening temporary file";
|
||||
ctx->fd = open(ctx->temp, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
if(ctx->fd < 0) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = errno;
|
||||
goto fail;
|
||||
}
|
||||
ctx->stage = "allocating upload buffer";
|
||||
if(!(ctx->buffer = malloc(UPLOAD_BUFFER_SIZE))) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = ENOMEM;
|
||||
goto fail;
|
||||
}
|
||||
|
||||
fail:
|
||||
free_paths(checked_dirs, checked_dir_count);
|
||||
free(base); free(rel); free(overwrite); free(size_text);
|
||||
if(ctx->failed) {
|
||||
finish_upload_context_error(ctx);
|
||||
if(ctx->fd >= 0) {
|
||||
close(ctx->fd);
|
||||
ctx->fd = -1;
|
||||
}
|
||||
if(ctx->temp[0]) {
|
||||
unlink(ctx->temp);
|
||||
}
|
||||
errno = ctx->error ? ctx->error : EIO;
|
||||
return -1;
|
||||
}
|
||||
ctx->stage = "writing file";
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
filemgr_upload_data(void *upload_ctx, const char *data, size_t size) {
|
||||
upload_context_t *ctx = upload_ctx;
|
||||
|
||||
if(!ctx || ctx->failed) {
|
||||
return -1;
|
||||
}
|
||||
if(ctx->task && task_cancel_requested(ctx->task)) {
|
||||
ctx->failed = 1;
|
||||
ctx->error = ECANCELED;
|
||||
finish_upload_context_error(ctx);
|
||||
return -1;
|
||||
}
|
||||
while(size) {
|
||||
size_t space = UPLOAD_BUFFER_SIZE - ctx->buffered;
|
||||
size_t take = size < space ? size : space;
|
||||
|
||||
memcpy(ctx->buffer + ctx->buffered, data, take);
|
||||
ctx->buffered += take;
|
||||
data += take;
|
||||
size -= take;
|
||||
if(ctx->buffered == UPLOAD_BUFFER_SIZE && upload_flush(ctx)) {
|
||||
finish_upload_context_error(ctx);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
enum MHD_Result
|
||||
filemgr_upload_finish(struct MHD_Connection *conn, void *upload_ctx) {
|
||||
upload_context_t *ctx = upload_ctx;
|
||||
int ret = -1;
|
||||
|
||||
if(!ctx) {
|
||||
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid path");
|
||||
}
|
||||
if(ctx->failed) {
|
||||
finish_upload_context_error(ctx);
|
||||
errno = ctx->error ? ctx->error : EIO;
|
||||
return send_json_error_detail(conn,
|
||||
errno == EEXIST ? MHD_HTTP_CONFLICT :
|
||||
errno == EBUSY ? MHD_HTTP_CONFLICT :
|
||||
errno == ECANCELED ? MHD_HTTP_CONFLICT :
|
||||
errno == ENOSPC ? MHD_HTTP_INSUFFICIENT_STORAGE :
|
||||
MHD_HTTP_INTERNAL_SERVER_ERROR,
|
||||
ctx->error == ECANCELED ? "canceled" : ctx->error_message,
|
||||
ctx->error == ECANCELED ? NULL : "upload_failed",
|
||||
ctx->target[0] ? ctx->target : NULL);
|
||||
}
|
||||
ctx->stage = "writing file";
|
||||
if(ctx->buffered && upload_flush(ctx)) {
|
||||
ctx->error = ctx->error ? ctx->error : EIO;
|
||||
goto done;
|
||||
}
|
||||
ctx->stage = "verifying uploaded size";
|
||||
if(ctx->expected && ctx->written != ctx->expected) {
|
||||
ctx->error = EIO;
|
||||
goto done;
|
||||
}
|
||||
ctx->stage = "setting file permissions";
|
||||
if(fchmod_0777(ctx->fd)) {
|
||||
ctx->error = errno;
|
||||
goto done;
|
||||
}
|
||||
ctx->stage = "syncing file data";
|
||||
if(fsync(ctx->fd)) {
|
||||
ctx->error = errno;
|
||||
goto done;
|
||||
}
|
||||
ctx->stage = "closing temporary file";
|
||||
if(close(ctx->fd)) {
|
||||
ctx->fd = -1;
|
||||
ctx->error = errno;
|
||||
goto done;
|
||||
}
|
||||
ctx->fd = -1;
|
||||
ctx->stage = "moving temporary file into place";
|
||||
if(rename(ctx->temp, ctx->target)) {
|
||||
ctx->error = errno;
|
||||
goto done;
|
||||
}
|
||||
if(ctx->task) {
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
ctx->task->upload_completed++;
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
}
|
||||
ret = 0;
|
||||
|
||||
done:
|
||||
if(ctx->fd >= 0) {
|
||||
close(ctx->fd);
|
||||
ctx->fd = -1;
|
||||
}
|
||||
if(ret) {
|
||||
upload_error_message(ctx);
|
||||
if(ctx->temp[0]) {
|
||||
unlink(ctx->temp);
|
||||
}
|
||||
finish_upload_context_error(ctx);
|
||||
errno = ctx->error ? ctx->error : EIO;
|
||||
return send_json_error_detail(conn,
|
||||
errno == ECANCELED ? MHD_HTTP_CONFLICT :
|
||||
MHD_HTTP_INTERNAL_SERVER_ERROR,
|
||||
ctx->error == ECANCELED ? "canceled" : ctx->error_message,
|
||||
ctx->error == ECANCELED ? NULL : "upload_failed",
|
||||
ctx->target[0] ? ctx->target : NULL);
|
||||
}
|
||||
return send_json_ok(conn);
|
||||
}
|
||||
|
||||
void
|
||||
filemgr_upload_free(void *upload_ctx) {
|
||||
upload_context_t *ctx = upload_ctx;
|
||||
|
||||
if(!ctx) {
|
||||
return;
|
||||
}
|
||||
if(ctx->fd >= 0) {
|
||||
close(ctx->fd);
|
||||
if(ctx->temp[0]) {
|
||||
unlink(ctx->temp);
|
||||
}
|
||||
if(ctx->task && !ctx->task_done) {
|
||||
int canceled = task_cancel_requested(ctx->task);
|
||||
finish_upload_task(ctx->task, canceled ? TASK_CANCELED : TASK_FAILED,
|
||||
ctx->target[0] ? ctx->target : ctx->task->current,
|
||||
canceled ? "canceled" : "client disconnected");
|
||||
ctx->task_done = 1;
|
||||
}
|
||||
}
|
||||
free(ctx->buffer);
|
||||
if(ctx->task) {
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
if(ctx->task->active_streams) {
|
||||
ctx->task->active_streams--;
|
||||
}
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
}
|
||||
free(ctx);
|
||||
}
|
||||
+6
-82
@@ -6,7 +6,6 @@
|
||||
#include <arpa/inet.h>
|
||||
#include <microhttpd.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -14,38 +13,19 @@
|
||||
#include "filemgr.h"
|
||||
#include "websrv.h"
|
||||
|
||||
#define REQUEST_BODY_MAX (4 * 1024 * 1024)
|
||||
#define HTTP_CONNECTION_MEMORY_LIMIT (8 * 1024 * 1024)
|
||||
#define HTTP_CONNECTION_MEMORY_INCREMENT (2 * 1024 * 1024)
|
||||
#define HTTP_SOCKET_RCVBUF_SIZE (4 * 1024 * 1024)
|
||||
#define HTTP_SOCKET_SNDBUF_SIZE (4 * 1024 * 1024)
|
||||
|
||||
static volatile sig_atomic_t g_stop_requested;
|
||||
static int g_listen_fd = -1;
|
||||
|
||||
static void
|
||||
websrv_tune_connection_socket(int fd) {
|
||||
const int sndbuf = HTTP_SOCKET_SNDBUF_SIZE;
|
||||
const int nodelay = 1;
|
||||
|
||||
/* OrbisOS HTTP sockets need an explicit send buffer to fill a GbE link. */
|
||||
(void)setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
|
||||
(void)setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));
|
||||
}
|
||||
#define REQUEST_BODY_MAX (1024 * 1024)
|
||||
|
||||
typedef struct request_context {
|
||||
char *body;
|
||||
size_t size;
|
||||
int too_large;
|
||||
int upload_stream;
|
||||
void *upload_ctx;
|
||||
} request_context_t;
|
||||
|
||||
static enum MHD_Result
|
||||
websrv_body_too_large(struct MHD_Connection *conn) {
|
||||
static const char json[] =
|
||||
"{\"ok\":false,\"error\":\"request body is too large\","
|
||||
"\"error_code\":\"request_body_too_large\",\"error_arg\":\"\"}";
|
||||
"\"error_code\":\"text_file_too_large\",\"error_arg\":\"\"}";
|
||||
struct MHD_Response *resp =
|
||||
MHD_create_response_from_buffer(sizeof(json) - 1, (void *)json,
|
||||
MHD_RESPMEM_PERSISTENT);
|
||||
@@ -69,19 +49,6 @@ websrv_queue_response(struct MHD_Connection *conn, unsigned int status,
|
||||
return MHD_queue_response(conn, status, resp);
|
||||
}
|
||||
|
||||
void
|
||||
websrv_stop(void) {
|
||||
g_stop_requested = 1;
|
||||
if(g_listen_fd >= 0) {
|
||||
shutdown(g_listen_fd, SHUT_RDWR);
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
websrv_stop_requested(void) {
|
||||
return g_stop_requested;
|
||||
}
|
||||
|
||||
static enum MHD_Result
|
||||
websrv_on_request(void *cls, struct MHD_Connection *conn, const char *url,
|
||||
const char *method, const char *version,
|
||||
@@ -101,8 +68,6 @@ websrv_on_request(void *cls, struct MHD_Connection *conn, const char *url,
|
||||
if(!(ctx = calloc(1, sizeof(*ctx)))) {
|
||||
return MHD_NO;
|
||||
}
|
||||
ctx->upload_stream = !strcmp(url, "/api/upload-file") &&
|
||||
!strcmp(method, MHD_HTTP_METHOD_POST);
|
||||
*con_cls = ctx;
|
||||
return MHD_YES;
|
||||
}
|
||||
@@ -110,18 +75,6 @@ websrv_on_request(void *cls, struct MHD_Connection *conn, const char *url,
|
||||
if(*upload_data_size) {
|
||||
size_t chunk_size = *upload_data_size;
|
||||
|
||||
if(ctx->upload_stream) {
|
||||
if(!ctx->upload_ctx && filemgr_upload_begin(conn, &ctx->upload_ctx)) {
|
||||
ctx->too_large = 1;
|
||||
}
|
||||
if(ctx->upload_ctx && filemgr_upload_data(ctx->upload_ctx, upload_data,
|
||||
chunk_size)) {
|
||||
ctx->too_large = 1;
|
||||
}
|
||||
*upload_data_size = 0;
|
||||
return MHD_YES;
|
||||
}
|
||||
|
||||
if(chunk_size > REQUEST_BODY_MAX - ctx->size) {
|
||||
ctx->too_large = 1;
|
||||
} else if(!ctx->too_large) {
|
||||
@@ -139,16 +92,7 @@ websrv_on_request(void *cls, struct MHD_Connection *conn, const char *url,
|
||||
}
|
||||
|
||||
if(ctx->too_large) {
|
||||
return ctx->upload_stream ?
|
||||
filemgr_upload_finish(conn, ctx->upload_ctx) :
|
||||
websrv_body_too_large(conn);
|
||||
}
|
||||
|
||||
if(ctx->upload_stream) {
|
||||
if(!ctx->upload_ctx && filemgr_upload_begin(conn, &ctx->upload_ctx)) {
|
||||
return filemgr_upload_finish(conn, ctx->upload_ctx);
|
||||
}
|
||||
return filemgr_upload_finish(conn, ctx->upload_ctx);
|
||||
return websrv_body_too_large(conn);
|
||||
}
|
||||
|
||||
if(!strncmp(url, "/api/", 5)) {
|
||||
@@ -160,9 +104,6 @@ websrv_on_request(void *cls, struct MHD_Connection *conn, const char *url,
|
||||
if(!strcmp(url, "/") || !url[0]) {
|
||||
return asset_request(conn, "/index.html");
|
||||
}
|
||||
if(!strcmp(url, "/favicon.ico")) {
|
||||
return asset_request(conn, "/icon0.png");
|
||||
}
|
||||
return asset_request(conn, url);
|
||||
}
|
||||
|
||||
@@ -175,9 +116,6 @@ websrv_on_completed(void *cls, struct MHD_Connection *connection,
|
||||
request_context_t *ctx = *con_cls;
|
||||
|
||||
if(ctx) {
|
||||
if(ctx->upload_ctx) {
|
||||
filemgr_upload_free(ctx->upload_ctx);
|
||||
}
|
||||
free(ctx->body);
|
||||
free(ctx);
|
||||
}
|
||||
@@ -205,12 +143,6 @@ websrv_listen(unsigned short port) {
|
||||
close(srvfd);
|
||||
return -1;
|
||||
}
|
||||
{
|
||||
const int rcvbuf = HTTP_SOCKET_RCVBUF_SIZE;
|
||||
|
||||
/* Set before listen so accepted PS5 sockets inherit the larger window. */
|
||||
(void)setsockopt(srvfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
|
||||
}
|
||||
|
||||
memset(&server_addr, 0, sizeof(server_addr));
|
||||
server_addr.sin_family = AF_INET;
|
||||
@@ -227,17 +159,11 @@ websrv_listen(unsigned short port) {
|
||||
close(srvfd);
|
||||
return -1;
|
||||
}
|
||||
g_stop_requested = 0;
|
||||
g_listen_fd = srvfd;
|
||||
|
||||
if(!(httpd = MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION | MHD_USE_ITC |
|
||||
MHD_USE_NO_LISTEN_SOCKET | MHD_USE_DEBUG |
|
||||
MHD_USE_INTERNAL_POLLING_THREAD | MHD_USE_TURBO,
|
||||
MHD_USE_INTERNAL_POLLING_THREAD,
|
||||
0, NULL, NULL, &websrv_on_request, NULL,
|
||||
MHD_OPTION_CONNECTION_MEMORY_LIMIT,
|
||||
(size_t)HTTP_CONNECTION_MEMORY_LIMIT,
|
||||
MHD_OPTION_CONNECTION_MEMORY_INCREMENT,
|
||||
(size_t)HTTP_CONNECTION_MEMORY_INCREMENT,
|
||||
MHD_OPTION_NOTIFY_COMPLETED,
|
||||
&websrv_on_completed, NULL, MHD_OPTION_END))) {
|
||||
perror("MHD_start_daemon");
|
||||
@@ -245,13 +171,12 @@ websrv_listen(unsigned short port) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
while(!g_stop_requested) {
|
||||
while(1) {
|
||||
addr_len = sizeof(client_addr);
|
||||
if((connfd = accept(srvfd, (struct sockaddr *)&client_addr, &addr_len)) < 0) {
|
||||
if(!g_stop_requested) perror("accept");
|
||||
perror("accept");
|
||||
break;
|
||||
}
|
||||
websrv_tune_connection_socket(connfd);
|
||||
if(MHD_add_connection(httpd, connfd, (struct sockaddr *)&client_addr,
|
||||
addr_len) != MHD_YES) {
|
||||
perror("MHD_add_connection");
|
||||
@@ -261,6 +186,5 @@ websrv_listen(unsigned short port) {
|
||||
}
|
||||
|
||||
MHD_stop_daemon(httpd);
|
||||
g_listen_fd = -1;
|
||||
return close(srvfd);
|
||||
}
|
||||
@@ -23,5 +23,3 @@ enum MHD_Result websrv_queue_response(struct MHD_Connection *conn,
|
||||
struct MHD_Response *resp);
|
||||
|
||||
int websrv_listen(unsigned short port);
|
||||
void websrv_stop(void);
|
||||
int websrv_stop_requested(void);
|
||||
-1303
File diff suppressed because it is too large.
Load diff
@@ -1,104 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
/* Standalone ZIP extraction engine.
|
||||
No HTTP / task dependencies: it can be compiled and tested on its own. */
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#define ZIPX_PATH_MAX 4096
|
||||
|
||||
typedef enum {
|
||||
ZIPX_PHASE_SCAN = 0,
|
||||
ZIPX_PHASE_EXTRACT = 1,
|
||||
ZIPX_PHASE_PUBLISH = 2,
|
||||
ZIPX_PHASE_CLEANUP = 3
|
||||
} zipx_phase_t;
|
||||
|
||||
typedef enum {
|
||||
ZIPX_OK = 0,
|
||||
ZIPX_ERR_CANCELED,
|
||||
ZIPX_ERR_OPEN, /* cannot open the archive */
|
||||
ZIPX_ERR_FORMAT, /* corrupt central directory / truncated */
|
||||
ZIPX_ERR_UNSUPPORTED,/* encryption, multipart or unsupported method */
|
||||
ZIPX_ERR_UNSAFE_NAME,/* traversal, absolute path, control chars, NUL */
|
||||
ZIPX_ERR_SPECIAL, /* symlink / device / fifo / socket entry */
|
||||
ZIPX_ERR_DUPLICATE, /* repeated entry or file/dir name clash inside zip */
|
||||
ZIPX_ERR_LIMIT_ENTRIES,
|
||||
ZIPX_ERR_LIMIT_FILE,
|
||||
ZIPX_ERR_LIMIT_TOTAL,
|
||||
ZIPX_ERR_LIMIT_RATIO,
|
||||
ZIPX_ERR_LIMIT_DEPTH,
|
||||
ZIPX_ERR_LIMIT_NAME,
|
||||
ZIPX_ERR_CONFLICT, /* target already exists for the chosen policy */
|
||||
ZIPX_ERR_SPACE,
|
||||
ZIPX_ERR_IO,
|
||||
ZIPX_ERR_CRC,
|
||||
ZIPX_ERR_INTERNAL
|
||||
} zipx_status_t;
|
||||
|
||||
typedef enum {
|
||||
ZIPX_CONFLICT_FAIL = 0,
|
||||
ZIPX_CONFLICT_OVERWRITE = 1,
|
||||
ZIPX_CONFLICT_MERGE = 2
|
||||
} zipx_conflict_t;
|
||||
|
||||
typedef struct {
|
||||
uint64_t max_entries;
|
||||
uint64_t max_total_bytes;
|
||||
uint64_t max_file_bytes;
|
||||
uint32_t max_ratio; /* uncompressed/compressed, 0 disables */
|
||||
uint32_t max_depth;
|
||||
uint32_t max_name_len;
|
||||
uint32_t max_path_len;
|
||||
} zipx_limits_t;
|
||||
|
||||
typedef struct {
|
||||
int phase;
|
||||
uint64_t entries_total;
|
||||
uint64_t entries_done;
|
||||
uint64_t bytes_total;
|
||||
uint64_t bytes_done;
|
||||
const char *current; /* entry name being processed, may be NULL */
|
||||
} zipx_progress_t;
|
||||
|
||||
/* Returns non-zero when the caller wants the operation to stop. */
|
||||
typedef int (*zipx_cancel_fn)(void *userdata);
|
||||
typedef void (*zipx_progress_fn)(void *userdata, const zipx_progress_t *progress);
|
||||
|
||||
typedef struct {
|
||||
zipx_status_t status;
|
||||
int sys_errno;
|
||||
uint64_t entries_total;
|
||||
uint64_t entries_done;
|
||||
uint64_t bytes_total;
|
||||
uint64_t files_created;
|
||||
uint64_t dirs_created;
|
||||
char detail[ZIPX_PATH_MAX]; /* offending path or the staging directory */
|
||||
char message[192];
|
||||
} zipx_result_t;
|
||||
|
||||
const zipx_limits_t *zipx_default_limits(void);
|
||||
|
||||
/* Pre-built limit profiles. Use zipx_limits_profile() to look one up.
|
||||
ZIPX_LIMITS_DEFAULT is the safe profile shipped by zipx_default_limits().
|
||||
ZIPX_LIMITS_LARGE allows archives up to 2 TiB total / 1 TiB per file and
|
||||
a 1000:1 compression ratio. The caller is responsible for verifying that
|
||||
the PS5 has enough free disk space. */
|
||||
#define ZIPX_LIMITS_DEFAULT 0
|
||||
#define ZIPX_LIMITS_LARGE 1
|
||||
|
||||
const zipx_limits_t *zipx_limits_profile(int profile);
|
||||
const char *zipx_status_string(zipx_status_t status);
|
||||
|
||||
/* Extract zip_path into dst_dir.
|
||||
Returns ZIPX_OK or an error code; *result is always filled in.
|
||||
On any failure the staging directory is removed and dst_dir is left as it
|
||||
was, except for objects already published with the overwrite policy. */
|
||||
zipx_status_t zipx_extract(const char *zip_path, const char *dst_dir,
|
||||
zipx_conflict_t conflict,
|
||||
const zipx_limits_t *limits,
|
||||
zipx_cancel_fn cancel,
|
||||
zipx_progress_fn progress,
|
||||
void *userdata,
|
||||
zipx_result_t *result);
|
||||
@@ -1,34 +0,0 @@
|
||||
/* Host test shim: provides <sys/statvfs.h> for MinGW hosts.
|
||||
Only used when building the test suite (see run-tests.sh). */
|
||||
|
||||
#ifndef WFM_TEST_SYS_STATVFS_H
|
||||
#define WFM_TEST_SYS_STATVFS_H
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
struct statvfs {
|
||||
unsigned long f_bsize;
|
||||
unsigned long f_frsize;
|
||||
unsigned long f_blocks;
|
||||
unsigned long f_bfree;
|
||||
unsigned long f_bavail;
|
||||
};
|
||||
|
||||
static int
|
||||
statvfs(const char *path, struct statvfs *buf) {
|
||||
ULARGE_INTEGER total;
|
||||
ULARGE_INTEGER free_bytes;
|
||||
char root[8];
|
||||
|
||||
snprintf(root, sizeof(root), "%.3s", path);
|
||||
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;
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
-1
@@ -1 +0,0 @@
|
||||
placeholder
|
||||
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.
Vendored
BIN
Binary file not shown.
Vendored
-1
@@ -1 +0,0 @@
|
||||
this is definitely not a zip file
|
||||
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.
@@ -1,298 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the ZIP fixtures used by test_zip_extract."""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import struct
|
||||
import sys
|
||||
import time as _time
|
||||
import zipfile
|
||||
import zlib
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
OUT = os.path.join(HERE, "fixtures")
|
||||
|
||||
# Force every ZIP entry's date_time to a fixed value (1980-01-01 00:00:00)
|
||||
# so generated archives are byte-stable across runs. Without this fix,
|
||||
# Python 3.13's zipfile.writestr() passes time.localtime(time.time())[:6]
|
||||
# into ZipInfo(...) when the caller supplies a string arcname, which makes
|
||||
# every fixture differ each run and the git diff stat balloons on every
|
||||
# "regenerate fixtures" pass. We swap the zipfile module's `time` symbol
|
||||
# for a fake that always returns the same struct_time; the fake also stubs
|
||||
# `time.time` since zipfile.writestr chains localtime(time.time()).
|
||||
_FIXED_DT = (1980, 1, 1, 0, 0, 0)
|
||||
zipfile.time = type("_FakeTimeMod", (), {
|
||||
"localtime": staticmethod(lambda *_a, **_k: _time.struct_time(_FIXED_DT + (0, 1, 0))),
|
||||
"time": staticmethod(lambda *_a, **_k: 0.0),
|
||||
})()
|
||||
|
||||
|
||||
def fresh():
|
||||
if os.path.isdir(OUT):
|
||||
shutil.rmtree(OUT)
|
||||
os.makedirs(OUT)
|
||||
|
||||
|
||||
def path(name):
|
||||
return os.path.join(OUT, name)
|
||||
|
||||
|
||||
def basic():
|
||||
with zipfile.ZipFile(path("basic.zip"), "w") as zf:
|
||||
zf.writestr("root.txt", "root content")
|
||||
zf.writestr("dir/nested.txt", "nested content")
|
||||
zf.writestr("dir/deep/deeper.txt", "deeper content")
|
||||
zi = zipfile.ZipInfo("empty_dir/")
|
||||
zi.external_attr = (stat.S_IFDIR | 0o755) << 16
|
||||
zi.create_system = 3
|
||||
zf.writestr(zi, b"")
|
||||
|
||||
|
||||
def stored():
|
||||
with zipfile.ZipFile(path("stored.zip"), "w", zipfile.ZIP_STORED) as zf:
|
||||
zf.writestr("stored.txt", "stored content" * 100)
|
||||
|
||||
|
||||
def unicode_names():
|
||||
with zipfile.ZipFile(path("unicode.zip"), "w") as zf:
|
||||
zf.writestr("中文目录/文件.txt", "unicode content")
|
||||
zf.writestr("emoji-\U0001f600.txt", "emoji content")
|
||||
|
||||
|
||||
def zip64():
|
||||
"""A genuine ZIP64 archive. Python's zipfile only emits zip64 fields when
|
||||
sizes exceed 4 GiB (impractical for a fixture), so the layout is written by
|
||||
hand: 32-bit size fields are set to 0xFFFFFFFF and the real values live in
|
||||
the zip64 extra fields and the zip64 end-of-central-directory record."""
|
||||
name = "big.bin"
|
||||
data = bytes(range(256)) * 16 # 4096 bytes, deterministic
|
||||
name_b = name.encode("utf-8")
|
||||
crc = zlib.crc32(data) & 0xFFFFFFFF
|
||||
size = len(data)
|
||||
|
||||
def extra_local():
|
||||
return struct.pack("<HHQQ", 0x0001, 16, size, size)
|
||||
|
||||
def extra_central(offset):
|
||||
return struct.pack("<HHQQQ", 0x0001, 24, size, size, offset)
|
||||
|
||||
out = bytearray()
|
||||
|
||||
# Local file header (stored, sizes deferred to zip64 extra field).
|
||||
local_offset = 0
|
||||
el = extra_local()
|
||||
out += struct.pack("<IHHHHHIIIHH", 0x04034B50, 45, 0, 0, 0, 0, crc,
|
||||
0xFFFFFFFF, 0xFFFFFFFF, len(name_b), len(el))
|
||||
out += name_b + el + data
|
||||
|
||||
# Central directory header.
|
||||
cd_offset = len(out)
|
||||
ec = extra_central(local_offset)
|
||||
out += struct.pack("<IHHHHHHIIIHHHHHII", 0x02014B50, 45, 45, 0, 0, 0, 0,
|
||||
crc, 0xFFFFFFFF, 0xFFFFFFFF, len(name_b), len(ec), 0,
|
||||
0, 0, 0, 0xFFFFFFFF)
|
||||
out += name_b + ec
|
||||
cd_size = len(out) - cd_offset
|
||||
|
||||
# ZIP64 end of central directory record.
|
||||
zip64_eocd_offset = len(out)
|
||||
out += struct.pack("<IQHHIIQQQQ", 0x06064B50, 44, 45, 45, 0, 0, 1, 1,
|
||||
cd_size, cd_offset)
|
||||
|
||||
# ZIP64 end of central directory locator.
|
||||
out += struct.pack("<IIQI", 0x07064B50, 0, zip64_eocd_offset, 1)
|
||||
|
||||
# End of central directory record (offsets deferred to zip64).
|
||||
out += struct.pack("<IHHHHIIH", 0x06054B50, 0, 0, 1, 1, 0xFFFFFFFF,
|
||||
0xFFFFFFFF, 0)
|
||||
|
||||
with open(path("zip64.zip"), "wb") as fh:
|
||||
fh.write(bytes(out))
|
||||
|
||||
|
||||
def traversal():
|
||||
with zipfile.ZipFile(path("traversal.zip"), "w") as zf:
|
||||
zf.writestr("ok.txt", "ok")
|
||||
zf.writestr("../evil.txt", "evil")
|
||||
|
||||
|
||||
def traversal_backslash():
|
||||
with zipfile.ZipFile(path("traversal_bs.zip"), "w") as zf:
|
||||
zf.writestr("ok.txt", "ok")
|
||||
zf.writestr("..\\evil.txt", "evil")
|
||||
|
||||
|
||||
def absolute():
|
||||
with zipfile.ZipFile(path("absolute.zip"), "w") as zf:
|
||||
zf.writestr("/tmp/evil.txt", "evil")
|
||||
|
||||
|
||||
def drive_letter():
|
||||
with zipfile.ZipFile(path("drive.zip"), "w") as zf:
|
||||
zf.writestr("C:/evil.txt", "evil")
|
||||
|
||||
|
||||
def duplicate():
|
||||
with zipfile.ZipFile(path("duplicate.zip"), "w") as zf:
|
||||
zf.writestr("a.txt", "first")
|
||||
zf.writestr("a.txt", "second")
|
||||
|
||||
|
||||
def file_dir_clash():
|
||||
with zipfile.ZipFile(path("clash.zip"), "w") as zf:
|
||||
zf.writestr("a", "file")
|
||||
zf.writestr("a/b.txt", "child")
|
||||
|
||||
|
||||
def symlink_entry():
|
||||
with zipfile.ZipFile(path("symlink.zip"), "w") as zf:
|
||||
zf.writestr("ok.txt", "ok")
|
||||
zi = zipfile.ZipInfo("link")
|
||||
zi.create_system = 3
|
||||
zi.external_attr = (stat.S_IFLNK | 0o777) << 16
|
||||
zf.writestr(zi, "/etc/passwd")
|
||||
|
||||
|
||||
def fifo_entry():
|
||||
with zipfile.ZipFile(path("fifo.zip"), "w") as zf:
|
||||
zi = zipfile.ZipInfo("pipe")
|
||||
zi.create_system = 3
|
||||
zi.external_attr = (stat.S_IFIFO | 0o644) << 16
|
||||
zf.writestr(zi, b"")
|
||||
|
||||
|
||||
def encrypted():
|
||||
with zipfile.ZipFile(path("encrypted.zip"), "w") as zf:
|
||||
zf.writestr("secret.txt", "secret")
|
||||
data = bytearray(open(path("encrypted.zip"), "rb").read())
|
||||
idx = data.find(b"PK\x03\x04")
|
||||
if idx < 0:
|
||||
raise SystemExit("local header not found")
|
||||
data[idx + 6] |= 0x01 # general purpose bit 0 = encrypted
|
||||
cd = data.find(b"PK\x01\x02")
|
||||
data[cd + 8] |= 0x01
|
||||
open(path("encrypted.zip"), "wb").write(bytes(data))
|
||||
|
||||
|
||||
def bad_crc():
|
||||
with zipfile.ZipFile(path("bad_crc.zip"), "w") as zf:
|
||||
zf.writestr("data.bin", "x" * 4096)
|
||||
data = bytearray(open(path("bad_crc.zip"), "rb").read())
|
||||
cd = data.find(b"PK\x01\x02")
|
||||
if cd < 0:
|
||||
raise SystemExit("central directory not found")
|
||||
data[cd + 16:cd + 20] = b"\xde\xad\xbe\xef"
|
||||
open(path("bad_crc.zip"), "wb").write(bytes(data))
|
||||
|
||||
|
||||
def truncated():
|
||||
with zipfile.ZipFile(path("full.zip"), "w") as zf:
|
||||
zf.writestr("data.txt", "y" * 4096)
|
||||
data = open(path("full.zip"), "rb").read()
|
||||
open(path("truncated.zip"), "wb").write(data[:-40])
|
||||
|
||||
|
||||
def not_a_zip():
|
||||
open(path("notazip.zip"), "wb").write(b"this is definitely not a zip file")
|
||||
|
||||
|
||||
def bomb():
|
||||
with zipfile.ZipFile(path("bomb.zip"), "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.writestr("bomb.bin", "A" * (4 * 1024 * 1024))
|
||||
|
||||
|
||||
def medium_bomb():
|
||||
"""1 MiB of 0..255 cycled, which deflate squeezes to ~4.4 KiB
|
||||
(ratio ~238). Sits between the default cap (200) and the large cap
|
||||
(1000) so the default profile rejects it and the large profile
|
||||
accepts it. Used by the large-profile host test."""
|
||||
with zipfile.ZipFile(path("medium_bomb.zip"), "w",
|
||||
zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.writestr("medium.bin", bytes(range(256)) * 4096)
|
||||
|
||||
|
||||
def many_files():
|
||||
with zipfile.ZipFile(path("many.zip"), "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
for i in range(500):
|
||||
zf.writestr("many/f%03d.txt" % i, "%d" % i)
|
||||
|
||||
|
||||
def conflict_source():
|
||||
"""A zip whose top level collides with an existing destination layout."""
|
||||
with zipfile.ZipFile(path("conflict.zip"), "w") as zf:
|
||||
zf.writestr("shared.txt", "from zip")
|
||||
zf.writestr("shareddir/inner.txt", "inner from zip")
|
||||
zf.writestr("shareddir/added.txt", "added from zip")
|
||||
|
||||
|
||||
def rar_fixtures():
|
||||
"""Generate RAR fixtures if a rar/7z writer is available.
|
||||
|
||||
We intentionally do not depend on a rar binary being installed in the
|
||||
host test environment; when neither `rar` nor `7z` is present we leave
|
||||
1-byte placeholders so that tests/test_rar_extract.c can still hit its
|
||||
"not a real archive" branches. See docs/HANDOVER.md for the manual
|
||||
fixture procedure used in CI on a developer workstation that has WinRAR.
|
||||
"""
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
candidates = []
|
||||
for cmd in ("rar", "7z", "7za"):
|
||||
if shutil.which(cmd):
|
||||
candidates.append(cmd)
|
||||
|
||||
staging_dir = tempfile.mkdtemp(prefix="wfm-rar-fixtures-")
|
||||
try:
|
||||
# Build a small directory we can compress into a RAR.
|
||||
staging_root = os.path.join(staging_dir, "stage")
|
||||
os.makedirs(staging_root)
|
||||
with open(os.path.join(staging_root, "root.txt"), "wb") as f:
|
||||
f.write(b"rar root content\n")
|
||||
nested = os.path.join(staging_root, "dir")
|
||||
os.makedirs(nested)
|
||||
with open(os.path.join(nested, "nested.txt"), "wb") as f:
|
||||
f.write(b"rar nested content\n")
|
||||
|
||||
out = path("basic.rar")
|
||||
ok = False
|
||||
for cmd in candidates:
|
||||
args = [cmd, "a", "-r", "-ep1", out,
|
||||
os.path.join(staging_root, "root.txt"),
|
||||
os.path.join(staging_root, "dir")]
|
||||
# 7z uses -t7z / -rr differently; for the purposes of a smoke
|
||||
# fixture we only need any small valid RAR.
|
||||
try:
|
||||
rc = subprocess.call(args, stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
if rc == 0 and os.path.exists(out) and os.path.getsize(out) > 16:
|
||||
print("rar_fixtures: built %s via %s" % (out, cmd))
|
||||
ok = True
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
if not ok:
|
||||
# Placeholder: the test suite only needs a file that dmc_unrar
|
||||
# will reject. 8 bytes is far too small to be a valid archive.
|
||||
with open(out, "wb") as f:
|
||||
f.write(b"placeholder")
|
||||
finally:
|
||||
shutil.rmtree(staging_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def main():
|
||||
fresh()
|
||||
for fn in (basic, stored, unicode_names, zip64, traversal,
|
||||
traversal_backslash, absolute, drive_letter, duplicate,
|
||||
file_dir_clash, symlink_entry, fifo_entry, encrypted, bad_crc,
|
||||
truncated, not_a_zip, bomb, medium_bomb, many_files,
|
||||
conflict_source, rar_fixtures):
|
||||
fn()
|
||||
print("fixtures written to %s" % OUT)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,33 +0,0 @@
|
||||
/* Host-only shim used to build minizip-ng's POSIX backend on MinGW.
|
||||
mz_os_posix.c needs utime(), lstat(), symlink() and readlink(); Windows has
|
||||
none of them. The stubs are only compiled into the host test binary. */
|
||||
|
||||
#ifndef WFM_TEST_MINGW_HOST_H
|
||||
#define WFM_TEST_MINGW_HOST_H
|
||||
|
||||
#include "posix_compat.h"
|
||||
|
||||
#if defined(__MINGW32__) || defined(_WIN32)
|
||||
|
||||
#include <sys/utime.h>
|
||||
|
||||
static int __attribute__((unused))
|
||||
symlink(const char *target, const char *linkpath) {
|
||||
(void)target;
|
||||
(void)linkpath;
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static ssize_t __attribute__((unused))
|
||||
readlink(const char *path, char *buf, size_t size) {
|
||||
(void)path;
|
||||
(void)buf;
|
||||
(void)size;
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -1,262 +0,0 @@
|
||||
/* 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 */
|
||||
@@ -1,83 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Host test runner for the ZIP and RAR extraction engines.
|
||||
# Builds the vendored minizip-ng/zlib/dmc_unrar sources, both engines and
|
||||
# the test suites with the host compiler and runs each suite.
|
||||
#
|
||||
# ./tests/run-tests.sh
|
||||
#
|
||||
# On Windows this expects MinGW gcc in PATH (Git Bash is fine).
|
||||
|
||||
set -e
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -W 2>/dev/null || pwd)"
|
||||
BUILD="$ROOT/.build/host-test"
|
||||
PYTHON="${PYTHON:-python3}"
|
||||
CC="${CC:-gcc}"
|
||||
|
||||
rm -rf "$BUILD"
|
||||
mkdir -p "$BUILD"
|
||||
|
||||
"$PYTHON" "$ROOT/tests/make_fixtures.py"
|
||||
|
||||
MZ_CFLAGS=(-I"$ROOT/third_party/minizip-ng/include" -DHAVE_ZLIB -DZLIB_COMPAT)
|
||||
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.
|
||||
case "$(uname -s)" in
|
||||
MINGW*|MSYS*|CYGWIN*) MZ_CFLAGS+=(-DO_NOFOLLOW=0); HOST_KIND=windows ;;
|
||||
esac
|
||||
|
||||
# zlib + minizip-ng (plain POSIX sources, no shim needed)
|
||||
for src in "$ROOT"/third_party/zlib/src/*.c "$ROOT"/third_party/minizip-ng/src/*.c; do
|
||||
name="$(basename "$src" .c)"
|
||||
extra=()
|
||||
# minizip's POSIX backend needs utime/lstat/symlink/readlink, stubbed on Windows.
|
||||
case "$name:$HOST_KIND" in
|
||||
mz_os_posix:windows) extra=(-include "$ROOT/tests/mingw_host.h") ;;
|
||||
esac
|
||||
"$CC" -c -O2 -w -I"$ROOT/third_party/zlib/include" -DHAVE_UNISTD_H=1 \
|
||||
"${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"
|
||||
|
||||
COMPAT_INC="$ROOT/tests/compat"
|
||||
|
||||
# The engines and the test suites get the POSIX shim on Windows hosts.
|
||||
"$CC" -c -O2 -Wall -Wextra -Wno-unused-parameter \
|
||||
-I"$ROOT/third_party/minizip-ng/include" -I"$ROOT/src" -I"$COMPAT_INC" \
|
||||
-include "$ROOT/tests/posix_compat.h" \
|
||||
-o "$BUILD/zip_extract.o" "$ROOT/src/zip_extract.c"
|
||||
|
||||
"$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" \
|
||||
-o "$BUILD/rar_extract.o" "$ROOT/src/rar_extract.c"
|
||||
|
||||
"$CC" -c -O2 -Wall -Wextra -Wno-unused-parameter -I"$ROOT/src" \
|
||||
-I"$COMPAT_INC" -include "$ROOT/tests/posix_compat.h" \
|
||||
-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" \
|
||||
-o "$BUILD/test_rar_extract.o" "$ROOT/tests/test_rar_extract.c"
|
||||
|
||||
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 ;;
|
||||
esac
|
||||
objs+=("$obj")
|
||||
done
|
||||
|
||||
"$CC" -O2 -o "$BUILD/test-zip-extract" \
|
||||
"$BUILD/zip_extract.o" "$BUILD/test_zip_extract.o" "${objs[@]}"
|
||||
|
||||
"$CC" -O2 -o "$BUILD/test-rar-extract" \
|
||||
"$BUILD/rar_extract.o" "$BUILD/test_rar_extract.o" \
|
||||
"$BUILD/zip_extract.o" "${objs[@]}"
|
||||
|
||||
"$BUILD/test-zip-extract" "$ROOT/tests/fixtures" "$BUILD/work-zip"
|
||||
"$BUILD/test-rar-extract" "$ROOT/tests/fixtures" "$BUILD/work-rar"
|
||||
@@ -1,347 +0,0 @@
|
||||
/* Host test suite for the RAR extraction engine.
|
||||
Build: see run-tests.sh (uses gcc + the MinGW POSIX shim).
|
||||
|
||||
Scope (v1.8):
|
||||
* Pure error-path coverage — we do not ship a genuine RAR fixture
|
||||
because no rar/7z writer is available in the sandboxed host test
|
||||
environment. The test suite therefore focuses on the negative
|
||||
branches that the dispatcher hits when a user uploads something
|
||||
that is not a valid single-volume, unencrypted RAR.
|
||||
* Happy-path smoke coverage is provided by make_fixtures.py when a
|
||||
rar or 7z binary is available; if neither is present, the script
|
||||
writes 1-byte placeholder files so that test_rar_extract.c still
|
||||
has something to point at for the "format rejected" assertions.
|
||||
* See docs/HANDOVER.md for the manual smoke procedure and the
|
||||
fixture TODO that should be cleared when opello/unrar is vendored. */
|
||||
|
||||
#include "../src/zip_extract.h"
|
||||
#include "../src/rar_extract.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "posix_compat.h"
|
||||
|
||||
static const char *g_fixtures;
|
||||
static char g_work[4096];
|
||||
static int g_failures;
|
||||
static int g_checks;
|
||||
|
||||
static void
|
||||
fixture_path(char *out, size_t size, const char *name) {
|
||||
snprintf(out, size, "%s/%s", g_fixtures, name);
|
||||
}
|
||||
|
||||
static void
|
||||
work_path(char *out, size_t size, const char *name) {
|
||||
snprintf(out, size, "%s/%s", g_work, name);
|
||||
}
|
||||
|
||||
static void
|
||||
check(int ok, const char *what) {
|
||||
g_checks++;
|
||||
if(!ok) {
|
||||
g_failures++;
|
||||
printf(" FAIL %s\n", what);
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
exists(const char *path) {
|
||||
struct stat st;
|
||||
|
||||
return !stat(path, &st);
|
||||
}
|
||||
|
||||
static int
|
||||
write_bytes(const char *path, const void *data, size_t n) {
|
||||
FILE *f = fopen(path, "wb");
|
||||
|
||||
if(!f) {
|
||||
return -1;
|
||||
}
|
||||
if(data && n) {
|
||||
fwrite(data, 1, n, f);
|
||||
}
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Writes a deterministic "random" byte run so the file is not empty. */
|
||||
static int
|
||||
write_junk(const char *path, size_t n) {
|
||||
unsigned char *buf = malloc(n);
|
||||
size_t i;
|
||||
FILE *f;
|
||||
int rc = -1;
|
||||
|
||||
if(!buf) {
|
||||
return -1;
|
||||
}
|
||||
for(i = 0; i < n; i++) {
|
||||
buf[i] = (unsigned char)((i * 131 + 17) & 0xff);
|
||||
}
|
||||
f = fopen(path, "wb");
|
||||
if(f) {
|
||||
if(fwrite(buf, 1, n, f) == n) {
|
||||
rc = 0;
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
free(buf);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int
|
||||
remove_dir(const char *path) {
|
||||
DIR *dir = opendir(path);
|
||||
struct dirent *ent;
|
||||
|
||||
if(!dir) {
|
||||
return rmdir(path);
|
||||
}
|
||||
while((ent = readdir(dir))) {
|
||||
char child[4096];
|
||||
struct stat st;
|
||||
|
||||
if(!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) {
|
||||
continue;
|
||||
}
|
||||
snprintf(child, sizeof(child), "%s/%s", path, ent->d_name);
|
||||
if(!stat(child, &st) && S_ISDIR(st.st_mode)) {
|
||||
remove_dir(child);
|
||||
} else {
|
||||
unlink(child);
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
return rmdir(path);
|
||||
}
|
||||
|
||||
static zipx_status_t
|
||||
run_rar(const char *fixture, const char *dst, zipx_status_t expected,
|
||||
const char *label) {
|
||||
char src[4096];
|
||||
zipx_result_t res;
|
||||
zipx_status_t st;
|
||||
|
||||
fixture_path(src, sizeof(src), fixture);
|
||||
if(!exists(src)) {
|
||||
printf(" SKIP %s (fixture %s missing)\n", label, fixture);
|
||||
return ZIPX_OK;
|
||||
}
|
||||
work_path((char *)dst, 0, dst); /* dst is already absolute under work */
|
||||
memset(&res, 0, sizeof(res));
|
||||
st = rar_extract(src, dst, ZIPX_CONFLICT_FAIL,
|
||||
zipx_default_limits(), NULL, NULL, NULL, &res);
|
||||
check(st == expected, label);
|
||||
if(st != ZIPX_OK) {
|
||||
check(res.message[0] != 0, " has error message");
|
||||
printf(" message=%s\n", res.message);
|
||||
}
|
||||
remove_dir(dst);
|
||||
return st;
|
||||
}
|
||||
|
||||
static void
|
||||
test_engine_dispatch(void) {
|
||||
char dst[4096];
|
||||
|
||||
printf("test_engine_dispatch\n");
|
||||
|
||||
/* A renamed .zip must NOT be accepted as a RAR. dmc_unrar's archive_open
|
||||
will detect the missing RAR signature and return BAD_SIGNATURE / OPEN_FAIL,
|
||||
which our wrapper translates to ZIPX_ERR_FORMAT / ZIPX_ERR_OPEN. Either
|
||||
is acceptable — what matters is that the call returns cleanly without
|
||||
crashing and without writing files to disk. */
|
||||
work_path(dst, sizeof(dst), "dst1");
|
||||
remove_dir(dst);
|
||||
{
|
||||
zipx_result_t res;
|
||||
zipx_status_t st;
|
||||
char src[4096];
|
||||
|
||||
fixture_path(src, sizeof(src), "notar.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, "notar.rar rejected");
|
||||
check(!exists(dst), " no files written for rejected archive");
|
||||
if(st == ZIPX_OK) {
|
||||
remove_dir(dst);
|
||||
}
|
||||
} else {
|
||||
printf(" SKIP notar.rar (missing)\n");
|
||||
}
|
||||
}
|
||||
|
||||
/* A truncated/random 1 KiB blob is also not a RAR. */
|
||||
work_path(dst, sizeof(dst), "dst2");
|
||||
remove_dir(dst);
|
||||
{
|
||||
char src[4096];
|
||||
fixture_path(src, sizeof(src), "junk.rar");
|
||||
if(exists(src)) {
|
||||
zipx_result_t res;
|
||||
zipx_status_t st = rar_extract(src, dst, ZIPX_CONFLICT_FAIL,
|
||||
zipx_default_limits(), NULL, NULL, NULL,
|
||||
&res);
|
||||
check(st != ZIPX_OK, "junk.rar rejected");
|
||||
check(!exists(dst), " no files written for junk archive");
|
||||
if(st == ZIPX_OK) {
|
||||
remove_dir(dst);
|
||||
}
|
||||
} else {
|
||||
printf(" SKIP junk.rar (missing)\n");
|
||||
}
|
||||
}
|
||||
|
||||
/* Missing source: must return a non-zero status without writing anything. */
|
||||
work_path(dst, sizeof(dst), "dst3");
|
||||
remove_dir(dst);
|
||||
{
|
||||
char src[4096];
|
||||
zipx_result_t res;
|
||||
zipx_status_t st;
|
||||
|
||||
fixture_path(src, sizeof(src), "does-not-exist.rar");
|
||||
memset(&res, 0, sizeof(res));
|
||||
st = rar_extract(src, dst, ZIPX_CONFLICT_FAIL,
|
||||
zipx_default_limits(), NULL, NULL, NULL, &res);
|
||||
check(st == ZIPX_ERR_OPEN, "missing source -> ZIPX_ERR_OPEN");
|
||||
check(!exists(dst), " no files written when source is missing");
|
||||
if(st == ZIPX_OK) {
|
||||
remove_dir(dst);
|
||||
}
|
||||
}
|
||||
|
||||
/* Null path / null destination guards. */
|
||||
{
|
||||
zipx_result_t res;
|
||||
check(rar_extract(NULL, "/tmp", ZIPX_CONFLICT_FAIL, NULL, NULL, NULL,
|
||||
NULL, &res) == ZIPX_ERR_INTERNAL,
|
||||
"null rar_path -> ZIPX_ERR_INTERNAL");
|
||||
check(rar_extract("/tmp", NULL, ZIPX_CONFLICT_FAIL, NULL, NULL, NULL,
|
||||
NULL, &res) == ZIPX_ERR_INTERNAL,
|
||||
"null dst_dir -> ZIPX_ERR_INTERNAL");
|
||||
check(rar_extract("/tmp", "", ZIPX_CONFLICT_FAIL, NULL, NULL, NULL,
|
||||
NULL, &res) == ZIPX_ERR_INTERNAL,
|
||||
"empty dst_dir -> ZIPX_ERR_INTERNAL");
|
||||
}
|
||||
|
||||
/* dst_dir that is actually a file should be rejected. */
|
||||
{
|
||||
char src[4096];
|
||||
char dst[4096];
|
||||
zipx_result_t res;
|
||||
fixture_path(src, sizeof(src), "junk.rar");
|
||||
work_path(dst, sizeof(dst), "not_a_dir");
|
||||
/* ensure parent exists */
|
||||
{
|
||||
char parent[4096];
|
||||
snprintf(parent, sizeof(parent), "%s", dst);
|
||||
char *slash = strrchr(parent, '/');
|
||||
if(slash) {
|
||||
*slash = 0;
|
||||
}
|
||||
mkdir(parent, 0777);
|
||||
}
|
||||
write_bytes(dst, "i am a file", 11);
|
||||
if(exists(src)) {
|
||||
zipx_status_t st = rar_extract(src, dst, ZIPX_CONFLICT_FAIL,
|
||||
zipx_default_limits(), NULL, NULL, NULL,
|
||||
&res);
|
||||
check(st == ZIPX_ERR_CONFLICT, "dst is a regular file -> ZIPX_ERR_CONFLICT");
|
||||
} else {
|
||||
printf(" SKIP dst-is-file (junk.rar missing)\n");
|
||||
}
|
||||
unlink(dst);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
test_format_translation(void) {
|
||||
/* Direct exercise of the error-code mapping: this only requires that the
|
||||
dispatcher classifies "encrypted" and "multi-volume" correctly. Since
|
||||
those states are reached only with a real RAR that has the right flags
|
||||
set, we cannot generate that fixture on the fly — but we can confirm
|
||||
that the rejection paths we *do* hit (FORMAT / OPEN / IO) never
|
||||
accidentally return ZIPX_OK. */
|
||||
printf("test_format_translation\n");
|
||||
/* The dispatch test already covered the negative paths; nothing more to do
|
||||
here for v1.8. Future fixtures can directly assert ZIPX_ERR_UNSUPPORTED
|
||||
once we have encrypted/multi-volume samples (see docs/HANDOVER.md). */
|
||||
}
|
||||
|
||||
static void
|
||||
test_limits_handoff(void) {
|
||||
/* Verify that rar_extract honours the ZIPX_LIMITS_LARGE profile by
|
||||
accepting the relaxed limits pointer without crashing. The actual
|
||||
cap is exercised via the engine's own counters (covered in the ZIP
|
||||
suite); for RAR we just need to know the profile switch is wired. */
|
||||
printf("test_limits_handoff\n");
|
||||
check(zipx_limits_profile(ZIPX_LIMITS_DEFAULT) != NULL,
|
||||
"ZIPX_LIMITS_DEFAULT pointer is non-NULL");
|
||||
check(zipx_limits_profile(ZIPX_LIMITS_LARGE) != NULL,
|
||||
"ZIPX_LIMITS_LARGE pointer is non-NULL");
|
||||
check(zipx_limits_profile(ZIPX_LIMITS_DEFAULT) !=
|
||||
zipx_limits_profile(ZIPX_LIMITS_LARGE),
|
||||
"default and large profiles are distinct");
|
||||
check(zipx_default_limits() ==
|
||||
zipx_limits_profile(ZIPX_LIMITS_DEFAULT),
|
||||
"default() matches ZIPX_LIMITS_DEFAULT");
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char **argv) {
|
||||
if(argc < 3) {
|
||||
fprintf(stderr, "usage: %s <fixtures-dir> <work-dir>\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
g_fixtures = argv[1];
|
||||
snprintf(g_work, sizeof(g_work), "%s", argv[2]);
|
||||
mkdir(g_work, 0777);
|
||||
|
||||
/* Generate the on-disk garbage fixtures we need for the error tests. */
|
||||
{
|
||||
char src[4096];
|
||||
fixture_path(src, sizeof(src), "notar.rar");
|
||||
/* Reuse basic.zip (fixture generator writes this anyway). */
|
||||
{
|
||||
char zip_src[4096];
|
||||
char buf[8192];
|
||||
FILE *in;
|
||||
FILE *out;
|
||||
size_t n;
|
||||
fixture_path(zip_src, sizeof(zip_src), "basic.zip");
|
||||
in = fopen(zip_src, "rb");
|
||||
out = fopen(src, "wb");
|
||||
if(in && out) {
|
||||
while((n = fread(buf, 1, sizeof(buf), in)) > 0) {
|
||||
fwrite(buf, 1, n, out);
|
||||
}
|
||||
}
|
||||
if(in) {
|
||||
fclose(in);
|
||||
}
|
||||
if(out) {
|
||||
fclose(out);
|
||||
}
|
||||
}
|
||||
fixture_path(src, sizeof(src), "junk.rar");
|
||||
write_junk(src, 1024);
|
||||
}
|
||||
|
||||
test_engine_dispatch();
|
||||
test_format_translation();
|
||||
test_limits_handoff();
|
||||
|
||||
printf("\nrar_extract: %d checks, %d failures\n", g_checks, g_failures);
|
||||
return g_failures == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -1,639 +0,0 @@
|
||||
/* Host test suite for the ZIP extraction engine.
|
||||
Build: see run-tests.sh (uses gcc + the MinGW POSIX shim). */
|
||||
|
||||
#include "../src/zip_extract.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* Pulled in by the test build only (see run-tests.sh). */
|
||||
#include "posix_compat.h"
|
||||
|
||||
static const char *g_fixtures;
|
||||
static char g_work[4096];
|
||||
static int g_failures;
|
||||
static int g_checks;
|
||||
|
||||
static void
|
||||
fixture_path(char *out, size_t size, const char *name) {
|
||||
snprintf(out, size, "%s/%s", g_fixtures, name);
|
||||
}
|
||||
|
||||
static void
|
||||
work_path(char *out, size_t size, const char *name) {
|
||||
snprintf(out, size, "%s/%s", g_work, name);
|
||||
}
|
||||
|
||||
static void
|
||||
check(int ok, const char *what) {
|
||||
g_checks++;
|
||||
if(!ok) {
|
||||
g_failures++;
|
||||
printf(" FAIL %s\n", what);
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
exists(const char *path) {
|
||||
struct stat st;
|
||||
|
||||
return !stat(path, &st);
|
||||
}
|
||||
|
||||
static int
|
||||
read_text(const char *path, char *buf, size_t size) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
size_t n;
|
||||
|
||||
if(!f) {
|
||||
return -1;
|
||||
}
|
||||
n = fread(buf, 1, size - 1, f);
|
||||
buf[n] = 0;
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
remove_dir(const char *path) {
|
||||
DIR *dir = opendir(path);
|
||||
struct dirent *ent;
|
||||
|
||||
if(!dir) {
|
||||
return rmdir(path);
|
||||
}
|
||||
while((ent = readdir(dir))) {
|
||||
char child[4096];
|
||||
struct stat st;
|
||||
|
||||
if(!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) {
|
||||
continue;
|
||||
}
|
||||
snprintf(child, sizeof(child), "%s/%s", path, ent->d_name);
|
||||
if(!stat(child, &st) && S_ISDIR(st.st_mode)) {
|
||||
remove_dir(child);
|
||||
} else {
|
||||
unlink(child);
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
return rmdir(path);
|
||||
}
|
||||
|
||||
static int
|
||||
make_dirs(const char *path) {
|
||||
char buf[4096];
|
||||
char *slash;
|
||||
struct stat st;
|
||||
|
||||
if(!*path || !stat(path, &st)) {
|
||||
return 0;
|
||||
}
|
||||
snprintf(buf, sizeof(buf), "%s", path);
|
||||
for(slash = buf + 1; *slash; slash++) {
|
||||
if(*slash != '/') {
|
||||
continue;
|
||||
}
|
||||
*slash = 0;
|
||||
if(mkdir(buf, 0777) && errno != EEXIST) {
|
||||
return -1;
|
||||
}
|
||||
*slash = '/';
|
||||
}
|
||||
return mkdir(buf, 0777) && errno != EEXIST ? -1 : 0;
|
||||
}
|
||||
|
||||
static int
|
||||
write_text(const char *path, const char *content) {
|
||||
FILE *f = fopen(path, "wb");
|
||||
|
||||
if(!f) {
|
||||
return -1;
|
||||
}
|
||||
fputs(content, f);
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
count_staging_leftovers(const char *dir) {
|
||||
DIR *d = opendir(dir);
|
||||
struct dirent *ent;
|
||||
int count = 0;
|
||||
|
||||
if(!d) {
|
||||
return 0;
|
||||
}
|
||||
while((ent = readdir(d))) {
|
||||
if(!strncmp(ent->d_name, ".wfm-extract-", 13)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
return count;
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
|
||||
typedef struct {
|
||||
int cancel_after_progress;
|
||||
zipx_progress_t last;
|
||||
int reports;
|
||||
char last_current[512];
|
||||
} test_ctx_t;
|
||||
|
||||
static int
|
||||
cb_cancel(void *userdata) {
|
||||
test_ctx_t *t = userdata;
|
||||
|
||||
return t->cancel_after_progress && t->reports >= t->cancel_after_progress;
|
||||
}
|
||||
|
||||
static void
|
||||
cb_progress(void *userdata, const zipx_progress_t *p) {
|
||||
test_ctx_t *t = userdata;
|
||||
|
||||
t->reports++;
|
||||
t->last = *p;
|
||||
if(p->current) {
|
||||
snprintf(t->last_current, sizeof(t->last_current), "%s", p->current);
|
||||
}
|
||||
}
|
||||
|
||||
static zipx_status_t
|
||||
run(const char *fixture, const char *dst_name, zipx_conflict_t conflict,
|
||||
const zipx_limits_t *limits, test_ctx_t *t, zipx_result_t *res) {
|
||||
char zip[4096];
|
||||
char dst[4096];
|
||||
|
||||
fixture_path(zip, sizeof(zip), fixture);
|
||||
work_path(dst, sizeof(dst), dst_name);
|
||||
return zipx_extract(zip, dst, conflict, limits, cb_cancel, cb_progress,
|
||||
t, res);
|
||||
}
|
||||
|
||||
static void
|
||||
expect_ok(zipx_result_t *res, zipx_status_t status, const char *label) {
|
||||
check(status == ZIPX_OK, label);
|
||||
if(status != ZIPX_OK) {
|
||||
printf(" status=%s (%d) %s / %s\n", zipx_status_string(status),
|
||||
status, res->message, res->detail);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
expect_status(zipx_result_t *res, zipx_status_t status,
|
||||
zipx_status_t expected, const char *label) {
|
||||
check(status == expected, label);
|
||||
if(status != expected) {
|
||||
printf(" expected %s, got %s (%d) %s / %s\n",
|
||||
zipx_status_string(expected), zipx_status_string(status), status,
|
||||
res->message, res->detail);
|
||||
}
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
|
||||
static void
|
||||
test_basic(void) {
|
||||
zipx_result_t res;
|
||||
test_ctx_t t = {0};
|
||||
char dst[4096];
|
||||
char file[4224];
|
||||
char buf[64];
|
||||
|
||||
printf("basic (deflate + nested dirs + empty dir)\n");
|
||||
expect_ok(&res, run("basic.zip", "out_basic", ZIPX_CONFLICT_FAIL, NULL, &t, &res),
|
||||
"extract succeeds");
|
||||
work_path(dst, sizeof(dst), "out_basic");
|
||||
check(exists(dst), "destination created");
|
||||
snprintf(file, sizeof(file), "%s/root.txt", dst);
|
||||
check(!read_text(file, buf, sizeof(buf)) && !strcmp(buf, "root content"),
|
||||
"root.txt content");
|
||||
snprintf(file, sizeof(file), "%s/dir/nested.txt", dst);
|
||||
check(!read_text(file, buf, sizeof(buf)) && !strcmp(buf, "nested content"),
|
||||
"nested.txt content");
|
||||
snprintf(file, sizeof(file), "%s/dir/deep/deeper.txt", dst);
|
||||
check(!read_text(file, buf, sizeof(buf)) && !strcmp(buf, "deeper content"),
|
||||
"deeper.txt content");
|
||||
snprintf(file, sizeof(file), "%s/empty_dir", dst);
|
||||
check(exists(file), "empty directory created");
|
||||
check(res.entries_total == 4, "entry count reported");
|
||||
check(res.bytes_total > 0, "byte total reported");
|
||||
check(count_staging_leftovers(dst) == 0, "no staging leftovers inside dst");
|
||||
}
|
||||
|
||||
static void
|
||||
test_stored(void) {
|
||||
zipx_result_t res;
|
||||
test_ctx_t t = {0};
|
||||
char dst[4096];
|
||||
char file[4224];
|
||||
char buf[2048];
|
||||
size_t i;
|
||||
int ok = 1;
|
||||
|
||||
printf("stored (no compression)\n");
|
||||
expect_ok(&res, run("stored.zip", "out_stored", ZIPX_CONFLICT_FAIL, NULL, &t, &res),
|
||||
"extract succeeds");
|
||||
work_path(dst, sizeof(dst), "out_stored");
|
||||
snprintf(file, sizeof(file), "%s/stored.txt", dst);
|
||||
if(read_text(file, buf, sizeof(buf))) {
|
||||
ok = 0;
|
||||
} else {
|
||||
for(i = 0; i < 1400; i++) {
|
||||
if(buf[i] != "stored content"[i % 14]) {
|
||||
ok = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
check(ok, "stored content matches");
|
||||
}
|
||||
|
||||
static void
|
||||
test_zip64(void) {
|
||||
zipx_result_t res;
|
||||
test_ctx_t t = {0};
|
||||
char dst[4096];
|
||||
char file[4224];
|
||||
struct stat st;
|
||||
|
||||
printf("zip64\n");
|
||||
expect_ok(&res, run("zip64.zip", "out_zip64", ZIPX_CONFLICT_FAIL, NULL, &t, &res),
|
||||
"extract succeeds");
|
||||
work_path(dst, sizeof(dst), "out_zip64");
|
||||
snprintf(file, sizeof(file), "%s/big.bin", dst);
|
||||
check(!stat(file, &st) && st.st_size == 4096, "zip64 size");
|
||||
}
|
||||
|
||||
static void
|
||||
test_unicode(void) {
|
||||
zipx_result_t res;
|
||||
test_ctx_t t = {0};
|
||||
char dst[4096];
|
||||
char file[4224];
|
||||
char buf[64];
|
||||
|
||||
printf("utf-8 entry names\n");
|
||||
expect_ok(&res, run("unicode.zip", "out_unicode", ZIPX_CONFLICT_FAIL, NULL,
|
||||
&t, &res), "extract succeeds");
|
||||
work_path(dst, sizeof(dst), "out_unicode");
|
||||
snprintf(file, sizeof(file), "%s/\xe4\xb8\xad\xe6\x96\x87\xe7\x9b\xae\xe5\xbd\x95/\xe6\x96\x87\xe4\xbb\xb6.txt", dst);
|
||||
check(!read_text(file, buf, sizeof(buf)) &&
|
||||
!strcmp(buf, "unicode content"), "chinese path content");
|
||||
}
|
||||
|
||||
static void
|
||||
test_conflict_fail(void) {
|
||||
zipx_result_t res;
|
||||
test_ctx_t t = {0};
|
||||
char dst[4096];
|
||||
char file[4224];
|
||||
char buf[64];
|
||||
|
||||
printf("conflict policy: fail\n");
|
||||
work_path(dst, sizeof(dst), "out_conflict_fail");
|
||||
remove_dir(dst);
|
||||
make_dirs(dst);
|
||||
snprintf(file, sizeof(file), "%s/shared.txt", dst);
|
||||
write_text(file, "original");
|
||||
expect_status(&res, run("conflict.zip", "out_conflict_fail",
|
||||
ZIPX_CONFLICT_FAIL, NULL, &t, &res),
|
||||
ZIPX_ERR_CONFLICT, "existing file fails the task");
|
||||
snprintf(file, sizeof(file), "%s/shared.txt", dst);
|
||||
check(!read_text(file, buf, sizeof(buf)) && !strcmp(buf, "original"),
|
||||
"existing file untouched");
|
||||
snprintf(file, sizeof(file), "%s/shareddir", dst);
|
||||
check(!exists(file), "nothing published");
|
||||
}
|
||||
|
||||
static void
|
||||
test_conflict_overwrite(void) {
|
||||
zipx_result_t res;
|
||||
test_ctx_t t = {0};
|
||||
char dst[4096];
|
||||
char file[4224];
|
||||
char buf[64];
|
||||
|
||||
printf("conflict policy: overwrite\n");
|
||||
work_path(dst, sizeof(dst), "out_overwrite");
|
||||
remove_dir(dst);
|
||||
make_dirs(dst);
|
||||
/* An existing directory where the archive has a file must still conflict,
|
||||
even under OVERWRITE (a directory is never replaced by a file). */
|
||||
snprintf(file, sizeof(file), "%s/shared.txt", dst);
|
||||
make_dirs(file);
|
||||
expect_status(&res, run("conflict.zip", "out_overwrite",
|
||||
ZIPX_CONFLICT_OVERWRITE, NULL, &t, &res),
|
||||
ZIPX_ERR_CONFLICT, "existing directory still blocks overwrite");
|
||||
snprintf(file, sizeof(file), "%s/shared.txt", dst);
|
||||
check(exists(file), "directory untouched while it conflicts");
|
||||
|
||||
remove_dir(dst);
|
||||
make_dirs(dst);
|
||||
snprintf(file, sizeof(file), "%s/shared.txt", dst);
|
||||
write_text(file, "original");
|
||||
snprintf(file, sizeof(file), "%s/shareddir", dst);
|
||||
remove_dir(file);
|
||||
expect_ok(&res, run("conflict.zip", "out_overwrite",
|
||||
ZIPX_CONFLICT_OVERWRITE, NULL, &t, &res),
|
||||
"overwrite succeeds without a directory clash");
|
||||
snprintf(file, sizeof(file), "%s/shared.txt", dst);
|
||||
check(!read_text(file, buf, sizeof(buf)) && !strcmp(buf, "from zip"),
|
||||
"file replaced");
|
||||
}
|
||||
|
||||
static void
|
||||
test_conflict_merge(void) {
|
||||
zipx_result_t res;
|
||||
test_ctx_t t = {0};
|
||||
char dst[4096];
|
||||
char file[4224];
|
||||
char buf[64];
|
||||
|
||||
printf("conflict policy: merge\n");
|
||||
work_path(dst, sizeof(dst), "out_merge");
|
||||
remove_dir(dst);
|
||||
make_dirs(dst);
|
||||
snprintf(file, sizeof(file), "%s/shareddir", dst);
|
||||
make_dirs(file);
|
||||
snprintf(file, sizeof(file), "%s/shareddir/inner.txt", dst);
|
||||
check(!write_text(file, "original inner"), "prepare merge destination");
|
||||
expect_ok(&res, run("conflict.zip", "out_merge", ZIPX_CONFLICT_MERGE, NULL,
|
||||
&t, &res), "merge succeeds");
|
||||
snprintf(file, sizeof(file), "%s/shareddir/inner.txt", dst);
|
||||
check(!read_text(file, buf, sizeof(buf)) && !strcmp(buf, "original inner"),
|
||||
"existing file preserved by merge");
|
||||
snprintf(file, sizeof(file), "%s/shareddir/added.txt", dst);
|
||||
check(!read_text(file, buf, sizeof(buf)) &&
|
||||
!strcmp(buf, "added from zip"), "new sibling merged in");
|
||||
snprintf(file, sizeof(file), "%s/shared.txt", dst);
|
||||
check(!read_text(file, buf, sizeof(buf)) && !strcmp(buf, "from zip"),
|
||||
"top level file published");
|
||||
}
|
||||
|
||||
static void
|
||||
test_unsafe_names(void) {
|
||||
zipx_result_t res;
|
||||
test_ctx_t t = {0};
|
||||
char parent[4096];
|
||||
char file[4224];
|
||||
|
||||
printf("path traversal variants\n");
|
||||
expect_status(&res, run("traversal.zip", "out_traversal",
|
||||
ZIPX_CONFLICT_FAIL, NULL, &t, &res),
|
||||
ZIPX_ERR_UNSAFE_NAME, "../ entry rejected");
|
||||
expect_status(&res, run("traversal_bs.zip", "out_traversal_bs",
|
||||
ZIPX_CONFLICT_FAIL, NULL, &t, &res),
|
||||
ZIPX_ERR_UNSAFE_NAME, "..\\ entry rejected");
|
||||
expect_status(&res, run("absolute.zip", "out_absolute",
|
||||
ZIPX_CONFLICT_FAIL, NULL, &t, &res),
|
||||
ZIPX_ERR_UNSAFE_NAME, "absolute entry rejected");
|
||||
expect_status(&res, run("drive.zip", "out_drive", ZIPX_CONFLICT_FAIL, NULL,
|
||||
&t, &res),
|
||||
ZIPX_ERR_UNSAFE_NAME, "drive letter entry rejected");
|
||||
|
||||
work_path(parent, sizeof(parent), ".");
|
||||
snprintf(file, sizeof(file), "%s/evil.txt", parent);
|
||||
check(!exists(file), "no file escaped into the work directory");
|
||||
snprintf(file, sizeof(file), "%s/out_traversal", parent);
|
||||
check(!exists(file), "no destination created for a rejected archive");
|
||||
}
|
||||
|
||||
static void
|
||||
test_duplicates(void) {
|
||||
zipx_result_t res;
|
||||
test_ctx_t t = {0};
|
||||
|
||||
printf("duplicate and clashing entries\n");
|
||||
expect_status(&res, run("duplicate.zip", "out_duplicate",
|
||||
ZIPX_CONFLICT_FAIL, NULL, &t, &res),
|
||||
ZIPX_ERR_DUPLICATE, "duplicate file rejected");
|
||||
expect_status(&res, run("clash.zip", "out_clash", ZIPX_CONFLICT_FAIL, NULL,
|
||||
&t, &res),
|
||||
ZIPX_ERR_DUPLICATE, "file used as a directory rejected");
|
||||
}
|
||||
|
||||
static void
|
||||
test_special_entries(void) {
|
||||
zipx_result_t res;
|
||||
test_ctx_t t = {0};
|
||||
|
||||
printf("symlink and fifo entries\n");
|
||||
expect_status(&res, run("symlink.zip", "out_symlink", ZIPX_CONFLICT_FAIL,
|
||||
NULL, &t, &res),
|
||||
ZIPX_ERR_SPECIAL, "symlink rejected");
|
||||
expect_status(&res, run("fifo.zip", "out_fifo", ZIPX_CONFLICT_FAIL, NULL,
|
||||
&t, &res),
|
||||
ZIPX_ERR_SPECIAL, "fifo rejected");
|
||||
}
|
||||
|
||||
static void
|
||||
test_unsupported(void) {
|
||||
zipx_result_t res;
|
||||
test_ctx_t t = {0};
|
||||
const zipx_limits_t *base = zipx_default_limits();
|
||||
zipx_limits_t limits;
|
||||
|
||||
printf("encrypted, corrupt and truncated archives\n");
|
||||
expect_status(&res, run("encrypted.zip", "out_encrypted",
|
||||
ZIPX_CONFLICT_FAIL, NULL, &t, &res),
|
||||
ZIPX_ERR_UNSUPPORTED, "encrypted entry rejected");
|
||||
expect_status(&res, run("bad_crc.zip", "out_bad_crc", ZIPX_CONFLICT_FAIL,
|
||||
NULL, &t, &res),
|
||||
ZIPX_ERR_CRC, "crc mismatch detected");
|
||||
check(run("truncated.zip", "out_truncated", ZIPX_CONFLICT_FAIL, NULL, &t, &res) !=
|
||||
ZIPX_OK, "truncated archive rejected");
|
||||
check(run("notazip.zip", "out_notazip", ZIPX_CONFLICT_FAIL, NULL, &t, &res) !=
|
||||
ZIPX_OK, "non-zip file rejected");
|
||||
|
||||
printf("limits\n");
|
||||
limits = *base;
|
||||
limits.max_entries = 2;
|
||||
expect_status(&res, run("many.zip", "out_limit_entries",
|
||||
ZIPX_CONFLICT_FAIL, &limits, &t, &res),
|
||||
ZIPX_ERR_LIMIT_ENTRIES, "entry limit enforced");
|
||||
|
||||
limits = *base;
|
||||
limits.max_ratio = 10;
|
||||
expect_status(&res, run("bomb.zip", "out_limit_ratio", ZIPX_CONFLICT_FAIL,
|
||||
&limits, &t, &res),
|
||||
ZIPX_ERR_LIMIT_RATIO, "compression ratio limit enforced");
|
||||
|
||||
limits = *base;
|
||||
limits.max_file_bytes = 1024;
|
||||
expect_status(&res, run("zip64.zip", "out_limit_file", ZIPX_CONFLICT_FAIL,
|
||||
&limits, &t, &res),
|
||||
ZIPX_ERR_LIMIT_FILE, "single file limit enforced");
|
||||
|
||||
limits = *base;
|
||||
limits.max_total_bytes = 1000; /* many.zip totals ~1390 bytes */
|
||||
expect_status(&res, run("many.zip", "out_limit_total", ZIPX_CONFLICT_FAIL,
|
||||
&limits, &t, &res),
|
||||
ZIPX_ERR_LIMIT_TOTAL, "total size limit enforced");
|
||||
|
||||
limits = *base;
|
||||
limits.max_depth = 1;
|
||||
expect_status(&res, run("basic.zip", "out_limit_depth", ZIPX_CONFLICT_FAIL,
|
||||
&limits, &t, &res),
|
||||
ZIPX_ERR_LIMIT_DEPTH, "depth limit enforced");
|
||||
}
|
||||
|
||||
static void
|
||||
test_cancel(void) {
|
||||
zipx_result_t res;
|
||||
test_ctx_t t;
|
||||
char dst[4096];
|
||||
|
||||
printf("cancel leaves nothing behind\n");
|
||||
memset(&t, 0, sizeof(t));
|
||||
t.cancel_after_progress = 1;
|
||||
expect_status(&res, run("many.zip", "out_cancel", ZIPX_CONFLICT_FAIL, NULL,
|
||||
&t, &res),
|
||||
ZIPX_ERR_CANCELED, "cancel honored");
|
||||
work_path(dst, sizeof(dst), "out_cancel");
|
||||
check(!exists(dst), "no destination after cancel");
|
||||
work_path(dst, sizeof(dst), ".");
|
||||
check(count_staging_leftovers(dst) == 0, "no staging left after cancel");
|
||||
}
|
||||
|
||||
static void
|
||||
test_many_files(void) {
|
||||
zipx_result_t res;
|
||||
test_ctx_t t = {0};
|
||||
char dst[4096];
|
||||
char file[4224];
|
||||
int missing = 0;
|
||||
int i;
|
||||
|
||||
printf("500 file archive\n");
|
||||
expect_ok(&res, run("many.zip", "out_many", ZIPX_CONFLICT_FAIL, NULL, &t, &res),
|
||||
"extract succeeds");
|
||||
work_path(dst, sizeof(dst), "out_many");
|
||||
for(i = 0; i < 500; i++) {
|
||||
snprintf(file, sizeof(file), "%s/many/f%03d.txt", dst, i);
|
||||
if(!exists(file)) {
|
||||
missing++;
|
||||
}
|
||||
}
|
||||
check(!missing, "all 500 files present");
|
||||
check(res.entries_total == 500, "entry count reported");
|
||||
check(res.bytes_total > 0, "byte total reported");
|
||||
}
|
||||
|
||||
static void
|
||||
test_large_profile(void) {
|
||||
zipx_result_t res;
|
||||
test_ctx_t t = {0};
|
||||
const zipx_limits_t *base = zipx_default_limits();
|
||||
const zipx_limits_t *large = zipx_limits_profile(ZIPX_LIMITS_LARGE);
|
||||
zipx_limits_t tight;
|
||||
|
||||
printf("large-file profile\n");
|
||||
|
||||
/* The profile must exist and be a real struct, distinct from default. */
|
||||
check(large != NULL, "large profile returned");
|
||||
check(large != base, "large profile differs from default");
|
||||
check(zipx_limits_profile(ZIPX_LIMITS_DEFAULT) == base,
|
||||
"ZIPX_LIMITS_DEFAULT == zipx_default_limits()");
|
||||
|
||||
/* Every cap in the large profile must be at least as large as the default
|
||||
cap. The profile is strictly an upper bound, never a tighter one. */
|
||||
check(large->max_entries > base->max_entries,
|
||||
"max_entries greater than default");
|
||||
check(large->max_total_bytes > base->max_total_bytes,
|
||||
"max_total_bytes greater than default");
|
||||
check(large->max_file_bytes > base->max_file_bytes,
|
||||
"max_file_bytes greater than default");
|
||||
check(large->max_ratio > base->max_ratio,
|
||||
"max_ratio greater than default");
|
||||
|
||||
/* Concrete advertised numbers. If anyone ever changes the profile these
|
||||
assertions keep the public promise honest. */
|
||||
check(large->max_entries == 500000, "max_entries == 500000");
|
||||
check(large->max_file_bytes == 1ULL * 1024 * 1024 * 1024 * 1024,
|
||||
"max_file_bytes == 1 TiB");
|
||||
check(large->max_total_bytes == 2ULL * 1024 * 1024 * 1024 * 1024,
|
||||
"max_total_bytes == 2 TiB");
|
||||
check(large->max_ratio == 1000, "max_ratio == 1000");
|
||||
|
||||
/* Behaviour: medium_bomb.zip is 1 MiB of 0..255 cycled, compressing to
|
||||
~4 KiB (ratio ~238). Default ratio cap 200 rejects it; large ratio
|
||||
cap 1000 accepts it. This is the headline behavioural difference
|
||||
between the two profiles. */
|
||||
printf("ratio cap\n");
|
||||
expect_status(&res, run("medium_bomb.zip", "out_large_medium_default",
|
||||
ZIPX_CONFLICT_FAIL, NULL, &t, &res),
|
||||
ZIPX_ERR_LIMIT_RATIO,
|
||||
"default ratio cap rejects medium_bomb.zip");
|
||||
expect_ok(&res, run("medium_bomb.zip", "out_large_medium_large",
|
||||
ZIPX_CONFLICT_FAIL, large, &t, &res),
|
||||
"large ratio cap accepts medium_bomb.zip");
|
||||
|
||||
/* bomb.zip is 4 MiB of identical 'A' bytes, compressing to ~4 KiB
|
||||
(ratio ~1026). The large profile's default cap of 1000 is still a
|
||||
real cap — it rejects the bomb too. The profile is a higher
|
||||
threshold, not the absence of one. */
|
||||
expect_status(&res, run("bomb.zip", "out_large_bomb_default",
|
||||
ZIPX_CONFLICT_FAIL, large, &t, &res),
|
||||
ZIPX_ERR_LIMIT_RATIO,
|
||||
"large ratio cap (1000) rejects bomb.zip (~1026:1)");
|
||||
|
||||
/* Lowering the large profile's ratio below the medium bomb's actual
|
||||
ratio still rejects the archive. The caps are still enforced; the
|
||||
profile just starts at a higher number. */
|
||||
tight = *large;
|
||||
tight.max_ratio = 200;
|
||||
expect_status(&res, run("medium_bomb.zip", "out_large_medium_tight",
|
||||
ZIPX_CONFLICT_FAIL, &tight, &t, &res),
|
||||
ZIPX_ERR_LIMIT_RATIO,
|
||||
"lowered large ratio still enforced");
|
||||
|
||||
/* Lowering the large profile's file cap below zip64.zip's 4 KiB still
|
||||
rejects the archive. */
|
||||
tight = *large;
|
||||
tight.max_file_bytes = 1024;
|
||||
expect_status(&res, run("zip64.zip", "out_large_file_cap",
|
||||
ZIPX_CONFLICT_FAIL, &tight, &t, &res),
|
||||
ZIPX_ERR_LIMIT_FILE,
|
||||
"lowered large file cap still enforced");
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char **argv) {
|
||||
if(argc < 3) {
|
||||
fprintf(stderr, "usage: %s <fixtures-dir> <work-dir>\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
g_fixtures = argv[1];
|
||||
snprintf(g_work, sizeof(g_work), "%s", argv[2]);
|
||||
remove_dir(g_work);
|
||||
if(make_dirs(g_work)) {
|
||||
fprintf(stderr, "cannot create work dir\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
test_basic();
|
||||
test_stored();
|
||||
test_zip64();
|
||||
test_unicode();
|
||||
test_conflict_fail();
|
||||
test_conflict_overwrite();
|
||||
test_conflict_merge();
|
||||
test_unsafe_names();
|
||||
test_duplicates();
|
||||
test_special_entries();
|
||||
test_unsupported();
|
||||
test_cancel();
|
||||
test_many_files();
|
||||
test_large_profile();
|
||||
|
||||
printf("\n%d checks, %d failures\n", g_checks, g_failures);
|
||||
return g_failures ? 1 : 0;
|
||||
}
|
||||
Vendored
-17
@@ -1,17 +0,0 @@
|
||||
Condition of use and distribution are the same as zlib:
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgement in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
minizip-ng 4.2.2 (https://github.com/zlib-ng/minizip-ng)
|
||||
Vendored
-277
@@ -1,277 +0,0 @@
|
||||
/* mz.h -- Errors codes, zip flags and magic
|
||||
part of the minizip-ng project
|
||||
|
||||
Copyright (C) Nathan Moinvaziri
|
||||
https://github.com/zlib-ng/minizip-ng
|
||||
|
||||
This program is distributed under the terms of the same license as zlib.
|
||||
See the accompanying LICENSE file for the full text of the license.
|
||||
*/
|
||||
|
||||
#ifndef MZ_H
|
||||
#define MZ_H
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
/* MZ_VERSION */
|
||||
#define MZ_VERSION ("4.2.2")
|
||||
#define MZ_VERSION_BUILD (0x040202)
|
||||
|
||||
/* MZ_ERROR */
|
||||
#define MZ_OK (0) /* zlib */
|
||||
#define MZ_STREAM_ERROR (-1) /* zlib */
|
||||
#define MZ_DATA_ERROR (-3) /* zlib */
|
||||
#define MZ_MEM_ERROR (-4) /* zlib */
|
||||
#define MZ_BUF_ERROR (-5) /* zlib */
|
||||
#define MZ_VERSION_ERROR (-6) /* zlib */
|
||||
|
||||
#define MZ_END_OF_LIST (-100)
|
||||
#define MZ_END_OF_STREAM (-101)
|
||||
|
||||
#define MZ_PARAM_ERROR (-102)
|
||||
#define MZ_FORMAT_ERROR (-103)
|
||||
#define MZ_INTERNAL_ERROR (-104)
|
||||
#define MZ_CRC_ERROR (-105)
|
||||
#define MZ_CRYPT_ERROR (-106)
|
||||
#define MZ_EXIST_ERROR (-107)
|
||||
#define MZ_PASSWORD_ERROR (-108)
|
||||
#define MZ_SUPPORT_ERROR (-109)
|
||||
#define MZ_HASH_ERROR (-110)
|
||||
#define MZ_OPEN_ERROR (-111)
|
||||
#define MZ_CLOSE_ERROR (-112)
|
||||
#define MZ_SEEK_ERROR (-113)
|
||||
#define MZ_TELL_ERROR (-114)
|
||||
#define MZ_READ_ERROR (-115)
|
||||
#define MZ_WRITE_ERROR (-116)
|
||||
#define MZ_SIGN_ERROR (-117)
|
||||
#define MZ_SYMLINK_ERROR (-118)
|
||||
|
||||
/* MZ_OPEN */
|
||||
#define MZ_OPEN_MODE_READ (0x01)
|
||||
#define MZ_OPEN_MODE_WRITE (0x02)
|
||||
#define MZ_OPEN_MODE_READWRITE (MZ_OPEN_MODE_READ | MZ_OPEN_MODE_WRITE)
|
||||
#define MZ_OPEN_MODE_APPEND (0x04)
|
||||
#define MZ_OPEN_MODE_CREATE (0x08)
|
||||
#define MZ_OPEN_MODE_EXISTING (0x10)
|
||||
#define MZ_OPEN_MODE_NOFOLLOW (0x20)
|
||||
|
||||
/* MZ_SEEK */
|
||||
#define MZ_SEEK_SET (0)
|
||||
#define MZ_SEEK_CUR (1)
|
||||
#define MZ_SEEK_END (2)
|
||||
|
||||
/* MZ_COMPRESS */
|
||||
#define MZ_COMPRESS_METHOD_STORE (0)
|
||||
#define MZ_COMPRESS_METHOD_DEFLATE (8)
|
||||
#define MZ_COMPRESS_METHOD_BZIP2 (12)
|
||||
#define MZ_COMPRESS_METHOD_LZMA (14)
|
||||
#define MZ_COMPRESS_METHOD_ZSTD (93)
|
||||
#define MZ_COMPRESS_METHOD_XZ (95)
|
||||
#define MZ_COMPRESS_METHOD_PPMD (98)
|
||||
#define MZ_COMPRESS_METHOD_AES (99)
|
||||
|
||||
#define MZ_COMPRESS_LEVEL_DEFAULT (-1)
|
||||
#define MZ_COMPRESS_LEVEL_FAST (2)
|
||||
#define MZ_COMPRESS_LEVEL_NORMAL (6)
|
||||
#define MZ_COMPRESS_LEVEL_BEST (9)
|
||||
|
||||
/* MZ_ZIP_FLAG */
|
||||
#define MZ_ZIP_FLAG_ENCRYPTED (1 << 0)
|
||||
#define MZ_ZIP_FLAG_LZMA_EOS_MARKER (1 << 1)
|
||||
#define MZ_ZIP_FLAG_DEFLATE_MAX (1 << 1)
|
||||
#define MZ_ZIP_FLAG_DEFLATE_NORMAL (0)
|
||||
#define MZ_ZIP_FLAG_DEFLATE_FAST (1 << 2)
|
||||
#define MZ_ZIP_FLAG_DEFLATE_SUPER_FAST (MZ_ZIP_FLAG_DEFLATE_FAST | MZ_ZIP_FLAG_DEFLATE_MAX)
|
||||
#define MZ_ZIP_FLAG_DATA_DESCRIPTOR (1 << 3)
|
||||
#define MZ_ZIP_FLAG_UTF8 (1 << 11)
|
||||
#define MZ_ZIP_FLAG_MASK_LOCAL_INFO (1 << 13)
|
||||
|
||||
/* MZ_ZIP_EXTENSION */
|
||||
#define MZ_ZIP_EXTENSION_ZIP64 (0x0001)
|
||||
#define MZ_ZIP_EXTENSION_NTFS (0x000a)
|
||||
#define MZ_ZIP_EXTENSION_AES (0x9901)
|
||||
#define MZ_ZIP_EXTENSION_UNIX1 (0x000d)
|
||||
#define MZ_ZIP_EXTENSION_SIGN (0x10c5)
|
||||
#define MZ_ZIP_EXTENSION_HASH (0x1a51)
|
||||
#define MZ_ZIP_EXTENSION_CDCD (0xcdcd)
|
||||
|
||||
/* MZ_ZIP64 */
|
||||
#define MZ_ZIP64_AUTO (0)
|
||||
#define MZ_ZIP64_FORCE (1)
|
||||
#define MZ_ZIP64_DISABLE (2)
|
||||
|
||||
/* MZ_HOST_SYSTEM */
|
||||
#define MZ_HOST_SYSTEM(VERSION_MADEBY) ((uint8_t)(VERSION_MADEBY >> 8))
|
||||
#define MZ_HOST_SYSTEM_MSDOS (0)
|
||||
#define MZ_HOST_SYSTEM_UNIX (3)
|
||||
#define MZ_HOST_SYSTEM_WINDOWS_NTFS (10)
|
||||
#define MZ_HOST_SYSTEM_RISCOS (13)
|
||||
#define MZ_HOST_SYSTEM_OSX_DARWIN (19)
|
||||
|
||||
/* MZ_PKCRYPT */
|
||||
#define MZ_PKCRYPT_HEADER_SIZE (12)
|
||||
|
||||
/* MZ_AES */
|
||||
#define MZ_AES_VERSION (1)
|
||||
#define MZ_AES_MODE_ECB (0)
|
||||
#define MZ_AES_MODE_CBC (1)
|
||||
#define MZ_AES_MODE_GCM (2)
|
||||
#define MZ_AES_STRENGTH_128 (1)
|
||||
#define MZ_AES_STRENGTH_192 (2)
|
||||
#define MZ_AES_STRENGTH_256 (3)
|
||||
#define MZ_AES_KEY_LENGTH_MAX (32)
|
||||
#define MZ_AES_BLOCK_SIZE (16)
|
||||
#define MZ_AES_FOOTER_SIZE (10)
|
||||
|
||||
/* MZ_HASH */
|
||||
#define MZ_HASH_MD5 (10)
|
||||
#define MZ_HASH_MD5_SIZE (16)
|
||||
#define MZ_HASH_SHA1 (20)
|
||||
#define MZ_HASH_SHA1_SIZE (20)
|
||||
#define MZ_HASH_SHA224 (22)
|
||||
#define MZ_HASH_SHA224_SIZE (28)
|
||||
#define MZ_HASH_SHA256 (23)
|
||||
#define MZ_HASH_SHA256_SIZE (32)
|
||||
#define MZ_HASH_SHA384 (24)
|
||||
#define MZ_HASH_SHA384_SIZE (48)
|
||||
#define MZ_HASH_SHA512 (25)
|
||||
#define MZ_HASH_SHA512_SIZE (64)
|
||||
#define MZ_HASH_MAX_SIZE (256)
|
||||
|
||||
/* MZ_ENCODING */
|
||||
#define MZ_ENCODING_CODEPAGE_437 (437)
|
||||
#define MZ_ENCODING_CODEPAGE_932 (932)
|
||||
#define MZ_ENCODING_CODEPAGE_936 (936)
|
||||
#define MZ_ENCODING_CODEPAGE_950 (950)
|
||||
#define MZ_ENCODING_UTF8 (65001)
|
||||
|
||||
/* MZ_UTILITY */
|
||||
#define MZ_UNUSED(SYMBOL) ((void)SYMBOL)
|
||||
|
||||
#if defined(_WIN32) && defined(MZ_EXPORTS)
|
||||
# define MZ_EXPORT __declspec(dllexport)
|
||||
#else
|
||||
# define MZ_EXPORT
|
||||
#endif
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
#include <stdlib.h> /* size_t, NULL, malloc */
|
||||
#include <time.h> /* time_t, time() */
|
||||
#include <string.h> /* memset, strncpy, strlen */
|
||||
#include <limits.h>
|
||||
|
||||
#include "mz_config.h"
|
||||
|
||||
#if HAVE_STDINT_H
|
||||
# include <stdint.h>
|
||||
#elif defined(__has_include)
|
||||
# if __has_include(<stdint.h>)
|
||||
# include <stdint.h>
|
||||
# endif
|
||||
#endif
|
||||
|
||||
#ifndef INT8_MAX
|
||||
typedef signed char int8_t;
|
||||
#endif
|
||||
#ifndef INT16_MAX
|
||||
typedef short int16_t;
|
||||
#endif
|
||||
#ifndef INT32_MAX
|
||||
typedef int int32_t;
|
||||
#endif
|
||||
#ifndef INT64_MAX
|
||||
typedef long long int64_t;
|
||||
#endif
|
||||
#ifndef UINT8_MAX
|
||||
typedef unsigned char uint8_t;
|
||||
#endif
|
||||
#ifndef UINT16_MAX
|
||||
typedef unsigned short uint16_t;
|
||||
#endif
|
||||
#ifndef UINT32_MAX
|
||||
typedef unsigned int uint32_t;
|
||||
#endif
|
||||
#ifndef UINT64_MAX
|
||||
typedef unsigned long long uint64_t;
|
||||
#endif
|
||||
|
||||
#if HAVE_INTTYPES_H
|
||||
# include <inttypes.h>
|
||||
#elif defined(__has_include)
|
||||
# if __has_include(<inttypes.h>)
|
||||
# include <inttypes.h>
|
||||
# endif
|
||||
#endif
|
||||
|
||||
#ifndef PRId8
|
||||
# define PRId8 "hhd"
|
||||
#endif
|
||||
#ifndef PRIu8
|
||||
# define PRIu8 "hhu"
|
||||
#endif
|
||||
#ifndef PRIx8
|
||||
# define PRIx8 "hhx"
|
||||
#endif
|
||||
#ifndef PRId16
|
||||
# define PRId16 "hd"
|
||||
#endif
|
||||
#ifndef PRIu16
|
||||
# define PRIu16 "hu"
|
||||
#endif
|
||||
#ifndef PRIx16
|
||||
# define PRIx16 "hx"
|
||||
#endif
|
||||
#ifndef PRId32
|
||||
# define PRId32 "d"
|
||||
#endif
|
||||
#ifndef PRIu32
|
||||
# define PRIu32 "u"
|
||||
#endif
|
||||
#ifndef PRIx32
|
||||
# define PRIx32 "x"
|
||||
#endif
|
||||
#if ULONG_MAX == 0xfffffffful
|
||||
# ifndef PRId64
|
||||
# define PRId64 "ld"
|
||||
# endif
|
||||
# ifndef PRIu64
|
||||
# define PRIu64 "lu"
|
||||
# endif
|
||||
# ifndef PRIx64
|
||||
# define PRIx64 "lx"
|
||||
# endif
|
||||
#else
|
||||
# ifndef PRId64
|
||||
# define PRId64 "lld"
|
||||
# endif
|
||||
# ifndef PRIu64
|
||||
# define PRIu64 "llu"
|
||||
# endif
|
||||
# ifndef PRIx64
|
||||
# define PRIx64 "llx"
|
||||
# endif
|
||||
#endif
|
||||
|
||||
#ifndef INT16_MAX
|
||||
# define INT16_MAX 32767
|
||||
#endif
|
||||
#ifndef INT32_MAX
|
||||
# define INT32_MAX 2147483647L
|
||||
#endif
|
||||
#ifndef INT64_MAX
|
||||
# define INT64_MAX 9223372036854775807LL
|
||||
#endif
|
||||
#ifndef UINT16_MAX
|
||||
# define UINT16_MAX 65535U
|
||||
#endif
|
||||
#ifndef UINT32_MAX
|
||||
# define UINT32_MAX 4294967295UL
|
||||
#endif
|
||||
#ifndef UINT64_MAX
|
||||
# define UINT64_MAX 18446744073709551615ULL
|
||||
#endif
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
#endif
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
/* mz_config.h -- hand maintained configuration for the vendored minizip-ng.
|
||||
part of the minizip-ng project
|
||||
|
||||
This file replaces the CMake generated mz_config.h. The vendored copy only
|
||||
builds the decompression path: HAVE_ZLIB is defined by the project Makefile
|
||||
and no other compression or crypto backend is enabled.
|
||||
*/
|
||||
|
||||
#ifndef MZ_CONFIG_H
|
||||
#define MZ_CONFIG_H
|
||||
|
||||
/* Define to 1 if you have the <dirent.h> header file. */
|
||||
#define HAVE_DIRENT_H 1
|
||||
|
||||
/* Define to 1 if you have the <sys/dirent.h> header file. */
|
||||
#define HAVE_SYS_DIRENT_H 0
|
||||
|
||||
/* Define to 1 if you have the <inttypes.h> header file. */
|
||||
#define HAVE_INTTYPES_H 1
|
||||
|
||||
/* Define to 1 if you have the <stdint.h> header file. */
|
||||
#define HAVE_STDINT_H 1
|
||||
|
||||
/* Define to 1 if DIR* is defined. */
|
||||
#define HAVE_PDIR 1
|
||||
|
||||
/* Define to 1 if fseeko() is defined. */
|
||||
#define HAVE_FSEEKO 1
|
||||
|
||||
/* Define to 1 if symlink() is defined. */
|
||||
#define HAVE_SYMLINK 1
|
||||
|
||||
/* Define to 1 if readlink() is defined. */
|
||||
#define HAVE_READLINK 1
|
||||
|
||||
#endif
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
/* mz_crypt.h -- Crypto/hash functions
|
||||
part of the minizip-ng project
|
||||
|
||||
Copyright (C) Nathan Moinvaziri
|
||||
https://github.com/zlib-ng/minizip-ng
|
||||
|
||||
This program is distributed under the terms of the same license as zlib.
|
||||
See the accompanying LICENSE file for the full text of the license.
|
||||
*/
|
||||
|
||||
#ifndef MZ_CRYPT_H
|
||||
#define MZ_CRYPT_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
uint32_t mz_crypt_crc32_update(uint32_t value, const uint8_t *buf, int32_t size);
|
||||
|
||||
int32_t mz_crypt_pbkdf2(const uint8_t *password, int32_t password_length, const uint8_t *salt, int32_t salt_length,
|
||||
uint32_t iteration_count, uint8_t *key, uint16_t key_length);
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
int32_t mz_crypt_rand(uint8_t *buf, int32_t size);
|
||||
|
||||
void mz_crypt_sha_reset(void *handle);
|
||||
int32_t mz_crypt_sha_begin(void *handle);
|
||||
int32_t mz_crypt_sha_update(void *handle, const void *buf, int32_t size);
|
||||
int32_t mz_crypt_sha_end(void *handle, uint8_t *digest, int32_t digest_size);
|
||||
int32_t mz_crypt_sha_set_algorithm(void *handle, uint16_t algorithm);
|
||||
void *mz_crypt_sha_create(void);
|
||||
void mz_crypt_sha_delete(void **handle);
|
||||
|
||||
void mz_crypt_aes_reset(void *handle);
|
||||
int32_t mz_crypt_aes_encrypt(void *handle, const void *aad, int32_t aad_size, uint8_t *buf, int32_t size);
|
||||
int32_t mz_crypt_aes_encrypt_final(void *handle, uint8_t *buf, int32_t size, uint8_t *tag, int32_t tag_size);
|
||||
int32_t mz_crypt_aes_decrypt(void *handle, const void *aad, int32_t aad_size, uint8_t *buf, int32_t size);
|
||||
int32_t mz_crypt_aes_decrypt_final(void *handle, uint8_t *buf, int32_t size, const uint8_t *tag, int32_t tag_size);
|
||||
int32_t mz_crypt_aes_set_encrypt_key(void *handle, const void *key, int32_t key_length, const void *iv,
|
||||
int32_t iv_length);
|
||||
int32_t mz_crypt_aes_set_decrypt_key(void *handle, const void *key, int32_t key_length, const void *iv,
|
||||
int32_t iv_length);
|
||||
void mz_crypt_aes_set_mode(void *handle, int32_t mode);
|
||||
void *mz_crypt_aes_create(void);
|
||||
void mz_crypt_aes_delete(void **handle);
|
||||
|
||||
void mz_crypt_hmac_reset(void *handle);
|
||||
int32_t mz_crypt_hmac_init(void *handle, const void *key, int32_t key_length);
|
||||
int32_t mz_crypt_hmac_update(void *handle, const void *buf, int32_t size);
|
||||
int32_t mz_crypt_hmac_end(void *handle, uint8_t *digest, int32_t digest_size);
|
||||
int32_t mz_crypt_hmac_copy(void *src_handle, void *target_handle);
|
||||
void mz_crypt_hmac_set_algorithm(void *handle, uint16_t algorithm);
|
||||
void *mz_crypt_hmac_create(void);
|
||||
void mz_crypt_hmac_delete(void **handle);
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
-201
@@ -1,201 +0,0 @@
|
||||
/* mz_os.h -- System functions
|
||||
part of the minizip-ng project
|
||||
|
||||
Copyright (C) Nathan Moinvaziri
|
||||
https://github.com/zlib-ng/minizip-ng
|
||||
|
||||
This program is distributed under the terms of the same license as zlib.
|
||||
See the accompanying LICENSE file for the full text of the license.
|
||||
*/
|
||||
|
||||
#ifndef MZ_OS_H
|
||||
#define MZ_OS_H
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
#if defined(__APPLE__)
|
||||
# define MZ_VERSION_MADEBY_HOST_SYSTEM (MZ_HOST_SYSTEM_OSX_DARWIN)
|
||||
#elif defined(__riscos__)
|
||||
# define MZ_VERSION_MADEBY_HOST_SYSTEM (MZ_HOST_SYSTEM_RISCOS)
|
||||
#elif defined(_WIN32)
|
||||
# define MZ_VERSION_MADEBY_HOST_SYSTEM (MZ_HOST_SYSTEM_WINDOWS_NTFS)
|
||||
#else
|
||||
# define MZ_VERSION_MADEBY_HOST_SYSTEM (MZ_HOST_SYSTEM_UNIX)
|
||||
#endif
|
||||
|
||||
#if defined(HAVE_LZMA) || defined(HAVE_LIBCOMP) || defined(HAVE_PPMD)
|
||||
# define MZ_VERSION_MADEBY_ZIP_VERSION (63)
|
||||
#elif defined(HAVE_WZAES)
|
||||
# define MZ_VERSION_MADEBY_ZIP_VERSION (51)
|
||||
#elif defined(HAVE_BZIP2)
|
||||
# define MZ_VERSION_MADEBY_ZIP_VERSION (46)
|
||||
#else
|
||||
# define MZ_VERSION_MADEBY_ZIP_VERSION (45)
|
||||
#endif
|
||||
|
||||
#define MZ_VERSION_MADEBY ((MZ_VERSION_MADEBY_HOST_SYSTEM << 8) | (MZ_VERSION_MADEBY_ZIP_VERSION))
|
||||
|
||||
#define MZ_PATH_SLASH_UNIX ('/')
|
||||
#define MZ_PATH_SLASH_WINDOWS ('\\')
|
||||
#if defined(_WIN32)
|
||||
# define MZ_PATH_SLASH_PLATFORM (MZ_PATH_SLASH_WINDOWS)
|
||||
#else
|
||||
# define MZ_PATH_SLASH_PLATFORM (MZ_PATH_SLASH_UNIX)
|
||||
#endif
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
#include "mz_config.h"
|
||||
|
||||
#if HAVE_DIRENT_H
|
||||
# include <dirent.h>
|
||||
#elif HAVE_SYS_DIRENT_H
|
||||
# include <sys/dirent.h>
|
||||
#else
|
||||
struct dirent {
|
||||
char d_name[256];
|
||||
};
|
||||
#endif
|
||||
|
||||
#if !HAVE_PDIR
|
||||
typedef struct DIR DIR;
|
||||
#endif
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/***************************************************************************/
|
||||
/* Shared functions */
|
||||
|
||||
int32_t mz_path_combine(char *path, const char *join, int32_t max_path);
|
||||
/* Combines two paths */
|
||||
|
||||
int32_t mz_path_append_slash(char *path, int32_t max_path, char slash);
|
||||
/* Appends a path slash on to the end of the path */
|
||||
|
||||
int32_t mz_path_remove_slash(char *path);
|
||||
/* Removes a path slash from the end of the path */
|
||||
|
||||
int32_t mz_path_has_slash(const char *path);
|
||||
/* Returns whether or not the path ends with slash */
|
||||
|
||||
int32_t mz_path_convert_slashes(char *path, char slash);
|
||||
/* Converts the slashes in a path */
|
||||
|
||||
int32_t mz_path_compare_wc(const char *path, const char *wildcard, uint8_t ignore_case);
|
||||
/* Compare two paths with wildcard */
|
||||
|
||||
int32_t mz_path_resolve(const char *path, char *target, int32_t max_target);
|
||||
/* Resolves path */
|
||||
|
||||
int32_t mz_path_remove_filename(char *path);
|
||||
/* Remove the filename from a path */
|
||||
|
||||
int32_t mz_path_remove_extension(char *path);
|
||||
/* Remove the extension from a path */
|
||||
|
||||
int32_t mz_path_get_filename(const char *path, const char **filename);
|
||||
/* Get the filename from a path */
|
||||
|
||||
int32_t mz_path_is_symlink_target_safe(const char *link_path, const char *target, const char *base_path);
|
||||
/* Checks if a symlink target resolves within base path. */
|
||||
|
||||
int32_t mz_dir_has_unsafe_symlink(const char *path, const char *base_path);
|
||||
/* Checks if any existing component of path is a symlink that escapes base path. */
|
||||
|
||||
int32_t mz_dir_make(const char *path);
|
||||
/* Creates a directory recursively */
|
||||
|
||||
int32_t mz_file_get_crc(const char *path, uint32_t *result_crc);
|
||||
/* Gets the crc32 hash of a file */
|
||||
|
||||
/***************************************************************************/
|
||||
/* Platform specific functions */
|
||||
|
||||
wchar_t *mz_os_unicode_string_create(const char *string, int32_t encoding);
|
||||
/* Create unicode string from a utf8 string */
|
||||
|
||||
void mz_os_unicode_string_delete(wchar_t **string);
|
||||
/* Delete a unicode string that was created */
|
||||
|
||||
char *mz_os_utf8_string_create(const char *string, int32_t encoding);
|
||||
/* Create a utf8 string from a string with another encoding */
|
||||
|
||||
void mz_os_utf8_string_delete(char **string);
|
||||
/* Delete a utf8 string that was created */
|
||||
|
||||
int32_t mz_os_rand(uint8_t *buf, int32_t size);
|
||||
/* Random number generator (not cryptographically secure) */
|
||||
|
||||
int32_t mz_os_rename(const char *source_path, const char *target_path);
|
||||
/* Rename a file */
|
||||
|
||||
int32_t mz_os_unlink(const char *path);
|
||||
/* Delete an existing file */
|
||||
|
||||
int32_t mz_os_file_exists(const char *path);
|
||||
/* Check to see if a file exists */
|
||||
|
||||
int64_t mz_os_get_file_size(const char *path);
|
||||
/* Gets the length of a file */
|
||||
|
||||
int32_t mz_os_get_file_date(const char *path, time_t *modified_date, time_t *accessed_date, time_t *creation_date);
|
||||
/* Gets a file's modified, access, and creation dates if supported */
|
||||
|
||||
int32_t mz_os_set_file_date(const char *path, time_t modified_date, time_t accessed_date, time_t creation_date);
|
||||
/* Sets a file's modified, access, and creation dates if supported */
|
||||
|
||||
int32_t mz_os_get_file_attribs(const char *path, uint32_t *attributes);
|
||||
/* Gets a file's attributes, following symbolic links */
|
||||
|
||||
int32_t mz_os_set_file_attribs(const char *path, uint32_t attributes);
|
||||
/* Sets a file's attributes */
|
||||
|
||||
int32_t mz_os_get_temp_path(char *path, int32_t max_path, const char *prefix);
|
||||
/* Gets a unique temporary file path */
|
||||
|
||||
int32_t mz_os_path_same_fs(const char *path_a, const char *path_b);
|
||||
/* Checks if both paths are on the same filesystem */
|
||||
|
||||
int32_t mz_os_make_dir(const char *path);
|
||||
/* Recursively creates a directory */
|
||||
|
||||
DIR *mz_os_open_dir(const char *path);
|
||||
/* Opens a directory for listing */
|
||||
struct dirent *mz_os_read_dir(DIR *dir);
|
||||
/* Reads a directory listing entry */
|
||||
|
||||
int32_t mz_os_close_dir(DIR *dir);
|
||||
/* Closes a directory that has been opened for listing */
|
||||
|
||||
int32_t mz_os_is_dir_separator(char c);
|
||||
/* Checks to see if character is a directory separator */
|
||||
|
||||
int32_t mz_os_is_dir(const char *path);
|
||||
/* Checks to see if path is a directory */
|
||||
|
||||
int32_t mz_os_is_symlink(const char *path);
|
||||
/* Checks to see if path is a symbolic link */
|
||||
|
||||
int32_t mz_os_get_link_attribs(const char *path, uint32_t *attributes);
|
||||
/* Gets a symbolic link's attributes */
|
||||
|
||||
int32_t mz_os_make_symlink(const char *path, const char *target_path);
|
||||
/* Creates a symbolic link pointing to a target */
|
||||
|
||||
int32_t mz_os_read_symlink(const char *path, char *target_path, int32_t max_target_path);
|
||||
/* Gets the target path for a symbolic link */
|
||||
|
||||
uint64_t mz_os_ms_time(void);
|
||||
/* Gets the time in milliseconds */
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
-135
@@ -1,135 +0,0 @@
|
||||
/* mz_strm.h -- Stream interface
|
||||
part of the minizip-ng project
|
||||
|
||||
Copyright (C) Nathan Moinvaziri
|
||||
https://github.com/zlib-ng/minizip-ng
|
||||
|
||||
This program is distributed under the terms of the same license as zlib.
|
||||
See the accompanying LICENSE file for the full text of the license.
|
||||
*/
|
||||
|
||||
#ifndef MZ_STREAM_H
|
||||
#define MZ_STREAM_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
#define MZ_STREAM_PROP_TOTAL_IN (1)
|
||||
#define MZ_STREAM_PROP_TOTAL_IN_MAX (2)
|
||||
#define MZ_STREAM_PROP_TOTAL_OUT (3)
|
||||
#define MZ_STREAM_PROP_TOTAL_OUT_MAX (4)
|
||||
#define MZ_STREAM_PROP_HEADER_SIZE (5)
|
||||
#define MZ_STREAM_PROP_FOOTER_SIZE (6)
|
||||
#define MZ_STREAM_PROP_DISK_SIZE (7)
|
||||
#define MZ_STREAM_PROP_DISK_NUMBER (8)
|
||||
#define MZ_STREAM_PROP_COMPRESS_LEVEL (9)
|
||||
#define MZ_STREAM_PROP_COMPRESS_METHOD (10)
|
||||
#define MZ_STREAM_PROP_COMPRESS_WINDOW (11)
|
||||
#define MZ_STREAM_PROP_COMPRESS_THREADS (12)
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
typedef int32_t (*mz_stream_open_cb)(void *stream, const char *path, int32_t mode);
|
||||
typedef int32_t (*mz_stream_is_open_cb)(void *stream);
|
||||
typedef int32_t (*mz_stream_read_cb)(void *stream, void *buf, int32_t size);
|
||||
typedef int32_t (*mz_stream_write_cb)(void *stream, const void *buf, int32_t size);
|
||||
typedef int64_t (*mz_stream_tell_cb)(void *stream);
|
||||
typedef int32_t (*mz_stream_seek_cb)(void *stream, int64_t offset, int32_t origin);
|
||||
typedef int32_t (*mz_stream_close_cb)(void *stream);
|
||||
typedef int32_t (*mz_stream_error_cb)(void *stream);
|
||||
typedef void *(*mz_stream_create_cb)(void);
|
||||
typedef void (*mz_stream_destroy_cb)(void **stream);
|
||||
|
||||
typedef int32_t (*mz_stream_get_prop_int64_cb)(void *stream, int32_t prop, int64_t *value);
|
||||
typedef int32_t (*mz_stream_set_prop_int64_cb)(void *stream, int32_t prop, int64_t value);
|
||||
|
||||
typedef int32_t (*mz_stream_find_cb)(void *stream, const void *find, int32_t find_size, int64_t max_seek,
|
||||
int64_t *position);
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
typedef struct mz_stream_vtbl_s {
|
||||
mz_stream_open_cb open;
|
||||
mz_stream_is_open_cb is_open;
|
||||
mz_stream_read_cb read;
|
||||
mz_stream_write_cb write;
|
||||
mz_stream_tell_cb tell;
|
||||
mz_stream_seek_cb seek;
|
||||
mz_stream_close_cb close;
|
||||
mz_stream_error_cb error;
|
||||
mz_stream_create_cb create;
|
||||
mz_stream_destroy_cb destroy;
|
||||
|
||||
mz_stream_get_prop_int64_cb get_prop_int64;
|
||||
mz_stream_set_prop_int64_cb set_prop_int64;
|
||||
} mz_stream_vtbl;
|
||||
|
||||
typedef struct mz_stream_s {
|
||||
mz_stream_vtbl *vtbl;
|
||||
struct mz_stream_s *base;
|
||||
} mz_stream;
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
int32_t mz_stream_open(void *stream, const char *path, int32_t mode);
|
||||
int32_t mz_stream_is_open(void *stream);
|
||||
int32_t mz_stream_read(void *stream, void *buf, int32_t size);
|
||||
int32_t mz_stream_read_uint8(void *stream, uint8_t *value);
|
||||
int32_t mz_stream_read_uint16(void *stream, uint16_t *value);
|
||||
int32_t mz_stream_read_uint32(void *stream, uint32_t *value);
|
||||
int32_t mz_stream_read_int64(void *stream, int64_t *value);
|
||||
int32_t mz_stream_read_uint64(void *stream, uint64_t *value);
|
||||
int32_t mz_stream_write(void *stream, const void *buf, int32_t size);
|
||||
int32_t mz_stream_write_uint8(void *stream, uint8_t value);
|
||||
int32_t mz_stream_write_uint16(void *stream, uint16_t value);
|
||||
int32_t mz_stream_write_uint32(void *stream, uint32_t value);
|
||||
int32_t mz_stream_write_int64(void *stream, int64_t value);
|
||||
int32_t mz_stream_write_uint64(void *stream, uint64_t value);
|
||||
int32_t mz_stream_copy(void *target, void *source, int32_t len);
|
||||
int32_t mz_stream_copy_to_end(void *target, void *source);
|
||||
int32_t mz_stream_copy_stream(void *target, mz_stream_write_cb write_cb, void *source, mz_stream_read_cb read_cb,
|
||||
int32_t len);
|
||||
int32_t mz_stream_copy_stream_to_end(void *target, mz_stream_write_cb write_cb, void *source,
|
||||
mz_stream_read_cb read_cb);
|
||||
int64_t mz_stream_tell(void *stream);
|
||||
int32_t mz_stream_seek(void *stream, int64_t offset, int32_t origin);
|
||||
int32_t mz_stream_find(void *stream, const void *find, int32_t find_size, int64_t max_seek, int64_t *position);
|
||||
int32_t mz_stream_find_reverse(void *stream, const void *find, int32_t find_size, int64_t max_seek, int64_t *position);
|
||||
int32_t mz_stream_close(void *stream);
|
||||
int32_t mz_stream_error(void *stream);
|
||||
|
||||
int32_t mz_stream_set_base(void *stream, void *base);
|
||||
void *mz_stream_get_interface(void *stream);
|
||||
int32_t mz_stream_get_prop_int64(void *stream, int32_t prop, int64_t *value);
|
||||
int32_t mz_stream_set_prop_int64(void *stream, int32_t prop, int64_t value);
|
||||
|
||||
void *mz_stream_create(mz_stream_vtbl *vtbl);
|
||||
void mz_stream_delete(void **stream);
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
int32_t mz_stream_raw_open(void *stream, const char *filename, int32_t mode);
|
||||
int32_t mz_stream_raw_is_open(void *stream);
|
||||
int32_t mz_stream_raw_read(void *stream, void *buf, int32_t size);
|
||||
int32_t mz_stream_raw_write(void *stream, const void *buf, int32_t size);
|
||||
int64_t mz_stream_raw_tell(void *stream);
|
||||
int32_t mz_stream_raw_seek(void *stream, int64_t offset, int32_t origin);
|
||||
int32_t mz_stream_raw_close(void *stream);
|
||||
int32_t mz_stream_raw_error(void *stream);
|
||||
|
||||
int32_t mz_stream_raw_get_prop_int64(void *stream, int32_t prop, int64_t *value);
|
||||
int32_t mz_stream_raw_set_prop_int64(void *stream, int32_t prop, int64_t value);
|
||||
|
||||
void *mz_stream_raw_create(void);
|
||||
void mz_stream_raw_delete(void **stream);
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
/* mz_strm_mem.h -- Stream for memory access
|
||||
part of the minizip-ng project
|
||||
|
||||
Copyright (C) Nathan Moinvaziri
|
||||
https://github.com/zlib-ng/minizip-ng
|
||||
|
||||
This program is distributed under the terms of the same license as zlib.
|
||||
See the accompanying LICENSE file for the full text of the license.
|
||||
*/
|
||||
|
||||
#ifndef MZ_STREAM_MEM_H
|
||||
#define MZ_STREAM_MEM_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
int32_t mz_stream_mem_open(void *stream, const char *filename, int32_t mode);
|
||||
int32_t mz_stream_mem_is_open(void *stream);
|
||||
int32_t mz_stream_mem_read(void *stream, void *buf, int32_t size);
|
||||
int32_t mz_stream_mem_write(void *stream, const void *buf, int32_t size);
|
||||
int64_t mz_stream_mem_tell(void *stream);
|
||||
int32_t mz_stream_mem_seek(void *stream, int64_t offset, int32_t origin);
|
||||
int32_t mz_stream_mem_close(void *stream);
|
||||
int32_t mz_stream_mem_error(void *stream);
|
||||
|
||||
void mz_stream_mem_set_buffer(void *stream, void *buf, int32_t size);
|
||||
int32_t mz_stream_mem_get_buffer(void *stream, const void **buf);
|
||||
int32_t mz_stream_mem_get_buffer_at(void *stream, int64_t position, const void **buf);
|
||||
int32_t mz_stream_mem_get_buffer_at_current(void *stream, const void **buf);
|
||||
void mz_stream_mem_get_buffer_length(void *stream, int32_t *length);
|
||||
void mz_stream_mem_set_buffer_limit(void *stream, int32_t limit);
|
||||
void mz_stream_mem_set_grow_size(void *stream, int32_t grow_size);
|
||||
|
||||
void *mz_stream_mem_create(void);
|
||||
void mz_stream_mem_delete(void **stream);
|
||||
|
||||
void *mz_stream_mem_get_interface(void);
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
/* mz_sstrm_os.h -- Stream for filesystem access
|
||||
part of the minizip-ng project
|
||||
|
||||
Copyright (C) Nathan Moinvaziri
|
||||
https://github.com/zlib-ng/minizip-ng
|
||||
|
||||
This program is distributed under the terms of the same license as zlib.
|
||||
See the accompanying LICENSE file for the full text of the license.
|
||||
*/
|
||||
|
||||
#ifndef MZ_STREAM_OS_H
|
||||
#define MZ_STREAM_OS_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
int32_t mz_stream_os_open(void *stream, const char *path, int32_t mode);
|
||||
int32_t mz_stream_os_is_open(void *stream);
|
||||
int32_t mz_stream_os_read(void *stream, void *buf, int32_t size);
|
||||
int32_t mz_stream_os_write(void *stream, const void *buf, int32_t size);
|
||||
int64_t mz_stream_os_tell(void *stream);
|
||||
int32_t mz_stream_os_seek(void *stream, int64_t offset, int32_t origin);
|
||||
int32_t mz_stream_os_close(void *stream);
|
||||
int32_t mz_stream_os_error(void *stream);
|
||||
|
||||
void *mz_stream_os_create(void);
|
||||
void mz_stream_os_delete(void **stream);
|
||||
|
||||
void *mz_stream_os_get_interface(void);
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
/* mz_strm_zlib.h -- Stream for zlib inflate/deflate
|
||||
part of the minizip-ng project
|
||||
|
||||
Copyright (C) Nathan Moinvaziri
|
||||
https://github.com/zlib-ng/minizip-ng
|
||||
|
||||
This program is distributed under the terms of the same license as zlib.
|
||||
See the accompanying LICENSE file for the full text of the license.
|
||||
*/
|
||||
|
||||
#ifndef MZ_STREAM_ZLIB_H
|
||||
#define MZ_STREAM_ZLIB_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
int32_t mz_stream_zlib_open(void *stream, const char *filename, int32_t mode);
|
||||
int32_t mz_stream_zlib_is_open(void *stream);
|
||||
int32_t mz_stream_zlib_read(void *stream, void *buf, int32_t size);
|
||||
int32_t mz_stream_zlib_write(void *stream, const void *buf, int32_t size);
|
||||
int64_t mz_stream_zlib_tell(void *stream);
|
||||
int32_t mz_stream_zlib_seek(void *stream, int64_t offset, int32_t origin);
|
||||
int32_t mz_stream_zlib_close(void *stream);
|
||||
int32_t mz_stream_zlib_error(void *stream);
|
||||
|
||||
int32_t mz_stream_zlib_get_prop_int64(void *stream, int32_t prop, int64_t *value);
|
||||
int32_t mz_stream_zlib_set_prop_int64(void *stream, int32_t prop, int64_t value);
|
||||
|
||||
void *mz_stream_zlib_create(void);
|
||||
void mz_stream_zlib_delete(void **stream);
|
||||
|
||||
void *mz_stream_zlib_get_interface(void);
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
-257
@@ -1,257 +0,0 @@
|
||||
/* mz_zip.h -- Zip manipulation
|
||||
part of the minizip-ng project
|
||||
|
||||
Copyright (C) Nathan Moinvaziri
|
||||
https://github.com/zlib-ng/minizip-ng
|
||||
Copyright (C) 2009-2010 Mathias Svensson
|
||||
Modifications for Zip64 support
|
||||
http://result42.com
|
||||
Copyright (C) 1998-2010 Gilles Vollant
|
||||
https://www.winimage.com/zLibDll/minizip.html
|
||||
|
||||
This program is distributed under the terms of the same license as zlib.
|
||||
See the accompanying LICENSE file for the full text of the license.
|
||||
*/
|
||||
|
||||
#ifndef MZ_ZIP_H
|
||||
#define MZ_ZIP_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
typedef struct mz_zip_file_s {
|
||||
uint16_t version_madeby; /* version made by */
|
||||
uint16_t version_needed; /* version needed to extract */
|
||||
uint16_t flag; /* general purpose bit flag */
|
||||
uint16_t compression_method; /* compression method */
|
||||
time_t modified_date; /* last modified date in unix time */
|
||||
time_t accessed_date; /* last accessed date in unix time */
|
||||
time_t creation_date; /* creation date in unix time */
|
||||
uint32_t crc; /* crc-32 */
|
||||
int64_t compressed_size; /* compressed size */
|
||||
int64_t uncompressed_size; /* uncompressed size */
|
||||
uint16_t filename_size; /* filename length */
|
||||
uint16_t extrafield_size; /* extra field length */
|
||||
uint16_t comment_size; /* file comment length */
|
||||
uint32_t disk_number; /* disk number start */
|
||||
int64_t disk_offset; /* relative offset of local header */
|
||||
uint16_t internal_fa; /* internal file attributes */
|
||||
uint32_t external_fa; /* external file attributes */
|
||||
const char *filename; /* filename utf8 null-terminated string */
|
||||
const uint8_t *extrafield; /* extrafield data */
|
||||
const char *comment; /* comment utf8 null-terminated string */
|
||||
const char *linkname; /* sym-link filename utf8 null-terminated string */
|
||||
uint16_t zip64; /* zip64 extension mode */
|
||||
uint16_t aes_version; /* winzip aes extension if not 0 */
|
||||
uint8_t aes_strength; /* winzip aes encryption strength */
|
||||
uint16_t pk_verify; /* pkware encryption verifier */
|
||||
} mz_zip_file, mz_zip_entry;
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
typedef int32_t (*mz_zip_locate_entry_cb)(void *handle, void *userdata, mz_zip_file *file_info);
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
void *mz_zip_create(void);
|
||||
/* Create zip instance for opening */
|
||||
|
||||
void mz_zip_delete(void **handle);
|
||||
/* Delete zip object */
|
||||
|
||||
int32_t mz_zip_open(void *handle, void *stream, int32_t mode);
|
||||
/* Create a zip file, no delete file in zip functionality */
|
||||
|
||||
int32_t mz_zip_close(void *handle);
|
||||
/* Close the zip file */
|
||||
|
||||
int32_t mz_zip_get_comment(void *handle, const char **comment);
|
||||
/* Get a pointer to the global comment */
|
||||
|
||||
int32_t mz_zip_set_comment(void *handle, const char *comment);
|
||||
/* Sets the global comment used for writing zip file */
|
||||
|
||||
int32_t mz_zip_get_version_madeby(void *handle, uint16_t *version_madeby);
|
||||
/* Get the version made by */
|
||||
|
||||
int32_t mz_zip_set_version_madeby(void *handle, uint16_t version_madeby);
|
||||
/* Sets the version made by used for writing zip file */
|
||||
|
||||
int32_t mz_zip_set_recover(void *handle, uint8_t recover);
|
||||
/* Sets the ability to recover the central dir by reading local file headers */
|
||||
|
||||
int32_t mz_zip_set_data_descriptor(void *handle, uint8_t data_descriptor);
|
||||
/* Sets the use of data descriptor flag when writing zip entries */
|
||||
|
||||
int32_t mz_zip_get_stream(void *handle, void **stream);
|
||||
/* Get a pointer to the stream used to open */
|
||||
|
||||
int32_t mz_zip_set_cd_stream(void *handle, int64_t cd_start_pos, void *cd_stream);
|
||||
/* Sets the stream to use for reading the central dir */
|
||||
|
||||
int32_t mz_zip_get_cd_mem_stream(void *handle, void **cd_mem_stream);
|
||||
/* Get a pointer to the stream used to store the central dir in memory */
|
||||
|
||||
int32_t mz_zip_set_number_entry(void *handle, uint64_t number_entry);
|
||||
/* Sets the total number of entries */
|
||||
|
||||
int32_t mz_zip_get_number_entry(void *handle, uint64_t *number_entry);
|
||||
/* Get the total number of entries */
|
||||
|
||||
int32_t mz_zip_set_disk_number_with_cd(void *handle, uint32_t disk_number_with_cd);
|
||||
/* Sets the disk number containing the central directory record */
|
||||
|
||||
int32_t mz_zip_get_disk_number_with_cd(void *handle, uint32_t *disk_number_with_cd);
|
||||
/* Get the disk number containing the central directory record */
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
int32_t mz_zip_entry_is_open(void *handle);
|
||||
/* Check to see if entry is open for read/write */
|
||||
|
||||
int32_t mz_zip_entry_read_open(void *handle, uint8_t raw, const char *password);
|
||||
/* Open for reading the current file in the zip file */
|
||||
|
||||
int32_t mz_zip_entry_read(void *handle, void *buf, int32_t len);
|
||||
/* Read bytes from the current file in the zip file */
|
||||
|
||||
int32_t mz_zip_entry_read_close(void *handle, uint32_t *crc32, int64_t *compressed_size, int64_t *uncompressed_size);
|
||||
/* Close the current file for reading and get data descriptor values */
|
||||
|
||||
int32_t mz_zip_entry_write_open(void *handle, const mz_zip_file *file_info, int16_t compress_level, uint8_t raw,
|
||||
const char *password);
|
||||
/* Open for writing the current file in the zip file */
|
||||
|
||||
int32_t mz_zip_entry_write(void *handle, const void *buf, int32_t len);
|
||||
/* Write bytes from the current file in the zip file */
|
||||
|
||||
int32_t mz_zip_entry_write_close(void *handle, uint32_t crc32, int64_t compressed_size, int64_t uncompressed_size);
|
||||
/* Close the current file for writing and set data descriptor values */
|
||||
|
||||
int32_t mz_zip_entry_seek_local_header(void *handle);
|
||||
/* Seeks to the local header for the entry */
|
||||
|
||||
int32_t mz_zip_entry_get_compress_stream(void *handle, void **compress_stream);
|
||||
/* Get a pointer to the compression stream used for the current entry */
|
||||
|
||||
int32_t mz_zip_entry_close_raw(void *handle, int64_t uncompressed_size, uint32_t crc32);
|
||||
/* Close the current file in the zip file where raw is compressed data */
|
||||
|
||||
int32_t mz_zip_entry_close(void *handle);
|
||||
/* Close the current file in the zip file */
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
int32_t mz_zip_entry_is_dir(void *handle);
|
||||
/* Checks to see if the entry is a directory */
|
||||
|
||||
int32_t mz_zip_entry_is_symlink(void *handle);
|
||||
/* Checks to see if the entry is a symbolic link */
|
||||
|
||||
int32_t mz_zip_entry_get_info(void *handle, mz_zip_file **file_info);
|
||||
/* Get info about the current file, only valid while current entry is open */
|
||||
|
||||
int32_t mz_zip_entry_get_local_info(void *handle, mz_zip_file **local_file_info);
|
||||
/* Get local info about the current file, only valid while current entry is being read */
|
||||
|
||||
int32_t mz_zip_entry_set_extrafield(void *handle, const uint8_t *extrafield, uint16_t extrafield_size);
|
||||
/* Sets or updates the extra field for the entry to be used before writing cd */
|
||||
|
||||
int64_t mz_zip_get_entry(void *handle);
|
||||
/* Return offset of the current entry in the zip file */
|
||||
|
||||
int32_t mz_zip_goto_entry(void *handle, int64_t cd_pos);
|
||||
/* Go to specified entry in the zip file */
|
||||
|
||||
int32_t mz_zip_goto_first_entry(void *handle);
|
||||
/* Go to the first entry in the zip file */
|
||||
|
||||
int32_t mz_zip_goto_next_entry(void *handle);
|
||||
/* Go to the next entry in the zip file or MZ_END_OF_LIST if reaching the end */
|
||||
|
||||
int32_t mz_zip_locate_entry(void *handle, const char *filename, uint8_t ignore_case);
|
||||
/* Locate the file with the specified name in the zip file or MZ_END_LIST if not found */
|
||||
|
||||
int32_t mz_zip_locate_first_entry(void *handle, void *userdata, mz_zip_locate_entry_cb cb);
|
||||
/* Locate the first matching entry based on a match callback */
|
||||
|
||||
int32_t mz_zip_locate_next_entry(void *handle, void *userdata, mz_zip_locate_entry_cb cb);
|
||||
/* Locate the next matching entry based on a match callback */
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
int32_t mz_zip_attrib_is_dir(uint32_t attrib, int32_t version_madeby);
|
||||
/* Checks to see if the attribute is a directory based on platform */
|
||||
|
||||
int32_t mz_zip_attrib_is_symlink(uint32_t attrib, int32_t version_madeby);
|
||||
/* Checks to see if the attribute is a symbolic link based on platform */
|
||||
|
||||
int32_t mz_zip_attrib_convert(uint8_t src_sys, uint32_t src_attrib, uint8_t target_sys, uint32_t *target_attrib);
|
||||
/* Converts file attributes from one host system to another */
|
||||
|
||||
int32_t mz_zip_attrib_posix_to_win32(uint32_t posix_attrib, uint32_t *win32_attrib);
|
||||
/* Converts posix file attributes to win32 file attributes */
|
||||
|
||||
int32_t mz_zip_attrib_win32_to_posix(uint32_t win32_attrib, uint32_t *posix_attrib);
|
||||
/* Converts win32 file attributes to posix file attributes */
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
int32_t mz_zip_extrafield_find(void *stream, uint16_t type, int32_t max_seek, uint16_t *length);
|
||||
/* Seeks to extra field by its type and returns its length */
|
||||
|
||||
int32_t mz_zip_extrafield_contains(const uint8_t *extrafield, int32_t extrafield_size, uint16_t type, uint16_t *length);
|
||||
/* Gets whether an extrafield exists and its size */
|
||||
|
||||
int32_t mz_zip_extrafield_read(void *stream, uint16_t *type, uint16_t *length);
|
||||
/* Reads an extrafield header from a stream */
|
||||
|
||||
int32_t mz_zip_extrafield_write(void *stream, uint16_t type, uint16_t length);
|
||||
/* Writes an extrafield header to a stream */
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
int32_t mz_zip_dosdate_to_tm(uint64_t dos_date, struct tm *ptm);
|
||||
/* Convert dos date/time format to struct tm */
|
||||
|
||||
time_t mz_zip_dosdate_to_time_t(uint64_t dos_date);
|
||||
/* Convert dos date/time format to time_t */
|
||||
|
||||
time_t mz_zip_tm_to_time_t(struct tm *ptm);
|
||||
/* Convert year-1900 indexed time struct to time_t */
|
||||
|
||||
int32_t mz_zip_time_t_to_tm(time_t unix_time, struct tm *ptm);
|
||||
/* Convert time_t to time struct */
|
||||
|
||||
uint32_t mz_zip_time_t_to_dos_date(time_t unix_time);
|
||||
/* Convert time_t to dos date/time format */
|
||||
|
||||
uint32_t mz_zip_tm_to_dosdate(const struct tm *ptm);
|
||||
/* Convert struct tm to dos date/time format */
|
||||
|
||||
int32_t mz_zip_ntfs_to_unix_time(uint64_t ntfs_time, time_t *unix_time);
|
||||
/* Convert ntfs time to unix time */
|
||||
|
||||
int32_t mz_zip_unix_to_ntfs_time(time_t unix_time, uint64_t *ntfs_time);
|
||||
/* Convert unix time to ntfs time */
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
int32_t mz_zip_path_compare(const char *path1, const char *path2, uint8_t ignore_case);
|
||||
/* Compare two paths without regard to slashes */
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
const char *mz_zip_get_compression_method_string(int32_t compression_method);
|
||||
/* Gets a string representing the compression method */
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* _ZIP_H */
|
||||
-182
@@ -1,182 +0,0 @@
|
||||
/* mz_crypt.c -- Crypto/hash functions
|
||||
part of the minizip-ng project
|
||||
|
||||
Copyright (C) Nathan Moinvaziri
|
||||
https://github.com/zlib-ng/minizip-ng
|
||||
|
||||
This program is distributed under the terms of the same license as zlib.
|
||||
See the accompanying LICENSE file for the full text of the license.
|
||||
*/
|
||||
|
||||
#include "mz.h"
|
||||
#include "mz_os.h"
|
||||
#include "mz_crypt.h"
|
||||
|
||||
#if defined(HAVE_ZLIB)
|
||||
# if !defined(ZLIB_COMPAT)
|
||||
# include "zlib-ng.h"
|
||||
# define ZLIB_PREFIX(x) zng_##x
|
||||
# else
|
||||
# include "zlib.h"
|
||||
# define ZLIB_PREFIX(x) x
|
||||
# endif
|
||||
#elif defined(HAVE_LZMA)
|
||||
# include "lzma.h"
|
||||
#endif
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
#if defined(MZ_ZIP_NO_CRYPTO)
|
||||
int32_t mz_crypt_rand(uint8_t *buf, int32_t size) {
|
||||
return mz_os_rand(buf, size);
|
||||
}
|
||||
#endif
|
||||
|
||||
uint32_t mz_crypt_crc32_update(uint32_t value, const uint8_t *buf, int32_t size) {
|
||||
#if defined(HAVE_ZLIB)
|
||||
# ifndef ZLIB_VERNUM
|
||||
/* HAVE_ZLIB but no ZLIB_VERNUM? */
|
||||
typedef uint32_t z_crc_t;
|
||||
# elif (ZLIB_VERNUM & 0xf != 0xf) && (ZLIB_VERNUM < 0x1270)
|
||||
/* Define z_crc_t in zlib 1.2.6 and less */
|
||||
typedef unsigned long z_crc_t;
|
||||
# elif (ZLIB_VERNUM & 0xf == 0xf) && (ZLIB_VERNUM < 0x12df)
|
||||
/* Define z_crc_t in zlib-ng 2.0.7 and less */
|
||||
typedef unsigned int z_crc_t;
|
||||
# endif
|
||||
return (uint32_t)ZLIB_PREFIX(crc32)((z_crc_t)value, buf, (uInt)size);
|
||||
#elif defined(HAVE_LZMA)
|
||||
return (uint32_t)lzma_crc32(buf, (size_t)size, (uint32_t)value);
|
||||
#else
|
||||
static uint32_t crc32_table[256] = {
|
||||
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832,
|
||||
0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
|
||||
0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a,
|
||||
0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
|
||||
0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3,
|
||||
0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
|
||||
0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab,
|
||||
0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
|
||||
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4,
|
||||
0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
|
||||
0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, 0xa3bc0074,
|
||||
0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
|
||||
0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525,
|
||||
0x206f85b3, 0xb966d409, 0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
|
||||
0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615,
|
||||
0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
|
||||
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76,
|
||||
0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
|
||||
0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 0xd80d2bda, 0xaf0a1b4c, 0x36034af6,
|
||||
0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
|
||||
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7,
|
||||
0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
|
||||
0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7,
|
||||
0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
|
||||
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278,
|
||||
0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
|
||||
0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330,
|
||||
0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
|
||||
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d};
|
||||
value = ~value;
|
||||
|
||||
while (size > 0) {
|
||||
value = (value >> 8) ^ crc32_table[(value ^ *buf) & 0xFF];
|
||||
|
||||
buf += 1;
|
||||
size -= 1;
|
||||
}
|
||||
|
||||
return ~value;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(HAVE_WZAES)
|
||||
int32_t mz_crypt_pbkdf2(const uint8_t *password, int32_t password_length, const uint8_t *salt, int32_t salt_length,
|
||||
uint32_t iteration_count, uint8_t *key, uint16_t key_length) {
|
||||
void *hmac1 = NULL;
|
||||
void *hmac2 = NULL;
|
||||
void *hmac3 = NULL;
|
||||
int32_t err = MZ_OK;
|
||||
uint16_t i = 0;
|
||||
uint32_t j = 0;
|
||||
uint16_t k = 0;
|
||||
uint16_t block_count = 0;
|
||||
uint8_t uu[MZ_HASH_SHA1_SIZE];
|
||||
uint8_t ux[MZ_HASH_SHA1_SIZE];
|
||||
|
||||
if (!password || !salt || !key)
|
||||
return MZ_PARAM_ERROR;
|
||||
|
||||
memset(key, 0, key_length);
|
||||
|
||||
hmac1 = mz_crypt_hmac_create();
|
||||
hmac2 = mz_crypt_hmac_create();
|
||||
hmac3 = mz_crypt_hmac_create();
|
||||
|
||||
if (!hmac1 || !hmac2 || !hmac3) {
|
||||
err = MZ_MEM_ERROR;
|
||||
goto pbkdf2_cleanup;
|
||||
}
|
||||
|
||||
mz_crypt_hmac_set_algorithm(hmac1, MZ_HASH_SHA1);
|
||||
mz_crypt_hmac_set_algorithm(hmac2, MZ_HASH_SHA1);
|
||||
mz_crypt_hmac_set_algorithm(hmac3, MZ_HASH_SHA1);
|
||||
|
||||
err = mz_crypt_hmac_init(hmac1, password, password_length);
|
||||
if (err == MZ_OK)
|
||||
err = mz_crypt_hmac_init(hmac2, password, password_length);
|
||||
if (err == MZ_OK)
|
||||
err = mz_crypt_hmac_update(hmac2, salt, salt_length);
|
||||
|
||||
block_count = 1 + ((uint16_t)key_length - 1) / MZ_HASH_SHA1_SIZE;
|
||||
|
||||
for (i = 0; (err == MZ_OK) && (i < block_count); i += 1) {
|
||||
memset(ux, 0, sizeof(ux));
|
||||
|
||||
err = mz_crypt_hmac_copy(hmac2, hmac3);
|
||||
if (err != MZ_OK)
|
||||
break;
|
||||
|
||||
uu[0] = (uint8_t)((i + 1) >> 24);
|
||||
uu[1] = (uint8_t)((i + 1) >> 16);
|
||||
uu[2] = (uint8_t)((i + 1) >> 8);
|
||||
uu[3] = (uint8_t)(i + 1);
|
||||
|
||||
for (j = 0, k = 4; j < iteration_count; j += 1) {
|
||||
err = mz_crypt_hmac_update(hmac3, uu, k);
|
||||
if (err == MZ_OK)
|
||||
err = mz_crypt_hmac_end(hmac3, uu, sizeof(uu));
|
||||
if (err != MZ_OK)
|
||||
break;
|
||||
|
||||
for (k = 0; k < MZ_HASH_SHA1_SIZE; k += 1)
|
||||
ux[k] ^= uu[k];
|
||||
|
||||
err = mz_crypt_hmac_copy(hmac1, hmac3);
|
||||
if (err != MZ_OK)
|
||||
break;
|
||||
}
|
||||
|
||||
if (err != MZ_OK)
|
||||
break;
|
||||
|
||||
j = 0;
|
||||
k = i * MZ_HASH_SHA1_SIZE;
|
||||
|
||||
while (j < MZ_HASH_SHA1_SIZE && k < key_length)
|
||||
key[k++] = ux[j++];
|
||||
}
|
||||
|
||||
pbkdf2_cleanup:
|
||||
/* hmac3 uses the same provider as hmac2, so it must be deleted
|
||||
before the context is destroyed. */
|
||||
mz_crypt_hmac_delete(&hmac3);
|
||||
mz_crypt_hmac_delete(&hmac1);
|
||||
mz_crypt_hmac_delete(&hmac2);
|
||||
|
||||
return err;
|
||||
}
|
||||
#endif
|
||||
|
||||
/***************************************************************************/
|
||||
Vendored
-495
@@ -1,495 +0,0 @@
|
||||
/* mz_os.c -- System functions
|
||||
part of the minizip-ng project
|
||||
|
||||
Copyright (C) Nathan Moinvaziri
|
||||
https://github.com/zlib-ng/minizip-ng
|
||||
Copyright (C) 1998-2010 Gilles Vollant
|
||||
https://www.winimage.com/zLibDll/minizip.html
|
||||
|
||||
This program is distributed under the terms of the same license as zlib.
|
||||
See the accompanying LICENSE file for the full text of the license.
|
||||
*/
|
||||
|
||||
#include "mz.h"
|
||||
#include "mz_crypt.h"
|
||||
#include "mz_os.h"
|
||||
#include "mz_strm.h"
|
||||
#include "mz_strm_os.h"
|
||||
|
||||
#include <ctype.h> /* tolower */
|
||||
#include <string.h>
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
int32_t mz_path_combine(char *path, const char *join, int32_t max_path) {
|
||||
int32_t path_len = 0;
|
||||
|
||||
if (!path || !join || !max_path)
|
||||
return MZ_PARAM_ERROR;
|
||||
|
||||
path_len = (int32_t)strlen(path);
|
||||
|
||||
if (path_len == 0) {
|
||||
strncpy(path, join, max_path - 1);
|
||||
path[max_path - 1] = 0;
|
||||
} else {
|
||||
mz_path_append_slash(path, max_path, MZ_PATH_SLASH_PLATFORM);
|
||||
path_len = (int32_t)strlen(path);
|
||||
if (max_path > path_len)
|
||||
strncat(path, join, max_path - path_len - 1);
|
||||
}
|
||||
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_path_append_slash(char *path, int32_t max_path, char slash) {
|
||||
int32_t path_len = (int32_t)strlen(path);
|
||||
if ((path_len + 2) >= max_path)
|
||||
return MZ_BUF_ERROR;
|
||||
if (!mz_os_is_dir_separator(path[path_len - 1])) {
|
||||
path[path_len] = slash;
|
||||
path[path_len + 1] = 0;
|
||||
}
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_path_remove_slash(char *path) {
|
||||
int32_t path_len = (int32_t)strlen(path);
|
||||
while (path_len > 0) {
|
||||
if (mz_os_is_dir_separator(path[path_len - 1]))
|
||||
path[path_len - 1] = 0;
|
||||
else
|
||||
break;
|
||||
|
||||
path_len -= 1;
|
||||
}
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_path_has_slash(const char *path) {
|
||||
int32_t path_len = (int32_t)strlen(path);
|
||||
if (path_len > 0 && !mz_os_is_dir_separator(path[path_len - 1]))
|
||||
return MZ_EXIST_ERROR;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_path_convert_slashes(char *path, char slash) {
|
||||
int32_t i = 0;
|
||||
|
||||
for (i = 0; i < (int32_t)strlen(path); i += 1) {
|
||||
if (mz_os_is_dir_separator(path[i]))
|
||||
path[i] = slash;
|
||||
}
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_path_compare_wc(const char *path, const char *wildcard, uint8_t ignore_case) {
|
||||
while (*path != 0) {
|
||||
switch (*wildcard) {
|
||||
case '*':
|
||||
|
||||
if (*(wildcard + 1) == 0)
|
||||
return MZ_OK;
|
||||
|
||||
while (*path != 0) {
|
||||
if (mz_path_compare_wc(path, (wildcard + 1), ignore_case) == MZ_OK)
|
||||
return MZ_OK;
|
||||
|
||||
path += 1;
|
||||
}
|
||||
|
||||
return MZ_EXIST_ERROR;
|
||||
|
||||
default:
|
||||
/* Ignore differences in path slashes on platforms */
|
||||
if ((*path == '\\' && *wildcard == '/') || (*path == '/' && *wildcard == '\\'))
|
||||
break;
|
||||
|
||||
if (ignore_case) {
|
||||
if (tolower(*path) != tolower(*wildcard))
|
||||
return MZ_EXIST_ERROR;
|
||||
} else {
|
||||
if (*path != *wildcard)
|
||||
return MZ_EXIST_ERROR;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
path += 1;
|
||||
wildcard += 1;
|
||||
}
|
||||
|
||||
if ((*wildcard != 0) && (*wildcard != '*'))
|
||||
return MZ_EXIST_ERROR;
|
||||
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_path_resolve(const char *path, char *output, int32_t max_output) {
|
||||
const char *source = path;
|
||||
const char *check = output;
|
||||
char *target = output;
|
||||
|
||||
if (max_output <= 0)
|
||||
return MZ_PARAM_ERROR;
|
||||
|
||||
while (*source != 0 && max_output > 1) {
|
||||
check = source;
|
||||
if (mz_os_is_dir_separator(*check))
|
||||
check += 1;
|
||||
|
||||
if (source == path || target == output || check != source) {
|
||||
/* Skip double paths */
|
||||
if (mz_os_is_dir_separator(*check)) {
|
||||
source += 1;
|
||||
continue;
|
||||
}
|
||||
if (*check == '.') {
|
||||
check += 1;
|
||||
|
||||
/* Remove . if at end of string and not at the beginning */
|
||||
if (*check == 0 && source != path && target != output) {
|
||||
/* Copy last slash */
|
||||
*target = *source;
|
||||
target += 1;
|
||||
max_output -= 1;
|
||||
source += (check - source);
|
||||
continue;
|
||||
}
|
||||
/* Remove . if not at end of string */
|
||||
else if (mz_os_is_dir_separator(*check)) {
|
||||
source += (check - source);
|
||||
/* Skip slash if at beginning of string */
|
||||
if (target == output && *source != 0)
|
||||
source += 1;
|
||||
continue;
|
||||
}
|
||||
/* Go to parent directory .. */
|
||||
else if (*check == '.') {
|
||||
check += 1;
|
||||
if (*check == 0 || mz_os_is_dir_separator(*check)) {
|
||||
source += (check - source);
|
||||
|
||||
/* Search backwards for previous slash or the start of the output string */
|
||||
if (target != output) {
|
||||
target -= 1;
|
||||
do {
|
||||
if (target == output || mz_os_is_dir_separator(*target))
|
||||
break;
|
||||
|
||||
target -= 1;
|
||||
max_output += 1;
|
||||
} while (target > output);
|
||||
}
|
||||
|
||||
if ((target == output) && *source != 0)
|
||||
source += 1;
|
||||
if (mz_os_is_dir_separator(*target) && *source == 0)
|
||||
target += 1;
|
||||
|
||||
*target = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*target = *source;
|
||||
|
||||
source += 1;
|
||||
target += 1;
|
||||
max_output -= 1;
|
||||
}
|
||||
|
||||
*target = 0;
|
||||
|
||||
if (*path == 0)
|
||||
return MZ_INTERNAL_ERROR;
|
||||
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_path_remove_filename(char *path) {
|
||||
char *path_ptr = NULL;
|
||||
|
||||
if (!path)
|
||||
return MZ_PARAM_ERROR;
|
||||
|
||||
path_ptr = path + strlen(path) - 1;
|
||||
|
||||
while (path_ptr > path) {
|
||||
if (mz_os_is_dir_separator(*path_ptr)) {
|
||||
*path_ptr = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
path_ptr -= 1;
|
||||
}
|
||||
|
||||
if (path_ptr == path)
|
||||
*path_ptr = 0;
|
||||
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_path_remove_extension(char *path) {
|
||||
char *path_ptr = NULL;
|
||||
|
||||
if (!path)
|
||||
return MZ_PARAM_ERROR;
|
||||
|
||||
path_ptr = path + strlen(path) - 1;
|
||||
|
||||
while (path_ptr > path) {
|
||||
if (mz_os_is_dir_separator(*path_ptr))
|
||||
break;
|
||||
if (*path_ptr == '.') {
|
||||
*path_ptr = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
path_ptr -= 1;
|
||||
}
|
||||
|
||||
if (path_ptr == path)
|
||||
*path_ptr = 0;
|
||||
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_path_get_filename(const char *path, const char **filename) {
|
||||
const char *match = NULL;
|
||||
|
||||
if (!path || !filename)
|
||||
return MZ_PARAM_ERROR;
|
||||
|
||||
*filename = NULL;
|
||||
|
||||
for (match = path; *match != 0; match += 1) {
|
||||
if (mz_os_is_dir_separator(*match))
|
||||
*filename = match + 1;
|
||||
}
|
||||
|
||||
if (!*filename)
|
||||
return MZ_EXIST_ERROR;
|
||||
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_path_is_symlink_target_safe(const char *link_path, const char *target, const char *base_path) {
|
||||
char *combined = NULL;
|
||||
char *resolved = NULL;
|
||||
size_t max_path = 1024;
|
||||
size_t base_len = 0;
|
||||
size_t parent_len = 0;
|
||||
int32_t err = MZ_OK;
|
||||
|
||||
if (!link_path || !target || !base_path)
|
||||
return MZ_PARAM_ERROR;
|
||||
|
||||
/* Absolute symlink targets are not allowed */
|
||||
if (mz_os_is_dir_separator(target[0]))
|
||||
return MZ_EXIST_ERROR;
|
||||
|
||||
base_len = strlen(base_path);
|
||||
|
||||
/* Remove trailing slash from base_path for comparison */
|
||||
while (base_len > 0 && mz_os_is_dir_separator(base_path[base_len - 1]))
|
||||
base_len--;
|
||||
|
||||
combined = (char *)calloc(1, max_path);
|
||||
resolved = (char *)calloc(1, max_path);
|
||||
|
||||
if (!combined || !resolved) {
|
||||
err = MZ_MEM_ERROR;
|
||||
goto target_cleanup;
|
||||
}
|
||||
|
||||
/* Find parent directory length by scanning backwards past filename and trailing slashes */
|
||||
parent_len = strlen(link_path);
|
||||
while (parent_len > 0 && !mz_os_is_dir_separator(link_path[parent_len - 1]))
|
||||
parent_len--;
|
||||
while (parent_len > 0 && mz_os_is_dir_separator(link_path[parent_len - 1]))
|
||||
parent_len--;
|
||||
|
||||
/* Combine parent + target */
|
||||
combined[0] = 0;
|
||||
if (parent_len > 0) {
|
||||
strncpy(combined, link_path, parent_len);
|
||||
combined[parent_len] = 0;
|
||||
mz_path_append_slash(combined, (int32_t)max_path, MZ_PATH_SLASH_PLATFORM);
|
||||
}
|
||||
strncat(combined, target, max_path - strlen(combined) - 1);
|
||||
|
||||
/* Resolve the combined path to eliminate .. */
|
||||
if (mz_path_resolve(combined, resolved, (int32_t)max_path) != MZ_OK) {
|
||||
err = MZ_EXIST_ERROR;
|
||||
goto target_cleanup;
|
||||
}
|
||||
|
||||
/* Check that resolved path stays within base_path */
|
||||
if (strlen(resolved) < base_len || strncmp(resolved, base_path, base_len) != 0 ||
|
||||
(resolved[base_len] != 0 && !mz_os_is_dir_separator(resolved[base_len])))
|
||||
err = MZ_EXIST_ERROR;
|
||||
|
||||
target_cleanup:
|
||||
free(combined);
|
||||
free(resolved);
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
int32_t mz_dir_has_unsafe_symlink(const char *path, const char *base_path) {
|
||||
char *check_path = NULL;
|
||||
char *symlink_target = NULL;
|
||||
size_t path_len = 0;
|
||||
size_t base_len = 0;
|
||||
size_t max_path = 1024;
|
||||
size_t pos = 0;
|
||||
size_t cmp_len = 0;
|
||||
int32_t err = MZ_OK;
|
||||
|
||||
if (!path || *path == 0 || !base_path)
|
||||
return MZ_PARAM_ERROR;
|
||||
|
||||
path_len = strlen(path);
|
||||
base_len = strlen(base_path);
|
||||
|
||||
/* Remove trailing slash from base_path for comparison */
|
||||
while (base_len > 0 && mz_os_is_dir_separator(base_path[base_len - 1]))
|
||||
base_len--;
|
||||
|
||||
check_path = (char *)calloc(1, path_len + 1);
|
||||
if (!check_path)
|
||||
return MZ_MEM_ERROR;
|
||||
|
||||
/* Walk through each path component */
|
||||
while (err == MZ_OK && pos < path_len) {
|
||||
/* Copy separator if present */
|
||||
if (mz_os_is_dir_separator(path[pos])) {
|
||||
check_path[pos] = path[pos];
|
||||
pos++;
|
||||
}
|
||||
|
||||
/* Copy next path component */
|
||||
while (pos < path_len && !mz_os_is_dir_separator(path[pos])) {
|
||||
check_path[pos] = path[pos];
|
||||
pos++;
|
||||
}
|
||||
check_path[pos] = 0;
|
||||
|
||||
/* Check if this existing path component is a symlink */
|
||||
if (mz_os_is_symlink(check_path) != MZ_OK)
|
||||
continue;
|
||||
|
||||
/* Skip components at or above the base dir. */
|
||||
cmp_len = pos;
|
||||
if (mz_path_has_slash(check_path) == MZ_OK)
|
||||
cmp_len--;
|
||||
if (cmp_len <= base_len && strncmp(check_path, base_path, cmp_len) == 0) {
|
||||
/* Verify that the prefix match is on a directory boundary. */
|
||||
if (cmp_len == base_len || mz_os_is_dir_separator(base_path[cmp_len]))
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Allocate symlink target buffer on first use */
|
||||
if (!symlink_target) {
|
||||
symlink_target = (char *)calloc(1, max_path);
|
||||
if (!symlink_target) {
|
||||
err = MZ_MEM_ERROR;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mz_os_read_symlink(check_path, symlink_target, max_path) != MZ_OK) {
|
||||
err = MZ_EXIST_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
/* Reject the component if its symlink target escapes the base path */
|
||||
err = mz_path_is_symlink_target_safe(check_path, symlink_target, base_path);
|
||||
if (err != MZ_OK)
|
||||
break;
|
||||
}
|
||||
|
||||
free(check_path);
|
||||
free(symlink_target);
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
int32_t mz_dir_make(const char *path) {
|
||||
int32_t err = MZ_OK;
|
||||
char *current_dir = NULL;
|
||||
char *match = NULL;
|
||||
char hold = 0;
|
||||
|
||||
if (!*path)
|
||||
return MZ_OK;
|
||||
|
||||
current_dir = strdup(path);
|
||||
if (!current_dir)
|
||||
return MZ_MEM_ERROR;
|
||||
|
||||
mz_path_remove_slash(current_dir);
|
||||
|
||||
err = mz_os_make_dir(current_dir);
|
||||
if (err != MZ_OK) {
|
||||
match = current_dir + 1;
|
||||
while (1) {
|
||||
while (*match != 0 && !mz_os_is_dir_separator(*match))
|
||||
match += 1;
|
||||
hold = *match;
|
||||
*match = 0;
|
||||
|
||||
err = mz_os_make_dir(current_dir);
|
||||
if (err != MZ_OK)
|
||||
break;
|
||||
if (hold == 0)
|
||||
break;
|
||||
|
||||
*match = hold;
|
||||
match += 1;
|
||||
}
|
||||
}
|
||||
|
||||
free(current_dir);
|
||||
return err;
|
||||
}
|
||||
|
||||
int32_t mz_file_get_crc(const char *path, uint32_t *result_crc) {
|
||||
void *stream = NULL;
|
||||
uint32_t crc32 = 0;
|
||||
int32_t read = 0;
|
||||
int32_t err = MZ_OK;
|
||||
uint8_t buf[16384];
|
||||
|
||||
stream = mz_stream_os_create();
|
||||
if (!stream)
|
||||
return MZ_MEM_ERROR;
|
||||
|
||||
err = mz_stream_os_open(stream, path, MZ_OPEN_MODE_READ);
|
||||
if (err == MZ_OK) {
|
||||
do {
|
||||
read = mz_stream_os_read(stream, buf, sizeof(buf));
|
||||
|
||||
if (read < 0) {
|
||||
err = read;
|
||||
break;
|
||||
}
|
||||
|
||||
crc32 = mz_crypt_crc32_update(crc32, buf, read);
|
||||
} while ((err == MZ_OK) && (read > 0));
|
||||
|
||||
mz_stream_os_close(stream);
|
||||
}
|
||||
|
||||
*result_crc = crc32;
|
||||
|
||||
mz_stream_os_delete(&stream);
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
/***************************************************************************/
|
||||
-445
@@ -1,445 +0,0 @@
|
||||
/* mz_os_posix.c -- System functions for posix
|
||||
part of the minizip-ng project
|
||||
|
||||
Copyright (C) Nathan Moinvaziri
|
||||
https://github.com/zlib-ng/minizip-ng
|
||||
|
||||
This program is distributed under the terms of the same license as zlib.
|
||||
See the accompanying LICENSE file for the full text of the license.
|
||||
*/
|
||||
|
||||
#include "mz.h"
|
||||
#include "mz_config.h"
|
||||
#include "mz_strm.h"
|
||||
#include "mz_os.h"
|
||||
|
||||
#include <stdio.h> /* rename */
|
||||
#include <errno.h>
|
||||
#if defined(HAVE_ICONV)
|
||||
# include <iconv.h>
|
||||
#endif
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#ifndef _WIN32
|
||||
# include <utime.h>
|
||||
# include <unistd.h>
|
||||
#endif
|
||||
#if defined(__APPLE__)
|
||||
# include <mach/clock.h>
|
||||
# include <mach/mach.h>
|
||||
#endif
|
||||
|
||||
#if defined(HAVE_GETRANDOM)
|
||||
# include <sys/random.h>
|
||||
#endif
|
||||
#if defined(HAVE_LIBBSD)
|
||||
# include <stdlib.h> /* arc4random_buf */
|
||||
#endif
|
||||
|
||||
#ifndef MZ_PRESERVE_NATIVE_STRUCTURE
|
||||
# define MZ_PRESERVE_NATIVE_STRUCTURE 1
|
||||
#endif
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
#if defined(HAVE_ICONV)
|
||||
char *mz_os_utf8_string_create(const char *string, int32_t encoding) {
|
||||
iconv_t cd;
|
||||
/// up to CP2147483647
|
||||
char string_encoding[13];
|
||||
const char *from_encoding = NULL;
|
||||
size_t result = 0;
|
||||
size_t string_length = 0;
|
||||
size_t string_utf8_size = 0;
|
||||
char *string_utf8 = NULL;
|
||||
char *string_utf8_ptr = NULL;
|
||||
|
||||
if (!string || encoding <= 0)
|
||||
return NULL;
|
||||
|
||||
if (encoding == MZ_ENCODING_UTF8)
|
||||
from_encoding = "UTF-8";
|
||||
else {
|
||||
snprintf(string_encoding, sizeof(string_encoding), "CP%03" PRId32, encoding);
|
||||
from_encoding = string_encoding;
|
||||
}
|
||||
|
||||
cd = iconv_open("UTF-8", from_encoding);
|
||||
if (cd == (iconv_t)-1)
|
||||
return NULL;
|
||||
|
||||
string_length = strlen(string);
|
||||
string_utf8_size = string_length * 2;
|
||||
string_utf8 = (char *)calloc((int32_t)(string_utf8_size + 1), sizeof(char));
|
||||
string_utf8_ptr = string_utf8;
|
||||
|
||||
if (string_utf8) {
|
||||
result = iconv(cd, (char **)&string, &string_length, (char **)&string_utf8_ptr, &string_utf8_size);
|
||||
}
|
||||
|
||||
iconv_close(cd);
|
||||
|
||||
if (result == (size_t)-1) {
|
||||
free(string_utf8);
|
||||
string_utf8 = NULL;
|
||||
}
|
||||
|
||||
return string_utf8;
|
||||
}
|
||||
#else
|
||||
char *mz_os_utf8_string_create(const char *string, int32_t encoding) {
|
||||
return strdup(string);
|
||||
}
|
||||
#endif
|
||||
|
||||
void mz_os_utf8_string_delete(char **string) {
|
||||
if (string) {
|
||||
free(*string);
|
||||
*string = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
#if defined(HAVE_GETRANDOM)
|
||||
int32_t mz_os_rand(uint8_t *buf, int32_t size) {
|
||||
int32_t left = size;
|
||||
int32_t written = 0;
|
||||
|
||||
while (left > 0) {
|
||||
written = getrandom(buf, left, 0);
|
||||
if (written < 0)
|
||||
return MZ_INTERNAL_ERROR;
|
||||
|
||||
buf += written;
|
||||
left -= written;
|
||||
}
|
||||
return size - left;
|
||||
}
|
||||
#elif defined(HAVE_ARC4RANDOM_BUF)
|
||||
int32_t mz_os_rand(uint8_t *buf, int32_t size) {
|
||||
if (size < 0)
|
||||
return 0;
|
||||
arc4random_buf(buf, (uint32_t)size);
|
||||
return size;
|
||||
}
|
||||
#elif defined(HAVE_ARC4RANDOM)
|
||||
int32_t mz_os_rand(uint8_t *buf, int32_t size) {
|
||||
int32_t left = size;
|
||||
for (; left > 2; left -= 3, buf += 3) {
|
||||
uint32_t val = arc4random();
|
||||
|
||||
buf[0] = (val) & 0xFF;
|
||||
buf[1] = (val >> 8) & 0xFF;
|
||||
buf[2] = (val >> 16) & 0xFF;
|
||||
}
|
||||
for (; left > 0; left--, buf++) {
|
||||
*buf = arc4random() & 0xFF;
|
||||
}
|
||||
return size - left;
|
||||
}
|
||||
#else
|
||||
int32_t mz_os_rand(uint8_t *buf, int32_t size) {
|
||||
static unsigned calls = 0;
|
||||
int32_t i = 0;
|
||||
|
||||
/* Ensure different random header each time */
|
||||
if (++calls == 1) {
|
||||
# define PI_SEED 3141592654UL
|
||||
srand((unsigned)(time(NULL) ^ PI_SEED));
|
||||
}
|
||||
|
||||
while (i < size)
|
||||
buf[i++] = (rand() >> 7) & 0xff;
|
||||
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
|
||||
int32_t mz_os_rename(const char *source_path, const char *target_path) {
|
||||
if (rename(source_path, target_path) == -1)
|
||||
return MZ_EXIST_ERROR;
|
||||
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_os_path_same_fs(const char *path_a, const char *path_b) {
|
||||
struct stat sa, sb;
|
||||
if (!path_a || !path_b)
|
||||
return MZ_PARAM_ERROR;
|
||||
if (stat(path_a, &sa) != 0 || stat(path_b, &sb) != 0)
|
||||
return MZ_EXIST_ERROR;
|
||||
return (sa.st_dev == sb.st_dev) ? MZ_OK : MZ_EXIST_ERROR;
|
||||
}
|
||||
|
||||
int32_t mz_os_unlink(const char *path) {
|
||||
if (unlink(path) == -1)
|
||||
return MZ_EXIST_ERROR;
|
||||
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_os_file_exists(const char *path) {
|
||||
struct stat path_stat;
|
||||
|
||||
memset(&path_stat, 0, sizeof(path_stat));
|
||||
if (stat(path, &path_stat) == 0)
|
||||
return MZ_OK;
|
||||
return MZ_EXIST_ERROR;
|
||||
}
|
||||
|
||||
int64_t mz_os_get_file_size(const char *path) {
|
||||
struct stat path_stat;
|
||||
|
||||
memset(&path_stat, 0, sizeof(path_stat));
|
||||
if (stat(path, &path_stat) == 0) {
|
||||
/* Stat returns size taken up by directory entry, so return 0 */
|
||||
if (S_ISDIR(path_stat.st_mode))
|
||||
return 0;
|
||||
|
||||
return path_stat.st_size;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t mz_os_get_file_date(const char *path, time_t *modified_date, time_t *accessed_date, time_t *creation_date) {
|
||||
struct stat path_stat;
|
||||
char *name = NULL;
|
||||
int32_t err = MZ_INTERNAL_ERROR;
|
||||
|
||||
memset(&path_stat, 0, sizeof(path_stat));
|
||||
|
||||
if (strcmp(path, "-") != 0) {
|
||||
/* Not all systems allow stat'ing a file with / appended */
|
||||
name = strdup(path);
|
||||
mz_path_remove_slash(name);
|
||||
|
||||
if (stat(name, &path_stat) == 0) {
|
||||
if (modified_date)
|
||||
*modified_date = path_stat.st_mtime;
|
||||
if (accessed_date)
|
||||
*accessed_date = path_stat.st_atime;
|
||||
/* Creation date not supported */
|
||||
if (creation_date)
|
||||
*creation_date = 0;
|
||||
|
||||
err = MZ_OK;
|
||||
}
|
||||
|
||||
free(name);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
int32_t mz_os_set_file_date(const char *path, time_t modified_date, time_t accessed_date, time_t creation_date) {
|
||||
struct utimbuf ut;
|
||||
|
||||
ut.actime = accessed_date;
|
||||
ut.modtime = modified_date;
|
||||
|
||||
/* Creation date not supported */
|
||||
MZ_UNUSED(creation_date);
|
||||
|
||||
if (utime(path, &ut) != 0)
|
||||
return MZ_INTERNAL_ERROR;
|
||||
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_os_get_file_attribs(const char *path, uint32_t *attributes) {
|
||||
struct stat path_stat;
|
||||
int32_t err = MZ_OK;
|
||||
|
||||
memset(&path_stat, 0, sizeof(path_stat));
|
||||
if (stat(path, &path_stat) == -1)
|
||||
err = MZ_INTERNAL_ERROR;
|
||||
*attributes = path_stat.st_mode;
|
||||
return err;
|
||||
}
|
||||
|
||||
int32_t mz_os_set_file_attribs(const char *path, uint32_t attributes) {
|
||||
int32_t err = MZ_OK;
|
||||
|
||||
if (chmod(path, (mode_t)attributes) == -1)
|
||||
err = MZ_INTERNAL_ERROR;
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
int32_t mz_os_make_dir(const char *path) {
|
||||
int32_t err = 0;
|
||||
|
||||
err = mkdir(path, 0755);
|
||||
|
||||
if (err != 0 && errno != EEXIST)
|
||||
return MZ_INTERNAL_ERROR;
|
||||
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
DIR *mz_os_open_dir(const char *path) {
|
||||
return opendir(path);
|
||||
}
|
||||
|
||||
struct dirent *mz_os_read_dir(DIR *dir) {
|
||||
if (!dir)
|
||||
return NULL;
|
||||
return readdir(dir);
|
||||
}
|
||||
|
||||
int32_t mz_os_close_dir(DIR *dir) {
|
||||
if (!dir)
|
||||
return MZ_PARAM_ERROR;
|
||||
if (closedir(dir) == -1)
|
||||
return MZ_INTERNAL_ERROR;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_os_is_dir_separator(char c) {
|
||||
#if MZ_PRESERVE_NATIVE_STRUCTURE
|
||||
// While not strictly adhering to 4.4.17.1,
|
||||
// this preserves UNIX filesystem structure.
|
||||
return c == '/';
|
||||
#else
|
||||
// While strictly adhering to 4.4.17.1,
|
||||
// this corrupts UNIX filesystem structure (a filename with a '\\' will become a folder + a file).
|
||||
return c == '\\' || c == '/';
|
||||
#endif
|
||||
}
|
||||
|
||||
int32_t mz_os_is_dir(const char *path) {
|
||||
struct stat path_stat;
|
||||
|
||||
memset(&path_stat, 0, sizeof(path_stat));
|
||||
stat(path, &path_stat);
|
||||
if (S_ISDIR(path_stat.st_mode))
|
||||
return MZ_OK;
|
||||
|
||||
return MZ_EXIST_ERROR;
|
||||
}
|
||||
|
||||
int32_t mz_os_is_symlink(const char *path) {
|
||||
struct stat path_stat;
|
||||
|
||||
memset(&path_stat, 0, sizeof(path_stat));
|
||||
lstat(path, &path_stat);
|
||||
if (S_ISLNK(path_stat.st_mode))
|
||||
return MZ_OK;
|
||||
|
||||
return MZ_EXIST_ERROR;
|
||||
}
|
||||
|
||||
int32_t mz_os_get_link_attribs(const char *path, uint32_t *attributes) {
|
||||
struct stat path_stat;
|
||||
int32_t err = MZ_OK;
|
||||
|
||||
memset(&path_stat, 0, sizeof(path_stat));
|
||||
if (lstat(path, &path_stat) == -1)
|
||||
err = MZ_INTERNAL_ERROR;
|
||||
*attributes = path_stat.st_mode;
|
||||
return err;
|
||||
}
|
||||
|
||||
int32_t mz_os_make_symlink(const char *path, const char *target_path) {
|
||||
#if !HAVE_SYMLINK
|
||||
return MZ_SUPPORT_ERROR;
|
||||
#else
|
||||
if (symlink(target_path, path) != 0)
|
||||
return MZ_INTERNAL_ERROR;
|
||||
return MZ_OK;
|
||||
#endif
|
||||
}
|
||||
|
||||
int32_t mz_os_read_symlink(const char *path, char *target_path, int32_t max_target_path) {
|
||||
#if !HAVE_READLINK
|
||||
return MZ_SUPPORT_ERROR;
|
||||
#else
|
||||
size_t length = 0;
|
||||
|
||||
length = (size_t)readlink(path, target_path, max_target_path - 1);
|
||||
if (length == (size_t)-1)
|
||||
return MZ_EXIST_ERROR;
|
||||
if (length >= (size_t)(max_target_path - 1))
|
||||
return MZ_BUF_ERROR;
|
||||
|
||||
target_path[length] = 0;
|
||||
return MZ_OK;
|
||||
#endif
|
||||
}
|
||||
|
||||
int32_t mz_os_get_temp_path(char *path, int32_t max_path, const char *prefix) {
|
||||
const char *tmp_dir = NULL;
|
||||
char *temp_path;
|
||||
int32_t result = 0;
|
||||
|
||||
if (!path || max_path <= 0)
|
||||
return MZ_PARAM_ERROR;
|
||||
|
||||
tmp_dir = getenv("TMPDIR");
|
||||
if (!tmp_dir)
|
||||
tmp_dir = getenv("TMP");
|
||||
if (!tmp_dir)
|
||||
tmp_dir = getenv("TEMP");
|
||||
if (!tmp_dir)
|
||||
tmp_dir = "/tmp";
|
||||
|
||||
/* Construct path for mkdtemp in the form <tmp_dir>/<prefix>XXXXXX */
|
||||
temp_path = (char *)calloc(max_path, sizeof(char));
|
||||
if (!temp_path)
|
||||
return MZ_MEM_ERROR;
|
||||
|
||||
/* mkdtemp replaces XXXXXX with unique characters */
|
||||
result = snprintf(temp_path, max_path, "%s/%sXXXXXX", tmp_dir, prefix ? prefix : "");
|
||||
if (result < 0 || result >= max_path) {
|
||||
free(temp_path);
|
||||
return MZ_BUF_ERROR;
|
||||
}
|
||||
|
||||
/* Create a temporary directory. */
|
||||
if (!mkdtemp(temp_path)) {
|
||||
free(temp_path);
|
||||
return MZ_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
/* Create a filename inside the temporary directory using current time */
|
||||
result = snprintf(path, max_path, "%s/%lux", temp_path, time(NULL));
|
||||
if (result < 0 || result >= max_path) {
|
||||
rmdir(temp_path);
|
||||
free(temp_path);
|
||||
return MZ_BUF_ERROR;
|
||||
}
|
||||
|
||||
free(temp_path);
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
uint64_t mz_os_ms_time(void) {
|
||||
struct timespec ts;
|
||||
|
||||
#if defined(__APPLE__)
|
||||
clock_serv_t cclock;
|
||||
mach_timespec_t mts;
|
||||
|
||||
host_get_clock_service(mach_host_self(), CALENDAR_CLOCK, &cclock);
|
||||
clock_get_time(cclock, &mts);
|
||||
mach_port_deallocate(mach_task_self(), cclock);
|
||||
|
||||
ts.tv_sec = mts.tv_sec;
|
||||
ts.tv_nsec = mts.tv_nsec;
|
||||
#elif !defined(_POSIX_MONOTONIC_CLOCK) || _POSIX_MONOTONIC_CLOCK < 0
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
#elif _POSIX_MONOTONIC_CLOCK > 0
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
#else
|
||||
if (sysconf(_SC_MONOTONIC_CLOCK) > 0)
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
else
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
#endif
|
||||
|
||||
return ((uint64_t)ts.tv_sec * 1000) + ((uint64_t)ts.tv_nsec / 1000000);
|
||||
}
|
||||
Vendored
-545
@@ -1,545 +0,0 @@
|
||||
/* mz_strm.c -- Stream interface
|
||||
part of the minizip-ng project
|
||||
|
||||
Copyright (C) Nathan Moinvaziri
|
||||
https://github.com/zlib-ng/minizip-ng
|
||||
|
||||
This program is distributed under the terms of the same license as zlib.
|
||||
See the accompanying LICENSE file for the full text of the license.
|
||||
*/
|
||||
|
||||
#include "mz.h"
|
||||
#include "mz_strm.h"
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
#define MZ_STREAM_FIND_SIZE (1024)
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
int32_t mz_stream_open(void *stream, const char *path, int32_t mode) {
|
||||
mz_stream *strm = (mz_stream *)stream;
|
||||
if (!strm || !strm->vtbl || !strm->vtbl->open)
|
||||
return MZ_STREAM_ERROR;
|
||||
return strm->vtbl->open(strm, path, mode);
|
||||
}
|
||||
|
||||
int32_t mz_stream_is_open(void *stream) {
|
||||
mz_stream *strm = (mz_stream *)stream;
|
||||
if (!strm || !strm->vtbl || !strm->vtbl->is_open)
|
||||
return MZ_STREAM_ERROR;
|
||||
return strm->vtbl->is_open(strm);
|
||||
}
|
||||
|
||||
int32_t mz_stream_read(void *stream, void *buf, int32_t size) {
|
||||
mz_stream *strm = (mz_stream *)stream;
|
||||
if (!strm || !strm->vtbl || !strm->vtbl->read)
|
||||
return MZ_PARAM_ERROR;
|
||||
if (mz_stream_is_open(strm) != MZ_OK)
|
||||
return MZ_STREAM_ERROR;
|
||||
return strm->vtbl->read(strm, buf, size);
|
||||
}
|
||||
|
||||
static int32_t mz_stream_read_value(void *stream, uint64_t *value, int32_t len) {
|
||||
uint8_t buf[8];
|
||||
int32_t n = 0;
|
||||
int32_t i = 0;
|
||||
|
||||
*value = 0;
|
||||
if (mz_stream_read(stream, buf, len) == len) {
|
||||
for (n = 0; n < len; n += 1, i += 8)
|
||||
*value += ((uint64_t)buf[n]) << i;
|
||||
} else if (mz_stream_error(stream))
|
||||
return MZ_STREAM_ERROR;
|
||||
else
|
||||
return MZ_END_OF_STREAM;
|
||||
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_stream_read_uint8(void *stream, uint8_t *value) {
|
||||
int32_t err = MZ_OK;
|
||||
uint64_t value64 = 0;
|
||||
|
||||
*value = 0;
|
||||
err = mz_stream_read_value(stream, &value64, sizeof(uint8_t));
|
||||
if (err == MZ_OK)
|
||||
*value = (uint8_t)value64;
|
||||
return err;
|
||||
}
|
||||
|
||||
int32_t mz_stream_read_uint16(void *stream, uint16_t *value) {
|
||||
int32_t err = MZ_OK;
|
||||
uint64_t value64 = 0;
|
||||
|
||||
*value = 0;
|
||||
err = mz_stream_read_value(stream, &value64, sizeof(uint16_t));
|
||||
if (err == MZ_OK)
|
||||
*value = (uint16_t)value64;
|
||||
return err;
|
||||
}
|
||||
|
||||
int32_t mz_stream_read_uint32(void *stream, uint32_t *value) {
|
||||
int32_t err = MZ_OK;
|
||||
uint64_t value64 = 0;
|
||||
|
||||
*value = 0;
|
||||
err = mz_stream_read_value(stream, &value64, sizeof(uint32_t));
|
||||
if (err == MZ_OK)
|
||||
*value = (uint32_t)value64;
|
||||
return err;
|
||||
}
|
||||
|
||||
int32_t mz_stream_read_int64(void *stream, int64_t *value) {
|
||||
return mz_stream_read_value(stream, (uint64_t *)value, sizeof(uint64_t));
|
||||
}
|
||||
|
||||
int32_t mz_stream_read_uint64(void *stream, uint64_t *value) {
|
||||
return mz_stream_read_value(stream, value, sizeof(uint64_t));
|
||||
}
|
||||
|
||||
int32_t mz_stream_write(void *stream, const void *buf, int32_t size) {
|
||||
mz_stream *strm = (mz_stream *)stream;
|
||||
if (size == 0)
|
||||
return size;
|
||||
if (!strm || !strm->vtbl || !strm->vtbl->write)
|
||||
return MZ_PARAM_ERROR;
|
||||
if (mz_stream_is_open(strm) != MZ_OK)
|
||||
return MZ_STREAM_ERROR;
|
||||
return strm->vtbl->write(strm, buf, size);
|
||||
}
|
||||
|
||||
static int32_t mz_stream_write_value(void *stream, uint64_t value, int32_t len) {
|
||||
mz_stream *strm = (mz_stream *)stream;
|
||||
uint8_t buf[8];
|
||||
int32_t n = 0;
|
||||
|
||||
if (!strm)
|
||||
return MZ_PARAM_ERROR;
|
||||
for (n = 0; n < len; n += 1) {
|
||||
buf[n] = (uint8_t)(value & 0xff);
|
||||
value >>= 8;
|
||||
}
|
||||
|
||||
if (value != 0) {
|
||||
/* Data overflow - hack for ZIP64 (X Roche) */
|
||||
for (n = 0; n < len; n += 1)
|
||||
buf[n] = 0xff;
|
||||
}
|
||||
|
||||
if (mz_stream_write(strm, buf, len) != len)
|
||||
return MZ_STREAM_ERROR;
|
||||
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_stream_write_uint8(void *stream, uint8_t value) {
|
||||
return mz_stream_write_value(stream, value, sizeof(uint8_t));
|
||||
}
|
||||
|
||||
int32_t mz_stream_write_uint16(void *stream, uint16_t value) {
|
||||
return mz_stream_write_value(stream, value, sizeof(uint16_t));
|
||||
}
|
||||
|
||||
int32_t mz_stream_write_uint32(void *stream, uint32_t value) {
|
||||
return mz_stream_write_value(stream, value, sizeof(uint32_t));
|
||||
}
|
||||
|
||||
int32_t mz_stream_write_int64(void *stream, int64_t value) {
|
||||
return mz_stream_write_value(stream, (uint64_t)value, sizeof(uint64_t));
|
||||
}
|
||||
|
||||
int32_t mz_stream_write_uint64(void *stream, uint64_t value) {
|
||||
return mz_stream_write_value(stream, value, sizeof(uint64_t));
|
||||
}
|
||||
|
||||
int32_t mz_stream_copy(void *target, void *source, int32_t len) {
|
||||
return mz_stream_copy_stream(target, NULL, source, NULL, len);
|
||||
}
|
||||
|
||||
int32_t mz_stream_copy_to_end(void *target, void *source) {
|
||||
return mz_stream_copy_stream_to_end(target, NULL, source, NULL);
|
||||
}
|
||||
|
||||
int32_t mz_stream_copy_stream(void *target, mz_stream_write_cb write_cb, void *source, mz_stream_read_cb read_cb,
|
||||
int32_t len) {
|
||||
uint8_t buf[16384];
|
||||
int32_t bytes_to_copy = 0;
|
||||
int32_t read = 0;
|
||||
int32_t written = 0;
|
||||
|
||||
if (!write_cb)
|
||||
write_cb = mz_stream_write;
|
||||
if (!read_cb)
|
||||
read_cb = mz_stream_read;
|
||||
|
||||
while (len > 0) {
|
||||
bytes_to_copy = len;
|
||||
if (bytes_to_copy > (int32_t)sizeof(buf))
|
||||
bytes_to_copy = sizeof(buf);
|
||||
read = read_cb(source, buf, bytes_to_copy);
|
||||
if (read <= 0)
|
||||
return MZ_STREAM_ERROR;
|
||||
written = write_cb(target, buf, read);
|
||||
if (written != read)
|
||||
return MZ_STREAM_ERROR;
|
||||
len -= read;
|
||||
}
|
||||
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_stream_copy_stream_to_end(void *target, mz_stream_write_cb write_cb, void *source,
|
||||
mz_stream_read_cb read_cb) {
|
||||
uint8_t buf[16384];
|
||||
int32_t read = 0;
|
||||
int32_t written = 0;
|
||||
|
||||
if (!write_cb)
|
||||
write_cb = mz_stream_write;
|
||||
if (!read_cb)
|
||||
read_cb = mz_stream_read;
|
||||
|
||||
read = read_cb(source, buf, sizeof(buf));
|
||||
while (read > 0) {
|
||||
written = write_cb(target, buf, read);
|
||||
if (written != read)
|
||||
return MZ_STREAM_ERROR;
|
||||
read = read_cb(source, buf, sizeof(buf));
|
||||
}
|
||||
|
||||
if (read < 0)
|
||||
return MZ_STREAM_ERROR;
|
||||
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int64_t mz_stream_tell(void *stream) {
|
||||
mz_stream *strm = (mz_stream *)stream;
|
||||
if (!strm || !strm->vtbl || !strm->vtbl->tell)
|
||||
return MZ_PARAM_ERROR;
|
||||
if (mz_stream_is_open(strm) != MZ_OK)
|
||||
return MZ_STREAM_ERROR;
|
||||
return strm->vtbl->tell(strm);
|
||||
}
|
||||
|
||||
int32_t mz_stream_seek(void *stream, int64_t offset, int32_t origin) {
|
||||
mz_stream *strm = (mz_stream *)stream;
|
||||
if (!strm || !strm->vtbl || !strm->vtbl->seek)
|
||||
return MZ_PARAM_ERROR;
|
||||
if (mz_stream_is_open(strm) != MZ_OK)
|
||||
return MZ_STREAM_ERROR;
|
||||
if (origin == MZ_SEEK_SET && offset < 0)
|
||||
return MZ_SEEK_ERROR;
|
||||
return strm->vtbl->seek(strm, offset, origin);
|
||||
}
|
||||
|
||||
int32_t mz_stream_find(void *stream, const void *find, int32_t find_size, int64_t max_seek, int64_t *position) {
|
||||
uint8_t buf[MZ_STREAM_FIND_SIZE];
|
||||
int32_t buf_pos = 0;
|
||||
int32_t read_size = sizeof(buf);
|
||||
int32_t read = 0;
|
||||
int64_t read_pos = 0;
|
||||
int64_t start_pos = 0;
|
||||
int64_t disk_pos = 0;
|
||||
int32_t i = 0;
|
||||
uint8_t first = 1;
|
||||
int32_t err = MZ_OK;
|
||||
|
||||
if (!stream || !find || !position)
|
||||
return MZ_PARAM_ERROR;
|
||||
if (find_size < 0 || find_size >= (int32_t)sizeof(buf))
|
||||
return MZ_PARAM_ERROR;
|
||||
|
||||
*position = -1;
|
||||
|
||||
start_pos = mz_stream_tell(stream);
|
||||
|
||||
while (read_pos < max_seek) {
|
||||
if (read_size > (int32_t)(max_seek - read_pos - buf_pos) &&
|
||||
(max_seek - read_pos - buf_pos) < (int64_t)sizeof(buf)) {
|
||||
read_size = (int32_t)(max_seek - read_pos - buf_pos);
|
||||
}
|
||||
|
||||
read = mz_stream_read(stream, buf + buf_pos, read_size);
|
||||
if ((read <= 0) || (read + buf_pos < find_size))
|
||||
break;
|
||||
|
||||
for (i = 0; i <= read + buf_pos - find_size; i += 1) {
|
||||
if (memcmp(&buf[i], find, find_size) != 0)
|
||||
continue;
|
||||
|
||||
disk_pos = mz_stream_tell(stream);
|
||||
|
||||
/* Seek to position on disk where the data was found */
|
||||
err = mz_stream_seek(stream, disk_pos - ((int64_t)read + buf_pos - i), MZ_SEEK_SET);
|
||||
if (err != MZ_OK)
|
||||
return MZ_EXIST_ERROR;
|
||||
|
||||
*position = start_pos + read_pos + i;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
if (first) {
|
||||
read -= find_size;
|
||||
read_size -= find_size;
|
||||
buf_pos = find_size;
|
||||
first = 0;
|
||||
}
|
||||
|
||||
memmove(buf, buf + read, find_size);
|
||||
read_pos += read;
|
||||
}
|
||||
|
||||
return MZ_EXIST_ERROR;
|
||||
}
|
||||
|
||||
int32_t mz_stream_find_reverse(void *stream, const void *find, int32_t find_size, int64_t max_seek, int64_t *position) {
|
||||
uint8_t buf[MZ_STREAM_FIND_SIZE];
|
||||
int32_t buf_pos = 0;
|
||||
int32_t read_size = MZ_STREAM_FIND_SIZE;
|
||||
int64_t read_pos = 0;
|
||||
int32_t read = 0;
|
||||
int64_t start_pos = 0;
|
||||
int64_t disk_pos = 0;
|
||||
uint8_t first = 1;
|
||||
int32_t i = 0;
|
||||
int32_t err = MZ_OK;
|
||||
|
||||
if (!stream || !find || !position)
|
||||
return MZ_PARAM_ERROR;
|
||||
if (find_size < 0 || find_size >= (int32_t)sizeof(buf))
|
||||
return MZ_PARAM_ERROR;
|
||||
|
||||
*position = -1;
|
||||
|
||||
start_pos = mz_stream_tell(stream);
|
||||
|
||||
while (read_pos < max_seek) {
|
||||
if (read_size > (int32_t)(max_seek - read_pos) && (max_seek - read_pos) < (int64_t)sizeof(buf))
|
||||
read_size = (int32_t)(max_seek - read_pos);
|
||||
|
||||
if (mz_stream_seek(stream, start_pos - (read_pos + read_size), MZ_SEEK_SET) != MZ_OK)
|
||||
break;
|
||||
read = mz_stream_read(stream, buf, read_size);
|
||||
if ((read <= 0) || (read + buf_pos < find_size))
|
||||
break;
|
||||
if (read + buf_pos < MZ_STREAM_FIND_SIZE)
|
||||
memmove(buf + MZ_STREAM_FIND_SIZE - (read + buf_pos), buf, read);
|
||||
|
||||
for (i = find_size; i <= (read + buf_pos); i += 1) {
|
||||
if (memcmp(&buf[MZ_STREAM_FIND_SIZE - i], find, find_size) != 0)
|
||||
continue;
|
||||
|
||||
disk_pos = mz_stream_tell(stream);
|
||||
|
||||
/* Seek to position on disk where the data was found */
|
||||
err = mz_stream_seek(stream, disk_pos + buf_pos - i, MZ_SEEK_SET);
|
||||
if (err != MZ_OK)
|
||||
return MZ_EXIST_ERROR;
|
||||
|
||||
*position = start_pos - (read_pos - buf_pos + i);
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
if (first) {
|
||||
read -= find_size;
|
||||
read_size -= find_size;
|
||||
buf_pos = find_size;
|
||||
first = 0;
|
||||
}
|
||||
|
||||
if (read == 0)
|
||||
break;
|
||||
|
||||
memmove(buf + read_size, buf, find_size);
|
||||
read_pos += read;
|
||||
}
|
||||
|
||||
return MZ_EXIST_ERROR;
|
||||
}
|
||||
|
||||
int32_t mz_stream_close(void *stream) {
|
||||
mz_stream *strm = (mz_stream *)stream;
|
||||
if (!strm || !strm->vtbl || !strm->vtbl->close)
|
||||
return MZ_PARAM_ERROR;
|
||||
if (mz_stream_is_open(stream) != MZ_OK)
|
||||
return MZ_STREAM_ERROR;
|
||||
return strm->vtbl->close(strm);
|
||||
}
|
||||
|
||||
int32_t mz_stream_error(void *stream) {
|
||||
mz_stream *strm = (mz_stream *)stream;
|
||||
if (!strm || !strm->vtbl || !strm->vtbl->error)
|
||||
return MZ_PARAM_ERROR;
|
||||
return strm->vtbl->error(strm);
|
||||
}
|
||||
|
||||
int32_t mz_stream_set_base(void *stream, void *base) {
|
||||
mz_stream *strm = (mz_stream *)stream;
|
||||
strm->base = (mz_stream *)base;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
void *mz_stream_get_interface(void *stream) {
|
||||
mz_stream *strm = (mz_stream *)stream;
|
||||
if (!strm || !strm->vtbl)
|
||||
return NULL;
|
||||
return (void *)strm->vtbl;
|
||||
}
|
||||
|
||||
int32_t mz_stream_get_prop_int64(void *stream, int32_t prop, int64_t *value) {
|
||||
mz_stream *strm = (mz_stream *)stream;
|
||||
if (!strm || !strm->vtbl || !strm->vtbl->get_prop_int64)
|
||||
return MZ_PARAM_ERROR;
|
||||
return strm->vtbl->get_prop_int64(strm, prop, value);
|
||||
}
|
||||
|
||||
int32_t mz_stream_set_prop_int64(void *stream, int32_t prop, int64_t value) {
|
||||
mz_stream *strm = (mz_stream *)stream;
|
||||
if (!strm || !strm->vtbl || !strm->vtbl->set_prop_int64)
|
||||
return MZ_PARAM_ERROR;
|
||||
return strm->vtbl->set_prop_int64(strm, prop, value);
|
||||
}
|
||||
|
||||
void *mz_stream_create(mz_stream_vtbl *vtbl) {
|
||||
if (!vtbl || !vtbl->create)
|
||||
return NULL;
|
||||
return vtbl->create();
|
||||
}
|
||||
|
||||
void mz_stream_delete(void **stream) {
|
||||
mz_stream *strm = NULL;
|
||||
if (!stream)
|
||||
return;
|
||||
strm = (mz_stream *)*stream;
|
||||
if (strm && strm->vtbl && strm->vtbl->destroy)
|
||||
strm->vtbl->destroy(stream);
|
||||
*stream = NULL;
|
||||
}
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
typedef struct mz_stream_raw_s {
|
||||
mz_stream stream;
|
||||
int64_t total_in;
|
||||
int64_t total_out;
|
||||
int64_t max_total_in;
|
||||
} mz_stream_raw;
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
int32_t mz_stream_raw_open(void *stream, const char *path, int32_t mode) {
|
||||
MZ_UNUSED(stream);
|
||||
MZ_UNUSED(path);
|
||||
MZ_UNUSED(mode);
|
||||
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_stream_raw_is_open(void *stream) {
|
||||
mz_stream_raw *raw = (mz_stream_raw *)stream;
|
||||
return mz_stream_is_open(raw->stream.base);
|
||||
}
|
||||
|
||||
int32_t mz_stream_raw_read(void *stream, void *buf, int32_t size) {
|
||||
mz_stream_raw *raw = (mz_stream_raw *)stream;
|
||||
int32_t bytes_to_read = size;
|
||||
int32_t read = 0;
|
||||
|
||||
if (raw->max_total_in > 0) {
|
||||
if ((int64_t)bytes_to_read > (raw->max_total_in - raw->total_in))
|
||||
bytes_to_read = (int32_t)(raw->max_total_in - raw->total_in);
|
||||
}
|
||||
|
||||
read = mz_stream_read(raw->stream.base, buf, bytes_to_read);
|
||||
|
||||
if (read > 0) {
|
||||
raw->total_in += read;
|
||||
raw->total_out += read;
|
||||
}
|
||||
|
||||
return read;
|
||||
}
|
||||
|
||||
int32_t mz_stream_raw_write(void *stream, const void *buf, int32_t size) {
|
||||
mz_stream_raw *raw = (mz_stream_raw *)stream;
|
||||
int32_t written = 0;
|
||||
|
||||
written = mz_stream_write(raw->stream.base, buf, size);
|
||||
|
||||
if (written > 0) {
|
||||
raw->total_out += written;
|
||||
raw->total_in += written;
|
||||
}
|
||||
|
||||
return written;
|
||||
}
|
||||
|
||||
int64_t mz_stream_raw_tell(void *stream) {
|
||||
mz_stream_raw *raw = (mz_stream_raw *)stream;
|
||||
return mz_stream_tell(raw->stream.base);
|
||||
}
|
||||
|
||||
int32_t mz_stream_raw_seek(void *stream, int64_t offset, int32_t origin) {
|
||||
mz_stream_raw *raw = (mz_stream_raw *)stream;
|
||||
return mz_stream_seek(raw->stream.base, offset, origin);
|
||||
}
|
||||
|
||||
int32_t mz_stream_raw_close(void *stream) {
|
||||
MZ_UNUSED(stream);
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_stream_raw_error(void *stream) {
|
||||
mz_stream_raw *raw = (mz_stream_raw *)stream;
|
||||
return mz_stream_error(raw->stream.base);
|
||||
}
|
||||
|
||||
int32_t mz_stream_raw_get_prop_int64(void *stream, int32_t prop, int64_t *value) {
|
||||
mz_stream_raw *raw = (mz_stream_raw *)stream;
|
||||
switch (prop) {
|
||||
case MZ_STREAM_PROP_TOTAL_IN:
|
||||
*value = raw->total_in;
|
||||
return MZ_OK;
|
||||
case MZ_STREAM_PROP_TOTAL_OUT:
|
||||
*value = raw->total_out;
|
||||
return MZ_OK;
|
||||
}
|
||||
return MZ_EXIST_ERROR;
|
||||
}
|
||||
|
||||
int32_t mz_stream_raw_set_prop_int64(void *stream, int32_t prop, int64_t value) {
|
||||
mz_stream_raw *raw = (mz_stream_raw *)stream;
|
||||
switch (prop) {
|
||||
case MZ_STREAM_PROP_TOTAL_IN_MAX:
|
||||
raw->max_total_in = value;
|
||||
return MZ_OK;
|
||||
}
|
||||
return MZ_EXIST_ERROR;
|
||||
}
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
static mz_stream_vtbl mz_stream_raw_vtbl = {
|
||||
mz_stream_raw_open, mz_stream_raw_is_open, mz_stream_raw_read, mz_stream_raw_write,
|
||||
mz_stream_raw_tell, mz_stream_raw_seek, mz_stream_raw_close, mz_stream_raw_error,
|
||||
mz_stream_raw_create, mz_stream_raw_delete, mz_stream_raw_get_prop_int64, mz_stream_raw_set_prop_int64};
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
void *mz_stream_raw_create(void) {
|
||||
mz_stream_raw *raw = (mz_stream_raw *)calloc(1, sizeof(mz_stream_raw));
|
||||
if (raw)
|
||||
raw->stream.vtbl = &mz_stream_raw_vtbl;
|
||||
return raw;
|
||||
}
|
||||
|
||||
void mz_stream_raw_delete(void **stream) {
|
||||
mz_stream_raw *raw = NULL;
|
||||
if (!stream)
|
||||
return;
|
||||
raw = (mz_stream_raw *)*stream;
|
||||
free(raw);
|
||||
*stream = NULL;
|
||||
}
|
||||
-265
@@ -1,265 +0,0 @@
|
||||
/* mz_strm_mem.c -- Stream for memory access
|
||||
part of the minizip-ng project
|
||||
|
||||
This interface is designed to access memory rather than files.
|
||||
We do use a region of memory to put data in to and take it out of.
|
||||
|
||||
Based on Unzip ioapi.c version 0.22, May 19th, 2003
|
||||
|
||||
Copyright (C) Nathan Moinvaziri
|
||||
https://github.com/zlib-ng/minizip-ng
|
||||
Copyright (C) 2003 Justin Fletcher
|
||||
Copyright (C) 1998-2003 Gilles Vollant
|
||||
https://www.winimage.com/zLibDll/minizip.html
|
||||
|
||||
This program is distributed under the terms of the same license as zlib.
|
||||
See the accompanying LICENSE file for the full text of the license.
|
||||
*/
|
||||
|
||||
#include "mz.h"
|
||||
#include "mz_strm.h"
|
||||
#include "mz_strm_mem.h"
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
static mz_stream_vtbl mz_stream_mem_vtbl = {mz_stream_mem_open,
|
||||
mz_stream_mem_is_open,
|
||||
mz_stream_mem_read,
|
||||
mz_stream_mem_write,
|
||||
mz_stream_mem_tell,
|
||||
mz_stream_mem_seek,
|
||||
mz_stream_mem_close,
|
||||
mz_stream_mem_error,
|
||||
mz_stream_mem_create,
|
||||
mz_stream_mem_delete,
|
||||
NULL,
|
||||
NULL};
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
typedef struct mz_stream_mem_s {
|
||||
mz_stream stream;
|
||||
int32_t mode;
|
||||
uint8_t *buffer; /* Memory buffer pointer */
|
||||
int32_t size; /* Size of the memory buffer */
|
||||
int32_t limit; /* Furthest we've written */
|
||||
int32_t position; /* Current position in the memory */
|
||||
int32_t grow_size; /* Size to grow when full */
|
||||
} mz_stream_mem;
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
static int32_t mz_stream_mem_set_size(void *stream, int32_t size) {
|
||||
mz_stream_mem *mem = (mz_stream_mem *)stream;
|
||||
int32_t new_size = size;
|
||||
uint8_t *new_buf = NULL;
|
||||
|
||||
new_buf = (uint8_t *)malloc((uint32_t)new_size);
|
||||
if (!new_buf)
|
||||
return MZ_BUF_ERROR;
|
||||
|
||||
if (mem->buffer) {
|
||||
memcpy(new_buf, mem->buffer, mem->size);
|
||||
free(mem->buffer);
|
||||
}
|
||||
|
||||
mem->buffer = new_buf;
|
||||
mem->size = new_size;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_stream_mem_open(void *stream, const char *path, int32_t mode) {
|
||||
mz_stream_mem *mem = (mz_stream_mem *)stream;
|
||||
int32_t err = MZ_OK;
|
||||
|
||||
MZ_UNUSED(path);
|
||||
|
||||
mem->mode = mode;
|
||||
mem->limit = 0;
|
||||
mem->position = 0;
|
||||
|
||||
if (mem->mode & MZ_OPEN_MODE_CREATE)
|
||||
err = mz_stream_mem_set_size(stream, mem->grow_size);
|
||||
else
|
||||
mem->limit = mem->size;
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
int32_t mz_stream_mem_is_open(void *stream) {
|
||||
mz_stream_mem *mem = (mz_stream_mem *)stream;
|
||||
if (!mem->buffer)
|
||||
return MZ_OPEN_ERROR;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_stream_mem_read(void *stream, void *buf, int32_t size) {
|
||||
mz_stream_mem *mem = (mz_stream_mem *)stream;
|
||||
|
||||
if (size > mem->size - mem->position)
|
||||
size = mem->size - mem->position;
|
||||
if (mem->position + size > mem->limit)
|
||||
size = mem->limit - mem->position;
|
||||
|
||||
if (size <= 0)
|
||||
return 0;
|
||||
|
||||
memcpy(buf, mem->buffer + mem->position, size);
|
||||
mem->position += size;
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
int32_t mz_stream_mem_write(void *stream, const void *buf, int32_t size) {
|
||||
mz_stream_mem *mem = (mz_stream_mem *)stream;
|
||||
int32_t new_size = 0;
|
||||
int32_t err = MZ_OK;
|
||||
|
||||
if (!size)
|
||||
return size;
|
||||
|
||||
if (size > mem->size - mem->position) {
|
||||
if (mem->mode & MZ_OPEN_MODE_CREATE) {
|
||||
new_size = mem->size;
|
||||
if (size < mem->grow_size)
|
||||
new_size += mem->grow_size;
|
||||
else
|
||||
new_size += size;
|
||||
|
||||
err = mz_stream_mem_set_size(stream, new_size);
|
||||
if (err != MZ_OK)
|
||||
return err;
|
||||
} else {
|
||||
size = mem->size - mem->position;
|
||||
}
|
||||
}
|
||||
|
||||
memcpy(mem->buffer + mem->position, buf, size);
|
||||
|
||||
mem->position += size;
|
||||
if (mem->position > mem->limit)
|
||||
mem->limit = mem->position;
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
int64_t mz_stream_mem_tell(void *stream) {
|
||||
mz_stream_mem *mem = (mz_stream_mem *)stream;
|
||||
return mem->position;
|
||||
}
|
||||
|
||||
int32_t mz_stream_mem_seek(void *stream, int64_t offset, int32_t origin) {
|
||||
mz_stream_mem *mem = (mz_stream_mem *)stream;
|
||||
int64_t new_pos = 0;
|
||||
int32_t err = MZ_OK;
|
||||
|
||||
switch (origin) {
|
||||
case MZ_SEEK_CUR:
|
||||
new_pos = mem->position + offset;
|
||||
break;
|
||||
case MZ_SEEK_END:
|
||||
new_pos = mem->limit + offset;
|
||||
break;
|
||||
case MZ_SEEK_SET:
|
||||
new_pos = offset;
|
||||
break;
|
||||
default:
|
||||
return MZ_SEEK_ERROR;
|
||||
}
|
||||
|
||||
// While `malloc` accepts a size_t, mz_stream_mem_s.size only supports an int32_t
|
||||
if (new_pos < 0 || new_pos > INT32_MAX) {
|
||||
return MZ_SEEK_ERROR;
|
||||
}
|
||||
|
||||
if (new_pos > mem->size) {
|
||||
if ((mem->mode & MZ_OPEN_MODE_CREATE) == 0)
|
||||
return MZ_SEEK_ERROR;
|
||||
|
||||
err = mz_stream_mem_set_size(stream, (int32_t)new_pos);
|
||||
if (err != MZ_OK)
|
||||
return err;
|
||||
}
|
||||
|
||||
mem->position = (int32_t)new_pos;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_stream_mem_close(void *stream) {
|
||||
MZ_UNUSED(stream);
|
||||
|
||||
/* We never return errors */
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_stream_mem_error(void *stream) {
|
||||
MZ_UNUSED(stream);
|
||||
|
||||
/* We never return errors */
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
void mz_stream_mem_set_buffer(void *stream, void *buf, int32_t size) {
|
||||
mz_stream_mem *mem = (mz_stream_mem *)stream;
|
||||
mem->buffer = (uint8_t *)buf;
|
||||
mem->size = size;
|
||||
mem->limit = size;
|
||||
}
|
||||
|
||||
int32_t mz_stream_mem_get_buffer(void *stream, const void **buf) {
|
||||
return mz_stream_mem_get_buffer_at(stream, 0, buf);
|
||||
}
|
||||
|
||||
int32_t mz_stream_mem_get_buffer_at(void *stream, int64_t position, const void **buf) {
|
||||
mz_stream_mem *mem = (mz_stream_mem *)stream;
|
||||
if (!buf || position < 0 || !mem->buffer || mem->size < position)
|
||||
return MZ_SEEK_ERROR;
|
||||
*buf = mem->buffer + position;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_stream_mem_get_buffer_at_current(void *stream, const void **buf) {
|
||||
mz_stream_mem *mem = (mz_stream_mem *)stream;
|
||||
return mz_stream_mem_get_buffer_at(stream, mem->position, buf);
|
||||
}
|
||||
|
||||
void mz_stream_mem_get_buffer_length(void *stream, int32_t *length) {
|
||||
mz_stream_mem *mem = (mz_stream_mem *)stream;
|
||||
*length = mem->limit;
|
||||
}
|
||||
|
||||
void mz_stream_mem_set_buffer_limit(void *stream, int32_t limit) {
|
||||
mz_stream_mem *mem = (mz_stream_mem *)stream;
|
||||
mem->limit = limit;
|
||||
}
|
||||
|
||||
void mz_stream_mem_set_grow_size(void *stream, int32_t grow_size) {
|
||||
mz_stream_mem *mem = (mz_stream_mem *)stream;
|
||||
mem->grow_size = grow_size;
|
||||
}
|
||||
|
||||
void *mz_stream_mem_create(void) {
|
||||
mz_stream_mem *mem = (mz_stream_mem *)calloc(1, sizeof(mz_stream_mem));
|
||||
if (mem) {
|
||||
mem->stream.vtbl = &mz_stream_mem_vtbl;
|
||||
mem->grow_size = 4096;
|
||||
}
|
||||
return mem;
|
||||
}
|
||||
|
||||
void mz_stream_mem_delete(void **stream) {
|
||||
mz_stream_mem *mem = NULL;
|
||||
if (!stream)
|
||||
return;
|
||||
mem = (mz_stream_mem *)*stream;
|
||||
if (mem) {
|
||||
if ((mem->mode & MZ_OPEN_MODE_CREATE) && (mem->buffer))
|
||||
free(mem->buffer);
|
||||
free(mem);
|
||||
}
|
||||
*stream = NULL;
|
||||
}
|
||||
|
||||
void *mz_stream_mem_get_interface(void) {
|
||||
return (void *)&mz_stream_mem_vtbl;
|
||||
}
|
||||
-218
@@ -1,218 +0,0 @@
|
||||
/* mz_strm_posix.c -- Stream for filesystem access for posix/linux
|
||||
part of the minizip-ng project
|
||||
|
||||
Copyright (C) Nathan Moinvaziri
|
||||
https://github.com/zlib-ng/minizip-ng
|
||||
Modifications for Zip64 support
|
||||
Copyright (C) 2009-2010 Mathias Svensson
|
||||
http://result42.com
|
||||
Copyright (C) 1998-2010 Gilles Vollant
|
||||
https://www.winimage.com/zLibDll/minizip.html
|
||||
|
||||
This program is distributed under the terms of the same license as zlib.
|
||||
See the accompanying LICENSE file for the full text of the license.
|
||||
*/
|
||||
|
||||
#include "mz.h"
|
||||
#include "mz_config.h"
|
||||
#include "mz_strm.h"
|
||||
#include "mz_strm_os.h"
|
||||
|
||||
#include <stdio.h> /* fopen, fread, ... */
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h> /* S_IRUSR, S_IWUSR, ... */
|
||||
#include <unistd.h> /* open, close, ... */
|
||||
#include <fcntl.h> /* O_NOFOLLOW, ... */
|
||||
#ifndef O_BINARY
|
||||
/* POSIX systems (Linux/BSD/PS5) have no O_BINARY; stub it to 0 so the
|
||||
* `mode_open | O_BINARY` open() call stays valid. Windows builds get the
|
||||
* real definition from <fcntl.h>. */
|
||||
# define O_BINARY 0
|
||||
#endif
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
#define fopen64 fopen
|
||||
#ifndef MZ_FILE32_API
|
||||
# if HAVE_FSEEKO
|
||||
# define ftello64 ftello
|
||||
# define fseeko64 fseeko
|
||||
# elif defined(_MSC_VER) && (_MSC_VER >= 1400)
|
||||
# define ftello64 _ftelli64
|
||||
# define fseeko64 _fseeki64
|
||||
# endif
|
||||
#endif
|
||||
#ifndef ftello64
|
||||
# define ftello64 ftell
|
||||
#endif
|
||||
#ifndef fseeko64
|
||||
# define fseeko64 fseek
|
||||
#endif
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
static mz_stream_vtbl mz_stream_os_vtbl = {mz_stream_os_open,
|
||||
mz_stream_os_is_open,
|
||||
mz_stream_os_read,
|
||||
mz_stream_os_write,
|
||||
mz_stream_os_tell,
|
||||
mz_stream_os_seek,
|
||||
mz_stream_os_close,
|
||||
mz_stream_os_error,
|
||||
mz_stream_os_create,
|
||||
mz_stream_os_delete,
|
||||
NULL,
|
||||
NULL};
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
typedef struct mz_stream_posix_s {
|
||||
mz_stream stream;
|
||||
int32_t error;
|
||||
FILE *handle;
|
||||
} mz_stream_posix;
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
int32_t mz_stream_os_open(void *stream, const char *path, int32_t mode) {
|
||||
mz_stream_posix *posix = (mz_stream_posix *)stream;
|
||||
const char *mode_fopen = NULL;
|
||||
int mode_open = 0;
|
||||
int fd;
|
||||
|
||||
if (!path)
|
||||
return MZ_PARAM_ERROR;
|
||||
|
||||
if ((mode & MZ_OPEN_MODE_READWRITE) == MZ_OPEN_MODE_READ) {
|
||||
mode_fopen = "rb";
|
||||
mode_open = O_RDONLY;
|
||||
} else if (mode & MZ_OPEN_MODE_APPEND) {
|
||||
mode_fopen = "r+b";
|
||||
mode_open = O_RDWR;
|
||||
} else if (mode & MZ_OPEN_MODE_CREATE) {
|
||||
mode_fopen = "wb";
|
||||
mode_open = O_WRONLY | O_CREAT | O_TRUNC;
|
||||
} else
|
||||
return MZ_OPEN_ERROR;
|
||||
|
||||
if (mode & MZ_OPEN_MODE_NOFOLLOW)
|
||||
mode_open |= O_NOFOLLOW;
|
||||
|
||||
fd = open(path, mode_open | O_BINARY, S_IRUSR | S_IWUSR | S_IRGRP);
|
||||
if (fd != -1) {
|
||||
posix->handle = fdopen(fd, mode_fopen);
|
||||
if (!posix->handle)
|
||||
close(fd);
|
||||
}
|
||||
if (!posix->handle) {
|
||||
posix->error = errno;
|
||||
return MZ_OPEN_ERROR;
|
||||
}
|
||||
|
||||
if (mode & MZ_OPEN_MODE_APPEND)
|
||||
return mz_stream_os_seek(stream, 0, MZ_SEEK_END);
|
||||
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_stream_os_is_open(void *stream) {
|
||||
mz_stream_posix *posix = (mz_stream_posix *)stream;
|
||||
if (!posix->handle)
|
||||
return MZ_OPEN_ERROR;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_stream_os_read(void *stream, void *buf, int32_t size) {
|
||||
mz_stream_posix *posix = (mz_stream_posix *)stream;
|
||||
int32_t read = (int32_t)fread(buf, 1, (size_t)size, posix->handle);
|
||||
if (read < size && ferror(posix->handle)) {
|
||||
posix->error = errno;
|
||||
return MZ_READ_ERROR;
|
||||
}
|
||||
return read;
|
||||
}
|
||||
|
||||
int32_t mz_stream_os_write(void *stream, const void *buf, int32_t size) {
|
||||
mz_stream_posix *posix = (mz_stream_posix *)stream;
|
||||
int32_t written = (int32_t)fwrite(buf, 1, (size_t)size, posix->handle);
|
||||
if (written < size && ferror(posix->handle)) {
|
||||
posix->error = errno;
|
||||
return MZ_WRITE_ERROR;
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
int64_t mz_stream_os_tell(void *stream) {
|
||||
mz_stream_posix *posix = (mz_stream_posix *)stream;
|
||||
int64_t position = ftello64(posix->handle);
|
||||
if (position == -1) {
|
||||
posix->error = errno;
|
||||
return MZ_TELL_ERROR;
|
||||
}
|
||||
return position;
|
||||
}
|
||||
|
||||
int32_t mz_stream_os_seek(void *stream, int64_t offset, int32_t origin) {
|
||||
mz_stream_posix *posix = (mz_stream_posix *)stream;
|
||||
int32_t fseek_origin = 0;
|
||||
|
||||
switch (origin) {
|
||||
case MZ_SEEK_CUR:
|
||||
fseek_origin = SEEK_CUR;
|
||||
break;
|
||||
case MZ_SEEK_END:
|
||||
fseek_origin = SEEK_END;
|
||||
break;
|
||||
case MZ_SEEK_SET:
|
||||
fseek_origin = SEEK_SET;
|
||||
break;
|
||||
default:
|
||||
return MZ_SEEK_ERROR;
|
||||
}
|
||||
|
||||
if (fseeko64(posix->handle, offset, fseek_origin) != 0) {
|
||||
posix->error = errno;
|
||||
return MZ_SEEK_ERROR;
|
||||
}
|
||||
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_stream_os_close(void *stream) {
|
||||
mz_stream_posix *posix = (mz_stream_posix *)stream;
|
||||
int32_t closed = 0;
|
||||
if (posix->handle) {
|
||||
closed = fclose(posix->handle);
|
||||
posix->handle = NULL;
|
||||
}
|
||||
if (closed != 0) {
|
||||
posix->error = errno;
|
||||
return MZ_CLOSE_ERROR;
|
||||
}
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_stream_os_error(void *stream) {
|
||||
mz_stream_posix *posix = (mz_stream_posix *)stream;
|
||||
return posix->error;
|
||||
}
|
||||
|
||||
void *mz_stream_os_create(void) {
|
||||
mz_stream_posix *posix = (mz_stream_posix *)calloc(1, sizeof(mz_stream_posix));
|
||||
if (posix)
|
||||
posix->stream.vtbl = &mz_stream_os_vtbl;
|
||||
return posix;
|
||||
}
|
||||
|
||||
void mz_stream_os_delete(void **stream) {
|
||||
mz_stream_posix *posix = NULL;
|
||||
if (!stream)
|
||||
return;
|
||||
posix = (mz_stream_posix *)*stream;
|
||||
free(posix);
|
||||
*stream = NULL;
|
||||
}
|
||||
|
||||
void *mz_stream_os_get_interface(void) {
|
||||
return (void *)&mz_stream_os_vtbl;
|
||||
}
|
||||
-378
@@ -1,378 +0,0 @@
|
||||
/* mz_strm_zlib.c -- Stream for zlib inflate/deflate
|
||||
part of the minizip-ng project
|
||||
|
||||
Copyright (C) Nathan Moinvaziri
|
||||
https://github.com/zlib-ng/minizip-ng
|
||||
|
||||
This program is distributed under the terms of the same license as zlib.
|
||||
See the accompanying LICENSE file for the full text of the license.
|
||||
*/
|
||||
|
||||
#include "mz.h"
|
||||
#include "mz_strm.h"
|
||||
#include "mz_strm_zlib.h"
|
||||
|
||||
#if !defined(ZLIB_COMPAT)
|
||||
# include "zlib-ng.h"
|
||||
#else
|
||||
# include "zlib.h"
|
||||
#endif
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
#if !defined(ZLIB_COMPAT)
|
||||
# define ZLIB_PREFIX(x) zng_##x
|
||||
typedef zng_stream zlib_stream;
|
||||
#else
|
||||
# define ZLIB_PREFIX(x) x
|
||||
typedef z_stream zlib_stream;
|
||||
#endif
|
||||
|
||||
#if !defined(DEF_MEM_LEVEL)
|
||||
# if MAX_MEM_LEVEL >= 8
|
||||
# define DEF_MEM_LEVEL 8
|
||||
# else
|
||||
# define DEF_MEM_LEVEL MAX_MEM_LEVEL
|
||||
# endif
|
||||
#endif
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
static mz_stream_vtbl mz_stream_zlib_vtbl = {
|
||||
mz_stream_zlib_open, mz_stream_zlib_is_open, mz_stream_zlib_read, mz_stream_zlib_write,
|
||||
mz_stream_zlib_tell, mz_stream_zlib_seek, mz_stream_zlib_close, mz_stream_zlib_error,
|
||||
mz_stream_zlib_create, mz_stream_zlib_delete, mz_stream_zlib_get_prop_int64, mz_stream_zlib_set_prop_int64};
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
typedef struct mz_stream_zlib_s {
|
||||
mz_stream stream;
|
||||
zlib_stream zstream;
|
||||
uint8_t buffer[INT16_MAX];
|
||||
int32_t buffer_len;
|
||||
int64_t total_in;
|
||||
int64_t total_out;
|
||||
int64_t max_total_in;
|
||||
int8_t initialized;
|
||||
int16_t level;
|
||||
int32_t window_bits;
|
||||
int32_t mode;
|
||||
int32_t error;
|
||||
} mz_stream_zlib;
|
||||
|
||||
/***************************************************************************/
|
||||
|
||||
int32_t mz_stream_zlib_open(void *stream, const char *path, int32_t mode) {
|
||||
mz_stream_zlib *zlib = (mz_stream_zlib *)stream;
|
||||
|
||||
MZ_UNUSED(path);
|
||||
|
||||
zlib->zstream.data_type = Z_BINARY;
|
||||
zlib->zstream.zalloc = Z_NULL;
|
||||
zlib->zstream.zfree = Z_NULL;
|
||||
zlib->zstream.opaque = Z_NULL;
|
||||
zlib->zstream.total_in = 0;
|
||||
zlib->zstream.total_out = 0;
|
||||
|
||||
zlib->total_in = 0;
|
||||
zlib->total_out = 0;
|
||||
|
||||
if (mode & MZ_OPEN_MODE_WRITE) {
|
||||
#ifdef MZ_ZIP_NO_COMPRESSION
|
||||
return MZ_SUPPORT_ERROR;
|
||||
#else
|
||||
zlib->zstream.next_out = zlib->buffer;
|
||||
zlib->zstream.avail_out = sizeof(zlib->buffer);
|
||||
|
||||
zlib->error = ZLIB_PREFIX(deflateInit2)(&zlib->zstream, (int8_t)zlib->level, Z_DEFLATED, zlib->window_bits,
|
||||
DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY);
|
||||
#endif
|
||||
} else if (mode & MZ_OPEN_MODE_READ) {
|
||||
#ifdef MZ_ZIP_NO_DECOMPRESSION
|
||||
return MZ_SUPPORT_ERROR;
|
||||
#else
|
||||
zlib->zstream.next_in = zlib->buffer;
|
||||
zlib->zstream.avail_in = 0;
|
||||
|
||||
zlib->error = ZLIB_PREFIX(inflateInit2)(&zlib->zstream, zlib->window_bits);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (zlib->error != Z_OK)
|
||||
return MZ_OPEN_ERROR;
|
||||
|
||||
zlib->initialized = 1;
|
||||
zlib->mode = mode;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_stream_zlib_is_open(void *stream) {
|
||||
mz_stream_zlib *zlib = (mz_stream_zlib *)stream;
|
||||
if (zlib->initialized != 1)
|
||||
return MZ_OPEN_ERROR;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_stream_zlib_read(void *stream, void *buf, int32_t size) {
|
||||
#ifdef MZ_ZIP_NO_DECOMPRESSION
|
||||
MZ_UNUSED(stream);
|
||||
MZ_UNUSED(buf);
|
||||
MZ_UNUSED(size);
|
||||
return MZ_SUPPORT_ERROR;
|
||||
#else
|
||||
mz_stream_zlib *zlib = (mz_stream_zlib *)stream;
|
||||
uint64_t total_in_before = 0;
|
||||
uint64_t total_in_after = 0;
|
||||
uint64_t total_out_before = 0;
|
||||
uint64_t total_out_after = 0;
|
||||
uint32_t total_in = 0;
|
||||
uint32_t total_out = 0;
|
||||
uint32_t in_bytes = 0;
|
||||
uint32_t out_bytes = 0;
|
||||
int32_t bytes_to_read = sizeof(zlib->buffer);
|
||||
int32_t read = 0;
|
||||
int32_t err = Z_OK;
|
||||
|
||||
zlib->zstream.next_out = (Bytef *)buf;
|
||||
zlib->zstream.avail_out = (uInt)size;
|
||||
|
||||
do {
|
||||
if (zlib->zstream.avail_in == 0) {
|
||||
if (zlib->max_total_in > 0) {
|
||||
if ((int64_t)bytes_to_read > (zlib->max_total_in - zlib->total_in))
|
||||
bytes_to_read = (int32_t)(zlib->max_total_in - zlib->total_in);
|
||||
}
|
||||
|
||||
read = mz_stream_read(zlib->stream.base, zlib->buffer, bytes_to_read);
|
||||
|
||||
if (read < 0)
|
||||
return read;
|
||||
|
||||
zlib->zstream.next_in = zlib->buffer;
|
||||
zlib->zstream.avail_in = read;
|
||||
}
|
||||
|
||||
total_in_before = zlib->zstream.avail_in;
|
||||
total_out_before = zlib->zstream.total_out;
|
||||
|
||||
err = ZLIB_PREFIX(inflate)(&zlib->zstream, Z_SYNC_FLUSH);
|
||||
if ((err >= Z_OK) && (zlib->zstream.msg)) {
|
||||
zlib->error = Z_DATA_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
total_in_after = zlib->zstream.avail_in;
|
||||
total_out_after = zlib->zstream.total_out;
|
||||
|
||||
in_bytes = (uint32_t)(total_in_before - total_in_after);
|
||||
out_bytes = (uint32_t)(total_out_after - total_out_before);
|
||||
|
||||
total_in += in_bytes;
|
||||
total_out += out_bytes;
|
||||
|
||||
zlib->total_in += in_bytes;
|
||||
zlib->total_out += out_bytes;
|
||||
|
||||
if (err == Z_STREAM_END)
|
||||
break;
|
||||
if (err != Z_OK) {
|
||||
zlib->error = err;
|
||||
break;
|
||||
}
|
||||
} while (zlib->zstream.avail_out > 0);
|
||||
|
||||
MZ_UNUSED(total_in);
|
||||
|
||||
if (zlib->error != 0) {
|
||||
/* Zlib errors are compatible with MZ */
|
||||
return zlib->error;
|
||||
}
|
||||
|
||||
return total_out;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifndef MZ_ZIP_NO_COMPRESSION
|
||||
static int32_t mz_stream_zlib_flush(void *stream) {
|
||||
mz_stream_zlib *zlib = (mz_stream_zlib *)stream;
|
||||
if (mz_stream_write(zlib->stream.base, zlib->buffer, zlib->buffer_len) != zlib->buffer_len)
|
||||
return MZ_WRITE_ERROR;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
static int32_t mz_stream_zlib_deflate(void *stream, int flush) {
|
||||
mz_stream_zlib *zlib = (mz_stream_zlib *)stream;
|
||||
uint64_t total_out_before = 0;
|
||||
uint64_t total_out_after = 0;
|
||||
int32_t out_bytes = 0;
|
||||
int32_t err = Z_OK;
|
||||
|
||||
do {
|
||||
if (zlib->zstream.avail_out == 0) {
|
||||
err = mz_stream_zlib_flush(zlib);
|
||||
if (err != MZ_OK)
|
||||
return err;
|
||||
|
||||
zlib->zstream.avail_out = sizeof(zlib->buffer);
|
||||
zlib->zstream.next_out = zlib->buffer;
|
||||
|
||||
zlib->buffer_len = 0;
|
||||
}
|
||||
|
||||
total_out_before = zlib->zstream.total_out;
|
||||
err = ZLIB_PREFIX(deflate)(&zlib->zstream, flush);
|
||||
total_out_after = zlib->zstream.total_out;
|
||||
|
||||
out_bytes = (uint32_t)(total_out_after - total_out_before);
|
||||
|
||||
zlib->buffer_len += out_bytes;
|
||||
zlib->total_out += out_bytes;
|
||||
|
||||
if (err == Z_STREAM_END)
|
||||
break;
|
||||
if (err != Z_OK) {
|
||||
zlib->error = err;
|
||||
return MZ_DATA_ERROR;
|
||||
}
|
||||
} while ((zlib->zstream.avail_in > 0) || (flush == Z_FINISH && err == Z_OK));
|
||||
|
||||
return MZ_OK;
|
||||
}
|
||||
#endif
|
||||
|
||||
int32_t mz_stream_zlib_write(void *stream, const void *buf, int32_t size) {
|
||||
#ifdef MZ_ZIP_NO_COMPRESSION
|
||||
MZ_UNUSED(stream);
|
||||
MZ_UNUSED(buf);
|
||||
MZ_UNUSED(size);
|
||||
return MZ_SUPPORT_ERROR;
|
||||
#else
|
||||
mz_stream_zlib *zlib = (mz_stream_zlib *)stream;
|
||||
int32_t err = MZ_OK;
|
||||
|
||||
zlib->zstream.next_in = (Bytef *)(intptr_t)buf;
|
||||
zlib->zstream.avail_in = (uInt)size;
|
||||
|
||||
err = mz_stream_zlib_deflate(stream, Z_NO_FLUSH);
|
||||
if (err != MZ_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
zlib->total_in += size;
|
||||
return size;
|
||||
#endif
|
||||
}
|
||||
|
||||
int64_t mz_stream_zlib_tell(void *stream) {
|
||||
MZ_UNUSED(stream);
|
||||
|
||||
return MZ_TELL_ERROR;
|
||||
}
|
||||
|
||||
int32_t mz_stream_zlib_seek(void *stream, int64_t offset, int32_t origin) {
|
||||
MZ_UNUSED(stream);
|
||||
MZ_UNUSED(offset);
|
||||
MZ_UNUSED(origin);
|
||||
|
||||
return MZ_SEEK_ERROR;
|
||||
}
|
||||
|
||||
int32_t mz_stream_zlib_close(void *stream) {
|
||||
mz_stream_zlib *zlib = (mz_stream_zlib *)stream;
|
||||
|
||||
if (zlib->mode & MZ_OPEN_MODE_WRITE) {
|
||||
#ifdef MZ_ZIP_NO_COMPRESSION
|
||||
return MZ_SUPPORT_ERROR;
|
||||
#else
|
||||
mz_stream_zlib_deflate(stream, Z_FINISH);
|
||||
mz_stream_zlib_flush(stream);
|
||||
|
||||
ZLIB_PREFIX(deflateEnd)(&zlib->zstream);
|
||||
#endif
|
||||
} else if (zlib->mode & MZ_OPEN_MODE_READ) {
|
||||
#ifdef MZ_ZIP_NO_DECOMPRESSION
|
||||
return MZ_SUPPORT_ERROR;
|
||||
#else
|
||||
ZLIB_PREFIX(inflateEnd)(&zlib->zstream);
|
||||
#endif
|
||||
}
|
||||
|
||||
zlib->initialized = 0;
|
||||
|
||||
if (zlib->error != Z_OK)
|
||||
return MZ_CLOSE_ERROR;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_stream_zlib_error(void *stream) {
|
||||
mz_stream_zlib *zlib = (mz_stream_zlib *)stream;
|
||||
return zlib->error;
|
||||
}
|
||||
|
||||
int32_t mz_stream_zlib_get_prop_int64(void *stream, int32_t prop, int64_t *value) {
|
||||
mz_stream_zlib *zlib = (mz_stream_zlib *)stream;
|
||||
switch (prop) {
|
||||
case MZ_STREAM_PROP_TOTAL_IN:
|
||||
*value = zlib->total_in;
|
||||
break;
|
||||
case MZ_STREAM_PROP_TOTAL_IN_MAX:
|
||||
*value = zlib->max_total_in;
|
||||
break;
|
||||
case MZ_STREAM_PROP_TOTAL_OUT:
|
||||
*value = zlib->total_out;
|
||||
break;
|
||||
case MZ_STREAM_PROP_HEADER_SIZE:
|
||||
*value = 0;
|
||||
break;
|
||||
case MZ_STREAM_PROP_COMPRESS_WINDOW:
|
||||
*value = zlib->window_bits;
|
||||
break;
|
||||
default:
|
||||
return MZ_EXIST_ERROR;
|
||||
}
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_stream_zlib_set_prop_int64(void *stream, int32_t prop, int64_t value) {
|
||||
mz_stream_zlib *zlib = (mz_stream_zlib *)stream;
|
||||
switch (prop) {
|
||||
case MZ_STREAM_PROP_COMPRESS_LEVEL:
|
||||
if (value == MZ_COMPRESS_LEVEL_DEFAULT)
|
||||
zlib->level = Z_DEFAULT_COMPRESSION;
|
||||
else
|
||||
zlib->level = (int16_t)value;
|
||||
break;
|
||||
case MZ_STREAM_PROP_TOTAL_IN_MAX:
|
||||
zlib->max_total_in = value;
|
||||
break;
|
||||
case MZ_STREAM_PROP_COMPRESS_WINDOW:
|
||||
zlib->window_bits = (int32_t)value;
|
||||
break;
|
||||
default:
|
||||
return MZ_EXIST_ERROR;
|
||||
}
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
void *mz_stream_zlib_create(void) {
|
||||
mz_stream_zlib *zlib = (mz_stream_zlib *)calloc(1, sizeof(mz_stream_zlib));
|
||||
if (zlib) {
|
||||
zlib->stream.vtbl = &mz_stream_zlib_vtbl;
|
||||
zlib->level = Z_DEFAULT_COMPRESSION;
|
||||
zlib->window_bits = -MAX_WBITS;
|
||||
}
|
||||
return zlib;
|
||||
}
|
||||
|
||||
void mz_stream_zlib_delete(void **stream) {
|
||||
mz_stream_zlib *zlib = NULL;
|
||||
if (!stream)
|
||||
return;
|
||||
zlib = (mz_stream_zlib *)*stream;
|
||||
free(zlib);
|
||||
*stream = NULL;
|
||||
}
|
||||
|
||||
void *mz_stream_zlib_get_interface(void) {
|
||||
return (void *)&mz_stream_zlib_vtbl;
|
||||
}
|
||||
Vendored
-2834
File diff suppressed because it is too large.
Load diff
Loaded 100 of 127 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user