use crate::config::AppConfig;
use crate::support::Result;
use std::fs;
use std::path::{Component, Path, PathBuf};
pub fn load_from_path(path: &Path) -> Result<AppConfig> {
let config_path = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()?.join(path)
};
let raw = fs::read_to_string(&config_path)?;
let mut config: AppConfig = toml::from_str(&raw)?;
let base_dir = config_path.parent().unwrap_or_else(|| Path::new("/"));
config.vault.root = resolve_path(&config.vault.root, base_dir);
config.developer.note_roots = config
.developer
.note_roots
.iter()
.map(|root| normalize_relative_note_path(root))
.collect::<Result<Vec<_>>>()?;
for project in &mut config.projects {
project.repo_paths = project
.repo_paths
.iter()
.map(|repo_path| resolve_path(repo_path, base_dir))
.collect();
project.note_roots = project
.note_roots
.iter()
.map(|root| normalize_relative_note_path(root))
.collect::<Result<Vec<_>>>()?;
}
for scene in &mut config.scenes {
scene.preferred_notes = scene
.preferred_notes
.iter()
.map(|note| normalize_relative_note_path(note))
.collect::<Result<Vec<_>>>()?;
}
Ok(config)
}
fn normalize_relative_note_path(path: &str) -> Result<String> {
let normalized = normalize_relative_path(Path::new(path))?;
Ok(normalized.to_string_lossy().replace('\\', "/"))
}
fn normalize_relative_path(path: &Path) -> Result<PathBuf> {
if path.is_absolute() {
anyhow::bail!(
"relative note path must not be absolute: {}",
path.display()
);
}
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
if !normalized.pop() {
anyhow::bail!(
"relative note path must not escape root: {}",
path.display()
);
}
}
Component::Normal(segment) => normalized.push(segment),
Component::RootDir | Component::Prefix(_) => {
anyhow::bail!(
"relative note path must be vault-relative: {}",
path.display()
);
}
}
}
Ok(normalized)
}
fn resolve_path(path: &Path, base_dir: &Path) -> PathBuf {
let resolved = if path.is_absolute() {
path.to_path_buf()
} else {
base_dir.join(path)
};
let normalized = normalize_absolute_path(&resolved);
if normalized.exists() {
normalized.canonicalize().unwrap_or(normalized)
} else {
normalized
}
}
fn normalize_absolute_path(path: &Path) -> PathBuf {
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
normalized.pop();
}
other => normalized.push(other.as_os_str()),
}
}
normalized
}
#[cfg(test)]
mod tests {
use super::{normalize_relative_note_path, resolve_path};
use std::path::Path;
#[test]
fn resolve_relative_path_against_config_dir() {
let base_dir = Path::new("/tmp/example/config");
let resolved = resolve_path(Path::new("../vault"), base_dir);
assert_eq!(resolved, Path::new("/tmp/example/vault"));
}
#[test]
fn normalize_preferred_note_path() {
assert_eq!(
normalize_relative_note_path("./20-Areas/../20-Areas/AI协作偏好.md").unwrap(),
"20-Areas/AI协作偏好.md"
);
}
#[test]
fn reject_escaping_preferred_note_path() {
let error = normalize_relative_note_path("../outside.md").unwrap_err();
assert!(error.to_string().contains("must not escape root"));
}
}