Decode percent-encoded tags and filter release artifacts

This commit is contained in:
2026-07-08 21:14:00 +03:00
parent f6b722b22a
commit 2e80f7b8eb
8 changed files with 179 additions and 9 deletions
+45
View File
@@ -1,4 +1,6 @@
use percent_encoding::percent_decode_str;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const DEFAULT_LOCAL_SINGBOX_LISTEN_HOST: &str = "127.0.0.1";
pub const DEFAULT_LOCAL_SINGBOX_LISTEN_PORT: u16 = 1080;
@@ -159,6 +161,12 @@ impl LocalSingBoxConfig {
.as_deref()
.map(redact_subscription_url)
}
pub fn normalize_percent_encoded_tags(&mut self) {
if let Some(selected_server_tag) = self.selected_server_tag.as_mut() {
*selected_server_tag = decode_percent_encoded_utf8(selected_server_tag);
}
}
}
impl Default for LocalSingBoxConfig {
@@ -186,6 +194,36 @@ pub struct SubscriptionCache {
pub fetched_at: String,
}
impl SubscriptionCache {
pub fn normalize_percent_encoded_tags(&mut self) {
for server in &mut self.servers {
server.tag = decode_percent_encoded_utf8(&server.tag);
}
let Some(outbounds) = self
.config
.get_mut("outbounds")
.and_then(Value::as_array_mut)
else {
return;
};
for outbound in outbounds {
let Some(decoded_tag) = outbound
.get("tag")
.and_then(Value::as_str)
.map(decode_percent_encoded_utf8)
else {
continue;
};
if let Some(object) = outbound.as_object_mut() {
object.insert("tag".to_string(), Value::String(decoded_tag));
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SubscriptionServer {
pub tag: String,
@@ -274,3 +312,10 @@ pub fn redact_subscription_url(raw_url: &str) -> String {
}
}
}
pub fn decode_percent_encoded_utf8(value: &str) -> String {
percent_decode_str(value)
.decode_utf8()
.map(|decoded| decoded.into_owned())
.unwrap_or_else(|_| value.to_string())
}
+10 -2
View File
@@ -96,7 +96,10 @@ impl JsonStorage {
}
pub fn read_local_singbox_config(&self) -> io::Result<LocalSingBoxConfig> {
self.read_json_or_default(&self.paths.local_singbox_file)
let mut config: LocalSingBoxConfig =
self.read_json_or_default(&self.paths.local_singbox_file)?;
config.normalize_percent_encoded_tags();
Ok(config)
}
pub fn write_local_singbox_config(&self, config: &LocalSingBoxConfig) -> io::Result<()> {
@@ -104,7 +107,12 @@ impl JsonStorage {
}
pub fn read_singbox_subscription_cache(&self) -> io::Result<Option<SubscriptionCache>> {
self.read_optional_json(&self.paths.singbox_subscription_cache_file)
let mut cache = self
.read_optional_json::<SubscriptionCache>(&self.paths.singbox_subscription_cache_file)?;
if let Some(cache) = cache.as_mut() {
cache.normalize_percent_encoded_tags();
}
Ok(cache)
}
pub fn write_singbox_subscription_cache(&self, cache: &SubscriptionCache) -> io::Result<()> {
+5 -2
View File
@@ -1,4 +1,4 @@
use crate::models::{SubscriptionCache, SubscriptionServer};
use crate::models::{decode_percent_encoded_utf8, SubscriptionCache, SubscriptionServer};
use base64::{engine::general_purpose, Engine};
use serde_json::{json, Map, Value};
use std::time::{SystemTime, UNIX_EPOCH};
@@ -192,7 +192,10 @@ fn parse_vless_url(raw_url: &str) -> Result<Value, SubscriptionError> {
}
let parsed = Url::parse(raw_url).map_err(|_| SubscriptionError::new("Invalid VLESS URL"))?;
let tag = parsed.fragment().unwrap_or("vless-out").to_string();
let tag = parsed
.fragment()
.map(decode_percent_encoded_utf8)
.unwrap_or_else(|| "vless-out".to_string());
let uuid = parsed.username().trim().to_string();
let server = parsed.host_str().map(str::to_string).unwrap_or_default();
let server_port = parsed.port_or_known_default().unwrap_or(443);