1use std::{
9 error::Error,
10 fmt,
11 path::{Component, Path, PathBuf},
12};
13
14use crate::{
15 env::{PathEnvOverrides, PlatformEnvironment, PlatformKind},
16 project::{
17 MODEL_CATALOG_CACHE_FILE_NAME, MODEL_FALLBACK_FILE_NAME, MODEL_PROFILES_FILE_NAME,
18 VERSION_CHECK_CACHE_FILE_NAME,
19 },
20};
21
22pub use crate::project::APP_DIR_NAME;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct RuntimePaths {
27 pub config_dir: PathBuf,
28 pub data_dir: PathBuf,
29 pub state_dir: PathBuf,
30 pub cache_dir: PathBuf,
31 pub log_dir: PathBuf,
32 pub temp_dir: PathBuf,
33 pub runtime_dir: PathBuf,
34 pub service_dir: PathBuf,
35}
36
37impl RuntimePaths {
38 pub fn resolve(
40 environment: &PlatformEnvironment,
41 overrides: &PathEnvOverrides,
42 ) -> Result<Self, PathError> {
43 let defaults = if let Some(root) = overrides.home.as_deref() {
44 runtime_home_defaults(root)?
45 } else {
46 platform_defaults(environment)?
47 };
48
49 let resolved = Self {
50 config_dir: override_path(
51 PathPurpose::Config,
52 defaults.config_dir,
53 overrides.config_dir.as_deref(),
54 )?,
55 data_dir: override_path(
56 PathPurpose::Data,
57 defaults.data_dir,
58 overrides.data_dir.as_deref(),
59 )?,
60 state_dir: override_path(
61 PathPurpose::State,
62 defaults.state_dir,
63 overrides.state_dir.as_deref(),
64 )?,
65 cache_dir: override_path(
66 PathPurpose::Cache,
67 defaults.cache_dir,
68 overrides.cache_dir.as_deref(),
69 )?,
70 log_dir: override_path(
71 PathPurpose::Log,
72 defaults.log_dir,
73 overrides.log_dir.as_deref(),
74 )?,
75 temp_dir: override_path(
76 PathPurpose::Temp,
77 defaults.temp_dir,
78 overrides.temp_dir.as_deref(),
79 )?,
80 runtime_dir: override_path(
81 PathPurpose::Runtime,
82 defaults.runtime_dir,
83 overrides.runtime_dir.as_deref(),
84 )?,
85 service_dir: override_path(
86 PathPurpose::Service,
87 defaults.service_dir,
88 overrides.service_dir.as_deref(),
89 )?,
90 };
91
92 validate_all(&resolved)?;
93 Ok(resolved)
94 }
95
96 pub fn agent_audit_log_file(&self) -> PathBuf {
98 self.log_dir.join("agent-audit.jsonl")
99 }
100
101 pub fn model_profiles_file(&self) -> PathBuf {
103 self.config_dir.join(MODEL_PROFILES_FILE_NAME)
104 }
105
106 pub fn model_fallback_file(&self) -> PathBuf {
108 self.config_dir.join(MODEL_FALLBACK_FILE_NAME)
109 }
110
111 pub fn model_catalog_cache_file(&self) -> PathBuf {
113 self.cache_dir.join(MODEL_CATALOG_CACHE_FILE_NAME)
114 }
115
116 pub fn version_check_cache_file(&self) -> PathBuf {
118 self.cache_dir.join(VERSION_CHECK_CACHE_FILE_NAME)
119 }
120}
121
122pub fn default_user_document_roots(
124 environment: &PlatformEnvironment,
125) -> Result<Vec<PathBuf>, PathError> {
126 let home = match environment.platform {
127 PlatformKind::Windows => environment
128 .home_dir
129 .as_deref()
130 .map(|path| validate_path(PathPurpose::Home, path).map(|_| path.to_path_buf()))
131 .transpose()?,
132 _ => validated_optional(PathPurpose::Home, environment.home_dir.as_deref())?
133 .map(Path::to_path_buf),
134 };
135 let Some(home) = home else {
136 return Ok(Vec::new());
137 };
138
139 Ok(["Documents", "Desktop", "Downloads"]
140 .into_iter()
141 .map(|child| home.join(child))
142 .collect())
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum PathPurpose {
148 Home,
149 Config,
150 Data,
151 State,
152 Cache,
153 Log,
154 Temp,
155 Runtime,
156 Service,
157}
158
159impl fmt::Display for PathPurpose {
160 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
161 match self {
162 Self::Home => write!(formatter, "home"),
163 Self::Config => write!(formatter, "config"),
164 Self::Data => write!(formatter, "data"),
165 Self::State => write!(formatter, "state"),
166 Self::Cache => write!(formatter, "cache"),
167 Self::Log => write!(formatter, "log"),
168 Self::Temp => write!(formatter, "temp"),
169 Self::Runtime => write!(formatter, "runtime"),
170 Self::Service => write!(formatter, "service"),
171 }
172 }
173}
174
175#[derive(Debug, Clone, PartialEq, Eq)]
177pub struct PathError {
178 pub purpose: PathPurpose,
179 pub kind: PathErrorKind,
180}
181
182impl PathError {
183 fn missing_base(purpose: PathPurpose, variable: &'static str) -> Self {
184 Self {
185 purpose,
186 kind: PathErrorKind::MissingBase { variable },
187 }
188 }
189
190 fn relative(purpose: PathPurpose, path: &Path) -> Self {
191 Self {
192 purpose,
193 kind: PathErrorKind::RelativePath {
194 path: path.to_path_buf(),
195 },
196 }
197 }
198
199 fn parent_component(purpose: PathPurpose, path: &Path) -> Self {
200 Self {
201 purpose,
202 kind: PathErrorKind::ParentComponent {
203 path: path.to_path_buf(),
204 },
205 }
206 }
207}
208
209#[derive(Debug, Clone, PartialEq, Eq)]
211pub enum PathErrorKind {
212 MissingBase { variable: &'static str },
213 RelativePath { path: PathBuf },
214 ParentComponent { path: PathBuf },
215}
216
217impl fmt::Display for PathError {
218 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
219 match &self.kind {
220 PathErrorKind::MissingBase { variable } => write!(
221 formatter,
222 "cannot resolve {} directory because {variable} is unavailable",
223 self.purpose
224 ),
225 PathErrorKind::RelativePath { path } => write!(
226 formatter,
227 "{} directory must be absolute, got {}",
228 self.purpose,
229 path.display()
230 ),
231 PathErrorKind::ParentComponent { path } => write!(
232 formatter,
233 "{} directory must not contain '..', got {}",
234 self.purpose,
235 path.display()
236 ),
237 }
238 }
239}
240
241impl Error for PathError {}
242
243fn runtime_home_defaults(root: &Path) -> Result<RuntimePaths, PathError> {
244 validate_path(PathPurpose::Home, root)?;
245
246 Ok(RuntimePaths {
247 config_dir: root.join("config"),
248 data_dir: root.join("data"),
249 state_dir: root.join("state"),
250 cache_dir: root.join("cache"),
251 log_dir: root.join("logs"),
252 temp_dir: root.join("tmp"),
253 runtime_dir: root.join("run"),
254 service_dir: root.join("service"),
255 })
256}
257
258fn platform_defaults(environment: &PlatformEnvironment) -> Result<RuntimePaths, PathError> {
259 match environment.platform {
260 PlatformKind::Macos => macos_defaults(environment),
261 PlatformKind::Windows => windows_defaults(environment),
262 PlatformKind::Unix | PlatformKind::Other => unix_defaults(environment),
263 }
264}
265
266fn unix_defaults(environment: &PlatformEnvironment) -> Result<RuntimePaths, PathError> {
267 let home = validated_optional(PathPurpose::Home, environment.home_dir.as_deref())?;
268 let config_base = base_or_home_child(
269 PathPurpose::Config,
270 environment.xdg_config_home.as_deref(),
271 home,
272 ".config",
273 PathBuf::from("/etc"),
274 )?;
275 let data_base = base_or_home_child(
276 PathPurpose::Data,
277 environment.xdg_data_home.as_deref(),
278 home,
279 ".local/share",
280 PathBuf::from("/var/lib"),
281 )?;
282 let state_base = base_or_home_child(
283 PathPurpose::State,
284 environment.xdg_state_home.as_deref(),
285 home,
286 ".local/state",
287 PathBuf::from("/var/lib"),
288 )?;
289 let cache_base = base_or_home_child(
290 PathPurpose::Cache,
291 environment.xdg_cache_home.as_deref(),
292 home,
293 ".cache",
294 PathBuf::from("/var/cache"),
295 )?;
296 let temp_base = optional_or_default(
297 PathPurpose::Temp,
298 environment.temp_dir.as_deref(),
299 PathBuf::from("/tmp"),
300 )?;
301 let state_dir = state_base.join(APP_DIR_NAME);
302 let runtime_dir = if let Some(runtime_base) =
303 validated_optional(PathPurpose::Runtime, environment.xdg_runtime_dir.as_deref())?
304 {
305 runtime_base.join(APP_DIR_NAME)
306 } else {
307 state_dir.join("run")
308 };
309
310 Ok(RuntimePaths {
311 config_dir: config_base.join(APP_DIR_NAME),
312 data_dir: data_base.join(APP_DIR_NAME),
313 state_dir: state_dir.clone(),
314 cache_dir: cache_base.join(APP_DIR_NAME),
315 log_dir: state_dir.join("logs"),
316 temp_dir: temp_base.join(APP_DIR_NAME),
317 runtime_dir,
318 service_dir: config_base.join(APP_DIR_NAME).join("service"),
319 })
320}
321
322fn macos_defaults(environment: &PlatformEnvironment) -> Result<RuntimePaths, PathError> {
323 let home = required_base(
324 PathPurpose::Home,
325 environment.home_dir.as_deref(),
326 HOME_REQUIRED,
327 )?;
328 let application_support = home.join("Library").join("Application Support");
329 let state_dir = application_support.join(APP_DIR_NAME).join("state");
330
331 Ok(RuntimePaths {
332 config_dir: application_support.join(APP_DIR_NAME).join("config"),
333 data_dir: application_support.join(APP_DIR_NAME).join("data"),
334 state_dir: state_dir.clone(),
335 cache_dir: home.join("Library").join("Caches").join(APP_DIR_NAME),
336 log_dir: home.join("Library").join("Logs").join(APP_DIR_NAME),
337 temp_dir: optional_or_default(
338 PathPurpose::Temp,
339 environment.temp_dir.as_deref(),
340 PathBuf::from("/tmp"),
341 )?
342 .join(APP_DIR_NAME),
343 runtime_dir: state_dir.join("run"),
344 service_dir: application_support.join(APP_DIR_NAME).join("service"),
345 })
346}
347
348fn windows_defaults(environment: &PlatformEnvironment) -> Result<RuntimePaths, PathError> {
349 let config_base = environment
350 .app_data
351 .as_deref()
352 .map(|path| validate_path(PathPurpose::Config, path).map(|_| path.to_path_buf()))
353 .transpose()?
354 .or_else(|| {
355 environment
356 .home_dir
357 .as_ref()
358 .map(|home| home.join("AppData/Roaming"))
359 })
360 .ok_or_else(|| PathError::missing_base(PathPurpose::Config, "APPDATA or HOME"))?;
361 let local_base = environment
362 .local_app_data
363 .as_deref()
364 .map(|path| validate_path(PathPurpose::Data, path).map(|_| path.to_path_buf()))
365 .transpose()?
366 .or_else(|| {
367 environment
368 .home_dir
369 .as_ref()
370 .map(|home| home.join("AppData/Local"))
371 })
372 .ok_or_else(|| PathError::missing_base(PathPurpose::Data, "LOCALAPPDATA or HOME"))?;
373 let root = local_base.join(APP_DIR_NAME);
374 let temp_dir = match environment.temp_dir.as_deref() {
375 Some(path) => {
376 validate_path(PathPurpose::Temp, path)?;
377 path.join(APP_DIR_NAME)
378 }
379 None => root.join("tmp"),
380 };
381
382 Ok(RuntimePaths {
383 config_dir: config_base.join(APP_DIR_NAME),
384 data_dir: root.join("data"),
385 state_dir: root.join("state"),
386 cache_dir: root.join("cache"),
387 log_dir: root.join("logs"),
388 temp_dir,
389 runtime_dir: root.join("run"),
390 service_dir: config_base.join(APP_DIR_NAME).join("service"),
391 })
392}
393
394const HOME_REQUIRED: &str = "HOME";
395
396fn base_or_home_child(
397 purpose: PathPurpose,
398 configured: Option<&Path>,
399 home: Option<&Path>,
400 home_child: &str,
401 fallback_base: PathBuf,
402) -> Result<PathBuf, PathError> {
403 if let Some(path) = configured {
404 validate_path(purpose, path)?;
405 return Ok(path.to_path_buf());
406 }
407
408 if let Some(path) = home {
409 validate_path(purpose, path)?;
410 return Ok(path.join(home_child));
411 }
412
413 validate_path(purpose, &fallback_base)?;
414 Ok(fallback_base)
415}
416
417fn required_base(
418 purpose: PathPurpose,
419 value: Option<&Path>,
420 variable: &'static str,
421) -> Result<PathBuf, PathError> {
422 value
423 .map(|path| validate_path(purpose, path).map(|_| path.to_path_buf()))
424 .transpose()?
425 .ok_or_else(|| PathError::missing_base(purpose, variable))
426}
427
428fn validated_optional(
429 purpose: PathPurpose,
430 value: Option<&Path>,
431) -> Result<Option<&Path>, PathError> {
432 if let Some(path) = value {
433 validate_path(purpose, path)?;
434 }
435
436 Ok(value)
437}
438
439fn optional_or_default(
440 purpose: PathPurpose,
441 value: Option<&Path>,
442 default: PathBuf,
443) -> Result<PathBuf, PathError> {
444 match value {
445 Some(path) => {
446 validate_path(purpose, path)?;
447 Ok(path.to_path_buf())
448 }
449 None => {
450 validate_path(purpose, &default)?;
451 Ok(default)
452 }
453 }
454}
455
456fn override_path(
457 purpose: PathPurpose,
458 default: PathBuf,
459 override_value: Option<&Path>,
460) -> Result<PathBuf, PathError> {
461 if let Some(path) = override_value {
462 validate_path(purpose, path)?;
463 Ok(path.to_path_buf())
464 } else {
465 Ok(default)
466 }
467}
468
469fn validate_all(paths: &RuntimePaths) -> Result<(), PathError> {
470 validate_path(PathPurpose::Config, &paths.config_dir)?;
471 validate_path(PathPurpose::Data, &paths.data_dir)?;
472 validate_path(PathPurpose::State, &paths.state_dir)?;
473 validate_path(PathPurpose::Cache, &paths.cache_dir)?;
474 validate_path(PathPurpose::Log, &paths.log_dir)?;
475 validate_path(PathPurpose::Temp, &paths.temp_dir)?;
476 validate_path(PathPurpose::Runtime, &paths.runtime_dir)?;
477 validate_path(PathPurpose::Service, &paths.service_dir)
478}
479
480fn validate_path(purpose: PathPurpose, path: &Path) -> Result<(), PathError> {
481 if !path.is_absolute() {
482 return Err(PathError::relative(purpose, path));
483 }
484
485 if path
486 .components()
487 .any(|component| matches!(component, Component::ParentDir))
488 {
489 return Err(PathError::parent_component(purpose, path));
490 }
491
492 Ok(())
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use crate::env::{PathEnvOverrides, PlatformEnvironment, PlatformKind};
499
500 fn unix_environment() -> PlatformEnvironment {
501 PlatformEnvironment {
502 platform: PlatformKind::Unix,
503 home_dir: Some(PathBuf::from("/home/alice")),
504 xdg_config_home: Some(PathBuf::from("/config")),
505 xdg_data_home: Some(PathBuf::from("/data")),
506 xdg_state_home: Some(PathBuf::from("/state")),
507 xdg_cache_home: Some(PathBuf::from("/cache")),
508 xdg_runtime_dir: Some(PathBuf::from("/run/user/1000")),
509 app_data: None,
510 local_app_data: None,
511 temp_dir: Some(PathBuf::from("/tmp")),
512 }
513 }
514
515 #[test]
516 fn resolves_unix_platform_paths() {
517 let paths = RuntimePaths::resolve(&unix_environment(), &PathEnvOverrides::default())
518 .expect("paths should resolve");
519
520 assert_eq!(paths.config_dir, PathBuf::from("/config/relay-knowledge"));
521 assert_eq!(paths.data_dir, PathBuf::from("/data/relay-knowledge"));
522 assert_eq!(paths.state_dir, PathBuf::from("/state/relay-knowledge"));
523 assert_eq!(paths.cache_dir, PathBuf::from("/cache/relay-knowledge"));
524 assert_eq!(paths.log_dir, PathBuf::from("/state/relay-knowledge/logs"));
525 assert_eq!(
526 paths.runtime_dir,
527 PathBuf::from("/run/user/1000/relay-knowledge")
528 );
529 }
530
531 #[test]
532 fn runtime_home_override_keeps_state_out_of_repository_paths() {
533 let overrides = PathEnvOverrides {
534 home: Some(PathBuf::from("/srv/relay")),
535 ..PathEnvOverrides::default()
536 };
537
538 let paths =
539 RuntimePaths::resolve(&unix_environment(), &overrides).expect("paths should resolve");
540
541 assert_eq!(paths.config_dir, PathBuf::from("/srv/relay/config"));
542 assert_eq!(paths.data_dir, PathBuf::from("/srv/relay/data"));
543 assert_eq!(paths.cache_dir, PathBuf::from("/srv/relay/cache"));
544 assert_eq!(paths.log_dir, PathBuf::from("/srv/relay/logs"));
545 }
546
547 #[test]
548 fn rejects_relative_overrides() {
549 let overrides = PathEnvOverrides {
550 data_dir: Some(PathBuf::from("relative-data")),
551 ..PathEnvOverrides::default()
552 };
553
554 let error = RuntimePaths::resolve(&unix_environment(), &overrides)
555 .expect_err("relative override should fail");
556
557 assert_eq!(error.purpose, PathPurpose::Data);
558 assert_eq!(
559 error.kind,
560 PathErrorKind::RelativePath {
561 path: PathBuf::from("relative-data")
562 }
563 );
564 }
565
566 #[test]
567 fn rejects_parent_components() {
568 let overrides = PathEnvOverrides {
569 cache_dir: Some(PathBuf::from("/var/cache/../relay")),
570 ..PathEnvOverrides::default()
571 };
572
573 let error = RuntimePaths::resolve(&unix_environment(), &overrides)
574 .expect_err("parent component should fail");
575
576 assert_eq!(error.purpose, PathPurpose::Cache);
577 assert!(matches!(error.kind, PathErrorKind::ParentComponent { .. }));
578 }
579
580 #[test]
581 fn resolves_unix_service_paths_without_home() {
582 let environment = PlatformEnvironment {
583 platform: PlatformKind::Unix,
584 home_dir: None,
585 xdg_config_home: None,
586 xdg_data_home: None,
587 xdg_state_home: None,
588 xdg_cache_home: None,
589 xdg_runtime_dir: None,
590 app_data: None,
591 local_app_data: None,
592 temp_dir: None,
593 };
594
595 let paths = RuntimePaths::resolve(&environment, &PathEnvOverrides::default())
596 .expect("service defaults should resolve without HOME");
597
598 assert_eq!(paths.config_dir, PathBuf::from("/etc/relay-knowledge"));
599 assert_eq!(paths.data_dir, PathBuf::from("/var/lib/relay-knowledge"));
600 assert_eq!(paths.cache_dir, PathBuf::from("/var/cache/relay-knowledge"));
601 assert_eq!(
602 paths.runtime_dir,
603 PathBuf::from("/var/lib/relay-knowledge/run")
604 );
605 }
606
607 #[test]
608 fn windows_temp_dir_is_scoped_under_application_directory() {
609 let environment = PlatformEnvironment {
610 platform: PlatformKind::Windows,
611 home_dir: None,
612 xdg_config_home: None,
613 xdg_data_home: None,
614 xdg_state_home: None,
615 xdg_cache_home: None,
616 xdg_runtime_dir: None,
617 app_data: Some(PathBuf::from("/roaming")),
618 local_app_data: Some(PathBuf::from("/local")),
619 temp_dir: Some(PathBuf::from("/shared-temp")),
620 };
621
622 let paths = RuntimePaths::resolve(&environment, &PathEnvOverrides::default())
623 .expect("windows paths should resolve");
624
625 assert_eq!(
626 paths.temp_dir,
627 PathBuf::from("/shared-temp/relay-knowledge")
628 );
629 }
630
631 #[test]
632 fn resolves_macos_application_support_paths() {
633 let environment = PlatformEnvironment {
634 platform: PlatformKind::Macos,
635 home_dir: Some(PathBuf::from("/Users/alice")),
636 xdg_config_home: None,
637 xdg_data_home: None,
638 xdg_state_home: None,
639 xdg_cache_home: None,
640 xdg_runtime_dir: None,
641 app_data: None,
642 local_app_data: None,
643 temp_dir: None,
644 };
645
646 let paths =
647 RuntimePaths::resolve(&environment, &PathEnvOverrides::default()).expect("mac paths");
648
649 assert_eq!(
650 paths.config_dir,
651 PathBuf::from("/Users/alice/Library/Application Support/relay-knowledge/config")
652 );
653 assert_eq!(
654 paths.cache_dir,
655 PathBuf::from("/Users/alice/Library/Caches/relay-knowledge")
656 );
657 assert_eq!(paths.temp_dir, PathBuf::from("/tmp/relay-knowledge"));
658 }
659
660 #[test]
661 fn macos_requires_home_directory() {
662 let environment = PlatformEnvironment {
663 platform: PlatformKind::Macos,
664 home_dir: None,
665 xdg_config_home: None,
666 xdg_data_home: None,
667 xdg_state_home: None,
668 xdg_cache_home: None,
669 xdg_runtime_dir: None,
670 app_data: None,
671 local_app_data: None,
672 temp_dir: None,
673 };
674
675 let error = RuntimePaths::resolve(&environment, &PathEnvOverrides::default())
676 .expect_err("missing HOME should fail");
677
678 assert_eq!(error.purpose, PathPurpose::Home);
679 assert_eq!(
680 error.to_string(),
681 "cannot resolve home directory because HOME is unavailable"
682 );
683 }
684
685 #[test]
686 fn windows_falls_back_to_home_appdata_paths() {
687 let environment = PlatformEnvironment {
688 platform: PlatformKind::Windows,
689 home_dir: Some(PathBuf::from("/Users/Alice")),
690 xdg_config_home: None,
691 xdg_data_home: None,
692 xdg_state_home: None,
693 xdg_cache_home: None,
694 xdg_runtime_dir: None,
695 app_data: None,
696 local_app_data: None,
697 temp_dir: None,
698 };
699
700 let paths = RuntimePaths::resolve(&environment, &PathEnvOverrides::default())
701 .expect("windows fallback should resolve");
702
703 assert_eq!(
704 paths.config_dir,
705 PathBuf::from("/Users/Alice/AppData/Roaming/relay-knowledge")
706 );
707 assert_eq!(
708 paths.data_dir,
709 PathBuf::from("/Users/Alice/AppData/Local/relay-knowledge/data")
710 );
711 assert_eq!(
712 paths.temp_dir,
713 PathBuf::from("/Users/Alice/AppData/Local/relay-knowledge/tmp")
714 );
715 }
716
717 #[test]
718 fn per_directory_overrides_replace_defaults() {
719 let overrides = PathEnvOverrides {
720 config_dir: Some(PathBuf::from("/custom/config")),
721 data_dir: Some(PathBuf::from("/custom/data")),
722 state_dir: Some(PathBuf::from("/custom/state")),
723 cache_dir: Some(PathBuf::from("/custom/cache")),
724 log_dir: Some(PathBuf::from("/custom/log")),
725 temp_dir: Some(PathBuf::from("/custom/tmp")),
726 runtime_dir: Some(PathBuf::from("/custom/run")),
727 service_dir: Some(PathBuf::from("/custom/service")),
728 ..PathEnvOverrides::default()
729 };
730
731 let paths = RuntimePaths::resolve(&unix_environment(), &overrides)
732 .expect("overrides should resolve");
733
734 assert_eq!(paths.config_dir, PathBuf::from("/custom/config"));
735 assert_eq!(paths.data_dir, PathBuf::from("/custom/data"));
736 assert_eq!(paths.state_dir, PathBuf::from("/custom/state"));
737 assert_eq!(paths.cache_dir, PathBuf::from("/custom/cache"));
738 assert_eq!(paths.log_dir, PathBuf::from("/custom/log"));
739 assert_eq!(paths.temp_dir, PathBuf::from("/custom/tmp"));
740 assert_eq!(paths.runtime_dir, PathBuf::from("/custom/run"));
741 assert_eq!(paths.service_dir, PathBuf::from("/custom/service"));
742 }
743}