use std::path::{Path, PathBuf};
use serde::Serialize;
use tokf::config;
use tokf::tracking;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
enum WriteAccess {
Writable,
ReadOnly,
WillCreate,
ParentReadOnly,
Unavailable,
}
impl WriteAccess {
const fn label(self) -> &'static str {
match self {
Self::Writable => "writable",
Self::ReadOnly => "read-only!",
Self::WillCreate => "will be created",
Self::ParentReadOnly => "dir not writable!",
Self::Unavailable => "unavailable",
}
}
}
fn is_writable(path: &Path) -> bool {
if path.is_file() {
std::fs::OpenOptions::new().write(true).open(path).is_ok()
} else if path.is_dir() {
let pid = std::process::id();
for attempt in 0..5u32 {
let probe = path.join(format!(".tokf_write_check_{pid}_{attempt}"));
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&probe)
{
Ok(_) => {
let _ = std::fs::remove_file(&probe);
return true;
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(_) => return false,
}
}
false
} else {
false
}
}
fn check_write_access(path: &Path) -> WriteAccess {
if path.exists() {
return if is_writable(path) {
WriteAccess::Writable
} else {
WriteAccess::ReadOnly
};
}
let mut ancestor = path.parent();
while let Some(a) = ancestor {
if a.exists() {
return if is_writable(a) {
WriteAccess::WillCreate
} else {
WriteAccess::ParentReadOnly
};
}
ancestor = a.parent();
}
WriteAccess::Unavailable
}
#[derive(Serialize)]
struct SearchDir {
scope: &'static str,
path: String,
exists: bool,
access: Option<WriteAccess>,
}
#[derive(Serialize)]
struct TrackingDb {
env_override: Option<String>,
path: Option<String>,
exists: bool,
access: Option<WriteAccess>,
}
#[derive(Serialize)]
struct CacheInfo {
path: Option<String>,
exists: bool,
access: Option<WriteAccess>,
}
#[derive(Serialize)]
struct FilterCounts {
local: usize,
user: usize,
builtin: usize,
total: usize,
}
#[derive(Serialize)]
struct ConfigFileEntry {
scope: &'static str,
path: String,
exists: bool,
}
#[derive(Serialize)]
struct InfoOutput {
version: String,
home_override: Option<String>,
search_dirs: Vec<SearchDir>,
tracking_db: TrackingDb,
cache: CacheInfo,
config_files: Vec<ConfigFileEntry>,
filters: Option<FilterCounts>,
}
pub fn cmd_info(json: bool) -> i32 {
let search_dirs = config::default_search_dirs();
let info = collect_info(&search_dirs);
if json {
crate::output::print_json(&info);
} else {
print_human(&info);
}
0
}
fn collect_search_dirs(search_dirs: &[PathBuf]) -> Vec<SearchDir> {
let mut dirs: Vec<SearchDir> = search_dirs
.iter()
.enumerate()
.map(|(i, dir)| SearchDir {
scope: if i == 0 { "local" } else { "user" },
path: dir.display().to_string(),
exists: dir.exists(),
access: dir.exists().then(|| {
if is_writable(dir) {
WriteAccess::Writable
} else {
WriteAccess::ReadOnly
}
}),
})
.collect();
dirs.push(SearchDir {
scope: "built-in",
path: "<embedded>".to_string(),
exists: true,
access: None,
});
dirs
}
fn collect_filter_counts(search_dirs: &[PathBuf]) -> Option<FilterCounts> {
match config::discover_all_filters(search_dirs) {
Ok(f) => {
let local = f.iter().filter(|fi| fi.priority == 0).count();
let user = f
.iter()
.filter(|fi| fi.priority > 0 && fi.priority < u8::MAX)
.count();
let builtin = f.iter().filter(|fi| fi.priority == u8::MAX).count();
Some(FilterCounts {
local,
user,
builtin,
total: f.len(),
})
}
Err(e) => {
eprintln!("[tokf] error discovering filters: {e:#}");
None
}
}
}
fn collect_info(search_dirs: &[PathBuf]) -> InfoOutput {
let dirs = collect_search_dirs(search_dirs);
let home_override = std::env::var("TOKF_HOME")
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let env_override = tokf::paths::db_path_override().map(|p| p.display().to_string());
let db_path = tracking::db_path();
let db_exists = db_path.as_ref().is_some_and(|p| p.exists());
let db_access = db_path.as_ref().map(|p| check_write_access(p));
let tracking_db = TrackingDb {
env_override,
path: db_path.map(|p| p.display().to_string()),
exists: db_exists,
access: db_access,
};
let cache_path = config::cache::cache_path(search_dirs);
let cache_exists = cache_path.as_ref().is_some_and(|p| p.exists());
let cache_access = cache_path.as_ref().map(|p| check_write_access(p));
let cache = CacheInfo {
path: cache_path.map(|p| p.display().to_string()),
exists: cache_exists,
access: cache_access,
};
let config_files = collect_config_files();
InfoOutput {
version: env!("CARGO_PKG_VERSION").to_string(),
home_override,
search_dirs: dirs,
tracking_db,
cache,
config_files,
filters: collect_filter_counts(search_dirs),
}
}
fn collect_config_files() -> Vec<ConfigFileEntry> {
let user_dir = tokf::paths::user_dir();
let cwd = std::env::current_dir().unwrap_or_default();
let project_root = tokf::history::project_root_for(&cwd);
let local_dir = project_root.join(".tokf");
let mut entries = Vec::new();
let global_files = ["config.toml", "auth.toml", "machine.toml", "rewrites.toml"];
for file in &global_files {
let path = user_dir.as_ref().map(|d| d.join(file));
let exists = path.as_ref().is_some_and(|p| p.exists());
entries.push(ConfigFileEntry {
scope: "global",
path: path.map_or_else(|| "(unavailable)".to_string(), |p| p.display().to_string()),
exists,
});
}
let local_files = ["config.toml", "rewrites.toml"];
for file in &local_files {
let path = local_dir.join(file);
let exists = path.exists();
entries.push(ConfigFileEntry {
scope: "local",
path: path.display().to_string(),
exists,
});
}
entries
}
fn print_human(info: &InfoOutput) {
println!("tokf {}", info.version);
match &info.home_override {
Some(p) => println!("TOKF_HOME: {p}"),
None => println!("TOKF_HOME: (not set)"),
}
println!("\nfilter search directories:");
for dir in &info.search_dirs {
if dir.scope == "built-in" {
println!(" [{}] {} (always available)", dir.scope, dir.path);
} else {
let status = if dir.exists {
match dir.access {
Some(WriteAccess::Writable) => "exists, writable",
Some(WriteAccess::ReadOnly) => "exists, read-only!",
_ => "exists",
}
} else {
"not found"
};
println!(" [{}] {} ({status})", dir.scope, dir.path);
}
}
println!("\ntracking database:");
match &info.tracking_db.env_override {
Some(p) => println!(" TOKF_DB_PATH: {p}"),
None => println!(" TOKF_DB_PATH: (not set)"),
}
match &info.tracking_db.path {
Some(p) => {
let status = info
.tracking_db
.access
.map_or("unknown", WriteAccess::label);
println!(" path: {p} ({status})");
}
None => println!(" path: (could not determine)"),
}
println!("\nfilter cache:");
match &info.cache.path {
Some(p) => {
let status = info.cache.access.map_or("unknown", WriteAccess::label);
println!(" path: {p} ({status})");
}
None => println!(" path: (could not determine)"),
}
println!("\nconfig files:");
for entry in &info.config_files {
let status = if entry.exists { "exists" } else { "not found" };
println!(" [{}] {} ({status})", entry.scope, entry.path);
}
if let Some(f) = &info.filters {
println!("\nfilters:");
println!(" local: {}", f.local);
println!(" user: {}", f.user);
println!(" built-in: {}", f.builtin);
println!(" total: {}", f.total);
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use tempfile::TempDir;
use super::*;
#[test]
fn is_writable_true_for_writable_file() {
let dir = TempDir::new().unwrap();
let file = dir.path().join("test.txt");
std::fs::write(&file, b"hello").unwrap();
assert!(is_writable(&file));
}
#[cfg(unix)]
#[test]
fn is_writable_false_for_readonly_file() {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let file = dir.path().join("ro.txt");
std::fs::write(&file, b"hello").unwrap();
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o444)).unwrap();
assert!(!is_writable(&file));
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o644)).unwrap();
}
#[test]
fn is_writable_true_for_writable_dir() {
let dir = TempDir::new().unwrap();
assert!(is_writable(dir.path()));
}
#[cfg(unix)]
#[test]
fn is_writable_false_for_readonly_dir() {
use std::os::unix::fs::PermissionsExt;
let tmp = TempDir::new().unwrap();
let ro_dir = tmp.path().join("ro_dir");
std::fs::create_dir(&ro_dir).unwrap();
std::fs::set_permissions(&ro_dir, std::fs::Permissions::from_mode(0o555)).unwrap();
assert!(!is_writable(&ro_dir));
std::fs::set_permissions(&ro_dir, std::fs::Permissions::from_mode(0o755)).unwrap();
}
#[test]
fn check_write_access_writable_for_existing_writable_file() {
let dir = TempDir::new().unwrap();
let file = dir.path().join("test.db");
std::fs::write(&file, b"").unwrap();
assert_eq!(check_write_access(&file), WriteAccess::Writable);
}
#[cfg(unix)]
#[test]
fn check_write_access_read_only_for_readonly_file() {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let file = dir.path().join("ro.db");
std::fs::write(&file, b"").unwrap();
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o444)).unwrap();
assert_eq!(check_write_access(&file), WriteAccess::ReadOnly);
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o644)).unwrap();
}
#[test]
fn check_write_access_will_create_for_nonexistent_in_writable_dir() {
let dir = TempDir::new().unwrap();
let nonexistent = dir.path().join("subdir").join("new.db");
assert_eq!(check_write_access(&nonexistent), WriteAccess::WillCreate);
}
#[cfg(unix)]
#[test]
fn check_write_access_parent_read_only_when_dir_not_writable() {
use std::os::unix::fs::PermissionsExt;
let tmp = TempDir::new().unwrap();
let ro_dir = tmp.path().join("ro");
std::fs::create_dir(&ro_dir).unwrap();
std::fs::set_permissions(&ro_dir, std::fs::Permissions::from_mode(0o555)).unwrap();
let nested = ro_dir.join("new.db");
assert_eq!(check_write_access(&nested), WriteAccess::ParentReadOnly);
std::fs::set_permissions(&ro_dir, std::fs::Permissions::from_mode(0o755)).unwrap();
}
}