feature: allow subpaths in xdg macros

Currently it is not possible to use XDG-related macros (such as
`${DOCUMENTS}`) with subpaths (such as `${DOCUMENTS}/foo`) and so
profiles just use `${HOME}` with a hardcoded path using the English
directory name and the subpath (such as `${HOME}/Documents/foo`).

Allow using subpaths after XDG macros, so that they automatically use
the auto-detected XDG path, just as when currently using the XDG macros
without subpaths.

Before:

    ${HOME}/Documents/foo

After:

    ${DOCUMENTS}/foo

This is a follow-up to #7147.

Closes #2359.

Relates to #4229.
This commit is contained in:
Kelvin M. Klann 2026-04-21 06:45:11 -03:00
parent c2f12016f2
commit aff7cb630c
8 changed files with 268 additions and 24 deletions

View file

@ -542,10 +542,10 @@ uint32_t arp_assign(const char *dev, Bridge *br);
// macros.c
char *expand_macros(const char *path);
char *resolve_macro(const char *name);
char *resolve_macro(const char *path);
void invalid_filename(const char *fname, int globbing);
int is_macro(const char *name);
int macro_id(const char *name);
int is_macro(const char *path);
int macro_id(const char *path);
// util.c

View file

@ -596,8 +596,8 @@ void fs_whitelist(void) {
if (is_macro(expanded) && macro_id(expanded) > -1) {
if (!nowhitelist_flag && (have_topdir(cfg.homedir, topdirs) || add_topdir(cfg.homedir, topdirs, expanded)) && !arg_quiet) {
fprintf(stderr, "***\n");
fprintf(stderr, "*** Warning: cannot whitelist %s directory\n", expanded);
fprintf(stderr, "*** Any file saved in this directory will be lost when the sandbox is closed.\n");
fprintf(stderr, "*** Warning: cannot whitelist %s path\n", expanded);
fprintf(stderr, "*** Any file saved in this path will be lost when the sandbox is closed.\n");
fprintf(stderr, "***\n");
}
entry = entry->next;

View file

@ -71,10 +71,11 @@ Macro macro[] = {
};
// return -1 if not found
int macro_id(const char *name) {
int macro_id(const char *path) {
int i = 0;
while (macro[i].name != NULL) {
if (strcmp(name, macro[i].name) == 0)
size_t len = strlen(macro[i].name);
if (strncmp(path, macro[i].name, len) == 0)
return i;
i++;
}
@ -82,12 +83,12 @@ int macro_id(const char *name) {
return -1;
}
int is_macro(const char *name) {
assert(name);
int len = strlen(name);
int is_macro(const char *path) {
assert(path);
int len = strlen(path);
if (len <= 4)
return 0;
if (*name == '$' && name[1] == '{' && name[len - 1] == '}')
if (*path == '$' && path[1] == '{' && strchr(&path[2], '}'))
return 1;
return 0;
}
@ -175,17 +176,25 @@ static char *resolve_hardcoded(char *entries[]) {
}
// returns mallocated memory
char *resolve_macro(const char *name) {
char *resolve_macro(const char *path) {
char *rv = NULL;
int id = macro_id(name);
int id = macro_id(path);
if (id == -1)
return NULL;
rv = resolve_xdg(macro[id].xdg);
if (rv == NULL)
rv = resolve_hardcoded(macro[id].translation);
char *directory = resolve_xdg(macro[id].xdg);
if (!directory)
directory = resolve_hardcoded(macro[id].translation);
if (!directory)
return NULL;
size_t len = strlen(macro[id].name);
if (asprintf(&rv, "%s/%s%s", cfg.homedir, directory, path + len) == -1)
errExit("asprintf");
free(directory);
if (rv && arg_debug)
printf("Directory %s resolved as %s\n", name, rv);
printf("Path %s resolved as %s\n", path, rv);
return rv;
}
@ -235,14 +244,10 @@ char *expand_macros(const char *path) {
goto out;
}
else {
char *directory = resolve_macro(path);
if (directory) {
if (asprintf(&rv, "%s/%s", cfg.homedir, directory) == -1)
errExit("asprintf");
free(directory);
rv = resolve_macro(path);
if (rv)
goto out;
}
}
assert(rv == NULL);
rv = strdup(path);
@ -269,7 +274,7 @@ void invalid_filename(const char *fname, int globbing) {
else {
int id = macro_id(fname);
if (id != -1)
return;
ptr = fname + strlen(macro[id].name);
}
reject_meta_chars(ptr, globbing);

View file

@ -105,6 +105,27 @@ rm -f ~/Music/_firejail_test_file
rm -f ~/Pictures/_firejail_test_file
rm -f ~/Videos/_firejail_test_file
mkdir -p ~/Desktop/_firejail_test_dir/a
mkdir -p ~/Desktop/_firejail_test_dir/b
mkdir -p ~/Documents/_firejail_test_dir/a
mkdir -p ~/Documents/_firejail_test_dir/b
mkdir -p ~/Downloads/_firejail_test_dir/a
mkdir -p ~/Downloads/_firejail_test_dir/b
mkdir -p ~/Music/_firejail_test_dir/a
mkdir -p ~/Music/_firejail_test_dir/b
mkdir -p ~/Pictures/_firejail_test_dir/a
mkdir -p ~/Pictures/_firejail_test_dir/b
mkdir -p ~/Videos/_firejail_test_dir/a
mkdir -p ~/Videos/_firejail_test_dir/b
echo "TESTING: macro subpaths (test/fs/macro-subpath.exp)"
./macro-subpath.exp
rm -fr ~/Desktop/_firejail_test_dir
rm -fr ~/Documents/_firejail_test_dir
rm -fr ~/Downloads/_firejail_test_dir
rm -fr ~/Music/_firejail_test_dir
rm -fr ~/Pictures/_firejail_test_dir
rm -fr ~/Videos/_firejail_test_dir
echo "TESTING: whitelist empty (test/fs/whitelist-empty.exp)"
./whitelist-empty.exp

View file

@ -0,0 +1,6 @@
blacklist ${DESKTOP}/_firejail_test_dir
blacklist ${DOCUMENTS}/_firejail_test_dir
blacklist ${DOWNLOADS}/_firejail_test_dir
blacklist ${MUSIC}/_firejail_test_dir
blacklist ${PICTURES}/_firejail_test_dir
blacklist ${VIDEOS}/_firejail_test_dir

View file

@ -0,0 +1,6 @@
read-only ${DESKTOP}/_firejail_test_dir
read-only ${DOCUMENTS}/_firejail_test_dir
read-only ${DOWNLOADS}/_firejail_test_dir
read-only ${MUSIC}/_firejail_test_dir
read-only ${PICTURES}/_firejail_test_dir
read-only ${VIDEOS}/_firejail_test_dir

View file

@ -0,0 +1,6 @@
whitelist ${DESKTOP}/_firejail_test_dir/a
whitelist ${DOCUMENTS}/_firejail_test_dir/a
whitelist ${DOWNLOADS}/_firejail_test_dir/a
whitelist ${MUSIC}/_firejail_test_dir/a
whitelist ${PICTURES}/_firejail_test_dir/a
whitelist ${VIDEOS}/_firejail_test_dir/a

200
test/fs/macro-subpath.exp Normal file
View file

@ -0,0 +1,200 @@
#!/usr/bin/expect -f
# This file is part of Firejail project
# Copyright (C) 2014-2026 Firejail Authors
# License GPL v2
set timeout 3
spawn $env(SHELL)
match_max 100000
# Test that macros work with subpaths (see #2359).
send -- "firejail --profile=./macro-subpath-whitelist.profile ls \
~/Desktop/_firejail_test_dir \
~/Documents/_firejail_test_dir \
~/Downloads/_firejail_test_dir \
~/Music/_firejail_test_dir \
~/Pictures/_firejail_test_dir \
~/Videos/_firejail_test_dir \
\r"
expect {
timeout {puts "TESTING ERROR 0\n";exit}
-re "Child process initialized in \[0-9\]+.\[0-9\]+ ms"
}
expect {
timeout {puts "TESTING ERROR 1\n";exit}
"Desktop/_firejail_test_dir/a" {}
"Desktop/_firejail_test_dir/b" {puts "TESTING ERROR 1.1\n";exit}
}
expect {
timeout {puts "TESTING ERROR 2\n";exit}
"Documents/_firejail_test_dir/a" {}
"Documents/_firejail_test_dir/b" {puts "TESTING ERROR 2.1\n";exit}
}
expect {
timeout {puts "TESTING ERROR 3\n";exit}
"Downloads/_firejail_test_dir/a" {}
"Downloads/_firejail_test_dir/b" {puts "TESTING ERROR 3.1\n";exit}
}
expect {
timeout {puts "TESTING ERROR 4\n";exit}
"Music/_firejail_test_dir/a" {}
"Music/_firejail_test_dir/b" {puts "TESTING ERROR 4.1\n";exit}
}
expect {
timeout {puts "TESTING ERROR 5\n";exit}
"Pictures/_firejail_test_dir/a" {}
"Pictures/_firejail_test_dir/b" {puts "TESTING ERROR 5.1\n";exit}
}
expect {
timeout {puts "TESTING ERROR 6\n";exit}
"Videos/_firejail_test_dir/a" {}
"Videos/_firejail_test_dir/b" {puts "TESTING ERROR 6.1\n";exit}
}
after 100
send -- "firejail --profile=./macro-subpath-blacklist.profile ls ~/Desktop/_firejail_test_dir; echo ret \$?\r"
expect {
timeout {puts "TESTING ERROR 7\n";exit}
-re "Child process initialized in \[0-9\]+.\[0-9\]+ ms"
}
expect {
timeout {puts "TESTING ERROR 8\n";exit}
"Permission denied" {}
-re {ret 0} {puts "TESTING ERROR 8.1\n";exit}
}
after 100
send -- "firejail --profile=./macro-subpath-blacklist.profile ls ~/Documents/_firejail_test_dir; echo ret \$?\r"
expect {
timeout {puts "TESTING ERROR 9\n";exit}
-re "Child process initialized in \[0-9\]+.\[0-9\]+ ms"
}
expect {
timeout {puts "TESTING ERROR 10\n";exit}
"Permission denied" {}
-re {ret 0} {puts "TESTING ERROR 10.1\n";exit}
}
after 100
send -- "firejail --profile=./macro-subpath-blacklist.profile ls ~/Downloads/_firejail_test_dir; echo ret \$?\r"
expect {
timeout {puts "TESTING ERROR 11\n";exit}
-re "Child process initialized in \[0-9\]+.\[0-9\]+ ms"
}
expect {
timeout {puts "TESTING ERROR 12\n";exit}
"Permission denied" {}
-re {ret 0} {puts "TESTING ERROR 12.1\n";exit}
}
after 100
send -- "firejail --profile=./macro-subpath-blacklist.profile ls ~/Music/_firejail_test_dir; echo ret \$?\r"
expect {
timeout {puts "TESTING ERROR 13\n";exit}
-re "Child process initialized in \[0-9\]+.\[0-9\]+ ms"
}
expect {
timeout {puts "TESTING ERROR 14\n";exit}
"Permission denied" {}
-re {ret 0} {puts "TESTING ERROR 14.1\n";exit}
}
after 100
send -- "firejail --profile=./macro-subpath-blacklist.profile ls ~/Pictures/_firejail_test_dir; echo ret \$?\r"
expect {
timeout {puts "TESTING ERROR 15\n";exit}
-re "Child process initialized in \[0-9\]+.\[0-9\]+ ms"
}
expect {
timeout {puts "TESTING ERROR 16\n";exit}
"Permission denied" {}
-re {ret 0} {puts "TESTING ERROR 16.1\n";exit}
}
after 100
send -- "firejail --profile=./macro-subpath-blacklist.profile ls ~/Videos/_firejail_test_dir; echo ret \$?\r"
expect {
timeout {puts "TESTING ERROR 17\n";exit}
-re "Child process initialized in \[0-9\]+.\[0-9\]+ ms"
}
expect {
timeout {puts "TESTING ERROR 18\n";exit}
"Permission denied" {}
-re {ret 0} {puts "TESTING ERROR 18.1\n";exit}
}
after 100
send -- "firejail --profile=./macro-subpath-readonly.profile touch ~/Desktop/_firejail_test_dir/_firejail_test_file; echo ret \$?\r"
expect {
timeout {puts "TESTING ERROR 19\n";exit}
-re "Child process initialized in \[0-9\]+.\[0-9\]+ ms"
}
expect {
timeout {puts "TESTING ERROR 20\n";exit}
"Read-only file system" {}
-re {ret 0} {puts "TESTING ERROR 20.1\n";exit}
}
after 100
send -- "firejail --profile=./macro-subpath-readonly.profile touch ~/Documents/_firejail_test_dir/_firejail_test_file; echo ret \$?\r"
expect {
timeout {puts "TESTING ERROR 21\n";exit}
-re "Child process initialized in \[0-9\]+.\[0-9\]+ ms"
}
expect {
timeout {puts "TESTING ERROR 22\n";exit}
"Read-only file system" {}
-re {ret 0} {puts "TESTING ERROR 22.1\n";exit}
}
after 100
send -- "firejail --profile=./macro-subpath-readonly.profile touch ~/Downloads/_firejail_test_dir/_firejail_test_file; echo ret \$?\r"
expect {
timeout {puts "TESTING ERROR 23\n";exit}
-re "Child process initialized in \[0-9\]+.\[0-9\]+ ms"
}
expect {
timeout {puts "TESTING ERROR 24\n";exit}
"Read-only file system" {}
-re {ret 0} {puts "TESTING ERROR 24.1\n";exit}
}
after 100
send -- "firejail --profile=./macro-subpath-readonly.profile touch ~/Music/_firejail_test_dir/_firejail_test_file; echo ret \$?\r"
expect {
timeout {puts "TESTING ERROR 25\n";exit}
-re "Child process initialized in \[0-9\]+.\[0-9\]+ ms"
}
expect {
timeout {puts "TESTING ERROR 26\n";exit}
"Read-only file system" {}
-re {ret 0} {puts "TESTING ERROR 26.1\n";exit}
}
after 100
send -- "firejail --profile=./macro-subpath-readonly.profile touch ~/Pictures/_firejail_test_dir/_firejail_test_file; echo ret \$?\r"
expect {
timeout {puts "TESTING ERROR 27\n";exit}
-re "Child process initialized in \[0-9\]+.\[0-9\]+ ms"
}
expect {
timeout {puts "TESTING ERROR 28\n";exit}
"Read-only file system" {}
-re {ret 0} {puts "TESTING ERROR 28.1\n";exit}
}
after 100
send -- "firejail --profile=./macro-subpath-readonly.profile touch ~/Videos/_firejail_test_dir/_firejail_test_file; echo ret \$?\r"
expect {
timeout {puts "TESTING ERROR 29\n";exit}
-re "Child process initialized in \[0-9\]+.\[0-9\]+ ms"
}
expect {
timeout {puts "TESTING ERROR 30\n";exit}
"Read-only file system" {}
-re {ret 0} {puts "TESTING ERROR 30.1\n";exit}
}
after 100
puts "\nall done\n"