My agentic slop goes here. Not intended for anyone else!

more

Changed files
+303 -65
xdg-eio
+123 -24
xdg-eio/lib/xdge.ml
···
; data_dirs : Eio.Fs.dir_ty Eio.Path.t list
}
-
let ensure_dir path = Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 path
+
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 *)
+
try
+
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
+
failwith
+
(Printf.sprintf
+
"XDG_RUNTIME_DIR base directory %s has incorrect permissions: %o (must be 0700)"
+
path_str
+
current_perm);
+
(* Check ownership - directory should be owned by current user *)
+
let uid = Unix.getuid () in
+
if stat.uid <> Int64.of_int uid then
+
failwith
+
(Printf.sprintf
+
"XDG_RUNTIME_DIR base directory %s not owned by current user (uid %d, owner %Ld)"
+
path_str
+
uid
+
stat.uid);
+
(* TODO: Check that directory is on local filesystem (not networked).
+
This would require filesystem type detection which is OS-specific. *)
+
with
+
| 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
let get_home_dir fs =
let home_str =
···
| _ -> failwith "Cannot determine home directory"))
in
Eio.Path.(fs / home_str)
-
;;
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 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.map (fun path -> Eio.Path.(resolve_path fs home_path path / app_name))
+
|> List.filter_map (fun path ->
+
try
+
validate_absolute_path override_var path;
+
Some (Eio.Path.(fs / path / app_name))
+
with Invalid_xdg_path _ -> None)
| Some _ | None ->
(match Sys.getenv_opt xdg_var with
| Some dirs when dirs <> "" ->
String.split_on_char ':' dirs
|> List.filter (fun s -> s <> "")
-
|> List.map (fun path -> Eio.Path.(resolve_path fs home_path path / app_name))
+
|> List.filter_map (fun path ->
+
try
+
validate_absolute_path xdg_var path;
+
Some (Eio.Path.(fs / path / app_name))
+
with Invalid_xdg_path _ -> None)
| Some _ | 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 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 <> "" -> resolve_path fs home_path dir, Env override_var
+
| 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 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 <> "" -> Some (resolve_path fs home_path dir), Env override_var
+
| 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
| Some _ | None ->
-
( Option.map (fun base -> Eio.Path.(fs / base / app_name)) (Xdg.runtime_dir xdg_ctx)
-
, Default )
-
;;
+
(match Xdg.runtime_dir xdg_ctx with
+
| Some base ->
+
(* 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 *)
+
let xdg_vars = [
+
"XDG_CONFIG_HOME";
+
"XDG_DATA_HOME";
+
"XDG_CACHE_HOME";
+
"XDG_STATE_HOME";
+
"XDG_RUNTIME_DIR";
+
"XDG_CONFIG_DIRS";
+
"XDG_DATA_DIRS";
+
] in
+
List.iter (fun var ->
+
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)
+
else
+
(* Single path *)
+
validate_absolute_path var value
+
| _ -> ()
+
) xdg_vars
let create fs app_name =
let fs = fs in
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
(* User directories *)
let config_dir, config_dir_source =
···
ensure_dir data_dir;
ensure_dir cache_dir;
ensure_dir state_dir;
-
Option.iter ensure_dir runtime_dir;
+
Option.iter (ensure_runtime_dir fs) runtime_dir;
{ app_name
; config_dir
; config_dir_source
···
; config_dirs
; data_dirs
}
-
;;
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
+
| [] -> None
+
| dir :: remaining_dirs ->
+
let file_path = Eio.Path.(dir / filename) in
+
(try
+
(* Try to check if file exists and is readable *)
+
let _ = Eio.Path.stat ~follow:true file_path in
+
Some file_path
+
with
+
| _ ->
+
(* File is inaccessible (non-existent, permissions, etc.)
+
Skip and continue with next directory per XDG spec *)
+
search_dirs remaining_dirs)
+
in
+
search_dirs 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
···
"data_dirs:"
pp_paths
t.data_dirs)
-
;;
module Cmd = struct
type xdg_t = t
···
ensure_dir data_dir;
ensure_dir cache_dir;
ensure_dir state_dir;
-
Option.iter ensure_dir runtime_dir;
+
Option.iter (ensure_runtime_dir fs) runtime_dir;
{ app_name
; config_dir
; config_dir_source
···
$ cache_dir
$ state_dir
$ runtime_dir)
-
;;
-
let env_docs app_name =
let app_upper = String.uppercase_ascii app_name in
···
app_name
app_name
app_name
-
;;
let pp ppf config =
let pp_source ppf = function
···
config.state_dir
(pp_with_source "runtime_dir")
config.runtime_dir
-
;;
-
end
+
end
+66 -7
xdg-eio/lib/xdge.mli
···
Eio filesystem. *)
type t
+
(** {1 Exceptions} *)
+
+
(** Exception raised when XDG environment variables contain invalid paths.
+
+
The XDG specification requires all paths in environment variables to be
+
absolute. This exception is raised when a relative path is found. *)
+
exception Invalid_xdg_path of string
+
(** {1 Construction} *)
(** [create fs app_name] creates an XDG context for the given application.
···
{b Example:}
{[
-
let xdg = Xdg_eio.create env#fs "myapp" in
-
let config = Xdg_eio.config_dir xdg in
+
let xdg = Xdge.create env#fs "myapp" in
+
let config = Xdge.config_dir xdg in
(* config is now <fs:$HOME/.config/myapp> or the overridden path *)
]}
{b Note:} All directories are created with permissions 0o755 if they don't exist,
-
except for runtime directories which follow stricter requirements. *)
+
except for runtime directories which are created with 0o700 permissions and
+
validated according to the XDG specification.
+
+
@raise Invalid_xdg_path if any environment variable contains a relative path *)
val create : Eio.Fs.dir_ty Eio.Path.t -> string -> t
(** {1 Accessors} *)
···
@see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> XDG_DATA_DIRS specification *)
val data_dirs : t -> Eio.Fs.dir_ty Eio.Path.t list
+
(** {1 File Search} *)
+
+
(** [find_config_file t filename] searches for a configuration file following XDG precedence.
+
+
This function searches for the given filename in the user configuration directory
+
first, then in system configuration directories in order of preference.
+
Files that are inaccessible (due to permissions, non-existence, etc.) are
+
silently skipped as per the XDG specification.
+
+
@param t The XDG context
+
@param filename The name of the file to search for
+
@return [Some path] if found, [None] if not found in any directory
+
+
{b Search Order:}
+
1. User config directory ({!config_dir})
+
2. System config directories ({!config_dirs}) in preference order
+
+
{b Example:}
+
{[
+
match Xdge.find_config_file xdg "myapp.conf" with
+
| Some path -> Printf.printf "Found config at: %s\n" (Eio.Path.native_exn path)
+
| None -> Printf.printf "No config file found\n"
+
]} *)
+
val find_config_file : t -> string -> Eio.Fs.dir_ty Eio.Path.t option
+
+
(** [find_data_file t filename] searches for a data file following XDG precedence.
+
+
This function searches for the given filename in the user data directory
+
first, then in system data directories in order of preference.
+
Files that are inaccessible (due to permissions, non-existence, etc.) are
+
silently skipped as per the XDG specification.
+
+
@param t The XDG context
+
@param filename The name of the file to search for
+
@return [Some path] if found, [None] if not found in any directory
+
+
{b Search Order:}
+
1. User data directory ({!data_dir})
+
2. System data directories ({!data_dirs}) in preference order
+
+
{b Example:}
+
{[
+
match Xdge.find_data_file xdg "templates/default.txt" with
+
| Some path -> (* read from path *)
+
| None -> (* use built-in default *)
+
]} *)
+
val find_data_file : t -> string -> Eio.Fs.dir_ty Eio.Path.t option
+
(** {1 Pretty Printing} *)
(** [pp ?brief ?sources ppf t] pretty prints the XDG directory configuration.
···
{b Example:}
{[
(* Normal output *)
-
Format.printf "%a" Xdg_eio.pp xdg
+
Format.printf "%a" Xdge.pp xdg
(* Brief output *)
-
Format.printf "%a" (Xdg_eio.pp ~brief:true) xdg
+
Format.printf "%a" (Xdge.pp ~brief:true) xdg
(* Show sources *)
-
Format.printf "%a" (Xdg_eio.pp ~sources:true) xdg
+
Format.printf "%a" (Xdge.pp ~sources:true) xdg
]} *)
val pp : ?brief:bool -> ?sources:bool -> Format.formatter -> t -> unit
···
{b Example:}
{[
let open Cmdliner in
-
let main xdg =
+
let main (xdg, _config) =
(* use xdg directly *)
in
let xdg_term = Cmd.term "myapp" env#fs in
+114 -34
xdg-eio/test/test_paths.ml
···
-
let () =
+
let test_path_validation () =
+
Printf.printf "Testing XDG path validation...\n";
+
+
(* Test absolute path validation for environment variables *)
+
let test_relative_path_rejection env_var relative_path =
+
Printf.printf "Testing rejection of relative path in %s...\n" env_var;
+
Unix.putenv env_var relative_path;
+
try
+
Eio_main.run @@ fun env ->
+
let _ = Xdge.create env#fs "test_validation" in
+
Printf.printf "ERROR: Should have rejected relative path\n";
+
false
+
with
+
| Xdge.Invalid_xdg_path msg ->
+
Printf.printf "SUCCESS: Correctly rejected relative path: %s\n" msg;
+
true
+
| exn ->
+
Printf.printf "ERROR: Wrong exception: %s\n" (Printexc.to_string exn);
+
false
+
in
+
+
let old_config_home = Sys.getenv_opt "XDG_CONFIG_HOME" in
+
let old_data_dirs = Sys.getenv_opt "XDG_DATA_DIRS" in
+
+
let success1 = test_relative_path_rejection "XDG_CONFIG_HOME" "relative/path" in
+
let success2 = test_relative_path_rejection "XDG_DATA_DIRS" "rel1:rel2:/abs/path" in
+
+
(* Restore original env vars *)
+
(match old_config_home with
+
| Some v -> Unix.putenv "XDG_CONFIG_HOME" v
+
| None -> (try Unix.putenv "XDG_CONFIG_HOME" ""; with _ -> ()));
+
(match old_data_dirs with
+
| Some v -> Unix.putenv "XDG_DATA_DIRS" v
+
| None -> (try Unix.putenv "XDG_DATA_DIRS" ""; with _ -> ()));
+
+
success1 && success2
+
+
let test_file_search () =
+
Printf.printf "\nTesting XDG file search...\n";
+
Eio_main.run @@ fun env ->
-
let xdg = Xdge.create env#fs "path_test" in
+
let xdg = Xdge.create env#fs "search_test" in
-
(* Test config subdirectory *)
-
let profiles_path = Eio.Path.(Xdge.config_dir xdg / "profiles") in
-
let profile_file = Eio.Path.(profiles_path / "default.json") in
-
(try
-
let content = Eio.Path.load profile_file in
-
Printf.printf "config file content: %s" (String.trim content)
-
with
-
| exn -> Printf.printf "config file error: %s" (Printexc.to_string exn));
+
(* Create test files *)
+
let config_file = Eio.Path.(Xdge.config_dir xdg / "test.conf") in
+
let data_file = Eio.Path.(Xdge.data_dir xdg / "test.dat") in
-
(* Test data subdirectory *)
-
let db_path = Eio.Path.(Xdge.data_dir xdg / "databases") in
-
let db_file = Eio.Path.(db_path / "main.db") in
-
(try
-
let content = Eio.Path.load db_file in
-
Printf.printf "\ndata file content: %s" (String.trim content)
-
with
-
| exn -> Printf.printf "\ndata file error: %s" (Printexc.to_string exn));
+
Eio.Path.save ~create:(`Or_truncate 0o644) config_file "config content";
+
Eio.Path.save ~create:(`Or_truncate 0o644) data_file "data content";
-
(* Test cache subdirectory *)
-
let cache_path = Eio.Path.(Xdge.cache_dir xdg / "thumbnails") in
-
let cache_file = Eio.Path.(cache_path / "thumb1.png") in
-
(try
-
let content = Eio.Path.load cache_file in
-
Printf.printf "\ncache file content: %s" (String.trim content)
-
with
-
| exn -> Printf.printf "\ncache file error: %s" (Printexc.to_string exn));
+
(* Test finding existing files *)
+
(match Xdge.find_config_file xdg "test.conf" with
+
| Some path ->
+
let content = Eio.Path.load path in
+
Printf.printf "Found config file: %s\n" (String.trim content)
+
| None -> Printf.printf "ERROR: Config file not found\n");
-
(* Test state subdirectory *)
-
let logs_path = Eio.Path.(Xdge.state_dir xdg / "logs") in
-
let log_file = Eio.Path.(logs_path / "app.log") in
-
(try
-
let content = Eio.Path.load log_file in
-
Printf.printf "\nstate file content: %s\n" (String.trim content)
-
with
-
| exn -> Printf.printf "\nstate file error: %s\n" (Printexc.to_string exn))
+
(match Xdge.find_data_file xdg "test.dat" with
+
| Some path ->
+
let content = Eio.Path.load path in
+
Printf.printf "Found data file: %s\n" (String.trim content)
+
| None -> Printf.printf "ERROR: Data file not found\n");
+
+
(* Test non-existent file *)
+
(match Xdge.find_config_file xdg "nonexistent.conf" with
+
| Some _ -> Printf.printf "ERROR: Should not have found nonexistent file\n"
+
| None -> Printf.printf "Correctly handled nonexistent file\n")
+
+
let () =
+
(* Check if we should run validation tests *)
+
if Array.length Sys.argv > 1 && Sys.argv.(1) = "--validate" then (
+
let validation_success = test_path_validation () in
+
test_file_search ();
+
+
if validation_success then
+
Printf.printf "\nAll path validation tests passed!\n"
+
else
+
Printf.printf "\nSome validation tests failed!\n"
+
) else (
+
(* Run original simple functionality test *)
+
Eio_main.run @@ fun env ->
+
let xdg = Xdge.create env#fs "path_test" in
+
+
(* Test config subdirectory *)
+
let profiles_path = Eio.Path.(Xdge.config_dir xdg / "profiles") in
+
let profile_file = Eio.Path.(profiles_path / "default.json") in
+
(try
+
let content = Eio.Path.load profile_file in
+
Printf.printf "config file content: %s" (String.trim content)
+
with
+
| exn -> Printf.printf "config file error: %s" (Printexc.to_string exn));
+
+
(* Test data subdirectory *)
+
let db_path = Eio.Path.(Xdge.data_dir xdg / "databases") in
+
let db_file = Eio.Path.(db_path / "main.db") in
+
(try
+
let content = Eio.Path.load db_file in
+
Printf.printf "\ndata file content: %s" (String.trim content)
+
with
+
| exn -> Printf.printf "\ndata file error: %s" (Printexc.to_string exn));
+
+
(* Test cache subdirectory *)
+
let cache_path = Eio.Path.(Xdge.cache_dir xdg / "thumbnails") in
+
let cache_file = Eio.Path.(cache_path / "thumb1.png") in
+
(try
+
let content = Eio.Path.load cache_file in
+
Printf.printf "\ncache file content: %s" (String.trim content)
+
with
+
| exn -> Printf.printf "\ncache file error: %s" (Printexc.to_string exn));
+
+
(* Test state subdirectory *)
+
let logs_path = Eio.Path.(Xdge.state_dir xdg / "logs") in
+
let log_file = Eio.Path.(logs_path / "app.log") in
+
(try
+
let content = Eio.Path.load log_file in
+
Printf.printf "\nstate file content: %s\n" (String.trim content)
+
with
+
| exn -> Printf.printf "\nstate file error: %s\n" (Printexc.to_string exn))
+
)