From e0bd37059e39f0ae5ee7c3387bb476744d6c0eae Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 22:51:27 +0200 Subject: [PATCH] fix(quadlet): convert leading `~` in host paths to `%h` systemd does not perform shell-style tilde expansion, so a host path starting with `~/` left in a generated Quadlet file is passed to Podman as an invalid relative path and the service fails to start. Replace the leading `~` with the systemd `%h` specifier instead. Also fix the systemd specifier check in `absolute_clean_path()`, which used `Path::starts_with()` and therefore only matched a path whose first component was exactly `%`, causing `--absolute-host-paths` to prepend the resolve directory to paths such as `%h/data`. Closes: #166 Signed-off-by: DualFroz --- src/cli.rs | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/src/cli.rs b/src/cli.rs index f68337d..43ec4a4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -450,6 +450,10 @@ multiple times."; } } + for file in &mut files { + file.host_paths_home_specifier(); + } + let downgrade = self.podman_version < PodmanVersion::LATEST; if resolve_dir.is_some() || self.no_start_with_pod || downgrade { for file in &mut files { @@ -826,6 +830,15 @@ impl File { } } + /// If a Quadlet file, replace a leading `~` in host paths with the systemd `%h` specifier. + fn host_paths_home_specifier(&mut self) { + for path in self.host_paths() { + if let Some(home_path) = home_specifier_path(path) { + *path = home_path; + } + } + } + /// If a Quadlet file, make all host paths absolute and clean. /// /// Relative paths are resolved using `resolve_dir` as the base. @@ -884,10 +897,20 @@ impl File { } } +/// If `path` starts with `~`, a version using the systemd `%h` specifier is returned. +/// +/// systemd does not perform shell-style tilde expansion, so a `~` left in a Quadlet file results in +/// an invalid relative path being given to Podman. +fn home_specifier_path(path: &Path) -> Option { + let rest = path.to_str()?.strip_prefix('~')?; + (rest.is_empty() || rest.starts_with('/')).then(|| format!("%h{rest}").into()) +} + /// If `path` is relative, it is resolved using `resolve_dir` and a cleaned version is returned. fn absolute_clean_path(resolve_dir: &Path, path: &Path) -> PathBuf { // Paths starting with "%" are also absolute because they start with a systemd specifier. - let path: Cow = if path.is_absolute() || path.starts_with("%") { + let specifier = path.to_str().is_some_and(|path| path.starts_with('%')); + let path: Cow = if path.is_absolute() || specifier { path.into() } else { resolve_dir.join(path).into() @@ -999,4 +1022,36 @@ mod tests { fn verify_cli() { Cli::command().debug_assert(); } + + #[test] + fn home_specifier_paths() { + assert_eq!( + home_specifier_path(Path::new("~/foo/bar")), + Some("%h/foo/bar".into()), + ); + assert_eq!(home_specifier_path(Path::new("~")), Some("%h".into())); + assert_eq!(home_specifier_path(Path::new("~user/foo")), None); + assert_eq!(home_specifier_path(Path::new("/foo/~/bar")), None); + assert_eq!(home_specifier_path(Path::new("./foo")), None); + } + + #[test] + fn volume_home_specifier() -> color_eyre::Result<()> { + let cli = Cli::parse_from(["podlet", "podman", "run", "-v", "~/data:/data:Z", "image"]); + let files = cli.try_into_files()?; + let file = files.first().expect("one file created"); + assert!( + file.serialize(&HashSet::new())? + .contains("Volume=%h/data:/data:Z") + ); + Ok(()) + } + + #[test] + fn specifier_path_not_resolved() { + assert_eq!( + absolute_clean_path(Path::new("/resolve/dir"), Path::new("%h/data")), + PathBuf::from("%h/data"), + ); + } }