#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# Basic tests for types creating/writing files
set -eux
set -o pipefail

rm -fr /tmp/{f,F,w}
mkdir  /tmp/{f,F,w}
touch /tmp/file-owned-by-root

#
# 'f'
#
systemd-tmpfiles --dry-run --create - <<EOF
f     /tmp/f/1    0644 - - - -
f     /tmp/f/2    0644 - - - This string should be written
EOF

test ! -e /tmp/f/1
test ! -e /tmp/f/2

systemd-tmpfiles --create - <<EOF
f     /tmp/f/1    0644 - - - -
f     /tmp/f/2    0644 - - - This string should be written
EOF

### '1' should exist and be empty
test -f /tmp/f/1; test ! -s /tmp/f/1
test "$(stat -c %U:%G:%a /tmp/f/1)" = "root:root:644"

test "$(stat -c %U:%G:%a /tmp/f/2)" = "root:root:644"
test "$(< /tmp/f/2)" = "This string should be written"

### The perms are supposed to be updated even if the file already exists.
systemd-tmpfiles --create - <<EOF
f     /tmp/f/1    0666 daemon daemon - This string should not be written
EOF

# file should be empty
test ! -s /tmp/f/1
test "$(stat -c %U:%G:%a /tmp/f/1)" = "daemon:daemon:666"

### But we shouldn't try to set perms on an existing file which is not a
### regular one.
mkfifo /tmp/f/fifo
chmod 644 /tmp/f/fifo

(! systemd-tmpfiles --create -) <<EOF
f     /tmp/f/fifo    0666 daemon daemon - This string should not be written
EOF

test -p /tmp/f/fifo
test "$(stat -c %U:%G:%a /tmp/f/fifo)" = "root:root:644"

### 'f' should not follow symlinks.
ln -s missing /tmp/f/dangling
ln -s /tmp/file-owned-by-root /tmp/f/symlink

(! systemd-tmpfiles --create -) <<EOF
f     /tmp/f/dangling    0644 daemon daemon - -
f     /tmp/f/symlink     0644 daemon daemon - -
EOF
test ! -e /tmp/f/missing
test "$(stat -c %U:%G:%a /tmp/file-owned-by-root)" = "root:root:644"

### Handle read-only filesystem gracefully: we shouldn't fail if the target
### already exists and have the correct perms.
mkdir /tmp/f/rw-fs
mkdir /tmp/f/ro-fs

touch /tmp/f/rw-fs/foo
chmod 644 /tmp/f/rw-fs/foo

mount -o bind,ro /tmp/f/rw-fs /tmp/f/ro-fs

systemd-tmpfiles --create - <<EOF
f     /tmp/f/ro-fs/foo    0644 - - - - This string should not be written
EOF
test -f /tmp/f/ro-fs/foo; test ! -s /tmp/f/ro-fs/foo

(! systemd-tmpfiles --create -) <<EOF
f     /tmp/f/ro-fs/foo    0666 - - - -
EOF
test "$(stat -c %U:%G:%a /tmp/f/fifo)" = "root:root:644"

(! systemd-tmpfiles --create -) <<EOF
f     /tmp/f/ro-fs/bar    0644 - - - -
EOF
test ! -e /tmp/f/ro-fs/bar

### 'f' shouldn't follow unsafe paths.
mkdir /tmp/f/daemon
ln -s /root /tmp/f/daemon/unsafe-symlink
chown -R --no-dereference daemon:daemon /tmp/f/daemon

(! systemd-tmpfiles --create -) <<EOF
f     /tmp/f/daemon/unsafe-symlink/exploit    0644 daemon daemon - -
EOF
test ! -e /tmp/f/daemon/unsafe-symlink/exploit

#
# 'F'
#
echo "This should be truncated" >/tmp/F/truncated
echo "This should be truncated" >/tmp/F/truncated-with-content

systemd-tmpfiles --create - <<EOF
F     /tmp/F/created                0644 - - - -
F     /tmp/F/created-with-content   0644 - - - new content
F     /tmp/F/truncated              0666 daemon daemon - -
F     /tmp/F/truncated-with-content 0666 daemon daemon - new content
EOF

test -f /tmp/F/created; test ! -s /tmp/F/created
test -f /tmp/F/created-with-content
test "$(< /tmp/F/created-with-content)" = "new content"
test -f /tmp/F/truncated; test ! -s /tmp/F/truncated
test "$(stat -c %U:%G:%a /tmp/F/truncated)" = "daemon:daemon:666"
test -s /tmp/F/truncated-with-content
test "$(stat -c %U:%G:%a /tmp/F/truncated-with-content)" = "daemon:daemon:666"

### We shouldn't try to truncate anything but regular files since the behavior is
### unspecified in the other cases.
mkfifo /tmp/F/fifo

(! systemd-tmpfiles --create -) <<EOF
F     /tmp/F/fifo                0644 - - - -
EOF

test -p /tmp/F/fifo

### 'F' should not follow symlinks.
ln -s missing /tmp/F/dangling
ln -s /tmp/file-owned-by-root /tmp/F/symlink

(! systemd-tmpfiles --create -) <<EOF
f     /tmp/F/dangling    0644 daemon daemon - -
f     /tmp/F/symlink     0644 daemon daemon - -
EOF
test ! -e /tmp/F/missing
test "$(stat -c %U:%G:%a /tmp/file-owned-by-root)" = "root:root:644"

### Handle read-only filesystem gracefully: we shouldn't fail if the target
### already exists and is empty.
mkdir /tmp/F/rw-fs
mkdir /tmp/F/ro-fs

touch /tmp/F/rw-fs/foo
chmod 644 /tmp/F/rw-fs/foo

mount -o bind,ro /tmp/F/rw-fs /tmp/F/ro-fs

systemd-tmpfiles --create - <<EOF
F     /tmp/F/ro-fs/foo    0644 - - - -
EOF
test -f /tmp/F/ro-fs/foo; test ! -s /tmp/F/ro-fs/foo

echo "truncating is not allowed anymore" >/tmp/F/rw-fs/foo
(! systemd-tmpfiles --create -) <<EOF
F     /tmp/F/ro-fs/foo    0644 - - - -
EOF

(! systemd-tmpfiles --create -) <<EOF
F     /tmp/F/ro-fs/foo    0644 - - - - This string should not be written
EOF
test -f /tmp/F/ro-fs/foo
grep -q 'truncating is not allowed' /tmp/F/ro-fs/foo

# Trying to change the perms should fail.
: >/tmp/F/rw-fs/foo
(! systemd-tmpfiles --create -) <<EOF
F     /tmp/F/ro-fs/foo    0666 - - - -
EOF
test "$(stat -c %U:%G:%a /tmp/F/ro-fs/foo)" = "root:root:644"

### Try to create a new file.
(! systemd-tmpfiles --create -) <<EOF
F     /tmp/F/ro-fs/bar    0644 - - - -
EOF
test ! -e /tmp/F/ro-fs/bar

### 'F' shouldn't follow unsafe paths.
mkdir /tmp/F/daemon
ln -s /root /tmp/F/daemon/unsafe-symlink
chown -R --no-dereference daemon:daemon /tmp/F/daemon

(! systemd-tmpfiles --create -) <<EOF
F     /tmp/F/daemon/unsafe-symlink/exploit    0644 daemon daemon - -
EOF
test ! -e /tmp/F/daemon/unsafe-symlink/exploit

#
# 'w'
#
touch /tmp/w/overwritten
touch /tmp/w/appended

### nop if the target does not exist.
systemd-tmpfiles --dry-run --create - <<EOF
w     /tmp/w/unexistent    0644 - - - new content
EOF
test ! -e /tmp/w/unexistent

systemd-tmpfiles --create - <<EOF
w     /tmp/w/unexistent    0644 - - - new content
EOF
test ! -e /tmp/w/unexistent

### no argument given -> fails.
(! systemd-tmpfiles --create -) <<EOF
w     /tmp/w/unexistent    0644 - - - -
EOF

### write into an empty file.
systemd-tmpfiles --dry-run --create - <<EOF
w     /tmp/w/overwritten    0644 - - - old content
EOF
test -f /tmp/w/overwritten
test -z "$(< /tmp/w/overwritten)"

systemd-tmpfiles --create - <<EOF
w     /tmp/w/overwritten    0644 - - - old content
EOF
test -f /tmp/w/overwritten
test "$(< /tmp/w/overwritten)" = "old content"

### old content is overwritten
systemd-tmpfiles --dry-run --create - <<EOF
w     /tmp/w/overwritten    0644 - - - new content
EOF
test -f /tmp/w/overwritten
test "$(< /tmp/w/overwritten)" = "old content"

systemd-tmpfiles --create - <<EOF
w     /tmp/w/overwritten    0644 - - - new content
EOF
test -f /tmp/w/overwritten
test "$(< /tmp/w/overwritten)" = "new content"

### append lines
systemd-tmpfiles --create - <<EOF
w+    /tmp/w/appended    0644 - - - 1
w+    /tmp/w/appended    0644 - - - 2\n
w+    /tmp/w/appended    0644 - - - 3
EOF
test -f /tmp/w/appended
test "$(< /tmp/w/appended)" = "$(echo -ne '12\n3')"

### writing into an 'exotic' file should be allowed.
systemd-tmpfiles --dry-run --create - <<EOF
w     /dev/null    - - - - new content
EOF

systemd-tmpfiles --create - <<EOF
w     /dev/null    - - - - new content
EOF

### 'w' follows symlinks
ln -s ./overwritten /tmp/w/symlink
systemd-tmpfiles --create - <<EOF
w     /tmp/w/symlink    - - - - $(readlink -e /tmp/w/symlink)
EOF
readlink -e /tmp/w/symlink
test "$(< /tmp/w/overwritten)" = "/tmp/w/overwritten"

### 'w' shouldn't follow unsafe paths.
mkdir /tmp/w/daemon
ln -s /root /tmp/w/daemon/unsafe-symlink
chown -R --no-dereference daemon:daemon /tmp/w/daemon

(! systemd-tmpfiles --create -) <<EOF
f     /tmp/w/daemon/unsafe-symlink/exploit    0644 daemon daemon - -
EOF
test ! -e /tmp/w/daemon/unsafe-symlink/exploit
