From de9cc5a3db53fc09ab82c11bcf11038e54880772 Mon Sep 17 00:00:00 2001 From: dev9-bb Date: Fri, 23 Oct 2026 14:31:45 +0800 Subject: [PATCH] test: cover scoped upload root path regression --- README.testing.md | 2 + tests/test_scoped_upload_paths.py | 71 +++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 tests/test_scoped_upload_paths.py diff --git a/README.testing.md b/README.testing.md index 3ddac5ac..effef3c9 100644 --- a/README.testing.md +++ b/README.testing.md @@ -14,6 +14,7 @@ ```bash ./tests/cf-fileop/run.sh python3 tests/cf-lock/run.py +python3 tests/test_scoped_upload_paths.py ./tests/cf-dir-share/run.sh cd fileserver @@ -31,6 +32,7 @@ go test -count=1 -run 'Cf[A-Z]' . |---|---|---| | `tests/cf-fileop/run.sh` | 路径/操作词汇、provider 分发、空 provider 透传、C 写入口字段与参数形状 | 运行时变量值是否传对、完整 seaf-server 编译 | | `tests/cf-dir-share/run.sh` | 目录共享资格的请求形状、路径规范化、r/rw 映射,以及拒绝/故障/缺字段的判别 | 真实 Hub HTTP、JWT 与进程间调用(channel 为 stub) | +| `python3 tests/test_scoped_upload_paths.py` | 编译实际 C 路径校验函数;上传根目录、下载根目录拒绝及非法路径边界;RPC 调用接线检查 | 真实用户授权、完整 Server 编译及上传链路 | | Go `Cf*` 测试 | C/Go 词汇、JSON 字段、错误码、共享用例 | MySQL 依赖的上游测试 | | `tests/cf-s3/run.sh` | C commit/fs/block S3 backend 与错误语义 | 未设置端点时会跳过,不构成 S3 验收 | | `ci/run.py` | 上游构建、MySQL、C/Go fileserver 和 pytest | 需要 Linux 与完整依赖链 | diff --git a/tests/test_scoped_upload_paths.py b/tests/test_scoped_upload_paths.py new file mode 100644 index 00000000..c71bff08 --- /dev/null +++ b/tests/test_scoped_upload_paths.py @@ -0,0 +1,71 @@ +"""Compile production path validators; no server, credentials or file writes. + +Run: python3 tests/test_scoped_upload_paths.py +Requires a C compiler, pkg-config and GLib development headers. +This checks path admission, not Hub authorization or end-to-end upload. +""" +import os +from pathlib import Path +import shlex +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = (ROOT / "common/rpc-service.c").read_text() + + +def function(name): + start = SOURCE.index(name + " (") + opening = SOURCE.index("{", start) + depth, end = 1, opening + 1 + while depth: + depth += (SOURCE[end] == "{") - (SOURCE[end] == "}") + end += 1 + return SOURCE[start:end] + + +# Guard the RPC wiring too: a correct helper is insufficient if uploads still +# use the download predicate that rejects the library root. +assert "!cf_upload_dir_valid (path)" in function("seafile_web_get_scoped_upload_token") +assert "!cf_download_path_valid (path)" in function("seafile_web_get_scoped_download_token") +fixture = "#include \n#include \n" +for name in ("cf_download_path_valid", "cf_upload_dir_valid"): + fixture += "\nstatic gboolean\n" + function(name) + "\n" +fixture += r''' +int main (void) { + g_assert_true (cf_upload_dir_valid ("/")); + g_assert_false (cf_download_path_valid ("/")); + const char *valid[] = {"/folder", "/folder/child", "/中文目录", "/..archive"}; + for (gsize i = 0; i < G_N_ELEMENTS (valid); ++i) { + g_assert_true (cf_upload_dir_valid (valid[i])); + g_assert_true (cf_download_path_valid (valid[i])); + } + const char *invalid[] = {NULL, "", "relative", "//", "/folder/", "/a//b", + "/.", "/..", "/a/../b", "/a/./b", "/a\\b", "/a\nb", "/a\x7f", "/\xff"}; + for (gsize i = 0; i < G_N_ELEMENTS (invalid); ++i) { + g_assert_false (cf_upload_dir_valid (invalid[i])); + g_assert_false (cf_download_path_valid (invalid[i])); + } + char *boundary = g_malloc (4098); + memset (boundary, 'a', 4097); + boundary[0] = '/'; boundary[4096] = '\0'; + g_assert_true (cf_upload_dir_valid (boundary)); + g_assert_true (cf_download_path_valid (boundary)); + boundary[4096] = 'a'; boundary[4097] = '\0'; + g_assert_false (cf_upload_dir_valid (boundary)); + g_assert_false (cf_download_path_valid (boundary)); + g_free (boundary); + g_print ("21 path cases passed for upload and download; RPC wiring checked\n"); + return 0; +} +''' +with tempfile.TemporaryDirectory(prefix="cf-upload-paths-") as temp: + source = Path(temp) / "paths.c" + binary = Path(temp) / "paths" + source.write_text(fixture) + flags = shlex.split(subprocess.check_output( + ["pkg-config", "--cflags", "--libs", "glib-2.0"], text=True)) + subprocess.run(shlex.split(os.environ.get("CC", "cc")) + + ["-Wall", "-Wextra", "-Werror", str(source), "-o", str(binary)] + flags, + check=True) + subprocess.run([str(binary)], check=True)