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 <me@dualfroz.com>
This commit is contained in:
dualfroz committed 2026-09-13 15:08:58 +02:00
1 parent 0632ed7659
commit 00a1dd936a
1 file changed
+56 -1
+56 -1
View File
@@ -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<PathBuf> {
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<Path> = if path.is_absolute() || path.starts_with("%") {
let specifier = path.to_str().is_some_and(|path| path.starts_with('%'));
let path: Cow<Path> = 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"),
);
}
}