use crate::error::VecboostError;
use hf_hub::{HFClientBuilder, HFRepositorySync, RepoTypeModel, split_id};
pub fn is_valid_hf_repo_id(repo_id: &str) -> bool {
if repo_id.is_empty() {
return false;
}
if repo_id.starts_with('/') || repo_id.ends_with('/') {
return false;
}
if repo_id.contains("..") || repo_id.contains("//") {
return false;
}
let segments: Vec<&str> = repo_id.split('/').collect();
if segments.len() > 2 {
return false;
}
segments.iter().all(|seg| {
!seg.is_empty()
&& *seg != "."
&& seg
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
})
}
fn detect_mirror_risk() -> Option<String> {
let endpoint = std::env::var("HF_ENDPOINT").ok()?;
if endpoint.is_empty() || endpoint.contains("huggingface.co") || endpoint.contains("hf.co") {
return None;
}
Some(endpoint)
}
pub(crate) fn build_hf_repo(
repo_id: &str,
) -> Result<HFRepositorySync<RepoTypeModel>, VecboostError> {
if !is_valid_hf_repo_id(repo_id) {
return Err(VecboostError::ModelLoadError(format!(
"Invalid HuggingFace repo ID '{}': must match 'organization/model-name' \
pattern with alphanumeric, dash, underscore, dot characters only",
repo_id
)));
}
if let Some(endpoint) = detect_mirror_risk() {
log::warn!(
"HF_ENDPOINT={} detected — hf-hub 1.0.0 requires ETag headers which \
some mirrors (e.g. hf-mirror.com) may not provide. If model download \
fails with 'missing ETag header', pre-download the model manually and \
set `model_path` in config to use local loading instead.",
endpoint
);
}
#[allow(unused_mut)]
let mut builder = HFClientBuilder::new().retry_max_attempts(2);
#[cfg(feature = "http")]
{
let http_client = reqwest::Client::builder()
.connect_timeout(std::time::Duration::from_secs(10))
.timeout(std::time::Duration::from_secs(300))
.build()
.map_err(|e| VecboostError::ModelLoadError(format!("HF client build failed: {e}")))?;
builder = builder.client(http_client);
}
let api = builder.build_sync().map_err(|e| {
let msg = e.to_string();
if msg.contains("ETag") || msg.contains("missing") {
VecboostError::ModelLoadError(format!(
"HuggingFace hub initialization failed: {}. \
If using HF_ENDPOINT mirror, it may be incompatible with hf-hub 1.0.0 \
Workaround: pre-download the model and set \
`model_path` in config.",
msg
))
} else {
VecboostError::ModelLoadError(msg)
}
})?;
let (owner, name) = split_id(repo_id);
Ok(api.model(owner, name))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_valid_hf_repo_id_valid_two_segments() {
assert!(is_valid_hf_repo_id("BAAI/bge-m3"));
assert!(is_valid_hf_repo_id(
"sentence-transformers/all-MiniLM-L6-v2"
));
assert!(is_valid_hf_repo_id("org/model_name"));
assert!(is_valid_hf_repo_id("org/model.v2"));
}
#[test]
fn test_is_valid_hf_repo_id_valid_single_segment() {
assert!(is_valid_hf_repo_id("bert-base-uncased"));
assert!(is_valid_hf_repo_id("gpt2"));
assert!(is_valid_hf_repo_id("model_v1.2"));
}
#[test]
fn test_is_valid_hf_repo_id_rejects_empty() {
assert!(!is_valid_hf_repo_id(""));
}
#[test]
fn test_is_valid_hf_repo_id_rejects_path_traversal() {
assert!(!is_valid_hf_repo_id("../etc/passwd"));
assert!(!is_valid_hf_repo_id("org/../../etc/passwd"));
assert!(!is_valid_hf_repo_id("./model"));
assert!(!is_valid_hf_repo_id("org/.."));
}
#[test]
fn test_is_valid_hf_repo_id_rejects_leading_trailing_slash() {
assert!(!is_valid_hf_repo_id("/etc/passwd"));
assert!(!is_valid_hf_repo_id("org/model/"));
assert!(!is_valid_hf_repo_id("/"));
}
#[test]
fn test_is_valid_hf_repo_id_rejects_double_slash() {
assert!(!is_valid_hf_repo_id("org//model"));
assert!(!is_valid_hf_repo_id("//model"));
}
#[test]
fn test_is_valid_hf_repo_id_rejects_more_than_two_segments() {
assert!(!is_valid_hf_repo_id("org/sub/model"));
assert!(!is_valid_hf_repo_id("a/b/c/d"));
}
#[test]
fn test_is_valid_hf_repo_id_rejects_special_chars() {
assert!(!is_valid_hf_repo_id("org/model:name"));
assert!(!is_valid_hf_repo_id("org/model@v1"));
assert!(!is_valid_hf_repo_id("org/model name"));
assert!(!is_valid_hf_repo_id("org/model$evil"));
}
#[test]
fn test_is_valid_hf_repo_id_rejects_dot_only_segment() {
assert!(!is_valid_hf_repo_id("."));
assert!(!is_valid_hf_repo_id("org/."));
assert!(!is_valid_hf_repo_id("./model"));
}
#[test]
fn test_build_hf_repo_invalid_repo_id() {
let result = build_hf_repo("../etc/passwd");
assert!(result.is_err());
match result.unwrap_err() {
VecboostError::ModelLoadError(msg) => {
assert!(msg.contains("Invalid HuggingFace repo ID"));
}
other => panic!("Expected ModelLoadError, got: {:?}", other),
}
}
#[test]
fn test_build_hf_repo_valid_repo_id() {
let result = build_hf_repo("BAAI/bge-m3");
assert!(result.is_ok() || result.is_err());
}
#[test]
fn test_detect_mirror_risk_no_env() {
let saved = std::env::var("HF_ENDPOINT").ok();
unsafe { std::env::remove_var("HF_ENDPOINT") };
assert!(detect_mirror_risk().is_none());
if let Some(v) = saved {
unsafe { std::env::set_var("HF_ENDPOINT", v) };
}
}
#[test]
fn test_detect_mirror_risk_official_endpoint() {
let saved = std::env::var("HF_ENDPOINT").ok();
unsafe { std::env::set_var("HF_ENDPOINT", "https://huggingface.co") };
assert!(detect_mirror_risk().is_none());
match saved {
Some(v) => unsafe { std::env::set_var("HF_ENDPOINT", v) },
None => unsafe { std::env::remove_var("HF_ENDPOINT") },
}
}
#[test]
fn test_detect_mirror_risk_mirror_endpoint() {
let saved = std::env::var("HF_ENDPOINT").ok();
unsafe { std::env::set_var("HF_ENDPOINT", "https://hf-mirror.com") };
let result = detect_mirror_risk();
assert!(result.is_some());
assert_eq!(result.unwrap(), "https://hf-mirror.com");
match saved {
Some(v) => unsafe { std::env::set_var("HF_ENDPOINT", v) },
None => unsafe { std::env::remove_var("HF_ENDPOINT") },
}
}
}