From 199384a28050c3291b6b96e37b6f5e55ed085943 Mon Sep 17 00:00:00 2001 From: NetworkCats Date: Wed, 4 Mar 2026 20:48:59 +0800 Subject: [PATCH] fix: resolve trailing slash 404 and untranslated escaped-quote strings --- src/handlers.rs | 16 ++++++- src/i18n.rs | 79 +++++++++++++++++++++++++++--- src/routes.rs | 9 ++-- templates/_footer_lang.html | 2 +- templates/_search_form.html | 2 +- templates/error_page.html | 2 +- templates/index.html | 6 +-- tests/integration.rs | 96 +++++++++++++++++++++++++++++++++---- 8 files changed, 181 insertions(+), 31 deletions(-) diff --git a/src/handlers.rs b/src/handlers.rs index cab5ce8..4cfe29c 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -461,6 +461,18 @@ pub async fn root_redirect( Redirect::temporary(&redirect_path).into_response() } +pub async fn root_trailing_slash( + Path(lang): Path, + Query(query): Query, +) -> Response { + let mut redirect_path = format!("/{lang}"); + if let Some(ref ip) = query.ip { + redirect_path.push_str("?ip="); + redirect_path.push_str(&urlencoding::encode(ip)); + } + Redirect::permanent(&redirect_path).into_response() +} + pub async fn root( State(state): State, Path(lang): Path, @@ -759,12 +771,12 @@ pub async fn sitemap_xml(State(state): State) -> Response { for locale in Locale::ALL { let tag = locale.tag(); urls.push_str(&format!( - " \n https://{domain}/{tag}/\n" + " \n https://{domain}/{tag}\n" )); for alt in Locale::ALL { let alt_tag = alt.tag(); urls.push_str(&format!( - " \n" + " \n" )); } urls.push_str( diff --git a/src/i18n.rs b/src/i18n.rs index ab0a6c7..a8c60c5 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -185,13 +185,18 @@ fn insert_entry(catalog: &mut Catalog, msgid: &str, msgstr: &str, source: &'stat if msgstr.is_empty() { return; } - // Find the msgstr in the static source to get a &'static str reference. - // This avoids allocating owned Strings at runtime. - let id_static = find_in_static(source, msgid); - let str_static = find_in_static(source, msgstr); - if let (Some(id), Some(s)) = (id_static, str_static) { - catalog.insert(id, s); - } + // Try to find the strings in the static source to get &'static str references, + // avoiding heap allocation. This works for most strings, but fails for strings + // containing escape sequences (e.g. \") since the unescaped text differs from + // the raw PO source. In that case, leak a heap-allocated copy. This is acceptable + // because translations are loaded once at startup. + let id_static = find_in_static(source, msgid).unwrap_or_else(|| leak_string(msgid)); + let str_static = find_in_static(source, msgstr).unwrap_or_else(|| leak_string(msgstr)); + catalog.insert(id_static, str_static); +} + +fn leak_string(s: &str) -> &'static str { + Box::leak(s.to_owned().into_boxed_str()) } fn find_in_static(source: &'static str, needle: &str) -> Option<&'static str> { @@ -452,4 +457,64 @@ mod tests { // static-reference optimization, but we can verify the PO files load. let _ = translate(Locale::Es, "FAQ"); } + + #[test] + fn translate_strings_with_escaped_quotes() { + // These strings contain \" in PO files. The parser must correctly unescape + // them and produce valid translations for all locales. + let msgid_rug_pull = "Rest assured, this service will definitely not \"Rug Pull.\""; + + for locale in Locale::ALL { + let result = translate(*locale, msgid_rug_pull); + assert!( + !result.is_empty(), + "locale {:?} has no translation for Rug Pull string", + locale + ); + if *locale != Locale::En { + assert_ne!( + result, msgid_rug_pull, + "locale {:?} should have a non-English translation for Rug Pull string", + locale + ); + } + } + } + + #[test] + fn translate_strings_with_html_href() { + // Strings containing have escaped quotes in PO files. + let msgid_api = "Our API is only suitable for manual calls or small-scale projects. If your website uses our API to query visitor IPs, please ensure you use a message queue to send requests to avoid blocking. If your project has high traffic or is latency-sensitive, please use our open-source offline database: Merged IP Database, which is the same database used by this project."; + let msgid_data = "IP geographic data primarily comes from the free databases of MaxMind and DB-IP; AS data comes from IPinfo's free database; and IP proxy data comes from my own OpenProxyDB database."; + + for locale in Locale::ALL { + let api_result = translate(*locale, msgid_api); + assert!( + !api_result.is_empty(), + "locale {:?} has no translation for API string", + locale + ); + if *locale != Locale::En { + assert_ne!( + api_result, msgid_api, + "locale {:?} should have a non-English translation for API string", + locale + ); + } + + let data_result = translate(*locale, msgid_data); + assert!( + !data_result.is_empty(), + "locale {:?} has no translation for data source string", + locale + ); + if *locale != Locale::En { + assert_ne!( + data_result, msgid_data, + "locale {:?} should have a non-English translation for data source string", + locale + ); + } + } + } } diff --git a/src/routes.rs b/src/routes.rs index 7bf7dba..294791a 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,16 +1,12 @@ -use axum::Router; use axum::middleware; use axum::routing::get; +use axum::Router; use crate::handlers::{self, AppState}; use crate::middleware::security_headers; use crate::static_assets; pub fn build_router(state: AppState) -> Router { - let localized = Router::new() - .route("/", get(handlers::root)) - .with_state(state.clone()); - Router::new() .route("/", get(handlers::root_redirect)) .route("/health", get(handlers::health)) @@ -28,7 +24,8 @@ pub fn build_router(state: AppState) -> Router { .route("/hosting", get(handlers::hosting_handler)) .route("/tor", get(handlers::tor_handler)) .route("/static/{*path}", get(static_assets::serve_static)) - .nest("/{lang}", localized) + .route("/{lang}", get(handlers::root)) + .route("/{lang}/", get(handlers::root_trailing_slash)) .fallback(handlers::not_found) .layer(middleware::from_fn(security_headers)) .with_state(state) diff --git a/templates/_footer_lang.html b/templates/_footer_lang.html index 6973f5d..8fb9f3e 100644 --- a/templates/_footer_lang.html +++ b/templates/_footer_lang.html @@ -2,7 +2,7 @@ diff --git a/templates/_search_form.html b/templates/_search_form.html index 730c346..f4b1394 100644 --- a/templates/_search_form.html +++ b/templates/_search_form.html @@ -1,4 +1,4 @@ -