1#!/usr/bin/env python3
2# SPDX-License-Identifier: LGPL-2.1-or-later
3#
4# systemd is free software; you can redistribute it and/or modify it
5# under the terms of the GNU Lesser General Public License as published by
6# the Free Software Foundation; either version 2.1 of the License, or
7# (at your option) any later version.
8
9import os
10import sys
11import socket
12import subprocess
13import tempfile
14import pwd
15import grp
16
17try:
18    from systemd import id128
19except ImportError:
20    id128 = None
21
22EX_DATAERR = 65 # from sysexits.h
23EXIT_TEST_SKIP = 77
24
25try:
26    subprocess.run
27except AttributeError:
28    sys.exit(EXIT_TEST_SKIP)
29
30exe_with_args = sys.argv[1:]
31
32def test_line(line, *, user, returncode=EX_DATAERR, extra={}):
33    args = ['--user'] if user else []
34    print('Running {} on {!r}'.format(' '.join(exe_with_args + args), line))
35    c = subprocess.run(exe_with_args + ['--create', '-'] + args,
36                       input=line, stdout=subprocess.PIPE, universal_newlines=True,
37                       **extra)
38    assert c.returncode == returncode, c
39
40def test_invalids(*, user):
41    test_line('asdfa', user=user)
42    test_line('f "open quote', user=user)
43    test_line('f closed quote""', user=user)
44    test_line('Y /unknown/letter', user=user)
45    test_line('w non/absolute/path', user=user)
46    test_line('s', user=user) # s is for short
47    test_line('f!! /too/many/bangs', user=user)
48    test_line('f++ /too/many/plusses', user=user)
49    test_line('f+!+ /too/many/plusses', user=user)
50    test_line('f!+! /too/many/bangs', user=user)
51    test_line('f== /too/many/equals', user=user)
52    test_line('w /unresolved/argument - - - - "%Y"', user=user)
53    test_line('w /unresolved/argument/sandwich - - - - "%v%Y%v"', user=user)
54    test_line('w /unresolved/filename/%Y - - - - "whatever"', user=user)
55    test_line('w /unresolved/filename/sandwich/%v%Y%v - - - - "whatever"', user=user)
56    test_line('w - - - - - "no file specified"', user=user)
57    test_line('C - - - - - "no file specified"', user=user)
58    test_line('C non/absolute/path - - - - -', user=user)
59    test_line('b - - - - - -', user=user)
60    test_line('b 1234 - - - - -', user=user)
61    test_line('c - - - - - -', user=user)
62    test_line('c 1234 - - - - -', user=user)
63    test_line('t - - -', user=user)
64    test_line('T - - -', user=user)
65    test_line('a - - -', user=user)
66    test_line('A - - -', user=user)
67    test_line('h - - -', user=user)
68    test_line('H - - -', user=user)
69
70def test_uninitialized_t():
71    if os.getuid() == 0:
72        return
73
74    test_line('w /foo - - - - "specifier for --user %t"',
75              user=True, returncode=0, extra={'env':{}})
76
77def test_content(line, expected, *, user, extra={}, subpath='/arg', path_cb=None):
78    d = tempfile.TemporaryDirectory(prefix='test-systemd-tmpfiles.')
79    if path_cb is not None:
80        path_cb(d.name, subpath)
81    arg = d.name + subpath
82    spec = line.format(arg)
83    test_line(spec, user=user, returncode=0, extra=extra)
84    content = open(arg).read()
85    print('expect: {!r}\nactual: {!r}'.format(expected, content))
86    assert content == expected
87
88def test_valid_specifiers(*, user):
89    test_content('f {} - - - - two words', 'two words', user=user)
90    if id128:
91        try:
92            test_content('f {} - - - - %m', '{}'.format(id128.get_machine().hex), user=user)
93        except AssertionError as e:
94            print(e)
95            print('/etc/machine-id: {!r}'.format(open('/etc/machine-id').read()))
96            print('/proc/cmdline: {!r}'.format(open('/proc/cmdline').read()))
97            print('skipping')
98        test_content('f {} - - - - %b', '{}'.format(id128.get_boot().hex), user=user)
99    test_content('f {} - - - - %H', '{}'.format(socket.gethostname()), user=user)
100    test_content('f {} - - - - %v', '{}'.format(os.uname().release), user=user)
101    test_content('f {} - - - - %U', '{}'.format(os.getuid() if user else 0), user=user)
102    test_content('f {} - - - - %G', '{}'.format(os.getgid() if user else 0), user=user)
103
104    puser = pwd.getpwuid(os.getuid() if user else 0)
105    test_content('f {} - - - - %u', '{}'.format(puser.pw_name), user=user)
106
107    pgroup = grp.getgrgid(os.getgid() if user else 0)
108    test_content('f {} - - - - %g', '{}'.format(pgroup.gr_name), user=user)
109
110    # Note that %h is the only specifier in which we look the environment,
111    # because we check $HOME. Should we even be doing that?
112    home = os.path.expanduser("~")
113    test_content('f {} - - - - %h', '{}'.format(home), user=user)
114
115    xdg_runtime_dir = os.getenv('XDG_RUNTIME_DIR')
116    if xdg_runtime_dir is not None or not user:
117        test_content('f {} - - - - %t',
118                     xdg_runtime_dir if user else '/run',
119                     user=user)
120
121    xdg_config_home = os.getenv('XDG_CONFIG_HOME')
122    if xdg_config_home is not None or not user:
123        test_content('f {} - - - - %S',
124                     xdg_config_home if user else '/var/lib',
125                     user=user)
126
127    xdg_cache_home = os.getenv('XDG_CACHE_HOME')
128    if xdg_cache_home is not None or not user:
129        test_content('f {} - - - - %C',
130                     xdg_cache_home if user else '/var/cache',
131                     user=user)
132
133    if xdg_config_home is not None or not user:
134        test_content('f {} - - - - %L',
135                     xdg_config_home + '/log' if user else '/var/log',
136                     user=user)
137
138    test_content('f {} - - - - %%', '%', user=user)
139
140def mkfifo(parent, subpath):
141    os.makedirs(parent, mode=0o755, exist_ok=True)
142    first_component = subpath.split('/')[1]
143    path = parent + '/' + first_component
144    print('path: {}'.format(path))
145    os.mkfifo(path)
146
147def mkdir(parent, subpath):
148    first_component = subpath.split('/')[1]
149    path = parent + '/' + first_component
150    os.makedirs(path, mode=0o755, exist_ok=True)
151    os.symlink(path, path + '/self', target_is_directory=True)
152
153def symlink(parent, subpath):
154    link_path = parent + '/link-target'
155    os.makedirs(parent, mode=0o755, exist_ok=True)
156    with open(link_path, 'wb') as f:
157        f.write(b'target')
158    first_component = subpath.split('/')[1]
159    path = parent + '/' + first_component
160    os.symlink(link_path, path, target_is_directory=True)
161
162def file(parent, subpath):
163    content = 'file-' + subpath.split('/')[1]
164    path = parent + subpath
165    os.makedirs(os.path.dirname(path), mode=0o755, exist_ok=True)
166    with open(path, 'wb') as f:
167        f.write(content.encode())
168
169def valid_symlink(parent, subpath):
170    target = 'link-target'
171    link_path = parent + target
172    os.makedirs(link_path, mode=0o755, exist_ok=True)
173    first_component = subpath.split('/')[1]
174    path = parent + '/' + first_component
175    os.symlink(target, path, target_is_directory=True)
176
177def test_hard_cleanup(*, user):
178    type_cbs = [None, file, mkdir, symlink]
179    if 'mkfifo' in dir(os):
180        type_cbs.append(mkfifo)
181
182    for type_cb in type_cbs:
183        for subpath in ['/shallow', '/deep/1/2']:
184            label = '{}-{}'.format('None' if type_cb is None else type_cb.__name__, subpath.split('/')[1])
185            test_content('f= {} - - - - ' + label, label, user=user, subpath=subpath, path_cb=type_cb)
186
187    # Test the case that a valid symlink is in the path.
188    label = 'valid_symlink-deep'
189    test_content('f= {} - - - - ' + label, label, user=user, subpath='/deep/1/2', path_cb=valid_symlink)
190
191if __name__ == '__main__':
192    test_invalids(user=False)
193    test_invalids(user=True)
194    test_uninitialized_t()
195
196    test_valid_specifiers(user=False)
197    test_valid_specifiers(user=True)
198
199    test_hard_cleanup(user=False)
200    test_hard_cleanup(user=True)
201