use std::path::PathBuf;
#[cfg_attr(alef, alef(skip))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TlsRootsMode {
#[default]
Platform,
WebPki,
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(
any(feature = "config", feature = "download"),
derive(serde::Serialize, serde::Deserialize)
)]
pub struct PackConfig {
#[cfg_attr(any(feature = "config", feature = "download"), serde(default))]
pub cache_dir: Option<PathBuf>,
#[cfg_attr(any(feature = "config", feature = "download"), serde(default))]
pub languages: Option<Vec<String>>,
#[cfg_attr(any(feature = "config", feature = "download"), serde(default))]
pub groups: Option<Vec<String>>,
}
impl PackConfig {
#[cfg_attr(alef, alef(skip))]
#[cfg(feature = "config")]
pub fn from_toml_file(path: &std::path::Path) -> Result<Self, crate::error::Error> {
let content = std::fs::read_to_string(path)
.map_err(|e| crate::error::Error::Config(format!("Failed to read {}: {e}", path.display())))?;
toml::from_str(&content)
.map_err(|e| crate::error::Error::Config(format!("Failed to parse {}: {e}", path.display())))
}
#[cfg_attr(alef, alef(skip))]
#[cfg(feature = "config")]
pub fn discover() -> Option<Self> {
match Self::try_discover() {
Ok(config) => config,
Err(error) => {
tracing::warn!(%error, "discovered language-pack.toml could not be loaded; treating as absent");
None
}
}
}
#[cfg_attr(alef, alef(skip))]
#[cfg(feature = "config")]
pub fn try_discover() -> Result<Option<Self>, crate::error::Error> {
if let Ok(cwd) = std::env::current_dir() {
let mut dir: &std::path::Path = cwd.as_path();
for _ in 0..10 {
let candidate = dir.join("language-pack.toml");
if candidate.exists() {
return Self::from_toml_file(&candidate).map(Some);
}
match dir.parent() {
Some(parent) => dir = parent,
None => break,
}
}
}
if let Some(config_dir) = dirs::config_dir() {
let candidate = config_dir.join("tree-sitter-language-pack").join("config.toml");
if candidate.exists() {
return Self::from_toml_file(&candidate).map(Some);
}
}
Ok(None)
}
}
#[cfg(all(test, feature = "config"))]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
static CWD_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
struct CwdOverride {
original: std::path::PathBuf,
_guard: std::sync::MutexGuard<'static, ()>,
}
impl CwdOverride {
fn new(dir: &std::path::Path) -> Self {
let guard = CWD_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let original = std::env::current_dir().expect("current dir should be readable");
std::env::set_current_dir(dir).expect("current dir should be settable");
Self { original, _guard: guard }
}
}
impl Drop for CwdOverride {
fn drop(&mut self) {
std::env::set_current_dir(&self.original).expect("original current dir should be restorable");
}
}
#[test]
fn should_return_err_naming_the_path_when_try_discover_finds_malformed_toml() {
let temp_dir = tempfile::Builder::new()
.prefix("tslp-pack-config-discover-")
.tempdir()
.expect("temp dir should be created");
let path = temp_dir.path().join("language-pack.toml");
std::fs::write(&path, "this is not [ valid toml").expect("malformed file should be written");
let _cwd = CwdOverride::new(temp_dir.path());
let error = PackConfig::try_discover().expect_err("malformed TOML must not silently succeed");
let message = error.to_string();
assert!(
message.contains(&path.display().to_string()),
"error must name the offending path; got: {message}"
);
}
#[test]
fn should_return_none_when_discover_finds_malformed_toml() {
let temp_dir = tempfile::Builder::new()
.prefix("tslp-pack-config-discover-")
.tempdir()
.expect("temp dir should be created");
let path = temp_dir.path().join("language-pack.toml");
std::fs::write(&path, "this is not [ valid toml").expect("malformed file should be written");
let _cwd = CwdOverride::new(temp_dir.path());
let config = PackConfig::discover();
assert!(
config.is_none(),
"a malformed config file must be reported via a warning and treated as absent, \
not silently accepted; got: {config:?}"
);
}
#[test]
fn should_return_err_naming_the_path_when_config_file_is_malformed_toml() {
let temp_dir = tempfile::Builder::new()
.prefix("tslp-pack-config-")
.tempdir()
.expect("temp dir should be created");
let path = temp_dir.path().join("language-pack.toml");
std::fs::write(&path, "this is not [ valid toml").expect("malformed file should be written");
let error = PackConfig::from_toml_file(&path).expect_err("malformed TOML must not silently succeed");
let message = error.to_string();
assert!(
message.contains(&path.display().to_string()),
"error must name the offending path; got: {message}"
);
}
#[test]
fn should_return_err_naming_the_path_when_config_file_is_missing() {
let temp_dir = tempfile::Builder::new()
.prefix("tslp-pack-config-")
.tempdir()
.expect("temp dir should be created");
let path = temp_dir.path().join("missing.toml");
let error = PackConfig::from_toml_file(&path).expect_err("missing file must error, not silently produce None");
let message = error.to_string();
assert!(
message.contains(&path.display().to_string()),
"error must name the missing path; got: {message}"
);
}
#[test]
fn should_return_ok_when_config_file_is_well_formed_toml() {
let temp_dir = tempfile::Builder::new()
.prefix("tslp-pack-config-")
.tempdir()
.expect("temp dir should be created");
let path = temp_dir.path().join("language-pack.toml");
std::fs::write(&path, "languages = [\"python\", \"rust\"]\n").expect("config file should be written");
let config = PackConfig::from_toml_file(&path).expect("well-formed config should parse");
assert_eq!(config.languages, Some(vec!["python".to_string(), "rust".to_string()]));
}
}