use std::path::{Path, PathBuf};
pub fn hub_dir() -> PathBuf {
absolute(resolve(&env, home_dir().as_deref()))
}
pub fn hf_home() -> PathBuf {
absolute(resolve_home(&env, home_dir().as_deref()))
}
pub fn repo_dir(repo_id: &str) -> PathBuf {
repo_dir_in(&hub_dir(), repo_id)
}
pub fn repo_dir_in(hub: &Path, repo_id: &str) -> PathBuf {
hub.join(format!("models--{}", repo_id.replace('/', "--")))
}
pub fn checked_repo_dir_in(hub: &Path, repo_id: &str) -> Option<PathBuf> {
let part_ok = |p: &str| {
!p.is_empty()
&& p != "."
&& p != ".."
&& p.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
};
let mut parts = repo_id.split('/');
let ok = match (parts.next(), parts.next(), parts.next()) {
(Some(name), None, None) => part_ok(name),
(Some(owner), Some(name), None) => part_ok(owner) && part_ok(name),
_ => false,
};
ok.then(|| repo_dir_in(hub, repo_id))
}
pub fn endpoint() -> String {
env("HF_ENDPOINT")
.map(|endpoint| endpoint.trim_end_matches('/').to_string())
.unwrap_or_else(|| "https://huggingface.co".to_string())
}
pub fn api() -> hf_hub::api::tokio::ApiBuilder {
let mut builder = hf_hub::api::tokio::ApiBuilder::from_cache(hf_hub::Cache::new(hub_dir()))
.with_token(token());
if let Some(endpoint) = env("HF_ENDPOINT") {
builder = builder.with_endpoint(endpoint);
}
builder
}
const HF_CHILD_CREDENTIALS: &[&str] = &["HF_TOKEN"];
pub fn apply_to(cmd: &mut tokio::process::Command) {
for (key, value) in child_hub_env() {
cmd.env(key, value);
}
}
pub fn apply_to_std(cmd: &mut std::process::Command) {
for (key, value) in child_hub_env() {
cmd.env(key, value);
}
}
fn child_hub_env() -> Vec<(&'static str, std::ffi::OsString)> {
let mut vars = vec![("HF_HUB_CACHE", hub_dir().into_os_string())];
if crate::registry::never_download_in_scope() {
vars.push(("HF_HUB_OFFLINE", "1".into()));
}
vars
}
pub fn apply_with_token(cmd: &mut tokio::process::Command) {
apply_to(cmd);
cmd.envs(car_secrets::child_credentials(HF_CHILD_CREDENTIALS));
}
pub fn apply_with_token_std(cmd: &mut std::process::Command) {
apply_to_std(cmd);
cmd.envs(car_secrets::child_credentials(HF_CHILD_CREDENTIALS));
}
fn env(name: &str) -> Option<String> {
std::env::var(name).ok().filter(|v| !v.is_empty())
}
fn token() -> Option<String> {
if let Some(token) = car_secrets::resolve_env_or_keychain("HF_TOKEN").filter(|t| !t.is_empty())
{
return Some(token);
}
let path = match env("HF_TOKEN_PATH") {
Some(path) => absolute(expand(&path, &env, home_dir().as_deref())),
None => hf_home().join("token"),
};
std::fs::read_to_string(path)
.ok()
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty())
}
fn home_dir() -> Option<PathBuf> {
let var = if cfg!(windows) { "USERPROFILE" } else { "HOME" };
env(var)
.map(PathBuf::from)
.or_else(std::env::home_dir)
.filter(|home| home.is_absolute())
}
fn absolute(path: PathBuf) -> PathBuf {
if path.is_absolute() {
return path;
}
std::path::absolute(&path).unwrap_or(path)
}
fn resolve(env: &dyn Fn(&str) -> Option<String>, home: Option<&Path>) -> PathBuf {
for name in ["HF_HUB_CACHE", "HUGGINGFACE_HUB_CACHE"] {
if let Some(value) = env(name) {
return expand(&value, env, home);
}
}
resolve_home(env, home).join("hub")
}
fn resolve_home(env: &dyn Fn(&str) -> Option<String>, home: Option<&Path>) -> PathBuf {
if let Some(value) = env("HF_HOME") {
return expand(&value, env, home);
}
let cache = match env("XDG_CACHE_HOME") {
Some(xdg) => PathBuf::from(xdg),
None => home.unwrap_or(Path::new(".")).join(".cache"),
};
expand(&cache.join("huggingface").to_string_lossy(), env, home)
}
fn expand(value: &str, env: &dyn Fn(&str) -> Option<String>, home: Option<&Path>) -> PathBuf {
let tilde = match (value.strip_prefix('~'), home) {
(Some(tail), Some(home))
if tail.is_empty()
|| tail.starts_with('/')
|| (cfg!(windows) && tail.starts_with('\\')) =>
{
home.join(tail.trim_start_matches(['/', '\\']))
.to_string_lossy()
.into_owned()
}
_ => value.to_string(),
};
let dollar = expand_dollar(&tilde, env);
PathBuf::from(if cfg!(windows) {
expand_percent(&dollar, env)
} else {
dollar
})
}
fn expand_dollar(value: &str, env: &dyn Fn(&str) -> Option<String>) -> String {
let mut out = String::with_capacity(value.len());
let mut rest = value;
while let Some(i) = rest.find('$') {
out.push_str(&rest[..i]);
let after = &rest[i + 1..];
let (name, consumed) = match after.strip_prefix('{') {
Some(braced) => match braced.find('}') {
Some(end) => (&braced[..end], end + 2),
None => ("", 0),
},
None => {
let end = after
.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
.unwrap_or(after.len());
(&after[..end], end)
}
};
match env(name).filter(|_| !name.is_empty()) {
Some(v) => out.push_str(&v),
None => out.push_str(&rest[i..i + 1 + consumed]),
}
rest = &rest[i + 1 + consumed..];
}
out.push_str(rest);
out
}
fn expand_percent(value: &str, env: &dyn Fn(&str) -> Option<String>) -> String {
let mut out = String::with_capacity(value.len());
let mut rest = value;
while let Some(i) = rest.find('%') {
out.push_str(&rest[..i]);
let after = &rest[i + 1..];
match after.find('%') {
Some(end) if end > 0 => {
let name = &after[..end];
match env(name) {
Some(v) => out.push_str(&v),
None => out.push_str(&rest[i..i + end + 2]),
}
rest = &after[end + 1..];
}
_ => {
out.push('%');
rest = after;
}
}
}
out.push_str(rest);
out
}
#[cfg(all(test, unix))]
pub(crate) mod token_probe {
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
pub(crate) struct Probe {
dir: tempfile::TempDir,
}
impl Probe {
pub(crate) fn new() -> Self {
Self {
dir: tempfile::tempdir().unwrap(),
}
}
pub(crate) fn dir(&self) -> &Path {
self.dir.path()
}
pub(crate) fn marker(&self) -> PathBuf {
self.dir.path().join("marker")
}
pub(crate) fn fake_program(&self, path: &Path) {
let script = format!(
"#!/bin/sh\nif [ \"$1\" = \"--help\" ]; then exit 0; fi\nif [ -n \"$HF_TOKEN\" ]; then echo yes; else echo no; fi > '{}'\n",
self.marker().display()
);
let _ = std::fs::remove_file(self.marker());
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, script).unwrap();
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
}
pub(crate) fn recorded(&self) -> String {
for _ in 0..crate::LOADED_WAIT.as_millis() / 50 {
if let Ok(text) = std::fs::read_to_string(self.marker()) {
if !text.is_empty() {
return text.trim().to_string();
}
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
"never ran".into()
}
}
pub(crate) const WITH_TOKEN: &[(&str, &str)] = &[("HF_TOKEN", "hf_fake_token")];
}
#[cfg(all(test, unix))]
mod token_tests {
use super::token_probe::{Probe, WITH_TOKEN};
use super::*;
const BUSY_ATTEMPTS: u32 = 50;
const BUSY_BACKOFF: std::time::Duration = std::time::Duration::from_millis(10);
fn output_riding_out_busy(cmd: &mut std::process::Command) -> std::process::Output {
for _ in 0..BUSY_ATTEMPTS {
match cmd.output() {
Err(e) if e.kind() == std::io::ErrorKind::ExecutableFileBusy => {
std::thread::sleep(BUSY_BACKOFF);
}
outcome => return outcome.unwrap(),
}
}
cmd.output().unwrap()
}
fn recorded_async(credentials: &[(&str, &str)]) -> String {
let probe = Probe::new();
let program = probe.dir().join("hf");
probe.fake_program(&program);
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
let mut cmd = tokio::process::Command::new(&program);
cmd.env_remove("HF_TOKEN");
car_secrets::with_child_credentials(credentials, || apply_with_token(&mut cmd));
for _ in 0..BUSY_ATTEMPTS {
match cmd.output().await {
Err(e) if e.kind() == std::io::ErrorKind::ExecutableFileBusy => {
tokio::time::sleep(BUSY_BACKOFF).await;
}
outcome => return outcome.unwrap(),
}
}
cmd.output().await.unwrap()
});
probe.recorded()
}
fn recorded_sync(credentials: &[(&str, &str)]) -> String {
let probe = Probe::new();
let program = probe.dir().join("hf");
probe.fake_program(&program);
let mut cmd = std::process::Command::new(&program);
cmd.env_remove("HF_TOKEN");
car_secrets::with_child_credentials(credentials, || apply_with_token_std(&mut cmd));
output_riding_out_busy(&mut cmd);
probe.recorded()
}
#[test]
fn a_hub_child_is_handed_the_token() {
assert_eq!(recorded_async(WITH_TOKEN), "yes");
assert_eq!(recorded_sync(WITH_TOKEN), "yes");
}
#[test]
fn a_hub_child_gets_no_token_when_there_is_none() {
assert_eq!(recorded_async(&[]), "no");
assert_eq!(recorded_sync(&[]), "no");
}
#[test]
fn the_cache_only_variant_never_hands_over_a_token() {
let probe = Probe::new();
let program = probe.dir().join("hf");
probe.fake_program(&program);
let mut cmd = std::process::Command::new(&program);
cmd.env_remove("HF_TOKEN");
car_secrets::with_child_credentials(WITH_TOKEN, || apply_to_std(&mut cmd));
output_riding_out_busy(&mut cmd);
assert_eq!(
probe.recorded(),
"no",
"passive probes must not read the store"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn with(vars: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> {
let map: HashMap<String, String> = vars
.iter()
.filter(|(_, v)| !v.is_empty())
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
move |name| map.get(name).cloned()
}
const HOME: &str = "/Users/someone";
fn hub(vars: &[(&str, &str)]) -> PathBuf {
resolve(&with(vars), Some(Path::new(HOME)))
}
fn home_of(vars: &[(&str, &str)]) -> PathBuf {
resolve_home(&with(vars), Some(Path::new(HOME)))
}
#[test]
fn resolves_the_hub_the_way_the_python_library_does() {
assert_eq!(
hub(&[]),
PathBuf::from("/Users/someone/.cache/huggingface/hub")
);
assert_eq!(
hub(&[("XDG_CACHE_HOME", "/xdg")]),
PathBuf::from("/xdg/huggingface/hub")
);
assert_eq!(
hub(&[("HF_HOME", "/hf"), ("XDG_CACHE_HOME", "/xdg")]),
PathBuf::from("/hf/hub")
);
assert_eq!(
hub(&[("HUGGINGFACE_HUB_CACHE", "/legacy"), ("HF_HOME", "/hf")]),
PathBuf::from("/legacy")
);
assert_eq!(
hub(&[
("HF_HUB_CACHE", "/hub"),
("HUGGINGFACE_HUB_CACHE", "/legacy"),
("HF_HOME", "/hf"),
]),
PathBuf::from("/hub")
);
assert_eq!(
home_of(&[("HF_HOME", "~/hf")]),
PathBuf::from("/Users/someone/hf")
);
assert_eq!(
home_of(&[("XDG_CACHE_HOME", "~/xdg")]),
PathBuf::from("/Users/someone/xdg/huggingface")
);
}
#[test]
fn empty_variables_count_as_unset() {
assert_eq!(
hub(&[("HF_HUB_CACHE", ""), ("HF_HOME", "/hf")]),
PathBuf::from("/hf/hub")
);
assert_eq!(
hub(&[("HF_HOME", "")]),
PathBuf::from("/Users/someone/.cache/huggingface/hub")
);
}
#[test]
fn expands_home_then_variables_as_python_does() {
assert_eq!(
hub(&[("HF_HOME", "~/hf")]),
PathBuf::from("/Users/someone/hf/hub")
);
assert_eq!(
hub(&[("HF_HUB_CACHE", "~")]),
PathBuf::from("/Users/someone")
);
assert_eq!(
hub(&[("HF_HUB_CACHE", "$ROOT/cache"), ("ROOT", "/data")]),
PathBuf::from("/data/cache")
);
assert_eq!(
hub(&[("HF_HUB_CACHE", "${ROOT}/cache"), ("ROOT", "/data")]),
PathBuf::from("/data/cache")
);
assert_eq!(
hub(&[("HF_HUB_CACHE", "$T/x"), ("T", "~")]),
PathBuf::from("~/x")
);
assert_eq!(
hub(&[("HF_HUB_CACHE", "$NOPE/c")]),
PathBuf::from("$NOPE/c")
);
assert_eq!(
hub(&[("HF_HUB_CACHE", "/a/${}/b")]),
PathBuf::from("/a/${}/b")
);
assert_eq!(hub(&[("HF_HUB_CACHE", "/a$")]), PathBuf::from("/a$"));
assert_eq!(
hub(&[("HF_HUB_CACHE", "/a/${OPEN")]),
PathBuf::from("/a/${OPEN")
);
assert_eq!(
hub(&[("HF_HUB_CACHE", "~root/x")]),
PathBuf::from("~root/x")
);
}
#[test]
fn percent_variables_expand_as_windows_does() {
let env = with(&[("LOCALAPPDATA", "C:\\Users\\me\\AppData\\Local")]);
assert_eq!(
expand_percent("%LOCALAPPDATA%\\hf", &env),
"C:\\Users\\me\\AppData\\Local\\hf"
);
assert_eq!(expand_percent("%NOPE%\\hf", &env), "%NOPE%\\hf");
assert_eq!(expand_percent("50%", &env), "50%");
}
#[test]
fn no_home_still_yields_an_absolute_hub() {
let relative = resolve(&with(&[]), None);
assert!(!relative.is_absolute(), "{relative:?}");
assert!(relative.ends_with(".cache/huggingface/hub"));
assert!(absolute(relative).is_absolute());
assert!(absolute(PathBuf::from("rel/hub")).is_absolute());
}
#[test]
fn a_repo_dir_is_the_hub_layout() {
assert_eq!(
repo_dir_in(Path::new("/h"), "mlx-community/Qwen3-4B-4bit"),
PathBuf::from("/h/models--mlx-community--Qwen3-4B-4bit")
);
}
#[test]
fn checked_repo_dirs_accept_only_hub_repo_ids() {
let hub = Path::new("/h");
assert_eq!(
checked_repo_dir_in(hub, "mlx-community/Qwen3-4B-4bit"),
Some(PathBuf::from("/h/models--mlx-community--Qwen3-4B-4bit"))
);
assert_eq!(
checked_repo_dir_in(hub, "gpt2"),
Some(PathBuf::from("/h/models--gpt2"))
);
for bad in [
"", "/", "a/", "/b", "a/b/c", "..", "a/..", ".", "a\\..\\x", "a b/c", "a/b:c",
] {
assert_eq!(checked_repo_dir_in(hub, bad), None, "{bad:?}");
}
}
}