mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-08 02:52:07 +08:00
@@ -2,7 +2,8 @@ use anyhow::{Context as _, Result, bail};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use clash_verge_service_ipc::{RemoteProvider, RuntimeAsset, RuntimeBundle};
|
||||
use serde_yaml_ng::{Mapping, Value};
|
||||
use std::collections::HashSet;
|
||||
use sha2::{Digest as _, Sha256};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
pub(crate) const GEO_ASSETS: &[&str] = &[
|
||||
@@ -54,6 +55,94 @@ pub(crate) fn remote_providers_of(config: &Mapping, config_root: &Path) -> Resul
|
||||
Ok(collect_runtime_parts(Value::Mapping(config.clone()), config_root)?.remote_providers)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ProviderPathOwners {
|
||||
remote: BTreeMap<String, Vec<(&'static str, Value)>>,
|
||||
local: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_provider_path_conflicts(config: &mut Mapping, config_root: &Path) -> Result<()> {
|
||||
let config_root = std::fs::canonicalize(config_root)?;
|
||||
let mut paths = BTreeMap::<String, ProviderPathOwners>::new();
|
||||
let mut reserved: HashSet<String> = GEO_ASSETS.iter().map(|name| (*name).to_owned()).collect();
|
||||
for section in ["proxy-providers", "rule-providers"] {
|
||||
let Some(providers) = config.get(section).and_then(Value::as_mapping) else {
|
||||
continue;
|
||||
};
|
||||
for (name, provider) in providers {
|
||||
let Some(raw_path) = provider.get("path").and_then(Value::as_str) else {
|
||||
continue;
|
||||
};
|
||||
let is_remote = provider.get("type").and_then(Value::as_str) == Some("http");
|
||||
let destination = if is_remote {
|
||||
provider_destination(&config_root, raw_path)
|
||||
} else {
|
||||
local_provider_source(&config_root, raw_path)
|
||||
.and_then(|source| destination_below_root(&config_root, &source))
|
||||
.or_else(|_| provider_destination(&config_root, raw_path))
|
||||
};
|
||||
let Ok(destination) = destination else {
|
||||
continue;
|
||||
};
|
||||
reserved.insert(destination.clone());
|
||||
let owners = paths.entry(destination).or_default();
|
||||
match (is_remote, provider.get("url").and_then(Value::as_str)) {
|
||||
(true, Some(url)) => owners
|
||||
.remote
|
||||
.entry(url.to_owned())
|
||||
.or_default()
|
||||
.push((section, name.clone())),
|
||||
_ => owners.local = true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (destination, owners) in paths {
|
||||
if owners.local || owners.remote.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
// Allocate every source in a conflict group, so declaration order cannot select a cache's owner.
|
||||
for (url, providers) in owners.remote {
|
||||
let replacement = allocate_provider_destination(&destination, &url, &mut reserved);
|
||||
for (section, name) in providers {
|
||||
config[section][&name]["path"] = Value::String(replacement.clone());
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"provider {section}/{name:?} shared cache {destination:?} with another source; using {replacement:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn allocate_provider_destination(destination: &str, url: &str, reserved: &mut HashSet<String>) -> String {
|
||||
let mut digest = Sha256::new();
|
||||
digest.update(destination.as_bytes());
|
||||
digest.update([0]);
|
||||
digest.update(url.as_bytes());
|
||||
let digest: String = digest.finalize().iter().map(|byte| format!("{byte:02x}")).collect();
|
||||
let path = Path::new(destination);
|
||||
let extension = path
|
||||
.extension()
|
||||
.map(|ext| format!(".{}", ext.to_string_lossy()))
|
||||
.unwrap_or_default();
|
||||
let mut suffix = String::new();
|
||||
let mut sequence = 0;
|
||||
loop {
|
||||
let candidate = path
|
||||
.with_file_name(format!("cvr-{digest}{suffix}{extension}"))
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
if reserved.insert(candidate.clone()) {
|
||||
return candidate;
|
||||
}
|
||||
sequence += 1;
|
||||
suffix = format!("-{sequence}");
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_runtime_parts(mut config: Value, config_root: &Path) -> Result<RuntimeParts> {
|
||||
let config_root = std::fs::canonicalize(config_root)?;
|
||||
let mut assets = Vec::new();
|
||||
@@ -255,7 +344,91 @@ fn normalized_destination(relative: &Path) -> Result<String> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::collect_runtime_bundle;
|
||||
use super::{collect_runtime_bundle, remote_providers_of, resolve_provider_path_conflicts};
|
||||
|
||||
#[tokio::test]
|
||||
async fn conflicting_sources_get_separate_paths_in_config_bundle_and_readback() -> anyhow::Result<()> {
|
||||
let root = std::env::temp_dir().join(format!("clash-verge-provider-paths-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&root)?;
|
||||
let mut config: serde_yaml_ng::Mapping = serde_yaml_ng::from_str(
|
||||
"rule-providers:\n AppleDev:\n type: http\n url: https://one.example/AppleDev.yaml\n path: ./providers/rule/AppleDev.yaml\n mirror:\n type: http\n url: https://two.example/AppleDev.yaml\n path: providers/rule/AppleDev.yaml\n",
|
||||
)?;
|
||||
let original = config.clone();
|
||||
let root = std::fs::canonicalize(root)?;
|
||||
let mut reordered = config.clone();
|
||||
reordered["rule-providers"] = serde_yaml_ng::from_str(&format!(
|
||||
"mirror:\n type: http\n url: https://two.example/AppleDev.yaml\n path: {}\nAppleDev:\n type: http\n url: https://one.example/AppleDev.yaml\n path: providers/rule/AppleDev.yaml\n",
|
||||
root.join("providers/rule/AppleDev.yaml").display(),
|
||||
))?;
|
||||
|
||||
resolve_provider_path_conflicts(&mut config, &root)?;
|
||||
let config_file = root.join("config.yaml");
|
||||
std::fs::write(&config_file, serde_yaml_ng::to_string(&config)?)?;
|
||||
let bundle = collect_runtime_bundle(&config_file, &root.join("mihomo")).await?;
|
||||
let paths = &config["rule-providers"];
|
||||
assert_ne!(paths["AppleDev"]["path"], paths["mirror"]["path"]);
|
||||
resolve_provider_path_conflicts(&mut reordered, &root)?;
|
||||
assert_eq!(
|
||||
config, reordered,
|
||||
"path allocation must not depend on declaration order or path spelling"
|
||||
);
|
||||
let resolved = config.clone();
|
||||
resolve_provider_path_conflicts(&mut config, &root)?;
|
||||
assert_eq!(
|
||||
config, resolved,
|
||||
"resolving an already generated config must be idempotent"
|
||||
);
|
||||
|
||||
assert_eq!(serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&bundle.yaml)?, config);
|
||||
let readback = remote_providers_of(&config, &root)?;
|
||||
assert_eq!(bundle.remote_providers.len(), 2);
|
||||
for declared in readback {
|
||||
assert!(bundle.remote_providers.contains(&declared.provider));
|
||||
assert_eq!(
|
||||
config[declared.section][&declared.name]["path"],
|
||||
declared.provider.destination
|
||||
);
|
||||
assert_eq!(
|
||||
config[declared.section][&declared.name]["url"],
|
||||
original[declared.section][&declared.name]["url"]
|
||||
);
|
||||
}
|
||||
std::fs::remove_dir_all(root)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocated_provider_paths_do_not_claim_another_declared_path() -> anyhow::Result<()> {
|
||||
let root = std::env::temp_dir();
|
||||
let original: serde_yaml_ng::Mapping = serde_yaml_ng::from_str(
|
||||
"proxy-providers:\n a:\n type: http\n url: https://one.example/p.yaml\n path: ./providers/shared.yaml\nrule-providers:\n b:\n type: http\n url: https://two.example/r.yaml\n path: ./providers/shared.yaml\n",
|
||||
)?;
|
||||
let mut config = original.clone();
|
||||
resolve_provider_path_conflicts(&mut config, &root)?;
|
||||
let allocated = config["proxy-providers"]["a"]["path"].clone();
|
||||
let mut config = original;
|
||||
config["rule-providers"]["reserved"] = serde_yaml_ng::from_str("type: file\npath: placeholder")?;
|
||||
config["rule-providers"]["reserved"]["path"] = allocated.clone();
|
||||
resolve_provider_path_conflicts(&mut config, &root)?;
|
||||
assert_ne!(config["proxy-providers"]["a"]["path"], allocated);
|
||||
assert_ne!(
|
||||
config["proxy-providers"]["a"]["path"],
|
||||
config["rule-providers"]["b"]["path"]
|
||||
);
|
||||
assert_eq!(config["rule-providers"]["reserved"]["path"], allocated);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_path_resolution_leaves_shared_sources_and_local_files_unchanged() -> anyhow::Result<()> {
|
||||
let mut config: serde_yaml_ng::Mapping = serde_yaml_ng::from_str(
|
||||
"rule-providers:\n a:\n type: http\n url: https://one.example/r.yaml\n path: ./rules/shared.yaml\n b:\n type: http\n url: https://one.example/r.yaml\n path: ./rules/shared.yaml\n local:\n type: file\n path: ./rules/local.yaml\n remote:\n type: http\n url: https://two.example/r.yaml\n path: ./rules/local.yaml\n mirror:\n type: http\n url: https://three.example/r.yaml\n path: ./rules/local.yaml\n",
|
||||
)?;
|
||||
let original = config.clone();
|
||||
resolve_provider_path_conflicts(&mut config, &std::env::temp_dir())?;
|
||||
assert_eq!(config, original);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn collects_only_local_providers_and_existing_geo_assets() -> anyhow::Result<()> {
|
||||
|
||||
Reference in New Issue
Block a user