pathrs 0.2.4

C-friendly API to make path resolution safer on Linux.
Documentation
#!/usr/bin/bats -t
# SPDX-License-Identifier: MPL-2.0
#
# libpathrs: safe path resolution on Linux
# Copyright (C) 2019-2025 SUSE LLC
# Copyright (C) 2026 Aleksa Sarai <cyphar@cyphar.com>
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.

load helpers

function setup() {
	setup_tmpdirs
}

function teardown() {
	teardown_tmpdirs
}

@test "root symlink" {
	ROOT="$(setup_tmpdir)"

	mkdir -p "$ROOT/etc"
	echo "passwd" >"$ROOT/etc/passwd"

	pathrs-cmd root --root "$ROOT" symlink /etc/passwd passwd-link
	[ "$status" -eq 0 ]

	[ -f "$ROOT/etc/passwd" ]
	[ -L "$ROOT/passwd-link" ]

	sane_run readlink "$ROOT/passwd-link"
	[ "$status" -eq 0 ]
	[[ "$output" == "/etc/passwd" ]]

	pathrs-cmd root --root "$ROOT" readlink /passwd-link
	[ "$status" -eq 0 ]
	grep -Fx 'LINK-TARGET /etc/passwd' <<<"$output"
}

@test "root symlink [no-clobber file]" {
	ROOT="$(setup_tmpdir)"

	mkdir -p "$ROOT/etc"
	touch "$ROOT/link"

	ino="$(stat -c '%i' "$ROOT/link")"

	pathrs-cmd root --root "$ROOT" symlink ../target link
	check-errno EEXIST

	[ -f "$ROOT/link" ]
	sane_run stat -c '%i' "$ROOT/link"
	[[ "$output" == "$ino" ]]
}

@test "root symlink [no-clobber symlink]" {
	ROOT="$(setup_tmpdir)"

	mkdir -p "$ROOT/etc"
	touch "$ROOT/new"
	ln -s /old/link "$ROOT/link"

	pathrs-cmd root --root "$ROOT" symlink /new link
	check-errno EEXIST

	[ -L "$ROOT/link" ]

	sane_run readlink "$ROOT/link"
	[ "$status" -eq 0 ]
	[[ "$output" == "/old/link" ]]

	pathrs-cmd root --root "$ROOT" readlink /link
	[ "$status" -eq 0 ]
	grep -Fx 'LINK-TARGET /old/link' <<<"$output"
}

@test "root symlink [directory]" {
	ROOT="$(setup_tmpdir)"

	mkdir -p "$ROOT/foo/bar/baz"

	pathrs-cmd root --root "$ROOT" symlink ../foo/bar link
	[ "$status" -eq 0 ]

	[ -d "$ROOT/foo/bar" ]
	[ -L "$ROOT/link" ]

	sane_run readlink "$ROOT/link"
	[ "$status" -eq 0 ]
	[[ "$output" == "../foo/bar" ]]

	pathrs-cmd root --root "$ROOT" readlink link
	[ "$status" -eq 0 ]
	grep -Fx 'LINK-TARGET ../foo/bar' <<<"$output"
}

@test "root symlink [symlink]" {
	ROOT="$(setup_tmpdir)"

	mkdir -p "$ROOT/foo"
	touch "$ROOT/foo/bar"
	ln -s /foo/bar "$ROOT/target-link"

	pathrs-cmd root --root "$ROOT" symlink target-link link
	[ "$status" -eq 0 ]

	[ -f "$ROOT/foo/bar" ]
	[ -L "$ROOT/target-link" ]
	[ -L "$ROOT/link" ]

	sane_run readlink "$ROOT/link"
	[ "$status" -eq 0 ]
	[[ "$output" == "target-link" ]]

	pathrs-cmd root --root "$ROOT" readlink link
	[ "$status" -eq 0 ]
	grep -Fx 'LINK-TARGET target-link' <<<"$output"
}

@test "root symlink [non-existent target]" {
	ROOT="$(setup_tmpdir)"

	pathrs-cmd root --root "$ROOT" symlink ../..//some/dummy/path link
	[ "$status" -eq 0 ]

	! [ -e "$ROOT/some/dummy/path" ]
	[ -L "$ROOT/link" ]

	sane_run readlink "$ROOT/link"
	[ "$status" -eq 0 ]
	[[ "$output" == "../..//some/dummy/path" ]]

	pathrs-cmd root --root "$ROOT" readlink link
	[ "$status" -eq 0 ]
	grep -Fx 'LINK-TARGET ../..//some/dummy/path' <<<"$output"
}

@test "root symlink [non-existent parent component]" {
	ROOT="$(setup_tmpdir)"

	mkdir -p "$ROOT/foo/bar/baz"
	mkdir -p "$ROOT/etc"
	echo "passwd" >"$ROOT/etc/passwd"

	pathrs-cmd root --root "$ROOT" symlink /etc/passwd foo/nope/baz
	check-errno ENOENT

	! [ -e "$ROOT/foo/nope" ]
	! [ -e "$ROOT/foo/nope/baz" ]
}

@test "root symlink [bad parent component]" {
	ROOT="$(setup_tmpdir)"

	mkdir -p "$ROOT/foo"
	touch "$ROOT/foo/bar"
	mkdir -p "$ROOT/etc"
	echo "passwd" >"$ROOT/etc/passwd"

	pathrs-cmd root --root "$ROOT" symlink /etc/passwd foo/bar/baz
	check-errno ENOTDIR

	[ -f "$ROOT/foo/bar" ]
	! [ -e "$ROOT/foo/bar/baz" ]
}

@test "root hardlink" {
	ROOT="$(setup_tmpdir)"

	mkdir -p "$ROOT/etc"
	echo "passwd" >"$ROOT/etc/passwd"

	pathrs-cmd root --root "$ROOT" hardlink /etc/passwd passwd-link
	[ "$status" -eq 0 ]

	[ -f "$ROOT/etc/passwd" ]
	[ -f "$ROOT/passwd-link" ]

	sane_run readlink "$ROOT/passwd-link"
	[ "$status" -ne 0 ] # not a symlink!

	# Hardlinks have the same inode.
	sane_run stat -c '%i' "$ROOT/etc/passwd"
	target_ino="$output"
	sane_run stat -c '%i' "$ROOT/passwd-link"
	link_ino="$output"
	[[ "$target_ino" == "$link_ino" ]]
}

@test "root hardlink [no-clobber file]" {
	ROOT="$(setup_tmpdir)"

	mkdir -p "$ROOT/etc"
	touch "$ROOT/target"
	touch "$ROOT/link"

	ino="$(stat -c '%i' "$ROOT/link")"

	pathrs-cmd root --root "$ROOT" hardlink target link
	check-errno EEXIST

	[ -f "$ROOT/link" ]
	sane_run stat -c '%i' "$ROOT/link"
	[[ "$output" == "$ino" ]]
}

@test "root hardlink [no-clobber symlink]" {
	ROOT="$(setup_tmpdir)"

	touch "$ROOT/foobar"
	touch "$ROOT/target"
	ln -s /foobar "$ROOT/link"

	ino="$(stat -c '%i' "$ROOT/link")"

	pathrs-cmd root --root "$ROOT" hardlink /target link
	check-errno EEXIST

	[ -L "$ROOT/link" ]
	sane_run stat -c '%i' "$ROOT/link"
	[[ "$output" == "$ino" ]]
}

@test "root hardlink [directory]" {
	ROOT="$(setup_tmpdir)"

	mkdir -p "$ROOT/foo/bar/baz"

	pathrs-cmd root --root "$ROOT" hardlink /foo/bar link
	check-errno EPERM

	[ -d "$ROOT/foo/bar" ]
	! [ -e "$ROOT/link" ]
}

@test "root hardlink [non-existent target]" {
	ROOT="$(setup_tmpdir)"

	pathrs-cmd root --root "$ROOT" hardlink ../..//some/dummy/path link
	check-errno ENOENT
}

@test "root hardlink [non-existent parent component]" {
	ROOT="$(setup_tmpdir)"

	mkdir -p "$ROOT/foo/bar/baz"
	mkdir -p "$ROOT/etc"
	echo "passwd" >"$ROOT/etc/passwd"

	pathrs-cmd root --root "$ROOT" hardlink /etc/passwd foo/nope/baz
	check-errno ENOENT

	! [ -e "$ROOT/foo/nope" ]
	! [ -e "$ROOT/foo/nope/baz" ]
}

@test "root hardlink [bad parent component]" {
	ROOT="$(setup_tmpdir)"

	mkdir -p "$ROOT/foo"
	touch "$ROOT/foo/bar"
	mkdir -p "$ROOT/etc"
	echo "passwd" >"$ROOT/etc/passwd"

	pathrs-cmd root --root "$ROOT" hardlink /etc/passwd foo/bar/baz
	check-errno ENOTDIR

	[ -f "$ROOT/foo/bar" ]
	! [ -e "$ROOT/foo/bar/baz" ]
}