feat: unify cache paths via platform-aware dirs::cache_dir() (#229)#230
Merged
Conversation
Route every `all-smi` cache writer — record output, energy WAL, and the users-tab CSV export — through a single platform-aware helper built on `dirs::cache_dir()`, retire the three hard-coded `~/.cache/all-smi/...` literals, and delete the hand-rolled `cache_dir_for_all_smi()` reimplementation in `src/view/event_handler.rs`.
The new `crate::common::paths::cache_dir()` helper mirrors the existing `config_dir()` so the cache layer is internally consistent with the config layer. `record.output_dir` and `energy.wal_path` now store `Option<String>` — `None` means "use the platform helper", `Some(s)` is an explicit operator override resolved through `expand_tilde`. Consumers (`record::RecorderOptions::from_args_with_settings`, `api::server::run_api_mode`) call the helper when no override is present.
Adds `crate::common::cache_migration::migrate_legacy_cache_paths`, called from `main()` before any cache file is touched. On Linux without `$XDG_CACHE_HOME` the old and new paths are identical and it is a no-op; on macOS, Windows, and Linux with `$XDG_CACHE_HOME` set, it best-effort-renames `~/.cache/all-smi/{records,energy-wal.bin}` into the new root only when the destination is empty (never overwrites) and refuses to follow a pre-planted symlink at the source (same threat model as the writer-side `O_NOFOLLOW` defences). The users-CSV exports are deliberately not migrated — they carry per-second timestamps and bulk-renaming hundreds of files would be more disruptive than helpful.
Tests added: `paths::tests::cache_dir_ends_with_app_name`, `record::tests::resolve_output_no_cli_no_config_uses_platform_cache_dir`, and a five-test `cache_migration::tests` module (`try_move_skips_when_source_missing`, `try_move_renames_file_when_dst_missing`, `try_move_renames_directory_when_dst_missing`, `try_move_no_op_when_dst_exists`, `try_move_refuses_symlink_source`). Existing `record::tests` resolver tests updated for the `Option<String>` shape; `config::tests::energy_config_defaults_match_issue_spec` and the `ALL_SMI_ENERGY_WAL_PATH` env test updated to assert `None` / `Some(...)`.
Docs: README updates the `[energy] wal_path`, `[record] output_dir`, `--output` and users-CSV export rows; manpage updates the FILES and ENERGY ACCOUNTING sections; the `record --output` `long_help` and the example `config.toml` text spell out the per-platform cache locations. The v0.21.0 changelog gets a closing sentence describing the unification and the migration. The `config print` / `config print --json` renderer now shows the resolved on-disk path when no override is set so operators can confirm at a glance where data will land.
Closes #229. Builds on #227 (Scope A of #223). The writer-side hardening (`O_NOFOLLOW`, mode `0600`, symlink refusal at rotation) lives in `src/record/writer.rs`, `src/metrics/energy_wal.rs::open_secure_append`, and `src/view/event_handler.rs::open_export_secure` and applies unchanged to the new resolved paths. `ALL_SMI_ENERGY_WAL_PATH` and `ALL_SMI_RECORD_OUTPUT_DIR` env-var support and the TOML key names are preserved.
The ALL_SMI_ENERGY_WAL_PATH help line still showed the Linux-only ~/.cache/all-smi/energy-wal.bin default, while the man page, README footnote, and RecordArgs long_help already describe the per-platform resolution. Use <platform cache dir>/all-smi/energy-wal.bin instead so all four doc surfaces describe the same default for #229.
Apply the two LOW defense-in-depth consistency fixes flagged by the security checker: **L1 — record resolver (src/record/mod.rs):** Add .map(str::trim) before .filter(!is_empty()) on the configured output_dir chain. A TOML value of " " (spaces only) now falls through to the platform cache helper instead of being honored as a literal relative directory, matching how resolve_wal_path() already handles whitespace-only wal_path values via str::trim. **L2 — TOML loader (src/common/config_apply.rs):** apply_file_energy and apply_file_record now trim and reject whitespace-only strings before writing to settings.energy.wal_path and settings.record.output_dir respectively, matching the existing behavior of apply_env_record which already calls trim().is_empty(). The collapsed let-chain form (if let Some(x) = ... && !x.trim().is_empty()) satisfies clippy's collapsible_if lint. **Tests added:** - src/record/mod.rs: resolve_output_whitespace_only_config_dir_falls_through — asserts a whitespace-only output_dir does not leak into the resolved output path. - tests/config_file_integration_test.rs: whitespace_only_wal_path_and_output_dir_treated_as_unset — loads a TOML with wal_path = " " and output_dir = " " and asserts both fields resolve to None. **Docs:** Changelog entry for the Users tab updated to reference the platform-aware cache path (via [^cache] footnote) instead of the Linux-only literal ~/.cache/all-smi/users-<timestamp>.csv. All targeted tests pass (1172 unit, 15 integration), clippy -D warnings clean, cargo fmt --check clean.
Member
Author
PR Finalization CompleteSummaryL1 and L2 LOW security-checker fixes applied in commit b169549. Tests added/updated:
Documentation:
Code quality:
L3 (dst.exists() vs dst.symlink_metadata() in cache_migration.rs) left as-is per the brief — the security checker confirmed no vulnerability; the tightening is follow-up if desired. |
This was referenced May 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Route every
all-smicache writer —recordoutput, energy WAL, and the users-tab CSV export — through a single platform-aware helper built ondirs::cache_dir(). Retires the three hard-coded~/.cache/all-smi/...literals and the hand-rolledcache_dir_for_all_smi()reimplementation, so the cache layer is finally as platform-correct as the config layer (config_dir()already was). Builds on #227 (Scope A of #223).What changed
crate::common::paths::cache_dir()mirrorsconfig_dir():dirs::cache_dir().map(|d| d.join(APP_DIR_NAME)). ReturnsOption<PathBuf>so callers can downgrade gracefully on bare CI shells / containers without$HOME.record.output_dirandenergy.wal_pathare nowOption<String>.None(the new compiled default) means "use the platform helper";Some(s)is an explicit operator override resolved throughexpand_tilde. The CLI flag surface and the TOML key names are unchanged;ALL_SMI_ENERGY_WAL_PATH/ALL_SMI_RECORD_OUTPUT_DIRcontinue to work.record::RecorderOptions::from_args_with_settingsnow layers-oflag → configoutput_dir(expand_tilde) →paths::cache_dir().join("records")→ CWD fallback.api::server::run_api_moderesolves the WAL path through a newmetrics::energy_wal::resolve_wal_path(...)helper that follows the same precedence.view::event_handler::export_users_csvcallspaths::cache_dir()directly. The hand-rolledcache_dir_for_all_smi()inevent_handler.rsis deleted.spawn_wal_flush_task/flush_cycle/compact_walnow takePathBuf/&Pathinstead ofString/&str, dropping the per-cycleto_string()andPath::new(...)re-wrapping.src/common/cache_migration.rsis called once frommain()before any subcommand touches the cache. Linux without$XDG_CACHE_HOMEis a no-op (old == new). On macOS, Windows, and Linux with$XDG_CACHE_HOMEset it moves~/.cache/all-smi/recordsand~/.cache/all-smi/energy-wal.bininto the new root only when the destination is empty (never overwrites) and refuses to follow a pre-planted symlink at the source (same threat model as the writer-sideO_NOFOLLOWdefences). The users-CSV exports are deliberately not migrated — they carry per-second timestamps and bulk-renaming hundreds of files would be more disruptive than helpful.config printshows the resolved on-disk path when no override is set, both in TOML and JSON outputs, so operators can confirm at a glance where data will land.[energy] wal_path,[record] output_dir,--outputand users-CSV export rows + the WAL persistence paragraph + the security note about the users-CSV export; manpage updates theFILESandENERGY ACCOUNTINGsections;RecordArgs::outputlong_helpand the exampleconfig.tomltext spell out the per-platform cache locations. The v0.21.0 changelog entry gets a closing sentence describing the unification and the migration.Migration
$XDG_CACHE_HOME(the common case):dirs::cache_dir() == ~/.cache, so the old path and the new path are identical and the migration helper is a no-op.$XDG_CACHE_HOMEset: the cache root relocates. Operators will see one-timeall-smi: migrated legacy cache <old> -> <new>lines on stderr at startup asrecords/andenergy-wal.binare moved into the new root. The migration never overwrites an existing destination — if you have data at both the old and the new path, the old data is left in place for manual reconciliation.all-smi: skipping cache migration of <path> (is a symlink)), matching the writer-sideO_NOFOLLOWrationale.$XDG_CACHE_HOMEpoints at a tmpfs while~is on persistent disk,std::fs::renamewill returnEXDEV, the helper logs the error, and recordings stay where they were. Operators can move them by hand.New tests
paths::tests::cache_dir_ends_with_app_name— sanity for the new helper.record::tests::resolve_output_no_cli_no_config_uses_platform_cache_dir— guards the issue-cache: unify record/energy-WAL/users-CSV paths via platform-aware dirs::cache_dir() (Scope B from #223) #229 promise that record output flows throughpaths::cache_dir()(skipped on hosts wherecache_dir()returnsNone, same pattern asconfig_dir_ends_with_app_name).cache_migration::tests(5 tests):try_move_skips_when_source_missing,try_move_renames_file_when_dst_missing,try_move_renames_directory_when_dst_missing,try_move_no_op_when_dst_exists,try_move_refuses_symlink_source(Unix-only — symlink creation requires it).record::testsresolver tests rewritten for theOption<String>shape;config::tests::energy_config_defaults_match_issue_specand theALL_SMI_ENERGY_WAL_PATHenv test now assertNone/Some(...).Security hardening preserved
The writer-side
O_NOFOLLOW+ mode0600defences insrc/record/writer.rs,src/metrics/energy_wal.rs::open_secure_append, andsrc/view/event_handler.rs::open_export_secureapply unchanged to the new resolved paths — only the resolver tier changes. The migration helper has its own defence: it refuses torenamethrough a symlink at the source.Test plan
cargo check --bin all-smi --testsRUSTFLAGS="-L /tmp/drmlinks" cargo test --bin all-smi paths::tests::RUSTFLAGS="-L /tmp/drmlinks" cargo test --bin all-smi record::tests::RUSTFLAGS="-L /tmp/drmlinks" cargo test --bin all-smi cache_migration::tests::RUSTFLAGS="-L /tmp/drmlinks" cargo test --bin all-smi config::tests::RUSTFLAGS="-L /tmp/drmlinks" cargo test --bin all-smi config_file::tests::RUSTFLAGS="-L /tmp/drmlinks" cargo test --bin all-smi metrics::energy_wal::RUSTFLAGS="-L /tmp/drmlinks" cargo test --bin all-smi --test config_file_integration_test(14/14)RUSTFLAGS="-L /tmp/drmlinks" cargo clippy --bin all-smi --tests -- -D warningscargo fmt --checkReferences #223 and #227. Closes #229.