mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-29 13:25:13 +08:00
🌴 Sync 2026-10-06 02:20:56
This commit is contained in:
1 parent
5c201dd3af
commit
487c4fad50
111 files changed
+4641
-746
No files matched your search
@@ -0,0 +1,209 @@
|
||||
-- protocol_registry_test.lua — 协议 × 目标格式的注册表一致性(防漂移)
|
||||
-- 用法:lua5.1 tests/protocol_registry_test.lua
|
||||
--
|
||||
-- 这个仓库最反复出现的失效模式是「协议清单散落多处,漏改一处 → 节点被静默丢弃」:
|
||||
-- 新增一个协议要同时改模型、解析器、若干输出模块和模板,而漏掉任何一处都**不报错**,
|
||||
-- 只是导出的订阅里少了那个节点(用户看到的仍是「导出成功」)。
|
||||
--
|
||||
-- 所以这里不逐个断言注册表常量(它们多是各模块内的 local,取值域也各不相同),
|
||||
-- 而是走**行为**:把每个协议的最小可用节点喂给每一种目标格式,断言「节点是否出现
|
||||
-- 在输出里」与下方 DROPPED 表完全一致。DROPPED 是唯一的显式声明,每条都注明
|
||||
-- 由哪个模块决定,便于审查它是「协议确实不支持」还是「漏接了」。
|
||||
--
|
||||
-- 两个方向都会红:
|
||||
-- * 新协议漏接某个输出 → 该格式下节点消失,但 DROPPED 里没写 → FAIL;
|
||||
-- * 某格式补上了对某协议的支持(或退化了)→ 与 DROPPED 不符 → FAIL;
|
||||
-- * 新增目标格式 → 格式清单直接取自 output.FORMAT_OPTIONS,新格式会被遍历到,
|
||||
-- 若它对某协议有过滤而表里没写 → FAIL。
|
||||
|
||||
package.path = "./root/usr/share/?.lua;" .. package.path
|
||||
|
||||
local util = require("substore.util")
|
||||
local node = require("substore.node")
|
||||
local output = require("substore.output")
|
||||
|
||||
local passed, failed = 0, 0
|
||||
|
||||
local function check(name, cond)
|
||||
if cond then
|
||||
passed = passed + 1
|
||||
print("PASS " .. name)
|
||||
else
|
||||
failed = failed + 1
|
||||
print("FAIL " .. name)
|
||||
end
|
||||
end
|
||||
|
||||
-- 每个协议的最小可用节点:只给身份字段,其余交给 node.normalize 补默认值。
|
||||
-- 样本缺身份字段时输出模块可能因校验而跳过该节点,那会把「协议不支持」与
|
||||
-- 「样本不全」混为一谈,测试就失去意义了。
|
||||
local SAMPLES = {
|
||||
vmess = { uuid = "u" },
|
||||
vless = { uuid = "u" },
|
||||
trojan = { password = "p" },
|
||||
shadowsocks = { method = "aes-256-gcm", password = "p" },
|
||||
ssr = { password = "p", method = "aes-128-cfb", protocol = "origin", obfs = "plain" },
|
||||
hysteria2 = { password = "p" },
|
||||
hysteria = { password = "p" },
|
||||
tuic = { uuid = "u", password = "p" },
|
||||
wireguard = { ["private-key"] = "k", ["public-key"] = "k2", ip = "10.0.0.2/32" },
|
||||
socks = { username = "u", password = "p" },
|
||||
anytls = { password = "p" },
|
||||
}
|
||||
|
||||
-- 分享链接的 scheme。URI 类格式(shadowrocket / v2rayuri)会把节点名编码进
|
||||
-- base64(vmess 甚至是「base64 里套 base64」),按名字查找必然漏判 ——
|
||||
-- 只能按 scheme 判断节点有没有被输出。
|
||||
local SCHEME = {
|
||||
vmess = "vmess://", vless = "vless://", trojan = "trojan://",
|
||||
shadowsocks = "ss://", ssr = "ssr://", hysteria2 = "hysteria2://",
|
||||
hysteria = "hysteria://", tuic = "tuic://", wireguard = "wireguard://",
|
||||
socks = "socks5://",
|
||||
anytls = "anytls://",
|
||||
}
|
||||
local URI_FORMATS = { shadowrocket = true, v2rayuri = true }
|
||||
|
||||
-- 目标格式清单取自唯一注册点 output.FORMAT_OPTIONS(不在这里另抄一份)
|
||||
local FORMATS = {}
|
||||
for _, opt in ipairs(output.FORMAT_OPTIONS) do FORMATS[#FORMATS + 1] = opt[1] end
|
||||
|
||||
-- 每个协议在哪些格式下**会被丢弃**,以及原因(决定它的模块)。
|
||||
-- 未列出的 (协议, 格式) 一律断言「节点必须出现在输出里」。
|
||||
local DROPPED = {
|
||||
-- output_wireguard_conf 只输出 wireguard 节点,其余一律过滤
|
||||
vmess = { wgconf = "wgconf 只收 wireguard" },
|
||||
vless = { clash = "原版 Clash 无 VLESS", wgconf = "wgconf 只收 wireguard" },
|
||||
trojan = { wgconf = "wgconf 只收 wireguard" },
|
||||
shadowsocks = { wgconf = "wgconf 只收 wireguard" },
|
||||
-- ssr:Surge/Surfboard/SurgeMac 不支持(surge_config 的 supports_ssr=false),
|
||||
-- Loon/Egern 支持;to_qx 只认 ss/vmess/vless/trojan;singbox 的 supported()
|
||||
-- 明确排除 ssr;v2ray 的 V2RAY_PROTOS 里没有 ssr。
|
||||
ssr = {
|
||||
surge = "Surge 无 SSR", surfboard = "Surfboard 无 SSR", surgemac = "SurgeMac 无 SSR",
|
||||
qx = "to_qx 只认 ss/vmess/vless/trojan", singbox = "singbox 不支持 ssr",
|
||||
v2ray = "V2RAY_PROTOS 无 ssr", wgconf = "wgconf 只收 wireguard",
|
||||
},
|
||||
-- hysteria2 / tuic / hysteria:原版 Clash、qx、v2ray 都不支持
|
||||
hysteria2 = { clash = "原版 Clash 无 hysteria2", qx = "to_qx 无 hysteria2",
|
||||
v2ray = "V2RAY_PROTOS 无 hysteria2", wgconf = "wgconf 只收 wireguard" },
|
||||
tuic = { clash = "原版 Clash 无 tuic", qx = "to_qx 无 tuic",
|
||||
v2ray = "V2RAY_PROTOS 无 tuic", wgconf = "wgconf 只收 wireguard" },
|
||||
hysteria = { clash = "原版 Clash 无 hysteria", qx = "to_qx 无 hysteria",
|
||||
v2ray = "V2RAY_PROTOS 无 hysteria", wgconf = "wgconf 只收 wireguard" },
|
||||
-- wireguard:Surge 家族需要专用的多段 [WireGuard] 配置,单行 [Proxy] 表达不了,
|
||||
-- surge_config 统一丢弃;原版 Clash / qx / v2ray 同样不支持。
|
||||
-- (shadowrocket / v2rayuri 会输出本项目自定义的 wireguard:// 链接 —— 这是既有
|
||||
-- 行为,此处如实记录,并不代表已验证该 scheme 被客户端接受。)
|
||||
wireguard = { clash = "原版 Clash 无 wireguard", surge = "Surge 单行表达不了",
|
||||
surfboard = "同 Surge", surgemac = "同 Surge", loon = "同 Surge", egern = "同 Surge",
|
||||
qx = "to_qx 无 wireguard", v2ray = "V2RAY_PROTOS 无 wireguard" },
|
||||
socks = { qx = "to_qx 无 socks", wgconf = "wgconf 只收 wireguard" },
|
||||
-- anytls:mihomo / Stash / sing-box 1.12+ / Shadowrocket / Surge 家族
|
||||
-- (Surge iOS 5.17.0+ 与 Mac 6.4.3+、Surfboard、Loon、Egern)以及
|
||||
-- QX 1.6.0+ 都支持;原版 Clash(Dreamacro)与 Xray/V2Ray 都不认识该协议
|
||||
-- (Xray issue #4428 以 not planned 关闭),wgconf 只收 wireguard。
|
||||
anytls = {
|
||||
clash = "原版 Clash 无 anytls", v2ray = "V2RAY_PROTOS 无 anytls",
|
||||
wgconf = "wgconf 只收 wireguard",
|
||||
},
|
||||
}
|
||||
|
||||
-- ---------- 表本身的自洽性(防止错别字 / 过期条目把测试变成空转) ----------
|
||||
local proto_set, fmt_set = {}, {}
|
||||
for _, p in ipairs(node.PROTOS) do proto_set[p] = true end
|
||||
for _, f in ipairs(FORMATS) do fmt_set[f] = true end
|
||||
|
||||
local unknown_proto = {}
|
||||
for p in pairs(DROPPED) do if not proto_set[p] then unknown_proto[#unknown_proto + 1] = p end end
|
||||
table.sort(unknown_proto)
|
||||
check("DROPPED keys are all in node.PROTOS"
|
||||
.. (#unknown_proto > 0 and (" (" .. table.concat(unknown_proto, ",") .. ")") or ""),
|
||||
#unknown_proto == 0)
|
||||
|
||||
local unknown_fmt = {}
|
||||
for _, p in pairs(DROPPED) do
|
||||
for f in pairs(p) do if not fmt_set[f] then unknown_fmt[f] = true end end
|
||||
end
|
||||
local uf = {}
|
||||
for f in pairs(unknown_fmt) do uf[#uf + 1] = f end
|
||||
table.sort(uf)
|
||||
check("DROPPED formats are all in FORMAT_OPTIONS"
|
||||
.. (#uf > 0 and (" (" .. table.concat(uf, ",") .. ")") or ""),
|
||||
#uf == 0)
|
||||
|
||||
-- 每个协议都要有样本与 scheme,否则下面的循环会静默跳过它(测试看着全绿其实漏测)
|
||||
local missing_sample, missing_scheme = {}, {}
|
||||
for _, p in ipairs(node.PROTOS) do
|
||||
if not SAMPLES[p] then missing_sample[#missing_sample + 1] = p end
|
||||
if not SCHEME[p] then missing_scheme[#missing_scheme + 1] = p end
|
||||
end
|
||||
check("every proto has a sample"
|
||||
.. (#missing_sample > 0 and (" (" .. table.concat(missing_sample, ",") .. ")") or ""),
|
||||
#missing_sample == 0)
|
||||
check("every proto has a scheme"
|
||||
.. (#missing_scheme > 0 and (" (" .. table.concat(missing_scheme, ",") .. ")") or ""),
|
||||
#missing_scheme == 0)
|
||||
check("FORMATS is non-empty", #FORMATS > 0)
|
||||
|
||||
-- ---------- 行为:逐个 (协议, 格式) 验证 ----------
|
||||
-- 名字带首尾标记,避免 MKhysteria 成为 MKhysteria2 的子串(子串匹配会误判)
|
||||
local function sample(proto)
|
||||
local n = { proto = proto, name = "MK" .. proto .. "X", server = "10.9.0.1", port = 443 }
|
||||
for k, v in pairs(SAMPLES[proto] or {}) do n[k] = v end
|
||||
return node.normalize(n)
|
||||
end
|
||||
|
||||
local function emitted(out, proto, fmt)
|
||||
if type(out) ~= "string" then return false end
|
||||
if URI_FORMATS[fmt] then
|
||||
local s = out
|
||||
if fmt == "shadowrocket" then
|
||||
local ok, dec = pcall(util.base64_decode, out)
|
||||
if not ok or type(dec) ~= "string" then return false end
|
||||
s = dec
|
||||
end
|
||||
return s:find(SCHEME[proto], 1, true) ~= nil
|
||||
end
|
||||
return out:find("MK" .. proto .. "X", 1, true) ~= nil
|
||||
end
|
||||
|
||||
local total_pairs = 0
|
||||
for _, proto in ipairs(node.PROTOS) do
|
||||
local drops = DROPPED[proto] or {}
|
||||
local bad = {}
|
||||
for _, fmt in ipairs(FORMATS) do
|
||||
total_pairs = total_pairs + 1
|
||||
local ok, out = pcall(output.generate, { sample(proto) }, fmt, {})
|
||||
local got, want
|
||||
if not ok then
|
||||
got, want = "error: " .. tostring(out), "kept"
|
||||
else
|
||||
got, want = emitted(out, proto, fmt), not drops[fmt]
|
||||
end
|
||||
if got ~= want then
|
||||
bad[#bad + 1] = fmt .. " want " .. (want and "kept" or "dropped")
|
||||
.. " got " .. tostring(got)
|
||||
end
|
||||
end
|
||||
check(proto .. " matches the format registry"
|
||||
.. (#bad > 0 and (" (" .. table.concat(bad, "; ") .. ")") or ""),
|
||||
#bad == 0)
|
||||
end
|
||||
check("registry covers every proto x format pair",
|
||||
total_pairs == #node.PROTOS * #FORMATS)
|
||||
|
||||
-- plain 是「无损导出」:它不做任何协议过滤,任何协议都必须出现在 Plain JSON 里。
|
||||
-- 单独命名这条不变量,是因为一旦有人给 to_plain 加上过滤,症状会非常隐蔽
|
||||
-- (备份/迁移用的导出静默少了节点)。
|
||||
local plain_lossy = {}
|
||||
for _, proto in ipairs(node.PROTOS) do
|
||||
local ok, out = pcall(output.generate, { sample(proto) }, "plain", {})
|
||||
if not ok or not emitted(out, proto, "plain") then plain_lossy[#plain_lossy + 1] = proto end
|
||||
end
|
||||
check("plain JSON is lossless for every proto"
|
||||
.. (#plain_lossy > 0 and (" (missing: " .. table.concat(plain_lossy, ",") .. ")") or ""),
|
||||
#plain_lossy == 0)
|
||||
|
||||
-- ---------- 结果 ----------
|
||||
print(string.format("\n%d passed, %d failed", passed, failed))
|
||||
os.exit(failed == 0 and 0 or 1)
|
||||
Reference in new issue
Block a user