···
; data_dirs : Eio.Fs.dir_ty Eio.Path.t list
+
let ensure_dir ?(perm = 0o755) path = Eio.Path.mkdirs ~exists_ok:true ~perm path
+
let validate_runtime_base_dir base_path =
+
(* Validate the base XDG_RUNTIME_DIR has correct permissions per spec *)
+
let path_str = Eio.Path.native_exn base_path in
+
let stat = Eio.Path.stat ~follow:true base_path in
+
let current_perm = stat.perm land 0o777 in
+
if current_perm <> 0o700 then
+
"XDG_RUNTIME_DIR base directory %s has incorrect permissions: %o (must be 0700)"
+
(* Check ownership - directory should be owned by current user *)
+
let uid = Unix.getuid () in
+
if stat.uid <> Int64.of_int uid then
+
"XDG_RUNTIME_DIR base directory %s not owned by current user (uid %d, owner %Ld)"
+
(* TODO: Check that directory is on local filesystem (not networked).
+
This would require filesystem type detection which is OS-specific. *)
+
| exn -> failwith (Printf.sprintf "Cannot validate XDG_RUNTIME_DIR: %s" (Printexc.to_string exn))
+
let ensure_runtime_dir _fs app_runtime_path =
+
(* Base directory validation is done in resolve_runtime_dir,
+
so we just create the app subdirectory *)
+
ensure_dir app_runtime_path
···
| _ -> failwith "Cannot determine home directory"))
let make_env_var_name app_name suffix = String.uppercase_ascii app_name ^ "_" ^ suffix
+
exception Invalid_xdg_path of string
+
let validate_absolute_path context path =
+
if Filename.is_relative path then
+
raise (Invalid_xdg_path
+
(Printf.sprintf "%s must be an absolute path, got: %s" context path))
let resolve_path fs home_path base_path =
if Filename.is_relative base_path
then Eio.Path.(home_path / base_path)
else Eio.Path.(fs / base_path)
(* Helper to resolve system directories (config_dirs or data_dirs) *)
+
let resolve_system_dirs fs _home_path app_name override_suffix xdg_var default_paths =
let override_var = make_env_var_name app_name override_suffix in
match Sys.getenv_opt override_var with
| Some dirs when dirs <> "" ->
String.split_on_char ':' dirs
|> List.filter (fun s -> s <> "")
+
|> List.filter_map (fun path ->
+
validate_absolute_path override_var path;
+
Some (Eio.Path.(fs / path / app_name))
+
with Invalid_xdg_path _ -> None)
(match Sys.getenv_opt xdg_var with
| Some dirs when dirs <> "" ->
String.split_on_char ':' dirs
|> List.filter (fun s -> s <> "")
+
|> List.filter_map (fun path ->
+
validate_absolute_path xdg_var path;
+
Some (Eio.Path.(fs / path / app_name))
+
with Invalid_xdg_path _ -> None)
List.map (fun path -> Eio.Path.(fs / path / app_name)) default_paths)
(* Helper to resolve a user directory with override precedence *)
+
let resolve_user_dir fs _home_path app_name xdg_ctx xdg_getter override_suffix =
let override_var = make_env_var_name app_name override_suffix in
match Sys.getenv_opt override_var with
+
| Some dir when dir <> "" ->
+
validate_absolute_path override_var dir;
+
Eio.Path.(fs / dir / app_name), Env override_var
| Some _ | None -> Eio.Path.(fs / xdg_getter xdg_ctx / app_name), Default
(* Helper to resolve runtime directory (special case since it can be None) *)
+
let resolve_runtime_dir fs _home_path app_name xdg_ctx =
let override_var = make_env_var_name app_name "RUNTIME_DIR" in
match Sys.getenv_opt override_var with
+
| Some dir when dir <> "" ->
+
validate_absolute_path override_var dir;
+
(* Validate the base runtime directory has correct permissions *)
+
let base_runtime_dir = Eio.Path.(fs / dir) in
+
validate_runtime_base_dir base_runtime_dir;
+
Some (Eio.Path.(fs / dir / app_name)), Env override_var
+
(match Xdg.runtime_dir xdg_ctx with
+
(* Validate the base runtime directory has correct permissions *)
+
let base_runtime_dir = Eio.Path.(fs / base) in
+
validate_runtime_base_dir base_runtime_dir;
+
Some (Eio.Path.(fs / base / app_name))
+
| None -> None), Default
+
let validate_standard_xdg_vars () =
+
(* Validate standard XDG environment variables for absolute paths *)
+
match Sys.getenv_opt var with
+
| Some value when value <> "" ->
+
if String.contains value ':' then
+
(* Colon-separated list - validate each part *)
+
String.split_on_char ':' value
+
|> List.filter (fun s -> s <> "")
+
|> List.iter (fun path -> validate_absolute_path var path)
+
validate_absolute_path var value
let home_path = get_home_dir fs in
+
(* First validate all standard XDG environment variables *)
+
validate_standard_xdg_vars ();
let xdg_ctx = Xdg.create ~env:Sys.getenv_opt () in
let config_dir, config_dir_source =
···
+
Option.iter (ensure_runtime_dir fs) runtime_dir;
···
let app_name t = t.app_name
let config_dir t = t.config_dir
···
let config_dirs t = t.config_dirs
let data_dirs t = t.data_dirs
+
(* File search following XDG specification *)
+
let find_file_in_dirs dirs filename =
+
let rec search_dirs = function
+
| dir :: remaining_dirs ->
+
let file_path = Eio.Path.(dir / filename) in
+
(* Try to check if file exists and is readable *)
+
let _ = Eio.Path.stat ~follow:true file_path in
+
(* File is inaccessible (non-existent, permissions, etc.)
+
Skip and continue with next directory per XDG spec *)
+
search_dirs remaining_dirs)
+
let find_config_file t filename =
+
(* Search user config dir first, then system config dirs *)
+
find_file_in_dirs (t.config_dir :: t.config_dirs) filename
+
let find_data_file t filename =
+
(* Search user data dir first, then system data dirs *)
+
find_file_in_dirs (t.data_dir :: t.data_dirs) filename
let pp ?(brief = false) ?(sources = false) ppf t =
let pp_source ppf = function
···
···
+
Option.iter (ensure_runtime_dir fs) runtime_dir;
···
let app_upper = String.uppercase_ascii app_name in
···
let pp_source ppf = function
···
(pp_with_source "runtime_dir")