mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-27 08:02:52 +08:00
217 lines
8.6 KiB
Ucode
217 lines
8.6 KiB
Ucode
/*
|
|
* SPDX-License-Identifier: GPL-2.0-only
|
|
*
|
|
* PR-03 (Subscription Transaction Boundary): the round-trip
|
|
* invariant for the canonical-Node pipeline. For every scheme the
|
|
* parser accepts, parse_uri() -> normalize() -> flatten() must
|
|
* produce a flat UCI dict that is field-for-field equal to what
|
|
* the parser would have written directly. Repository then writes
|
|
* that flat dict to UCI, so a saved canonical Node round-trips
|
|
* back into an equivalent canonical Node when the Loader next
|
|
* reads it.
|
|
*
|
|
* Two important properties this test pins:
|
|
*
|
|
* - canonical-to-flat mapping is consistent: parser and
|
|
* flatten() agree on every UCI key (otherwise the
|
|
* Repository's "delete what is no longer carried" walk
|
|
* would silently drop fields the parser still emits);
|
|
* - canonical -> flat -> canonical is identity for the data
|
|
* the Loader reads (the Repository's actual contract).
|
|
*
|
|
* Run through tests/ucode/run.sh's parser tree staging (same as
|
|
* test_parse_uri.uc): the staged work dir has the production
|
|
* parser/ tree plus the homeproxy-pro mock.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
import { parse_uri } from 'parser/uri.uc';
|
|
import { normalize } from 'parser/normalize.uc';
|
|
import { flatten } from 'parser/flatten.uc';
|
|
|
|
const FEATURES = { with_quic: true, with_utls: true };
|
|
const LOG = (..._args) => {};
|
|
|
|
let failures = 0, checks = 0;
|
|
|
|
function expect(name, got, want) {
|
|
checks++;
|
|
if (sprintf('%J', got) !== sprintf('%J', want)) {
|
|
printf('FAIL %s: got %J want %J\n', name, got, want);
|
|
failures++;
|
|
}
|
|
}
|
|
|
|
/* The invariant that matters for the Repository: every value the parser
|
|
* actually set must survive parse -> normalize -> flatten unchanged.
|
|
*
|
|
* Nulls are excluded on purpose - removeBlankAttrs()/flatten() drop them
|
|
* and the Loader reads an absent option as null, so a null round-trip is
|
|
* observably lossless. A non-null value that disappears is data loss:
|
|
* the Repository writes `flatten()`'s output, so on the next subscription
|
|
* update that UCI option would be *deleted* from the stored node.
|
|
*
|
|
* The reverse direction is checked too: flatten() must not invent keys
|
|
* the parser never produced, or the Repository would write options the
|
|
* node form does not own. */
|
|
function round_trip(name, uri, features) {
|
|
const flat = parse_uri(uri, features, LOG);
|
|
|
|
/* A sample the parser rejects is a broken fixture, not a reason to skip.
|
|
* Returning silently left that scheme's round trip unverified while the
|
|
* suite stayed green. */
|
|
if (!flat) {
|
|
checks++;
|
|
failures++;
|
|
printf('FAIL %s: parse_uri() rejected a sample this test is meant to cover\n', name);
|
|
return;
|
|
}
|
|
|
|
const node = normalize(flat);
|
|
const back = flatten(node);
|
|
|
|
/* 1. every non-null parser value survives, unchanged. */
|
|
for (let k, v in flat) {
|
|
if (v === null)
|
|
continue;
|
|
expect(sprintf('%s: %s survives round-trip', name, k), back[k], v);
|
|
}
|
|
|
|
/* 2. flatten() only emits keys the parser produced (plus the
|
|
* metadata the orchestrator attaches). */
|
|
const flat_keys = keys(flat);
|
|
for (let k, v in back) {
|
|
if (v === null)
|
|
continue;
|
|
if (index(flat_keys, k) === -1)
|
|
expect(sprintf('%s: %s is not a parser field', name, k), k, null);
|
|
}
|
|
|
|
/* 3. and the canonical Node must carry the identity the Loader's
|
|
* read path would reconstruct. */
|
|
expect(sprintf('%s: canonical type', name), node.type, flat.type);
|
|
expect(sprintf('%s: canonical address', name), node.address, flat.address);
|
|
expect(sprintf('%s: canonical port', name), node.port, flat.port);
|
|
expect(sprintf('%s: canonical name from label', name), node.name, flat.label);
|
|
}
|
|
|
|
/* The same URI corpus test_parse_uri.uc asserts on, so any
|
|
* regression in the mapping table will surface in BOTH tests.
|
|
* Array destructuring is rejected by the target ucode, so each
|
|
* sample is a 3-element object the loop indexes manually. */
|
|
const SAMPLES = [
|
|
{ name: 'anytls', uri: 'anytls://secret@a.example.com:443?sni=a.example.com#AnyTLS', features: FEATURES },
|
|
{ name: 'https', uri: 'https://user:pass@b.example.com:8443#HTTPS', features: FEATURES },
|
|
{ name: 'http', uri: 'http://c.example.com:8080#HTTP', features: FEATURES },
|
|
{ name: 'hy1', uri: 'hysteria://d.example.com:36712?auth=pass&peer=d.example.com&protocol=udp#Hy1', features: FEATURES },
|
|
{ name: 'hy2', uri: 'hysteria2://pass@e.example.com:443?sni=e.example.com&obfs=salamander&obfs-password=op#Hy2', features: FEATURES },
|
|
{ name: 'hy2al', uri: 'hy2://pass@e.example.com:443#Hy2Alias', features: FEATURES },
|
|
{ name: 'snell', uri: 'snell://f.example.com:443?psk=pskvalue&version=4&obfs=http&obfs-host=bing.com#Snell', features: FEATURES },
|
|
{ name: 'socks5', uri: 'socks5://user:pass@g.example.com:1080#Socks', features: FEATURES },
|
|
{ name: 'ss', uri: 'ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@i.example.com:8388#SS', features: FEATURES },
|
|
{ name: 'trojan', uri: 'trojan://pass@l.example.com:443?type=grpc&serviceName=gs&sni=l.example.com#Trojan', features: FEATURES },
|
|
{ name: 'tuic', uri: 'tuic://tuic-uuid:pass@m.example.com:443?congestion_control=bbr&sni=m.example.com#Tuic', features: FEATURES },
|
|
{ name: 'vless-r', uri: 'vless://vless-uuid@n.example.com:443?security=reality&pbk=PUBKEY&sid=abcd&type=grpc&serviceName=gs&sni=n.example.com#Vless', features: FEATURES },
|
|
{ name: 'vmess', uri: 'vmess://eyJ2IjoiMiIsInBzIjoiVk1lc3MiLCJhZGQiOiJwLmV4YW1wbGUuY29tIiwicG9ydCI6IjQ0MyIsImlkIjoiM2FmODg1NjEtOWM2OS00YjE5LThmN2UtZjA4ZDU4MGJjMzM5IiwiYWlkIjoiMCIsIm5ldCI6IndzIiwidHlwZSI6Im5vbmUiLCJob3N0IjoicC5leGFtcGxlLmNvbSIsInBhdGgiOiIvd3MiLCJ0bHMiOiJ0bHMiLCJzbmkiOiJwLmV4YW1wbGUuY29tIn0=', features: FEATURES }
|
|
];
|
|
|
|
/* ucode's for-in over an array yields the elements, not the indices. */
|
|
for (let s in SAMPLES)
|
|
round_trip(s.name, s.uri, s.features);
|
|
|
|
/* --- explicit canonical -> flat shape checks --------------------------- */
|
|
|
|
/* vless reality: many sub-objects must round-trip cleanly. */
|
|
{
|
|
const flat = parse_uri(
|
|
'vless://vless-uuid@n.example.com:443?security=reality&pbk=PUBKEY&sid=abcd&type=grpc&serviceName=gs&sni=n.example.com#Vless',
|
|
FEATURES, LOG
|
|
);
|
|
const node = normalize(flat);
|
|
const back = flatten(node);
|
|
expect('vless.reality: tls_reality_public_key',
|
|
back.tls_reality_public_key, flat.tls_reality_public_key);
|
|
expect('vless.reality: tls_reality_short_id',
|
|
back.tls_reality_short_id, flat.tls_reality_short_id);
|
|
expect('vless.reality: tls_sni',
|
|
back.tls_sni, flat.tls_sni);
|
|
expect('vless.reality: grpc_servicename',
|
|
back.grpc_servicename, flat.grpc_servicename);
|
|
expect('vless.reality: vless_flow',
|
|
back.vless_flow, flat.vless_flow);
|
|
}
|
|
|
|
/* shadowsocks plugin + udp_over_tcp must survive. */
|
|
{
|
|
const flat = parse_uri(
|
|
'ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@i.example.com:8388#SS',
|
|
FEATURES, LOG
|
|
);
|
|
const node = normalize(flat);
|
|
const back = flatten(node);
|
|
expect('ss: shadowsocks_encrypt_method',
|
|
back.shadowsocks_encrypt_method, flat.shadowsocks_encrypt_method);
|
|
expect('ss: label survives canonical->flat',
|
|
back.label, flat.label);
|
|
}
|
|
|
|
/* --- grouphash metadata pass-through ---------------------------------- */
|
|
|
|
/* Repository attaches grouphash to the canonical Node after the
|
|
* orchestrator builds it. flatten() must copy grouphash through
|
|
* because the Repository's foreach walks `node_cache` keyed by
|
|
* grouphash, not by what flatten emits. */
|
|
{
|
|
const flat = parse_uri(
|
|
'vless://vless-uuid@n.example.com:443?security=reality&pbk=PUBKEY&sid=abcd&sni=n.example.com#Vless',
|
|
FEATURES, LOG
|
|
);
|
|
const node = normalize(flat);
|
|
node.grouphash = 'groupHash-XYZ';
|
|
const back = flatten(node);
|
|
expect('grouphash: passed through',
|
|
back.grouphash, 'groupHash-XYZ');
|
|
}
|
|
|
|
/* isExisting is the within-run marker Repository sets after a
|
|
* successful write; it must NOT be emitted into UCI (else it
|
|
* becomes a stale field on the next round). */
|
|
{
|
|
const flat = parse_uri(
|
|
'vless://vless-uuid@n.example.com:443?sni=n.example.com#Vless',
|
|
FEATURES, LOG
|
|
);
|
|
const node = normalize(flat);
|
|
node.isExisting = true;
|
|
const back = flatten(node);
|
|
expect('isExisting: NOT in flat output',
|
|
'isExisting' in back, false);
|
|
}
|
|
|
|
/* --- null / empty canonical values are dropped ------------------------ */
|
|
|
|
/* Empty / null canonical values must not show up as `key ''`
|
|
* entries in UCI - the Loader treats null and absent the same,
|
|
* so the round-trip is still lossless. */
|
|
{
|
|
const node = {
|
|
id: null,
|
|
name: 'X',
|
|
type: 'vless',
|
|
address: 'a',
|
|
port: '1',
|
|
common: {},
|
|
credentials: { uuid: 'u' },
|
|
tls: { enabled: null, ech: {}, utls: {}, reality: {} },
|
|
transport: { type: null },
|
|
multiplex: { enabled: null, brutal: {} },
|
|
protocol_options: { flow: '' }
|
|
};
|
|
const back = flatten(node);
|
|
expect('flatten: empty string protocol_option is dropped',
|
|
'flow' in back, false);
|
|
}
|
|
|
|
printf('%d checks, %d failures\n', checks, failures);
|
|
exit(failures === 0 ? 0 : 1); |