mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
87190b7af3
Split the 1691-line deeplink.rs into a well-organized module directory: - mod.rs (120 lines): DeepLinkImportRequest struct and public exports - parser.rs (321 lines): URL parsing logic for all resource types - provider.rs (510 lines): Provider import and config merge logic - mcp.rs (191 lines): MCP server batch import - prompt.rs (86 lines): Prompt import - skill.rs (52 lines): Skill repository import - utils.rs (99 lines): Shared utilities (URL validation, Base64 decoding) - tests.rs (384 lines): All unit tests Benefits: - Max file size reduced from 1691 to 510 lines - Each resource type has its own import module - Shared utilities extracted for reuse - All 17 deeplink tests + 107 total tests passing
100 lines
3.2 KiB
Rust
100 lines
3.2 KiB
Rust
//! Deep link utility functions
|
|
//!
|
|
//! Common helpers for URL validation, Base64 decoding, etc.
|
|
|
|
use crate::error::AppError;
|
|
use base64::prelude::*;
|
|
use url::Url;
|
|
|
|
/// Validate that a string is a valid HTTP(S) URL
|
|
pub fn validate_url(url_str: &str, field_name: &str) -> Result<(), AppError> {
|
|
let url = Url::parse(url_str)
|
|
.map_err(|e| AppError::InvalidInput(format!("Invalid URL for '{field_name}': {e}")))?;
|
|
|
|
let scheme = url.scheme();
|
|
if scheme != "http" && scheme != "https" {
|
|
return Err(AppError::InvalidInput(format!(
|
|
"Invalid URL scheme for '{field_name}': must be http or https, got '{scheme}'"
|
|
)));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Decode a Base64 parameter from deep link URL
|
|
///
|
|
/// This function handles common issues with Base64 in URLs:
|
|
/// - `+` being decoded as space
|
|
/// - Missing padding `=`
|
|
/// - Both standard and URL-safe Base64 variants
|
|
pub fn decode_base64_param(field: &str, raw: &str) -> Result<Vec<u8>, AppError> {
|
|
let mut candidates: Vec<String> = Vec::new();
|
|
// Keep spaces (to restore `+`), but remove newlines
|
|
let trimmed = raw.trim_matches(|c| c == '\r' || c == '\n');
|
|
|
|
// First try restoring spaces to "+"
|
|
if trimmed.contains(' ') {
|
|
let replaced = trimmed.replace(' ', "+");
|
|
if !replaced.is_empty() && !candidates.contains(&replaced) {
|
|
candidates.push(replaced);
|
|
}
|
|
}
|
|
|
|
// Original value
|
|
if !trimmed.is_empty() && !candidates.contains(&trimmed.to_string()) {
|
|
candidates.push(trimmed.to_string());
|
|
}
|
|
|
|
// Add padding variants
|
|
let existing = candidates.clone();
|
|
for candidate in existing {
|
|
let mut padded = candidate.clone();
|
|
let remainder = padded.len() % 4;
|
|
if remainder != 0 {
|
|
padded.extend(std::iter::repeat_n('=', 4 - remainder));
|
|
}
|
|
if !candidates.contains(&padded) {
|
|
candidates.push(padded);
|
|
}
|
|
}
|
|
|
|
let mut last_error: Option<String> = None;
|
|
for candidate in candidates {
|
|
for engine in [
|
|
&BASE64_STANDARD,
|
|
&BASE64_STANDARD_NO_PAD,
|
|
&BASE64_URL_SAFE,
|
|
&BASE64_URL_SAFE_NO_PAD,
|
|
] {
|
|
match engine.decode(&candidate) {
|
|
Ok(bytes) => return Ok(bytes),
|
|
Err(err) => last_error = Some(err.to_string()),
|
|
}
|
|
}
|
|
}
|
|
|
|
Err(AppError::InvalidInput(format!(
|
|
"{field} 参数 Base64 解码失败:{}。请确认链接参数已用 Base64 编码并经过 URL 转义(尤其是将 '+' 编码为 %2B,或使用 URL-safe Base64)。",
|
|
last_error.unwrap_or_else(|| "未知错误".to_string())
|
|
)))
|
|
}
|
|
|
|
/// Infer homepage URL from API endpoint
|
|
///
|
|
/// Examples:
|
|
/// - https://api.anthropic.com/v1 → https://anthropic.com
|
|
/// - https://api.openai.com/v1 → https://openai.com
|
|
/// - https://api-test.company.com/v1 → https://company.com
|
|
pub fn infer_homepage_from_endpoint(endpoint: &str) -> Option<String> {
|
|
let url = Url::parse(endpoint).ok()?;
|
|
let host = url.host_str()?;
|
|
|
|
// Remove common API prefixes
|
|
let clean_host = host
|
|
.strip_prefix("api.")
|
|
.or_else(|| host.strip_prefix("api-"))
|
|
.unwrap_or(host);
|
|
|
|
Some(format!("https://{clean_host}"))
|
|
}
|