From ae394d4754cc77d72a5ad832d4825b87ba0f877a Mon Sep 17 00:00:00 2001 From: Danlian Akhmedzianov Date: Sun, 17 May 2026 02:08:48 +0300 Subject: [PATCH 01/10] Propagate config errors and add URI tests Update configuration helper functions to return `anyhow::Result` and use the `?` operator for error propagation. Extend `get_uri_data` and related functions accordingly. Add comprehensive unit tests for URI parsing, including supported protocols and expected panic cases. --- src/cmd/perf.rs | 2 +- src/cmd/select.rs | 2 +- src/v2parser/parser/mod.rs | 88 +++++++++++++++++++++++++++++++------- 3 files changed, 74 insertions(+), 18 deletions(-) diff --git a/src/cmd/perf.rs b/src/cmd/perf.rs index db6343c..d39cfb3 100644 --- a/src/cmd/perf.rs +++ b/src/cmd/perf.rs @@ -103,7 +103,7 @@ impl PerfParams { let result = portman .lease_port(async move |port| -> anyhow::Result { - let config_json = create_json_config(url.as_str(), Some(port), None, None); + let config_json = create_json_config(url.as_str(), Some(port), None, None)?; let mut command = self.run_xray(config_json).await?; let measure_result = ProxiedClient::try_new(PROXY_HOST, port, PROXY_TEST_TIMEOUT)? .measure_first_successful( diff --git a/src/cmd/select.rs b/src/cmd/select.rs index faa41d4..22c6eaf 100644 --- a/src/cmd/select.rs +++ b/src/cmd/select.rs @@ -215,7 +215,7 @@ impl SelectParams { }; let config_json = - create_json_config(url.as_str(), socks_port, http_port, Some(self.get_log())); + create_json_config(url.as_str(), socks_port, http_port, Some(self.get_log()))?; match self.dry_run { true => { diff --git a/src/v2parser/parser/mod.rs b/src/v2parser/parser/mod.rs index efa7393..b6ba969 100644 --- a/src/v2parser/parser/mod.rs +++ b/src/v2parser/parser/mod.rs @@ -45,10 +45,10 @@ pub fn create_json_config( socks_port: Option, http_port: Option, log: Option, -) -> String { - let config = create_config(uri, socks_port, http_port, log); - let serialized = serde_json::to_string_pretty(&config).unwrap(); - return serialized; +) -> anyhow::Result { + let config = create_config(uri, socks_port, http_port, log)?; + let serialized = serde_json::to_string_pretty(&config)?; + return Ok(serialized); } fn create_config( @@ -56,8 +56,8 @@ fn create_config( socks_port: Option, http_port: Option, log: Option, -) -> Config { - let outbound_object = create_outbound_object(uri); +) -> anyhow::Result { + let outbound_object = create_outbound_object(uri)?; let inbound_config = generate_inbound_config(InboundGenerationOptions { socks_port, http_port, @@ -67,11 +67,11 @@ fn create_config( outbounds: vec![outbound_object], inbounds: inbound_config, }; - return config; + return Ok(config); } -fn create_outbound_object(uri: &str) -> Outbound { - let (name, data, outbound_settings) = get_uri_data(uri); +fn create_outbound_object(uri: &str) -> anyhow::Result { + let (name, data, outbound_settings) = get_uri_data(uri)?; let network_type = data.r#type.clone().unwrap_or(String::from("")); let allow_insecure = data.allow_insecure == Some(String::from("true")) @@ -178,36 +178,36 @@ fn create_outbound_object(uri: &str) -> Outbound { settings: outbound_settings, }; - return outbound; + return Ok(outbound); } -fn get_uri_data(uri: &str) -> (String, RawData, OutboundSettings) { +fn get_uri_data(uri: &str) -> anyhow::Result<(String, RawData, OutboundSettings)> { let protocol = uri_identifier::get_uri_protocol(uri); return match protocol { Some(uri_identifier::Protocols::Vless) => { let d = vless::data::get_data(uri); let s = vless::create_outbound_settings(&d); - (String::from("vless"), d, s) + Ok((String::from("vless"), d, s)) } Some(uri_identifier::Protocols::Vmess) => { let d = vmess::data::get_data(uri); let s = vmess::create_outbound_settings(&d); - (String::from("vmess"), d, s) + Ok((String::from("vmess"), d, s)) } Some(uri_identifier::Protocols::Trojan) => { let d = trojan::data::get_data(uri); let s = trojan::create_outbound_settings(&d); - (String::from("trojan"), d, s) + Ok((String::from("trojan"), d, s)) } Some(uri_identifier::Protocols::Shadowsocks) => { let d = shadow_socks::data::get_data(uri); let s = shadow_socks::create_outbound_settings(&d); - (String::from("shadowsocks"), d, s) + Ok((String::from("shadowsocks"), d, s)) } Some(uri_identifier::Protocols::Socks) => { let d = socks::data::get_data(uri); let s = socks::create_outbound_settings(&d); - (String::from("socks"), d, s) + Ok((String::from("socks"), d, s)) } Some(_) => { panic!("The protocol was recognized but is not supported yet"); @@ -217,3 +217,59 @@ fn get_uri_data(uri: &str) -> (String, RawData, OutboundSettings) { } }; } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_uri_data_vless() { + let uri = "vless://3d1c3f04-729d-59d3-bdb6-3f3f4352e173@root.ii.one:2083?security=reality&sni=www.spamhaus.org&fp=safari&pbk=7xhH4b_VkliBxGulljcyPOH-bYUA2dl-XAdZAsfhk04&sid=6ba85179e30d4fc2&type=tcp&flow=xtls-rprx-vision#Ha-ac"; + let (name, _data, _settings) = get_uri_data(uri).unwrap(); + assert_eq!(name, "vless"); + } + + #[test] + fn test_get_uri_data_vmess() { + let uri = "vmess://eyJhZGQiOiIxMjcuMC4wLjEiLCJhaWQiOiIwIiwiaG9zdCI6IiIsImlkIjoiOHM2OTdlMmMtZXMxNy00MDNkLTI0ZjMtZHMyYzYwc2I4ZjUiLCJuZXQiOiJ0Y3AiLCJwYXRoIjoiIiwicG9ydCI6IjgwODAiLCJwcyI6InRlc3QiLCJzY3kiOiJhdXRvIiwic25pIjoiIiwidGxzIjoiIiwidHlwZSI6Im5vbmUiLCJ2IjoiMiJ9"; + let (name, _data, _settings) = get_uri_data(uri).unwrap(); + assert_eq!(name, "vmess"); + } + + #[test] + fn test_get_uri_data_trojan() { + let uri = "trojan://test-pw@13.50.100.84:22222?security=tls&sni=trj.rollingnext.co.uk&type=tcp#test"; + let (name, _data, _settings) = get_uri_data(uri).unwrap(); + assert_eq!(name, "trojan"); + } + + #[test] + fn test_get_uri_data_shadowsocks() { + let uri = "ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpXNzRYRkFMS0t1dzZtNUlB@www.outline.aasf.cyou:443#test"; + let (name, _data, _settings) = get_uri_data(uri).unwrap(); + assert_eq!(name, "shadowsocks"); + } + + #[test] + fn test_get_uri_data_socks() { + let uri = "socks5://username:password@127.0.0.1:1080"; + let (name, _data, _settings) = get_uri_data(uri).unwrap(); + assert_eq!(name, "socks"); + } + + #[test] + #[should_panic(expected = "The protocol was recognized but is not supported yet")] + fn test_get_uri_data_http_unimplemented() { + let uri = "http://example.com"; + let res = get_uri_data(uri); + assert_eq!(res.is_ok(), false); + } + + #[test] + #[should_panic(expected = "The protocol is not supported")] + fn test_get_uri_data_unknown() { + let uri = "ftp://example.com"; + let res = get_uri_data(uri); + assert_eq!(res.is_ok(), false); + } +} From c7dae8941c9c9e017f4e261137614e68f4f621dd Mon Sep 17 00:00:00 2001 From: Danlian Akhmedzianov Date: Sun, 17 May 2026 02:10:11 +0300 Subject: [PATCH 02/10] Remove panics --- src/v2parser/parser/mod.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/v2parser/parser/mod.rs b/src/v2parser/parser/mod.rs index b6ba969..8cb675c 100644 --- a/src/v2parser/parser/mod.rs +++ b/src/v2parser/parser/mod.rs @@ -210,10 +210,10 @@ fn get_uri_data(uri: &str) -> anyhow::Result<(String, RawData, OutboundSettings) Ok((String::from("socks"), d, s)) } Some(_) => { - panic!("The protocol was recognized but is not supported yet"); + anyhow::bail!("The protocol was recognized but is not supported yet"); } None => { - panic!("The protocol is not supported"); + anyhow::bail!("The protocol is not supported") } }; } @@ -258,7 +258,6 @@ mod tests { } #[test] - #[should_panic(expected = "The protocol was recognized but is not supported yet")] fn test_get_uri_data_http_unimplemented() { let uri = "http://example.com"; let res = get_uri_data(uri); @@ -266,7 +265,6 @@ mod tests { } #[test] - #[should_panic(expected = "The protocol is not supported")] fn test_get_uri_data_unknown() { let uri = "ftp://example.com"; let res = get_uri_data(uri); From 175e8ac46518f6f395032d32dd03a072addf2e1a Mon Sep 17 00:00:00 2001 From: Danlian Akhmedzianov Date: Sun, 17 May 2026 02:21:55 +0300 Subject: [PATCH 03/10] Make Shadowsocks parsing return Result and add tests Propagate parsing errors with anyhow::Result instead of panicking, and add unit tests for address parsing. --- src/v2parser/parser/mod.rs | 2 +- src/v2parser/parser/shadow_socks/data.rs | 74 +++++++++++++++++++++--- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/v2parser/parser/mod.rs b/src/v2parser/parser/mod.rs index 8cb675c..018ba5b 100644 --- a/src/v2parser/parser/mod.rs +++ b/src/v2parser/parser/mod.rs @@ -200,7 +200,7 @@ fn get_uri_data(uri: &str) -> anyhow::Result<(String, RawData, OutboundSettings) Ok((String::from("trojan"), d, s)) } Some(uri_identifier::Protocols::Shadowsocks) => { - let d = shadow_socks::data::get_data(uri); + let d = shadow_socks::data::get_data(uri)?; let s = shadow_socks::create_outbound_settings(&d); Ok((String::from("shadowsocks"), d, s)) } diff --git a/src/v2parser/parser/shadow_socks/data.rs b/src/v2parser/parser/shadow_socks/data.rs index f6bd2fc..47a513f 100644 --- a/src/v2parser/parser/shadow_socks/data.rs +++ b/src/v2parser/parser/shadow_socks/data.rs @@ -1,16 +1,16 @@ use http::Uri; -use base64::{engine::general_purpose, Engine}; use crate::v2parser::entities::raw_data::RawData; use crate::v2parser::parser::shadow_socks::models::ShadowSocksAddress; use crate::v2parser::utils::{url_decode, url_decode_str}; +use base64::{Engine, engine::general_purpose}; -pub fn get_data(uri: &str) -> RawData { +pub fn get_data(uri: &str) -> anyhow::Result { let data = uri.split_once("ss://").unwrap().1; let (raw_data, name) = data.split_once("#").unwrap_or((data, "")); let (raw_uri, _) = raw_data.split_once("?").unwrap_or((raw_data, "")); - let parsed_address = parse_ss_address(raw_uri); - return RawData { + let parsed_address = parse_ss_address(raw_uri)?; + let raw_data = RawData { remarks: url_decode(Some(String::from(name))).unwrap_or(String::from("")), server_method: url_decode(Some(parsed_address.method)), address: Some(parsed_address.address), @@ -41,12 +41,14 @@ pub fn get_data(uri: &str) -> RawData { vnext_security: None, username: None, }; + + Ok(raw_data) } -fn parse_ss_address(raw_data: &str) -> ShadowSocksAddress { +fn parse_ss_address(raw_data: &str) -> anyhow::Result { let (userinfo, raw_address): (String, &str) = match raw_data.split_once("@") { None => { - panic!("Wrong shadowsocks format, no `@` found in the address"); + anyhow::bail!("Wrong shadowsocks format, no `@` found in the address"); } Some(data) => (String::from(data.0), data.1), }; @@ -63,10 +65,66 @@ fn parse_ss_address(raw_data: &str) -> ShadowSocksAddress { .split_once(":") .expect("No `:` found in the decoded base64"); - return ShadowSocksAddress { + return Ok(ShadowSocksAddress { method: String::from(method), password: String::from(password), address: parsed.host().unwrap().to_string(), port: parsed.port().unwrap().as_u16(), - }; + }); +} + +// Tests for the internal `parse_ss_address` function +#[cfg(test)] +mod tests { + use super::*; + use base64::{Engine, engine::general_purpose}; + use urlencoding; + + #[test] + fn errors_when_missing_at_symbol() { + let raw = "example.com:8388"; + let result = parse_ss_address(&raw); + assert!(result.is_err()); + } + + fn encode_userinfo(method: &str, password: &str) -> String { + let combined = format!("{}:{}", method, password); + general_purpose::STANDARD.encode(combined.as_bytes()) + } + + #[test] + fn parses_basic_address_without_trailing_slash() { + let method = "aes-256-cfb"; + let password = "myPass"; + let userinfo = encode_userinfo(method, password); + let raw = format!("{}@example.com:8388", userinfo); + let result = parse_ss_address(&raw).unwrap(); + assert_eq!(result.method, method); + assert_eq!(result.password, password); + assert_eq!(result.address, "example.com"); + assert_eq!(result.port, 8388); + } + + #[test] + fn parses_address_with_trailing_slash() { + let method = "aes-256-cfb"; + let password = "myPass"; + let userinfo = encode_userinfo(method, password); + let raw = format!("{}@example.com:8388/", userinfo); + let result = parse_ss_address(&raw).unwrap(); + assert_eq!(result.address, "example.com"); + assert_eq!(result.port, 8388); + } + + #[test] + fn parses_percent_encoded_userinfo() { + let method = "aes-256-cfb"; + let password = "myPass"; + let userinfo = encode_userinfo(method, password); + let encoded = urlencoding::encode(&userinfo).into_owned(); + let raw = format!("{}@example.com:8388", encoded); + let result = parse_ss_address(&raw).unwrap(); + assert_eq!(result.method, method); + assert_eq!(result.password, password); + } } From e97da40864cb4a040be0a413775b48b89d6606df Mon Sep 17 00:00:00 2001 From: Danlian Akhmedzianov Date: Sun, 17 May 2026 02:45:57 +0300 Subject: [PATCH 04/10] Handle Trojan URI parse errors --- src/v2parser/parser/mod.rs | 2 +- src/v2parser/parser/trojan/data.rs | 77 ++++++++++++++++++++++++++---- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/src/v2parser/parser/mod.rs b/src/v2parser/parser/mod.rs index 018ba5b..c64c574 100644 --- a/src/v2parser/parser/mod.rs +++ b/src/v2parser/parser/mod.rs @@ -195,7 +195,7 @@ fn get_uri_data(uri: &str) -> anyhow::Result<(String, RawData, OutboundSettings) Ok((String::from("vmess"), d, s)) } Some(uri_identifier::Protocols::Trojan) => { - let d = trojan::data::get_data(uri); + let d = trojan::data::get_data(uri)?; let s = trojan::create_outbound_settings(&d); Ok((String::from("trojan"), d, s)) } diff --git a/src/v2parser/parser/trojan/data.rs b/src/v2parser/parser/trojan/data.rs index 81744b0..1dd9248 100644 --- a/src/v2parser/parser/trojan/data.rs +++ b/src/v2parser/parser/trojan/data.rs @@ -1,18 +1,28 @@ -use http::Uri; use crate::v2parser::entities::raw_data::RawData; use crate::v2parser::parser::trojan::models::TrojanAddress; use crate::v2parser::utils::{get_parameter_value, url_decode}; +use http::Uri; -pub fn get_data(uri: &str) -> RawData { - let data = uri.split_once("trojan://").unwrap().1; - let query_and_name = uri.split_once("?").unwrap().1; +pub fn get_data(uri: &str) -> anyhow::Result { + let data = uri + .split_once("trojan://") + .ok_or_else(|| anyhow::Error::msg("Incorrect URI format"))? + .1; + let query_and_name = uri + .split_once("?") + .ok_or_else(|| anyhow::Error::msg("Incorrect URI format"))? + .1; let (raw_query, name) = query_and_name .split_once("#") .unwrap_or((query_and_name, "")); - let parsed_address = parse_trojan_address(data.split_once("?").unwrap().0); + let trojan_address = data + .split_once("?") + .ok_or_else(|| anyhow::Error::msg("Incorrect URI format"))? + .0; + let parsed_address = parse_trojan_address(trojan_address)?; let query: Vec<(&str, &str)> = querystring::querify(raw_query); - return RawData { + let result = RawData { remarks: url_decode(Some(String::from(name))).unwrap_or(String::from("")), uuid: Some(parsed_address.uuid), port: Some(parsed_address.port), @@ -43,12 +53,14 @@ pub fn get_data(uri: &str) -> RawData { server_method: None, username: None, }; + + Ok(result) } -fn parse_trojan_address(raw_data: &str) -> TrojanAddress { +fn parse_trojan_address(raw_data: &str) -> anyhow::Result { let (uuid, raw_address): (String, &str) = match raw_data.split_once("@") { None => { - panic!("Wrong trojan format, no `@` found in the address"); + anyhow::bail!("Wrong trojan format, no `@` found in the address"); } Some(data) => (String::from(data.0), data.1), }; @@ -56,9 +68,56 @@ fn parse_trojan_address(raw_data: &str) -> TrojanAddress { let parsed = address_wo_slash.parse::().unwrap(); - return TrojanAddress { + let result = TrojanAddress { uuid: url_decode(Some(uuid)).unwrap(), address: parsed.host().unwrap().to_string(), port: parsed.port().unwrap().as_u16(), }; + + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::parse_trojan_address; + + #[test] + fn parse_trojan_address_trims_trailing_slash() { + let result = parse_trojan_address("test-pw%2Bencoded@13.50.100.84:22222/").unwrap(); + + assert_eq!(result.uuid, "test-pw+encoded"); + assert_eq!(result.address, "13.50.100.84"); + assert_eq!(result.port, 22222); + } + + #[test] + fn parse_trojan_address_without_trailing_slash() { + let result = parse_trojan_address("plain-password@trj.rollingnext.co.uk:443").unwrap(); + + assert_eq!(result.uuid, "plain-password"); + assert_eq!(result.address, "trj.rollingnext.co.uk"); + assert_eq!(result.port, 443); + } + + #[test] + fn parse_trojan_address_returns_error_when_at_is_missing() { + let result = parse_trojan_address("plain-password13.50.100.84:443"); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap().to_string(), + "Wrong trojan format, no `@` found in the address" + ); + } + + #[test] + fn parse_trojan_address_returns_error_for_empty_input() { + let result = parse_trojan_address(""); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap().to_string(), + "Wrong trojan format, no `@` found in the address" + ); + } } From a14b8a7564af3766fe5cb6b5f2f50d1a8eca03fd Mon Sep 17 00:00:00 2001 From: Danlian Akhmedzianov Date: Sun, 17 May 2026 15:59:50 +0300 Subject: [PATCH 05/10] Add anyhow conversion helper --- src/utils/mod.rs | 1 + src/utils/to_err.rs | 15 +++++++++++++++ src/v2parser/parser/trojan/data.rs | 13 ++++--------- 3 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 src/utils/to_err.rs diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 72f1008..270cbca 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,2 +1,3 @@ pub(crate) mod tap; +pub(crate) mod to_err; pub(crate) mod urldecode; diff --git a/src/utils/to_err.rs b/src/utils/to_err.rs new file mode 100644 index 0000000..e33eff4 --- /dev/null +++ b/src/utils/to_err.rs @@ -0,0 +1,15 @@ +pub(crate) trait ToAnyhow { + fn anyhow(self, description: &'static str) -> anyhow::Result; +} + +impl ToAnyhow for Option { + fn anyhow(self, description: &'static str) -> anyhow::Result { + self.ok_or_else(|| anyhow::Error::msg(description)) + } +} + +impl ToAnyhow for Result { + fn anyhow(self, description: &'static str) -> anyhow::Result { + self.map_err(|_| anyhow::Error::msg(description)) + } +} diff --git a/src/v2parser/parser/trojan/data.rs b/src/v2parser/parser/trojan/data.rs index 1dd9248..fe50e11 100644 --- a/src/v2parser/parser/trojan/data.rs +++ b/src/v2parser/parser/trojan/data.rs @@ -1,3 +1,4 @@ +use crate::utils::to_err::ToAnyhow; use crate::v2parser::entities::raw_data::RawData; use crate::v2parser::parser::trojan::models::TrojanAddress; use crate::v2parser::utils::{get_parameter_value, url_decode}; @@ -6,19 +7,13 @@ use http::Uri; pub fn get_data(uri: &str) -> anyhow::Result { let data = uri .split_once("trojan://") - .ok_or_else(|| anyhow::Error::msg("Incorrect URI format"))? - .1; - let query_and_name = uri - .split_once("?") - .ok_or_else(|| anyhow::Error::msg("Incorrect URI format"))? + .anyhow("Incorrect URI format")? .1; + let query_and_name = uri.split_once("?").anyhow("Incorrect URI format")?.1; let (raw_query, name) = query_and_name .split_once("#") .unwrap_or((query_and_name, "")); - let trojan_address = data - .split_once("?") - .ok_or_else(|| anyhow::Error::msg("Incorrect URI format"))? - .0; + let trojan_address = data.split_once("?").anyhow("Incorrect URI format")?.0; let parsed_address = parse_trojan_address(trojan_address)?; let query: Vec<(&str, &str)> = querystring::querify(raw_query); From 0f3e12e22fb9c9ec7f6b1f9d389443596da9a01a Mon Sep 17 00:00:00 2001 From: Danlian Akhmedzianov Date: Sun, 17 May 2026 16:10:24 +0300 Subject: [PATCH 06/10] Handle invalid VLESS and Trojan URIs --- src/v2parser/parser/mod.rs | 3 +- src/v2parser/parser/trojan/data.rs | 6 ++-- src/v2parser/parser/vless/data.rs | 54 ++++++++++++++++++++++------- src/v2parser/utils/constants.rs | 1 + src/v2parser/utils/incorrect_uri.rs | 17 +++++++++ src/v2parser/utils/mod.rs | 2 ++ 6 files changed, 65 insertions(+), 18 deletions(-) create mode 100644 src/v2parser/utils/constants.rs create mode 100644 src/v2parser/utils/incorrect_uri.rs diff --git a/src/v2parser/parser/mod.rs b/src/v2parser/parser/mod.rs index c64c574..bcab892 100644 --- a/src/v2parser/parser/mod.rs +++ b/src/v2parser/parser/mod.rs @@ -17,6 +17,7 @@ use crate::v2parser::entities::xhttp_settings::XHTTPSettings; use crate::v2parser::utils::inbound_generator::{ InboundGenerationOptions, generate_inbound_config, }; +use crate::v2parser::utils::incorrect_uri::IncorrectURI; use crate::v2parser::utils::parse_raw_json; mod shadow_socks; @@ -185,7 +186,7 @@ fn get_uri_data(uri: &str) -> anyhow::Result<(String, RawData, OutboundSettings) let protocol = uri_identifier::get_uri_protocol(uri); return match protocol { Some(uri_identifier::Protocols::Vless) => { - let d = vless::data::get_data(uri); + let d = vless::data::get_data(uri).incorrect_uri()?; let s = vless::create_outbound_settings(&d); Ok((String::from("vless"), d, s)) } diff --git a/src/v2parser/parser/trojan/data.rs b/src/v2parser/parser/trojan/data.rs index fe50e11..e25cfc4 100644 --- a/src/v2parser/parser/trojan/data.rs +++ b/src/v2parser/parser/trojan/data.rs @@ -1,14 +1,12 @@ use crate::utils::to_err::ToAnyhow; use crate::v2parser::entities::raw_data::RawData; use crate::v2parser::parser::trojan::models::TrojanAddress; +use crate::v2parser::utils::incorrect_uri::IncorrectURI; use crate::v2parser::utils::{get_parameter_value, url_decode}; use http::Uri; pub fn get_data(uri: &str) -> anyhow::Result { - let data = uri - .split_once("trojan://") - .anyhow("Incorrect URI format")? - .1; + let data = uri.split_once("trojan://").incorrect_uri()?.1; let query_and_name = uri.split_once("?").anyhow("Incorrect URI format")?.1; let (raw_query, name) = query_and_name .split_once("#") diff --git a/src/v2parser/parser/vless/data.rs b/src/v2parser/parser/vless/data.rs index 5994185..2d31050 100644 --- a/src/v2parser/parser/vless/data.rs +++ b/src/v2parser/parser/vless/data.rs @@ -1,18 +1,19 @@ -use http::Uri; use crate::v2parser::entities::raw_data::RawData; use crate::v2parser::parser::vless::models::VLessAddress; +use crate::v2parser::utils::incorrect_uri::IncorrectURI; use crate::v2parser::utils::{get_parameter_value, url_decode}; +use http::Uri; -pub fn get_data(uri: &str) -> RawData { - let data = uri.split_once("vless://").unwrap().1; - let query_and_name = uri.split_once("?").unwrap().1; +pub fn get_data(uri: &str) -> anyhow::Result { + let data = uri.split_once("vless://").incorrect_uri()?.1; + let query_and_name = uri.split_once("?").incorrect_uri()?.1; let (raw_query, name) = query_and_name .split_once("#") .unwrap_or((query_and_name, "")); - let parsed_address = parse_vless_address(data.split_once("?").unwrap().0); + let parsed_address = parse_vless_address(data.split_once("?").incorrect_uri()?.0)?; let query: Vec<(&str, &str)> = querystring::querify(raw_query); - return RawData { + let result = RawData { remarks: url_decode(Some(String::from(name))).unwrap_or(String::from("")), uuid: Some(parsed_address.uuid), port: Some(parsed_address.port), @@ -43,22 +44,49 @@ pub fn get_data(uri: &str) -> RawData { server_method: None, username: None, }; + + Ok(result) } -fn parse_vless_address(raw_data: &str) -> VLessAddress { +fn parse_vless_address(raw_data: &str) -> anyhow::Result { let (uuid, raw_address): (String, &str) = match raw_data.split_once("@") { None => { - panic!("Wrong vless format, no `@` found in the address"); + anyhow::bail!("Wrong vless format, no `@` found in the address"); } Some(data) => (String::from(data.0), data.1), }; let address_wo_slash = raw_address.strip_suffix("/").unwrap_or(raw_address); - let parsed = address_wo_slash.parse::().unwrap(); + let parsed = address_wo_slash.parse::().incorrect_uri()?; - return VLessAddress { - uuid: url_decode(Some(uuid)).unwrap(), - address: parsed.host().unwrap().to_string(), - port: parsed.port().unwrap().as_u16(), + let result = VLessAddress { + uuid: url_decode(Some(uuid)).incorrect_uri()?, + address: parsed.host().incorrect_uri()?.to_string(), + port: parsed.port().incorrect_uri()?.as_u16(), }; + + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::parse_vless_address; + + #[test] + fn parse_vless_address_with_trailing_slash_and_encoded_uuid() { + let parsed = parse_vless_address("my%2Duuid@https://example.com:443/").unwrap(); + + assert_eq!(parsed.uuid, "my-uuid"); + assert_eq!(parsed.address, "example.com"); + assert_eq!(parsed.port, 443); + } + + #[test] + fn parse_vless_address_without_trailing_slash() { + let parsed = parse_vless_address("uuid123@https://server.test:8443").unwrap(); + + assert_eq!(parsed.uuid, "uuid123"); + assert_eq!(parsed.address, "server.test"); + assert_eq!(parsed.port, 8443); + } } diff --git a/src/v2parser/utils/constants.rs b/src/v2parser/utils/constants.rs new file mode 100644 index 0000000..56916fd --- /dev/null +++ b/src/v2parser/utils/constants.rs @@ -0,0 +1 @@ +pub static INCORRECT_URI_FORMAT_ERR_MSG: &'static str = "Incorrect URI format"; diff --git a/src/v2parser/utils/incorrect_uri.rs b/src/v2parser/utils/incorrect_uri.rs new file mode 100644 index 0000000..cd8ab59 --- /dev/null +++ b/src/v2parser/utils/incorrect_uri.rs @@ -0,0 +1,17 @@ +use crate::{utils::to_err::ToAnyhow, v2parser::utils::constants::INCORRECT_URI_FORMAT_ERR_MSG}; + +pub(crate) trait IncorrectURI { + fn incorrect_uri(self) -> anyhow::Result; +} + +impl IncorrectURI for Option { + fn incorrect_uri(self) -> anyhow::Result { + self.anyhow(INCORRECT_URI_FORMAT_ERR_MSG) + } +} + +impl IncorrectURI for Result { + fn incorrect_uri(self) -> anyhow::Result { + self.anyhow(INCORRECT_URI_FORMAT_ERR_MSG) + } +} diff --git a/src/v2parser/utils/mod.rs b/src/v2parser/utils/mod.rs index 8d9ecff..bddfd4c 100644 --- a/src/v2parser/utils/mod.rs +++ b/src/v2parser/utils/mod.rs @@ -1,4 +1,6 @@ +pub mod constants; pub mod inbound_generator; +pub mod incorrect_uri; pub fn url_decode_str(value: &str) -> Option { return urlencoding::decode(value) From bb5d3979e47d1f7ec55f9a3d0de4c74c2b5b809e Mon Sep 17 00:00:00 2001 From: Danlian Akhmedzianov Date: Sun, 17 May 2026 16:19:18 +0300 Subject: [PATCH 07/10] Simplify remarks string conversion and decoding --- src/v2parser/parser/vless/data.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/v2parser/parser/vless/data.rs b/src/v2parser/parser/vless/data.rs index 2d31050..e43aafc 100644 --- a/src/v2parser/parser/vless/data.rs +++ b/src/v2parser/parser/vless/data.rs @@ -14,7 +14,7 @@ pub fn get_data(uri: &str) -> anyhow::Result { let query: Vec<(&str, &str)> = querystring::querify(raw_query); let result = RawData { - remarks: url_decode(Some(String::from(name))).unwrap_or(String::from("")), + remarks: url_decode(Some(name.to_string())).unwrap_or_default(), uuid: Some(parsed_address.uuid), port: Some(parsed_address.port), address: Some(parsed_address.address), From e74ce80c451a1112fdf141951ec8d6348d2fc661 Mon Sep 17 00:00:00 2001 From: Danlian Akhmedzianov Date: Sun, 17 May 2026 16:27:15 +0300 Subject: [PATCH 08/10] Replace panics with Result in VMess parser Replace unwrap and panic calls with proper error handling using anyhow::Result and IncorrectURI. Includes unit tests for parse_vmess_address. --- src/v2parser/parser/mod.rs | 2 +- src/v2parser/parser/vmess/data.rs | 79 +++++++++++++++++++++++-------- 2 files changed, 61 insertions(+), 20 deletions(-) diff --git a/src/v2parser/parser/mod.rs b/src/v2parser/parser/mod.rs index bcab892..37e339c 100644 --- a/src/v2parser/parser/mod.rs +++ b/src/v2parser/parser/mod.rs @@ -191,7 +191,7 @@ fn get_uri_data(uri: &str) -> anyhow::Result<(String, RawData, OutboundSettings) Ok((String::from("vless"), d, s)) } Some(uri_identifier::Protocols::Vmess) => { - let d = vmess::data::get_data(uri); + let d = vmess::data::get_data(uri)?; let s = vmess::create_outbound_settings(&d); Ok((String::from("vmess"), d, s)) } diff --git a/src/v2parser/parser/vmess/data.rs b/src/v2parser/parser/vmess/data.rs index 70f0043..fb3fc62 100644 --- a/src/v2parser/parser/vmess/data.rs +++ b/src/v2parser/parser/vmess/data.rs @@ -1,12 +1,13 @@ use crate::v2parser::entities::raw_data::RawData; use crate::v2parser::parser::vmess::models::VMessAddress; +use crate::v2parser::utils::incorrect_uri::IncorrectURI; use crate::v2parser::utils::{get_parameter_value, url_decode, url_decode_str}; -use base64::{engine::general_purpose, Engine}; +use base64::{Engine, engine::general_purpose}; use http::Uri; use serde_json::Value; -pub fn get_data(uri: &str) -> RawData { - let data = uri.split_once("vmess://").unwrap().1; +pub fn get_data(uri: &str) -> anyhow::Result { + let data = uri.split_once("vmess://").incorrect_uri()?.1; return match general_purpose::STANDARD .decode(url_decode_str(data).unwrap_or(String::from(data))) @@ -16,11 +17,11 @@ pub fn get_data(uri: &str) -> RawData { }; } -fn get_raw_data_from_base64(decoded_base64: &Vec) -> RawData { - let json_str = std::str::from_utf8(decoded_base64).unwrap(); - let json = serde_json::from_str::(json_str).unwrap(); +fn get_raw_data_from_base64(decoded_base64: &Vec) -> anyhow::Result { + let json_str = std::str::from_utf8(decoded_base64).incorrect_uri()?; + let json = serde_json::from_str::(json_str).incorrect_uri()?; - return RawData { + let result = RawData { remarks: url_decode(get_str_field(&json, "ps")).unwrap_or(String::from("")), uuid: get_str_field(&json, "id"), port: get_str_field(&json, "port") @@ -60,22 +61,24 @@ fn get_raw_data_from_base64(decoded_base64: &Vec) -> RawData { server_method: None, username: None, }; + + Ok(result) } fn get_str_field(json: &Value, field: &str) -> Option { return json.get(field).and_then(|v| v.as_str()).map(String::from); } -fn get_raw_data_from_uri(data: &str) -> RawData { - let query_and_name = data.split_once("?").unwrap().1; +fn get_raw_data_from_uri(data: &str) -> anyhow::Result { + let query_and_name = data.split_once("?").incorrect_uri()?.1; let (raw_query, name) = query_and_name .split_once("#") .unwrap_or((query_and_name, "")); - let parsed_address = parse_vmess_address(data.split_once("?").unwrap().0); + let parsed_address = parse_vmess_address(data.split_once("?").incorrect_uri()?.0)?; let query: Vec<(&str, &str)> = querystring::querify(raw_query); - return RawData { + let result = RawData { remarks: url_decode(Some(String::from(name))).unwrap_or(String::from("")), uuid: Some(parsed_address.uuid), port: Some(parsed_address.port), @@ -106,22 +109,60 @@ fn get_raw_data_from_uri(data: &str) -> RawData { server_method: None, username: None, }; + + Ok(result) } -fn parse_vmess_address(raw_data: &str) -> VMessAddress { +fn parse_vmess_address(raw_data: &str) -> anyhow::Result { let (uuid, raw_address): (String, &str) = match raw_data.split_once("@") { - None => { - panic!("Wrong vmess format, no `@` found in the address and it was not a valid base64"); - } + None => anyhow::bail!( + "Wrong vmess format, no `@` found in the address and it was not a valid base64" + ), Some(data) => (String::from(data.0), data.1), }; let address_wo_slash = raw_address.strip_suffix("/").unwrap_or(raw_address); let parsed = address_wo_slash.parse::().unwrap(); - return VMessAddress { - uuid: url_decode(Some(uuid)).unwrap(), - address: parsed.host().unwrap().to_string(), - port: parsed.port().unwrap().as_u16(), + let result = VMessAddress { + uuid: url_decode(Some(uuid)).incorrect_uri()?, + address: parsed.host().incorrect_uri()?.to_string(), + port: parsed.port().incorrect_uri()?.as_u16(), }; + + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::parse_vmess_address; + + #[test] + fn parse_vmess_address_with_trailing_slash_and_encoded_uuid() { + let parsed = parse_vmess_address("my%2Duuid@example.com:443/").unwrap(); + + assert_eq!(parsed.uuid, "my-uuid"); + assert_eq!(parsed.address, "example.com"); + assert_eq!(parsed.port, 443); + } + + #[test] + fn parse_vmess_address_without_trailing_slash() { + let parsed = parse_vmess_address("plain-uuid@server.test:8443").unwrap(); + + assert_eq!(parsed.uuid, "plain-uuid"); + assert_eq!(parsed.address, "server.test"); + assert_eq!(parsed.port, 8443); + } + + #[test] + fn parse_vmess_address_returns_error_when_at_is_missing() { + let result = parse_vmess_address("plain-uuidserver.test:443"); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap().to_string(), + "Wrong vmess format, no `@` found in the address and it was not a valid base64" + ); + } } From 938296e6f51d6954e8a522b8050b2992ee0925b0 Mon Sep 17 00:00:00 2001 From: Danlian Akhmedzianov Date: Sun, 17 May 2026 16:40:34 +0300 Subject: [PATCH 09/10] Replace panics with error handling in URI parsing Replace `.unwrap()` calls with `.incorrect_uri()?` across Socks, ShadowSocks, Trojan, and VMess parsers to handle malformed URIs gracefully. --- src/v2parser/parser/mod.rs | 2 +- src/v2parser/parser/shadow_socks/data.rs | 9 ++++--- src/v2parser/parser/socks/data.rs | 33 ++++++++++++++---------- src/v2parser/parser/trojan/data.rs | 8 +++--- src/v2parser/parser/vmess/data.rs | 2 +- 5 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/v2parser/parser/mod.rs b/src/v2parser/parser/mod.rs index 37e339c..9176a8e 100644 --- a/src/v2parser/parser/mod.rs +++ b/src/v2parser/parser/mod.rs @@ -206,7 +206,7 @@ fn get_uri_data(uri: &str) -> anyhow::Result<(String, RawData, OutboundSettings) Ok((String::from("shadowsocks"), d, s)) } Some(uri_identifier::Protocols::Socks) => { - let d = socks::data::get_data(uri); + let d = socks::data::get_data(uri)?; let s = socks::create_outbound_settings(&d); Ok((String::from("socks"), d, s)) } diff --git a/src/v2parser/parser/shadow_socks/data.rs b/src/v2parser/parser/shadow_socks/data.rs index 47a513f..3939f19 100644 --- a/src/v2parser/parser/shadow_socks/data.rs +++ b/src/v2parser/parser/shadow_socks/data.rs @@ -2,11 +2,12 @@ use http::Uri; use crate::v2parser::entities::raw_data::RawData; use crate::v2parser::parser::shadow_socks::models::ShadowSocksAddress; +use crate::v2parser::utils::incorrect_uri::IncorrectURI; use crate::v2parser::utils::{url_decode, url_decode_str}; use base64::{Engine, engine::general_purpose}; pub fn get_data(uri: &str) -> anyhow::Result { - let data = uri.split_once("ss://").unwrap().1; + let data = uri.split_once("ss://").incorrect_uri()?.1; let (raw_data, name) = data.split_once("#").unwrap_or((data, "")); let (raw_uri, _) = raw_data.split_once("?").unwrap_or((raw_data, "")); let parsed_address = parse_ss_address(raw_uri)?; @@ -54,7 +55,7 @@ fn parse_ss_address(raw_data: &str) -> anyhow::Result { }; let address_wo_slash = raw_address.strip_suffix("/").unwrap_or(raw_address); - let parsed = address_wo_slash.parse::().unwrap(); + let parsed = address_wo_slash.parse::().incorrect_uri()?; let method_and_password = general_purpose::STANDARD .decode(url_decode_str(&userinfo).unwrap_or(userinfo)) @@ -68,8 +69,8 @@ fn parse_ss_address(raw_data: &str) -> anyhow::Result { return Ok(ShadowSocksAddress { method: String::from(method), password: String::from(password), - address: parsed.host().unwrap().to_string(), - port: parsed.port().unwrap().as_u16(), + address: parsed.host().incorrect_uri()?.to_string(), + port: parsed.port().incorrect_uri()?.as_u16(), }); } diff --git a/src/v2parser/parser/socks/data.rs b/src/v2parser/parser/socks/data.rs index 0db5947..f2265e9 100644 --- a/src/v2parser/parser/socks/data.rs +++ b/src/v2parser/parser/socks/data.rs @@ -1,16 +1,17 @@ -use base64::Engine; -use base64::engine::general_purpose; -use http::Uri; use crate::v2parser::entities::raw_data::RawData; use crate::v2parser::parser::socks::models::SocksAddress; +use crate::v2parser::utils::incorrect_uri::IncorrectURI; use crate::v2parser::utils::{url_decode, url_decode_str}; +use base64::Engine; +use base64::engine::general_purpose; +use http::Uri; -pub fn get_data(uri: &str) -> RawData { - let data = uri.split_once("://").unwrap().1; +pub fn get_data(uri: &str) -> anyhow::Result { + let data = uri.split_once("://").incorrect_uri()?.1; let (raw_data, name) = data.split_once("#").unwrap_or((data, "")); let (raw_uri, _) = raw_data.split_once("?").unwrap_or((raw_data, "")); - let parsed_address = parse_socks_address(raw_uri); - return RawData { + let parsed_address = parse_socks_address(raw_uri).incorrect_uri()?; + let result = RawData { remarks: url_decode(Some(String::from(name))).unwrap_or(String::from("")), username: url_decode(parsed_address.username), address: Some(parsed_address.address), @@ -41,18 +42,20 @@ pub fn get_data(uri: &str) -> RawData { allow_insecure: None, vnext_security: None, }; + + Ok(result) } -fn parse_socks_address(raw_data: &str) -> SocksAddress { +fn parse_socks_address(raw_data: &str) -> anyhow::Result { let (maybe_userinfo, raw_address): (Option, &str) = match raw_data.split_once("@") { Some(data) => (Some(String::from(data.0)), data.1), None => (None, raw_data), }; let address_wo_slash = raw_address.strip_suffix("/").unwrap_or(raw_address); - let parsed = address_wo_slash.parse::().unwrap(); + let parsed = address_wo_slash.parse::().incorrect_uri()?; - return match maybe_userinfo { + let result = match maybe_userinfo { Some(userinfo) => { let url_decoded = url_decode_str(&userinfo).unwrap_or(userinfo); let username_and_password = general_purpose::STANDARD @@ -71,15 +74,17 @@ fn parse_socks_address(raw_data: &str) -> SocksAddress { SocksAddress { username: Some(String::from(username)), password: Some(String::from(password)), - address: parsed.host().unwrap().to_string(), - port: parsed.port().unwrap().as_u16(), + address: parsed.host().incorrect_uri()?.to_string(), + port: parsed.port().incorrect_uri()?.as_u16(), } } None => SocksAddress { username: None, password: None, - address: parsed.host().unwrap().to_string(), - port: parsed.port().unwrap().as_u16(), + address: parsed.host().incorrect_uri()?.to_string(), + port: parsed.port().incorrect_uri()?.as_u16(), }, }; + + Ok(result) } diff --git a/src/v2parser/parser/trojan/data.rs b/src/v2parser/parser/trojan/data.rs index e25cfc4..12d8cfa 100644 --- a/src/v2parser/parser/trojan/data.rs +++ b/src/v2parser/parser/trojan/data.rs @@ -59,12 +59,12 @@ fn parse_trojan_address(raw_data: &str) -> anyhow::Result { }; let address_wo_slash = raw_address.strip_suffix("/").unwrap_or(raw_address); - let parsed = address_wo_slash.parse::().unwrap(); + let parsed = address_wo_slash.parse::().incorrect_uri()?; let result = TrojanAddress { - uuid: url_decode(Some(uuid)).unwrap(), - address: parsed.host().unwrap().to_string(), - port: parsed.port().unwrap().as_u16(), + uuid: url_decode(Some(uuid)).incorrect_uri()?, + address: parsed.host().incorrect_uri()?.to_string(), + port: parsed.port().incorrect_uri()?.as_u16(), }; Ok(result) diff --git a/src/v2parser/parser/vmess/data.rs b/src/v2parser/parser/vmess/data.rs index fb3fc62..3e65a2b 100644 --- a/src/v2parser/parser/vmess/data.rs +++ b/src/v2parser/parser/vmess/data.rs @@ -122,7 +122,7 @@ fn parse_vmess_address(raw_data: &str) -> anyhow::Result { }; let address_wo_slash = raw_address.strip_suffix("/").unwrap_or(raw_address); - let parsed = address_wo_slash.parse::().unwrap(); + let parsed = address_wo_slash.parse::().incorrect_uri()?; let result = VMessAddress { uuid: url_decode(Some(uuid)).incorrect_uri()?, From ebcab5505822135425427482ec10abe48688260e Mon Sep 17 00:00:00 2001 From: Danlian Akhmedzianov Date: Sun, 17 May 2026 16:44:12 +0300 Subject: [PATCH 10/10] Bump version to 0.5.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed2e55b..9b95d6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1321,7 +1321,7 @@ checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rayconf" -version = "0.5.0" +version = "0.5.1" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index 2b97d8c..66de2d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rayconf" -version = "0.5.0" +version = "0.5.1" edition = "2024" [dependencies]