Initial import of PS5 Web File Manager v1.7

Homebrew HTTP file manager payload for jailbroken PS5 consoles (x86_64-sie-ps5,
title id FMGR88888). Browse, edit, upload, download and extract ZIPs through
any browser on the LAN; single self-contained ELF, no external services.

Highlights (v1.7):
- ZIP large-file profile (large=1): 2 TiB total / 1 TiB per entry / 1000:1
  ratio. Frontend prompts for confirmation whenever archive > 60 GiB on disk.
- Default ZIP profile stays safe: 512 GiB / 64 GiB / 200:1.
- 69 host-side C tests (tests/run-tests.sh) covering traversal, ZIP64,
  encryption rejection, conflict policies and the large-file profile.
- Three-phase extraction engine (scan -> extract -> publish -> cleanup) with
  atomic staging + rename, located in src/zip_extract.{c,h}.

Project layout:
  src/          C payload sources
  assets/       HTML / CSS / JS / icons / param.json
  tests/        POSIX/host test suite + fixtures
  third_party/  vendored zlib + minizip-ng (zlib license)
  docs/screenshots/  README screenshot images
  LICENSE       GPLv3+

Build:
  make         # cross-compile with PS5_PAYLOAD_SDK
  make linux   # Linux host binary for UI dev
This commit is contained in:
Songlx516 committed 2026-09-04 14:21:15 +08:00
commit 5cb0b76e7a
133 files changed
+44512

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
==========================================
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
+203
View File
@@ -0,0 +1,203 @@
#!/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"
+51
View File
@@ -0,0 +1,51 @@
"""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]}")
+29
View File
@@ -0,0 +1,29 @@
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
+25
View File
@@ -0,0 +1,25 @@
# Build artifacts
*.elf
*.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
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+90
View File
@@ -0,0 +1,90 @@
ifneq ($(filter-out linux linux-deps clean,$(MAKECMDGOALS)),)
ifdef PS5_PAYLOAD_SDK
include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
else
$(error PS5_PAYLOAD_SDK is undefined)
endif
endif
ifeq ($(MAKECMDGOALS),)
ifdef PS5_PAYLOAD_SDK
include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
else
$(error PS5_PAYLOAD_SDK is undefined)
endif
endif
VERSION_TAG := v1.7
TITLE_ID := FMGR88888
PYTHON ?= python3
STRIP ?= $(PS5_PAYLOAD_SDK)/bin/prospero-strip
PKG_CONFIG ?= $(PS5_PAYLOAD_SDK)/bin/prospero-pkg-config
HOST_CC ?= cc
HOST_STRIP ?= strip
HOST_PKG_CONFIG ?= pkg-config
BIN := web-file-mgr.elf
LINUX_BIN := web-file-mgr-linux
COMMON_SRCS := src/main.c src/websrv.c src/filemgr.c src/file_response.c src/task.c src/upload.c src/download.c src/text.c src/list.c src/space.c src/fs_util.c src/json_util.c src/path_util.c src/asset.c src/mime.c src/notify.c src/pkg_installer.c src/pkg_info.c src/extract.c src/zip_extract.c
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
GEN_SRCS := $(patsubst assets/%,gen/%, $(ASSETS:=.c))
# Vendored minizip-ng + zlib, compiled with relaxed warnings (third-party code).
THIRD_PARTY_SRCS := $(wildcard third_party/zlib/src/*.c) $(wildcard third_party/minizip-ng/src/*.c)
THIRD_PARTY_CFLAGS := -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
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 += `$(PKG_CONFIG) libmicrohttpd --cflags`
LDFLAGS := -Wl,--gc-sections
LDADD := `$(PKG_CONFIG) libmicrohttpd --libs`
LDADD += -lSceIpmi -lSceAppInstUtil -lSceUserService
LINUX_CFLAGS := -O2 -flto -Wall -Werror -Isrc -Ithird_party/minizip-ng/include -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
all: deps $(BIN)
linux: linux-deps $(LINUX_BIN)
deps:
@$(PKG_CONFIG) --exists libmicrohttpd || ./install-libmicrohttpd.sh
linux-deps:
@$(HOST_PKG_CONFIG) --exists libmicrohttpd || \
(echo "libmicrohttpd development package is required for make linux" >&2; exit 1)
gen:
mkdir gen
clean:
rm -rf $(BIN) $(LINUX_BIN) gen ps5-obj linux-obj
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)
$(STRIP) $@
$(LINUX_BIN): $(LINUX_SRCS) $(GEN_SRCS) $(LINUX_TP_OBJS)
$(HOST_CC) $(LINUX_CFLAGS) -o $@ $(filter %.c,$^) $(LINUX_TP_OBJS) $(LINUX_LDADD)
$(HOST_STRIP) $@
+267
View File
@@ -0,0 +1,267 @@
# PS5 Web File Manager
> Homebrew HTTP file manager for jailbroken PS5 consoles. Browse, edit, upload, download and extract ZIPs through any browser on the same network — single self-contained ELF payload, no external services, no telemetry.
**Version:** v1.7 · **Title ID:** `FMGR88888` · **License:** GPLv3+ · **Target:** `x86_64-sie-ps5`
---
## 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.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.
## Screenshots
<p>
<a href="docs/screenshots/20260617_231827.376.jpg" target="_blank"><img src="docs/screenshots/20260617_231827.376.jpg" width="31%" alt="PS5 Web File Manager screenshot 1"></a>
<a href="docs/screenshots/20260619_131432.399.jpg" target="_blank"><img src="docs/screenshots/20260619_131432.399.jpg" width="31%" alt="PS5 Web File Manager screenshot 2"></a>
<a href="docs/screenshots/20260617_232348.855.jpg" target="_blank"><img src="docs/screenshots/20260617_232348.855.jpg" width="31%" alt="PS5 Web File Manager screenshot 3"></a>
<a href="docs/screenshots/20260619_131811.644.jpg" target="_blank"><img src="docs/screenshots/20260619_131811.644.jpg" width="31%" alt="PS5 Web File Manager screenshot 4"></a>
<a href="docs/screenshots/20260619_131535.239.jpg" target="_blank"><img src="docs/screenshots/20260619_131535.239.jpg" width="31%" alt="PS5 Web File Manager screenshot 5"></a>
<a href="docs/screenshots/20260620_232728.533.jpg" target="_blank"><img src="docs/screenshots/20260620_232728.533.jpg" width="31%" alt="PS5 Web File Manager screenshot 6"></a>
</p>
## 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.
- **ZIP extraction** — see the [dedicated section](#zip-extraction) below.
- **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.
## Build
Requires [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:
```sh
make
```
If the build host has no network access, drop the libmicrohttpd tarball in advance and run the installer manually:
```sh
LIBMICROHTTPD_TARBALL=/path/to/libmicrohttpd-1.0.1.tar.gz \
./install-libmicrohttpd.sh
make
```
Output:
```text
web-file-mgr.elf (~418 KiB, x86_64-sie-ps5)
```
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:
```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:
```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.
## Verification
After `make`, sanity-check the produced ELF:
```sh
ls -la web-file-mgr.elf # size ~418 KiB
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 — **69 checks** on the current `main`. 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.
## Project layout
```
.
├── Makefile # PS5 + Linux builds
├── 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 zip_extract.{c,h} # /api/extract + standalone engine
│ └── app_installer.c # PS5 Media launcher installer
├── third_party/ # vendored: zlib, minizip-ng
├── tests/ # POSIX/host test suite
│ ├── test_zip_extract.c
│ ├── make_fixtures.py # regenerate test fixtures
│ ├── run-tests.sh # one-shot runner
│ ├── compat/ # tiny Win32/MSYS shims
│ └── fixtures/ # generated test ZIPs
├── docs/screenshots/ # README screenshot images
├── THIRD_PARTY_NOTICES # bundled-library credits
├── LICENSE # GPLv3+
└── README.md
```
## Notes
- Copy, move, delete, upload and download 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.
- 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.
## 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 ZIP caps are 64 GiB per entry / 200:1 ratio. Confirm the large-file prompt (appears for archives > 60 GiB on disk), shrink the archive, or pass `large=1` directly to the API.
## 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/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.
## License
The project is distributed under **GPLv3 or later**, matching the GPLv3+ projects used as implementation references. See [`LICENSE`](./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.
## 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.
+27
View File
@@ -0,0 +1,27 @@
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)
Both libraries 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. See the individual LICENSE files for
the complete terms.
Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+199
View File
@@ -0,0 +1,199 @@
<!doctype html>
<html>
<head>
<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>
<button id="exitBtn" class="icon-button power-button" type="button" aria-label="Exit"></button>
</header>
<section class="toolbar">
<div class="tool-left">
<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="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>
</tr>
</thead>
<tbody id="files"></tbody>
</table>
<div id="empty" class="empty" data-i18n="empty" hidden></div>
<div id="contentLoading" class="content-loading" hidden>
<div class="content-loading-text" data-i18n="readDir"></div>
</div>
</section>
<footer class="status">
<div id="statusText" data-i18n="ready"></div>
<div id="versionText" class="version-text"></div>
</footer>
</main>
<div id="textEditorOverlay" class="text-editor-overlay" hidden>
<section class="text-editor-panel">
<div id="textEditorPath" class="text-editor-path"></div>
<textarea id="textEditor" class="text-editor" wrap="off" spellcheck="false"
autocomplete="off" autocorrect="off" autocapitalize="off"></textarea>
<div id="textEditorStatus" class="text-editor-status"></div>
<div class="text-editor-actions">
<button id="textEditorCloseBtn" class="secondary" data-i18n="close"></button>
<button id="textEditorSaveBtn" class="primary" data-i18n="save"></button>
</div>
</section>
</div>
<div id="imagePreviewOverlay" class="text-editor-overlay" hidden>
<section class="text-editor-panel image-preview-panel">
<div id="imagePreviewName" class="text-editor-path"></div>
<div class="image-preview-stage">
<img id="imagePreview" class="image-preview" alt="">
</div>
<button id="imagePreviewCloseBtn" class="secondary" data-i18n="close"></button>
</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>
<script src="/main.js"></script>
</body>
</html>
+195
View File
@@ -0,0 +1,195 @@
window.WFM_LANG = {
appTitle: "PS5 Web File Manager",
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",
type: "Type",
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",
queued: "Queued",
running: "Running",
done: "Done",
failed: "Failed",
canceled: "Canceled",
finishing: "Finishing",
preparing: "Preparing",
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",
durationMinutes: "{minutes}m {seconds}s",
durationSeconds: "{seconds}s",
preparingTask: "Preparing task",
canceling: "Canceling...",
selectedItems: "{name} and {count} items",
countItems: "{count} items",
totalItems: "{count} items",
readDir: "Reading folder...",
processing: "Processing...",
actionBusy: "{label}...",
taskCreated: "{label} task created",
actionDone: "{label} done",
actionFailed: "{label} failed: {error}",
taskFailed: "{label} failed: {error}",
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",
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...",
preparingStatus: "{label} preparing...",
cancelPrepare: "Preparation canceled",
cancelTaskConfirm: "Cancel current {label} task?",
cancelFailed: "Cancel failed: {error}",
tasksPollFailed: "Failed to read task status: {error}",
transferComplete: "{label} completed: {name}\n\nTotal time: {duration}\nTransferred: {size}",
transferCompleteFiles: "{label} completed: {name}\n\nTotal time: {duration}\nTransferred: {size}\nFiles: {count}",
renamePrompt: "New name",
mkdirPrompt: "Folder name",
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.",
loadingText: "Loading text...",
savingText: "Saving...",
textSaved: "Text saved",
openTextFailed: "Failed to open text: {error}",
saveTextFailed: "Failed to save text: {error}",
deleteConfirm: "Delete {name}?",
deleteConfirmRecursive: "Delete {name}?\n\nFolders will be deleted recursively.",
activeTask: "A task is running",
pasteTitle: "{label} {name} to {path}",
pasteToCurrent: "to current folder",
backendError: "Operation failed",
storageInternal: "Internal",
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}",
err_target_check_failed: "Cannot check target path: {path}",
err_target_path_too_long: "Target path is too long",
err_path_too_long: "Path is too long",
err_space_check_failed: "Cannot read target free space: {path}",
err_no_space: "Not enough target space. Required {required}, available {available}",
err_active_task: "A task is running",
err_active_task_not_found: "No active task was found",
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",
err_text_type_not_editable: "This file type is not supported by the text editor",
err_text_file_too_large: "The text file is too large. The maximum size is 1 MiB",
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 is 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}"
};
+195
View File
@@ -0,0 +1,195 @@
window.WFM_LANG = {
appTitle: "PS5 Web File Manager",
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: "名称",
type: "类型",
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: "可用空间",
queued: "等待中",
running: "进行中",
done: "完成",
failed: "失败",
canceled: "已取消",
finishing: "完成中",
preparing: "准备中",
checking: "检查中",
pleaseWait: "请稍候",
speedLabel: "速度",
itemsPerSecond: "{count} 个对象/秒",
progressLabel: "进度",
permissionProgress: "{done} / {total} 个对象",
etaLabel: "剩余",
elapsedLabel: "耗时",
durationHours: "{hours}小时 {minutes}分",
durationMinutes: "{minutes}分 {seconds}秒",
durationSeconds: "{seconds}秒",
preparingTask: "正在准备任务",
canceling: "正在取消...",
selectedItems: "{name} 等 {count} 项",
countItems: "{count} 项",
totalItems: "共 {count} 项",
readDir: "读取目录中...",
processing: "处理中...",
actionBusy: "{label}...",
taskCreated: "{label}任务已创建",
actionDone: "{label}完成",
actionFailed: "{label}失败: {error}",
taskFailed: "{label}失败: {error}",
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: "此任务已启用大文件模式",
sameSourceTarget: "源和目标相同,不能{label} {name}",
removeConflictFirst: "目标中已存在同名{existingType} {name}。要{label}{sourceType},请先删除该{existingType}才能继续。",
overwriteFiles: "同名文件将被覆盖: {names}",
mergeDirs: "同名目录将被合并,内部同名文件会被覆盖: {names}",
continueConfirm: "继续?",
preparingPaste: "准备中:正在计算大小并检查目标空间...",
preparingStatus: "{label}准备中...",
cancelPrepare: "已取消准备操作",
cancelTaskConfirm: "取消当前{label}任务?",
cancelFailed: "取消失败: {error}",
tasksPollFailed: "任务状态读取失败: {error}",
transferComplete: "{label}完成:{name}\n\n总耗时:{duration}\n传输大小:{size}",
transferCompleteFiles: "{label}完成:{name}\n\n总耗时:{duration}\n传输大小:{size}\n文件数量:{count}",
renamePrompt: "新名称",
mkdirPrompt: "目录名",
newTextPrompt: "文本文件名",
creatingText: "正在新建文本...",
createTextFailed: "新建文本失败: {error}",
unsavedSaveConfirm: "文本有未保存的修改,是否不保存并关闭?\n\n取消将留在编辑器中。",
loadingText: "正在读取文本...",
savingText: "正在保存...",
textSaved: "文本已保存",
openTextFailed: "打开文本失败: {error}",
saveTextFailed: "保存文本失败: {error}",
deleteConfirm: "确认删除 {name}?",
deleteConfirmRecursive: "确认删除 {name}?\n\n目录会递归删除。",
activeTask: "有任务正在执行",
pasteTitle: "{label} {name} 到 {path}",
pasteToCurrent: "到当前目录",
backendError: "操作失败",
storageInternal: "内部存储",
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}",
err_target_check_failed: "无法检查目标路径: {path}",
err_target_path_too_long: "目标路径过长",
err_path_too_long: "路径过长",
err_space_check_failed: "无法读取目标剩余空间: {path}",
err_no_space: "目标空间不足,需要 {required},可用 {available}",
err_active_task: "有任务正在执行",
err_active_task_not_found: "没有找到正在执行的任务",
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: "请求方式无效",
err_text_type_not_editable: "该文件类型不支持文本编辑",
err_text_file_too_large: "文本文件过大,最大支持 1 MiB",
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): {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}"
};
+1513
View File
File diff suppressed because it is too large. Load diff
+2562
View File
File diff suppressed because it is too large. Load diff
+17
View File
@@ -0,0 +1,17 @@
{
"applicationCategoryType": 65536,
"titleId": "FMGR88888",
"deeplinkUri": "http://127.0.0.1:8888/",
"localizedParameters": {
"defaultLanguage": "en-US",
"en-US": {
"titleName": "Web File Manager"
},
"zh-Hans": {
"titleName": "文件管理器网页"
},
"zh-Hant": {
"titleName": "文件管理器网页"
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
# encoding: utf-8
# Copyright (C) 2024 John Törnblom
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; see the file COPYING. If not see
# <http://www.gnu.org/licenses/>.
import argparse
import gzip
import string
import mimetypes
tmpl = string.Template('''
void asset_register(const char*, const void*, unsigned long, const char*, const char*);
static const unsigned char data[] = $data;
__attribute__((constructor)) static void
constructor(void) {
asset_register("/$path", data, sizeof(data), $mime, $encoding);
}
''')
GZIP_MIMES = {
'application/javascript',
'application/json',
'text/css',
'text/html',
'text/javascript',
}
def cstr(value):
if value is None:
return '0'
return '"' + value.replace('\\', '\\\\').replace('"', '\\"') + '"'
def gen_data(data):
yield '{\n '
for n, b in enumerate(data, 1):
yield hex(b)
yield ', '
if n % 16 == 0:
yield '\n '
yield '\n}'
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-p', '--path', default=None)
parser.add_argument('FILE')
args = parser.parse_args()
if args.path is None:
args.path = args.FILE
mime = mimetypes.guess_type(args.path)[0]
encoding = None
with open(args.FILE, mode='rb') as f:
payload = f.read()
if mime in GZIP_MIMES:
zipped = gzip.compress(payload, compresslevel=9, mtime=0)
if len(zipped) < len(payload):
payload = zipped
encoding = 'gzip'
data = ''.join(gen_data(payload))
print(tmpl.substitute(data=data, path=args.path,
mime=cstr(mime), encoding=cstr(encoding)))
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -euo pipefail
LIB_VER="${LIBMICROHTTPD_VERSION:-1.0.1}"
LIB_URL="${LIBMICROHTTPD_URL:-https://ftp.gnu.org/gnu/libmicrohttpd/libmicrohttpd-${LIB_VER}.tar.gz}"
LIB_TARBALL="${LIBMICROHTTPD_TARBALL:-}"
if [[ -z "${PS5_PAYLOAD_SDK:-}" ]]; then
echo "error: PS5_PAYLOAD_SDK is not set" >&2
exit 1
fi
source "${PS5_PAYLOAD_SDK}/toolchain/prospero.sh"
if "${PS5_PAYLOAD_SDK}/bin/prospero-pkg-config" --exists libmicrohttpd; then
echo "libmicrohttpd is already available in PS5_PAYLOAD_SDK"
exit 0
fi
tmpdir="$(mktemp -d)"
trap 'rm -rf -- "${tmpdir}"' EXIT
archive="${tmpdir}/libmicrohttpd-${LIB_VER}.tar.gz"
if [[ -n "${LIB_TARBALL}" ]]; then
cp "${LIB_TARBALL}" "${archive}"
else
if command -v wget >/dev/null 2>&1; then
wget -O "${archive}" "${LIB_URL}"
elif command -v curl >/dev/null 2>&1; then
curl -L -o "${archive}" "${LIB_URL}"
else
echo "error: install wget or curl, or set LIBMICROHTTPD_TARBALL" >&2
exit 1
fi
fi
tar xf "${archive}" -C "${tmpdir}"
cd "${tmpdir}/libmicrohttpd-${LIB_VER}"
export CFLAGS="${CFLAGS:-} -O1"
./configure --prefix="${PREFIX}" \
--host=x86_64-pc-freebsd \
--enable-static \
--disable-shared \
--disable-doc \
--disable-curl \
--disable-examples
"${MAKE:-make}"
"${MAKE:-make}" install
if [[ -n "${PS5_CROSS_FIX_ROOT:-}" ]]; then
"${PS5_CROSS_FIX_ROOT}" "${DESTDIR}/${PREFIX}"
fi
"${PS5_PAYLOAD_SDK}/bin/prospero-pkg-config" --exists libmicrohttpd
echo "libmicrohttpd installed for ps5-payload-sdk"
+137
View File
@@ -0,0 +1,137 @@
#include "app_installer.h"
#include <errno.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.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" \
".global " #name "_end\n" \
".global " #name "_size\n" \
".align 16\n" \
#name ":\n" \
".incbin \"" file "\"\n" \
#name "_end:\n" \
#name "_size:\n" \
".quad " #name "_end - " #name "\n" \
".previous\n"); \
extern const uint8_t name[]; \
extern const size_t name##_size;
INCASSET(param_json, "assets/param.json");
INCASSET(icon0_png, "assets/icon0.png");
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"))) {
return -1;
}
if(fwrite(data, size, 1, f) != 1) {
fclose(f);
return -1;
}
fclose(f);
return 0;
}
static int
install_app(const char *title_id, const char *dir) {
int (*sceAppInstUtilAppInstallTitleDir)(const char *, const char *, void *) = 0;
const char *nid = "Wudg3Xe3heE";
uint32_t handle;
if(!kernel_dynlib_handle(-1, "libSceAppInstUtil.sprx", &handle)) {
sceAppInstUtilAppInstallTitleDir =
(void *)kernel_dynlib_resolve(-1, handle, nid);
}
if(sceAppInstUtilAppInstallTitleDir) {
return sceAppInstUtilAppInstallTitleDir(title_id, dir, 0);
}
return sceAppInstUtilAppInstallAll(0);
}
static int
needs_install_file(const char *path) {
struct stat st;
if(stat(path, &st)) {
return 1;
}
return 0;
}
int
app_install_if_needed(void) {
const char *title_id = TITLE_ID;
char base_dir[256];
char sce_sys_dir[256];
char param_path[256];
char icon_path[256];
struct stat st;
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);
snprintf(icon_path, sizeof(icon_path), "%s/icon0.png", sce_sys_dir);
if(stat(base_dir, &st)) {
update_needed = 1;
} else if(needs_install_file(param_path) || needs_install_file(icon_path)) {
update_needed = 1;
}
if(!update_needed) {
return 0;
}
printf("Installing launcher app %s\n", title_id);
if((err = pkg_installer_initialize())) {
printf("sceAppInstUtilInitialize: error 0x%08X\n", err);
return -1;
}
if(mkdir(base_dir, 0755) && errno != EEXIST) {
perror("mkdir app dir");
return -1;
}
if(mkdir(sce_sys_dir, 0755) && errno != EEXIST) {
perror("mkdir sce_sys dir");
return -1;
}
if(install_file(param_path, param_json, param_json_size) ||
install_file(icon_path, icon0_png, icon0_png_size)) {
perror("install launcher assets");
return -1;
}
if((err = install_app(title_id, "/user/app/"))) {
printf("install_app: error 0x%08X\n", err);
return -1;
}
return 0;
}
+3
View File
@@ -0,0 +1,3 @@
#pragma once
int app_install_if_needed(void);
+120
View File
@@ -0,0 +1,120 @@
/* Copyright (C) 2024 John Törnblom
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 3, or (at your option) any
later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; see the file COPYING. If not, see
<http://www.gnu.org/licenses/>. */
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <microhttpd.h>
#include "asset.h"
#include "websrv.h"
/**
* File not found (404)
**/
#define PAGE_404 \
"<html>" \
"<head>" \
"<title>File not found</title>" \
"</head>" \
"<body>File not found</body>" \
"</html>"
typedef struct asset {
const char *path;
const char *mime;
const char *encoding;
const void *data;
size_t size;
struct asset *next;
} asset_t;
static asset_t* g_asset_head = 0;
static void
asset_normalize_path(const char *url, char* path) {
char* ptr = path;
for(size_t i=0; i<strlen(url); i++) {
if(url[i] == '/' && url[i+1] == '/') {
continue;
}
*ptr = url[i];
ptr++;
}
*ptr = '\0';
}
void
asset_register(const char* path, const void* data, size_t size,
const char* mime, const char* encoding) {
asset_t* a = calloc(1, sizeof(asset_t));
a->path = path;
a->mime = mime;
a->encoding = encoding;
a->data = data;
a->size = size;
a->next = g_asset_head;
g_asset_head = a;
}
enum MHD_Result
asset_request(struct MHD_Connection *conn, const char* url) {
unsigned int status = MHD_HTTP_NOT_FOUND;
enum MHD_Result ret = MHD_NO;
size_t size = strlen(PAGE_404);
struct MHD_Response *resp;
const void* data = PAGE_404;
const char* mime = 0;
const char* encoding = 0;
char path[PATH_MAX];
asset_normalize_path(url, path);
for(asset_t* a=g_asset_head; a!=0; a=a->next) {
if(!strcmp(path, a->path)) {
data = a->data;
size = a->size;
mime = a->mime;
encoding = a->encoding;
status = MHD_HTTP_OK;
break;
}
}
if((resp=MHD_create_response_from_buffer(size, (void *)data,
MHD_RESPMEM_PERSISTENT))) {
if(mime) {
MHD_add_response_header(resp, MHD_HTTP_HEADER_CONTENT_TYPE, mime);
}
if(encoding) {
MHD_add_response_header(resp, MHD_HTTP_HEADER_CONTENT_ENCODING, encoding);
}
ret = websrv_queue_response(conn, status, resp);
MHD_destroy_response(resp);
}
return ret;
}
+32
View File
@@ -0,0 +1,32 @@
/* Copyright (C) 2024 John Törnblom
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 3, or (at your option) any
later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; see the file COPYING. If not, see
<http://www.gnu.org/licenses/>. */
#pragma once
#include <microhttpd.h>
/**
* Respond to an asset request.
**/
enum MHD_Result asset_request(struct MHD_Connection *conn,
const char* url);
/**
* Register the presence of an asset.
**/
void asset_register(const char* path, const void* data, size_t size,
const char* mime, const char* encoding);
+761
View File
@@ -0,0 +1,761 @@
#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;
}
+221
View File
@@ -0,0 +1,221 @@
#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 "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);
}
/* 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 = zipx_extract(task->src, task->dst, conflict,
zipx_limits_profile(task->extract_large),
extract_cancel, extract_progress, task, &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");
}
+22
View File
@@ -0,0 +1,22 @@
#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);
+68
View File
@@ -0,0 +1,68 @@
#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;
}
+2458
View File
File diff suppressed because it is too large. Load diff
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <microhttpd.h>
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);
+134
View File
@@ -0,0 +1,134 @@
#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
View File
@@ -0,0 +1,371 @@
#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;
}
+90
View File
@@ -0,0 +1,90 @@
#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, "\"");
}
+14
View File
@@ -0,0 +1,14 @@
#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
View File
@@ -0,0 +1,84 @@
#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");
}
+160
View File
@@ -0,0 +1,160 @@
#include <signal.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#ifdef __SCE__
#include <sys/syscall.h>
#include <sys/sysctl.h>
#endif
#include "app_installer.h"
#include "notify.h"
#include "websrv.h"
#define PROCESS_NAME "web-file-mgr.elf"
#define DEFAULT_PORT 8888
static int
port_available(unsigned short port) {
struct sockaddr_in addr;
int fd;
int ret;
if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
return -1;
}
if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int)) < 0) {
perror("setsockopt");
close(fd);
return -1;
}
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(port);
ret = !bind(fd, (struct sockaddr *)&addr, sizeof(addr));
close(fd);
return ret;
}
static unsigned short
find_available_port(unsigned short start) {
unsigned int port;
for(port = start; port <= 65535; port++) {
int available = port_available((unsigned short)port);
if(available < 0) {
return 0;
}
if(available) {
return (unsigned short)port;
}
}
return 0;
}
#ifdef __SCE__
static pid_t
find_pid(const char *name) {
int mib[4] = {1, 14, 8, 0};
pid_t mypid = getpid();
pid_t pid = -1;
size_t buf_size;
uint8_t *buf;
if(sysctl(mib, 4, 0, &buf_size, 0, 0)) {
perror("sysctl");
return -1;
}
if(!(buf = malloc(buf_size))) {
perror("malloc");
return -1;
}
if(sysctl(mib, 4, buf, &buf_size, 0, 0)) {
perror("sysctl");
free(buf);
return -1;
}
for(uint8_t *ptr = buf; ptr < buf + buf_size;) {
int ki_structsize = *(int *)ptr;
pid_t ki_pid = *(pid_t *)&ptr[72];
char *ki_tdname = (char *)&ptr[447];
ptr += ki_structsize;
if(!strcmp(name, ki_tdname) && ki_pid != mypid) {
pid = ki_pid;
}
}
free(buf);
return pid;
}
#endif
int
main(int argc, char **argv) {
unsigned short port;
#ifdef __SCE__
unsigned short notified_port = 0;
pid_t pid;
#endif
(void)argc;
(void)argv;
#ifdef __SCE__
syscall(SYS_thr_set_name, -1, PROCESS_NAME);
while((pid = find_pid(PROCESS_NAME)) > 0) {
if(kill(pid, SIGKILL)) {
perror("kill");
return 1;
}
sleep(1);
}
#else
#endif
puts(PROCESS_NAME);
printf("version: %s\n", VERSION_TAG);
#ifdef __SCE__
app_install_if_needed();
#endif
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
while(1) {
port = find_available_port(DEFAULT_PORT);
if(!port) {
fprintf(stderr, "no available port from %u\n", DEFAULT_PORT);
sleep(3);
continue;
}
printf("listening on port %u\n", port);
#ifdef __SCE__
if(notified_port != port) {
notify_user("Web File Manager\nVersion: %s\nPort: %u", VERSION_TAG, port);
notified_port = port;
}
#endif
websrv_listen(port);
if(websrv_stop_requested()) {
break;
}
sleep(3);
}
return 0;
}
+49
View File
@@ -0,0 +1,49 @@
#include "mime.h"
#include <strings.h>
typedef struct mime_entry {
const char *ext;
const char *mime;
} mime_entry_t;
static const mime_entry_t g_mimes[] = {
{"bmp", "image/bmp"},
{"css", "text/css"},
{"elf", "application/octet-stream"},
{"gif", "image/gif"},
{"html", "text/html"},
{"jpeg", "image/jpeg"},
{"jpg", "image/jpeg"},
{"js", "application/javascript"},
{"json", "application/json"},
{"log", "text/plain"},
{"md", "text/plain"},
{"png", "image/png"},
{"txt", "text/plain"},
{"xml", "text/xml"},
{"webp", "image/webp"},
{"zip", "application/zip"},
};
const char *
mime_get_type(const char *filename) {
const char *ext = 0;
size_t i;
for(const char *p = filename; *p; p++) {
if(*p == '.') {
ext = p + 1;
}
}
if(!ext || !*ext) {
return "application/octet-stream";
}
for(i = 0; i < sizeof(g_mimes) / sizeof(g_mimes[0]); i++) {
if(!strcasecmp(ext, g_mimes[i].ext)) {
return g_mimes[i].mime;
}
}
return "application/octet-stream";
}
+23
View File
@@ -0,0 +1,23 @@
/* Copyright (C) 2025 John Törnblom
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 3, or (at your option) any
later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; see the file COPYING. If not, see
<http://www.gnu.org/licenses/>. */
#pragma once
/**
* Lookup the mime type of a file.
**/
const char* mime_get_type(const char *filename);
+32
View File
@@ -0,0 +1,32 @@
#include "notify.h"
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#ifdef __SCE__
typedef struct notify_request {
char unused[45];
char message[3075];
} notify_request_t;
int sceKernelSendNotificationRequest(int device, notify_request_t *request,
size_t size, int unused);
#endif
void
notify_user(const char *fmt, ...) {
#ifdef __SCE__
notify_request_t req;
va_list args;
memset(&req, 0, sizeof(req));
va_start(args, fmt);
vsnprintf(req.message, sizeof(req.message), fmt, args);
va_end(args);
sceKernelSendNotificationRequest(0, &req, sizeof(req), 0);
#else
(void)fmt;
#endif
}
+3
View File
@@ -0,0 +1,3 @@
#pragma once
void notify_user(const char *fmt, ...);
+401
View File
@@ -0,0 +1,401 @@
#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;
}
+24
View File
@@ -0,0 +1,24 @@
#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
View File
@@ -0,0 +1,591 @@
#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;
}
+6
View File
@@ -0,0 +1,6 @@
#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);
+119
View File
@@ -0,0 +1,119 @@
#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
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#define PKG_INSTALL_UNSUPPORTED 0x7fffffff
int pkg_installer_initialize(void);
int pkg_installer_install(const char *path);
+128
View File
@@ -0,0 +1,128 @@
#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
View File
@@ -0,0 +1,221 @@
#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
View File
@@ -0,0 +1,445 @@
#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, &current, &current_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
View File
@@ -0,0 +1,582 @@
#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);
}
+266
View File
@@ -0,0 +1,266 @@
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <microhttpd.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/socket.h>
#include <unistd.h>
#include "asset.h"
#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));
}
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\":\"\"}";
struct MHD_Response *resp =
MHD_create_response_from_buffer(sizeof(json) - 1, (void *)json,
MHD_RESPMEM_PERSISTENT);
enum MHD_Result ret;
if(!resp) {
return MHD_NO;
}
MHD_add_response_header(resp, MHD_HTTP_HEADER_CONTENT_TYPE,
"application/json");
ret = websrv_queue_response(conn, MHD_HTTP_CONTENT_TOO_LARGE, resp);
MHD_destroy_response(resp);
return ret;
}
enum MHD_Result
websrv_queue_response(struct MHD_Connection *conn, unsigned int status,
struct MHD_Response *resp) {
MHD_add_response_header(resp, MHD_HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, "*");
MHD_add_response_header(resp, MHD_HTTP_HEADER_CACHE_CONTROL, "no-store");
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,
const char *upload_data, size_t *upload_data_size,
void **con_cls) {
request_context_t *ctx = *con_cls;
(void)cls;
(void)version;
if(strcmp(method, MHD_HTTP_METHOD_GET) &&
strcmp(method, MHD_HTTP_METHOD_POST) &&
strcmp(method, MHD_HTTP_METHOD_HEAD)) {
return MHD_NO;
}
if(!ctx) {
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;
}
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) {
char *body = realloc(ctx->body, ctx->size + chunk_size + 1);
if(!body) {
return MHD_NO;
}
ctx->body = body;
memcpy(ctx->body + ctx->size, upload_data, chunk_size);
ctx->size += chunk_size;
ctx->body[ctx->size] = 0;
}
*upload_data_size = 0;
return MHD_YES;
}
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);
}
if(!strncmp(url, "/api/", 5)) {
return filemgr_api_request(conn, url, method, ctx->body, ctx->size);
}
if(!strcmp(url, "/fs")) {
return filemgr_fs_request(conn);
}
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);
}
static void
websrv_on_completed(void *cls, struct MHD_Connection *connection,
void **con_cls, enum MHD_RequestTerminationCode toe) {
(void)cls;
(void)connection;
(void)toe;
request_context_t *ctx = *con_cls;
if(ctx) {
if(ctx->upload_ctx) {
filemgr_upload_free(ctx->upload_ctx);
}
free(ctx->body);
free(ctx);
}
*con_cls = NULL;
}
int
websrv_listen(unsigned short port) {
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
struct MHD_Daemon *httpd;
socklen_t addr_len;
int connfd;
int srvfd;
signal(SIGPIPE, SIG_IGN);
if((srvfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
return -1;
}
if(setsockopt(srvfd, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int)) < 0) {
perror("setsockopt");
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;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(port);
if(bind(srvfd, (struct sockaddr *)&server_addr, sizeof(server_addr))) {
perror("bind");
close(srvfd);
return -1;
}
if(listen(srvfd, 16)) {
perror("listen");
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,
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");
close(srvfd);
return -1;
}
while(!g_stop_requested) {
addr_len = sizeof(client_addr);
if((connfd = accept(srvfd, (struct sockaddr *)&client_addr, &addr_len)) < 0) {
if(!g_stop_requested) 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");
close(connfd);
break;
}
}
MHD_stop_daemon(httpd);
g_listen_fd = -1;
return close(srvfd);
}
+27
View File
@@ -0,0 +1,27 @@
/* Copyright (C) 2024 John Törnblom
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 3, or (at your option) any
later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; see the file COPYING. If not, see
<http://www.gnu.org/licenses/>. */
#pragma once
#include <microhttpd.h>
enum MHD_Result websrv_queue_response(struct MHD_Connection *conn,
unsigned int status,
struct MHD_Response *resp);
int websrv_listen(unsigned short port);
void websrv_stop(void);
int websrv_stop_requested(void);
+1303
View File
File diff suppressed because it is too large. Load diff
+104
View File
@@ -0,0 +1,104 @@
#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);
+34
View File
@@ -0,0 +1,34 @@
/* 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
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
this is definitely not a zip file
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+227
View File
@@ -0,0 +1,227 @@
#!/usr/bin/env python3
"""Generate the ZIP fixtures used by test_zip_extract."""
import os
import shutil
import stat
import struct
import sys
import zipfile
import zlib
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(HERE, "fixtures")
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 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):
fn()
print("fixtures written to %s" % OUT)
return 0
if __name__ == "__main__":
sys.exit(main())
+33
View File
@@ -0,0 +1,33 @@
/* 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
+262
View File
@@ -0,0 +1,262 @@
/* 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 */
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Host test runner for the ZIP extraction engine.
# Builds the vendored minizip-ng/zlib sources, the engine and the test suite
# with the host compiler and runs the 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)
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
COMPAT_INC="$ROOT/tests/compat"
# The engine and the test suite 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/src" \
-I"$COMPAT_INC" -include "$ROOT/tests/posix_compat.h" \
-o "$BUILD/test_zip_extract.o" "$ROOT/tests/test_zip_extract.c"
objs=()
for obj in "$BUILD"/*.o; do
case "$obj" in
*/zip_extract.o|*/test_zip_extract.o) continue ;;
esac
objs+=("$obj")
done
"$CC" -O2 -o "$BUILD/test-zip-extract" \
"$BUILD/zip_extract.o" "$BUILD/test_zip_extract.o" "${objs[@]}"
"$BUILD/test-zip-extract" "$ROOT/tests/fixtures" "$BUILD/work"
+639
View File
@@ -0,0 +1,639 @@
/* 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;
}
+17
View File
@@ -0,0 +1,17 @@
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.
+1
View File
@@ -0,0 +1 @@
minizip-ng 4.2.2 (https://github.com/zlib-ng/minizip-ng)
+277
View File
@@ -0,0 +1,277 @@
/* 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
View File
@@ -0,0 +1,36 @@
/* 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
View File
@@ -0,0 +1,65 @@
/* 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
View File
@@ -0,0 +1,201 @@
/* 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
View File
@@ -0,0 +1,135 @@
/* 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
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff. Show more