'example.invalid', 'port' => '443', 'id' => self::UUID, 'net' => 'ws', 'host' => 'cdn.example.invalid', 'sni' => 'tls.example.invalid', 'tls' => 'tls', 'ps' => 'original']; $node = Codec::parse("\xEF\xBB\xBFvmess://".base64_encode(json_encode($config))); $uri = Codec::uri($node, '中文 + name'); self::assertSame('中文 + name', json_decode(Codec::decode64(substr($uri, 8)), true)['ps']); self::assertSame('tls.example.invalid', Codec::singbox($node, 'x')['tls']['server_name']); } public function testPasswordCharactersAreNotDecodedTwice(): void { $node = Codec::parse('trojan://a%2Bb%40c%23d@example.invalid:443?security=tls#test'); self::assertSame('a+b@c#d', Codec::singbox($node, 'x')['password']); } public function testRawPasswordBracketsAreEscapedInPublishedUri(): void { $node = Codec::parse('trojan://pass[word]@example.invalid:443?security=tls#test'); $uri = Codec::uri($node, 'test'); self::assertStringStartsWith('trojan://pass%5Bword%5D@example.invalid:443?', $uri); self::assertSame('pass[word]', Codec::singbox($node, 'test')['password']); self::assertSame($node['id'], Codec::parse($uri)['id']); } public function testEscapedUserinfoKeepsExactDecodedPassword(): void { $password = 'a[b]+c@d#e:f%2F'; $node = Codec::parse('trojan://'.rawurlencode($password).'@example.invalid:443?security=tls#test'); $uri = Codec::uri($node, 'test'); self::assertStringStartsWith('trojan://'.rawurlencode($password).'@', $uri); self::assertSame($password, Codec::singbox(Codec::parse($uri), 'test')['password']); self::assertSame($node['id'], Codec::parse($uri)['id']); } public function testPasswordBracketsDoNotChangeIpv6Host(): void { $node = Codec::parse('trojan://a[b]@[2001:db8::1]:443?security=tls#test'); self::assertStringStartsWith('trojan://a%5Bb%5D@[2001:db8::1]:443?', Codec::uri($node, 'test')); self::assertSame('2001:db8::1', Codec::singbox($node, 'test')['server']); } public function testSsBase64UrlAndPlugin(): void { $auth = rtrim(strtr(base64_encode('aes-128-gcm:a+b'), '+/', '-_'), '='); $node = Codec::parse('ss://'.$auth.'@example.invalid:443?plugin=v2ray-plugin%3Btls%3Bhost%3Dexample.invalid#test'); $out = Codec::singbox($node, 'x'); self::assertSame('a+b', $out['password']); self::assertSame('v2ray-plugin', $out['plugin']); self::assertSame('tls;host=example.invalid', $out['plugin_opts']); self::assertSame($node['id'], Codec::parse(Codec::uri($node, 'new'))['id']); } public function testQueryOrderAndNamesDoNotChangeIdentity(): void { $a = Codec::parse('vless://'.self::UUID.'@example.invalid:443?type=ws&path=%2Fx#first'); $b = Codec::parse('vless://'.self::UUID.'@example.invalid:443?path=%2Fx&type=ws#second'); self::assertSame($a['id'], $b['id']); } public function testUnknownTransportIsNotSilentlyConverted(): void { $node = Codec::parse('vless://'.self::UUID.'@example.invalid:443?type=xhttp#test'); $this->expectException(InvalidArgumentException::class); Codec::singbox($node, 'x'); } public function testInvalidPortIsRejected(): void { $this->expectException(InvalidArgumentException::class); Codec::parse('socks://example.invalid:0#test'); } }