aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Stapelberg <stapelberg@users.noreply.github.com>2022-09-28 18:29:26 +0200
committerMichael Stapelberg <michael@stapelberg.de>2022-10-24 21:12:27 +0200
commitdecc37eba12baf3e8f92a4e56bd04e35a8544a96 (patch)
treeaa4af6396e31f56194ef105487ccb7c7b5997ed3
parent3f58d51ec6c672013d87acbd29231ef8971aa68d (diff)
downloadi3-decc37eba12baf3e8f92a4e56bd04e35a8544a96.tar.gz
i3-decc37eba12baf3e8f92a4e56bd04e35a8544a96.zip
Fix i3-dmenu-desktop quoting (#5162)
Commit 70f23caa9a18afc146f696fdf7d2481e5f7f0101 introduced new issues. Instead of distinguishing " and \, as that commit attempted, let’s instead keep the level of escaping by escaping each backslash, just like each double quote. I tested this with: # recommended way to quote $ and " in quoted arguments, not ambiguous Exec=/tmp/logargs "hello \\$PWD \\"and\\" more" # permitted way to quote $ and " in quoted arguments, but ambiguous Exec=/tmp/logargs "hello \$PWD \"and\" more" # permitted way to quote arguments, slightly unusual to quote first arg Exec="/tmp/logargs" hey # a complicated shell expression, not ambiguous Exec=sh -c "if [ -n \\"\\$*\\" ]; then exec /tmp/logargs --alternate-editor= --display=\\"\\$DISPLAY\\" \\"\\$@\\"; else exec /tmp/logargs --alternate-editor= --create-frame; fi" placeholder %F related to https://github.com/i3/i3/issues/4697 (electrum, original) related to https://github.com/i3/i3/issues/5152 (phpstorm, breakage) related to https://github.com/i3/i3/issues/5156 (emacsclient, breakage)
-rwxr-xr-xi3-dmenu-desktop20
-rw-r--r--testcases/t/318-i3-dmenu-desktop.t125
2 files changed, 141 insertions, 4 deletions
diff --git a/i3-dmenu-desktop b/i3-dmenu-desktop
index e43d95aa..bb693350 100755
--- a/i3-dmenu-desktop
+++ b/i3-dmenu-desktop
@@ -413,7 +413,7 @@ my $exec = $app->{Exec};
my $location = $app->{_Location};
# Quote as described by “The Exec key”:
-# https://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html
+# https://standards.freedesktop.org/desktop-entry-spec/latest/ar01s07.html
sub quote {
my ($str) = @_;
$str =~ s/("|`|\$|\\)/\\$1/g;
@@ -425,6 +425,17 @@ $choice = quote($choice);
$location = quote($location);
$name = quote($name);
+# https://standards.freedesktop.org/desktop-entry-spec/latest/ar01s07.html:
+#
+# Note that the general escape rule for values of type string states that the
+# backslash character can be escaped as ("\\") as well and that this escape rule
+# is applied before the quoting rule. As such, to unambiguously represent a
+# literal backslash character in a quoted argument in a desktop entry file
+# requires the use of four successive backslash characters ("\\\\"). Likewise, a
+# literal dollar sign in a quoted argument in a desktop entry file is
+# unambiguously represented with ("\\$").
+$exec =~ s/\\\\/\\/g;
+
# Remove deprecated field codes, as the spec dictates.
$exec =~ s/%[dDnNvm]//g;
@@ -481,9 +492,10 @@ EOT
# starts with a double quote ("), everything is parsed as-is until the next
# double quote which is NOT preceded by a backslash (\).
#
- # Therefore, we escape all double quotes (") by replacing them with \"
- $exec =~ s/\\"/\\\\\\"/g;
- $exec =~ s/([^\\])"/$1\\"/g;
+ # Therefore, we escape all double quotes (") by replacing them with \".
+ # To not change the meaning of any double quote, backslashes need to be
+ # escaped as well.
+ $exec =~ s/(["\\])/\\$1/g;
if (exists($app->{StartupNotify}) && !$app->{StartupNotify}) {
$nosn = '--no-startup-id';
diff --git a/testcases/t/318-i3-dmenu-desktop.t b/testcases/t/318-i3-dmenu-desktop.t
new file mode 100644
index 00000000..75d983ef
--- /dev/null
+++ b/testcases/t/318-i3-dmenu-desktop.t
@@ -0,0 +1,125 @@
+#!perl
+# vim:ts=4:sw=4:expandtab
+#
+# Please read the following documents before working on tests:
+# • https://build.i3wm.org/docs/testsuite.html
+# (or docs/testsuite)
+#
+# • https://build.i3wm.org/docs/lib-i3test.html
+# (alternatively: perldoc ./testcases/lib/i3test.pm)
+#
+# • https://build.i3wm.org/docs/ipc.html
+# (or docs/ipc)
+#
+# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
+# (unless you are already familiar with Perl)
+#
+# Verifies that i3-dmenu-desktop correctly parses Exec= lines in .desktop files
+# and sends the command to i3 for execution.
+# Ticket: #5152, #5156
+# Bug still in: 4.21-17-g389d555d
+use i3test;
+use i3test::Util qw(slurp);
+use File::Temp qw(tempfile tempdir);
+use POSIX qw(mkfifo);
+use JSON::XS qw(decode_json);
+
+my $desktopdir = tempdir(CLEANUP => 1);
+
+$ENV{XDG_DATA_DIRS} = "$desktopdir";
+
+mkdir("$desktopdir/applications");
+
+# Create an i3-msg executable that dumps command line flags to a FIFO
+my $tmpdir = tempdir(CLEANUP => 1);
+
+$ENV{PATH} = "$tmpdir:" . $ENV{PATH};
+
+mkfifo("$tmpdir/fifo", 0600) or BAIL_OUT "Could not create FIFO: $!";
+
+open(my $i3msg_dump, '>', "$tmpdir/i3-msg");
+say $i3msg_dump <<EOT;
+#!/usr/bin/env perl
+use strict;
+use warnings;
+use JSON::XS qw(encode_json);
+open(my \$f, '>', "$tmpdir/fifo");
+say \$f encode_json(\\\@ARGV);
+close(\$f);
+EOT
+close($i3msg_dump);
+chmod 0755, "$tmpdir/i3-msg";
+
+my $testcnt = 0;
+sub verify_exec {
+ my ($execline, $want_arg) = @_;
+
+ $testcnt++;
+
+ open(my $desktop, '>', "$desktopdir/applications/desktop$testcnt.desktop");
+ say $desktop <<EOT;
+[Desktop Entry]
+Name=i3-testsuite-$testcnt
+Type=Application
+Exec=$execline
+EOT
+ close($desktop);
+
+ # complete-run.pl arranges for $PATH to be set up such that the
+ # i3-dmenu-desktop version we execute is the one from the build directory.
+ my $exit = system("i3-dmenu-desktop --dmenu 'echo i3-testsuite-$testcnt' &");
+ if ($exit != 0) {
+ die "failed to run i3-dmenu-desktop";
+ }
+
+ chomp($want_arg); # trim trailing newline
+ my $got_args = decode_json(slurp("$tmpdir/fifo"));
+ is_deeply($got_args, [ $want_arg ], 'i3-dmenu-desktop executed command as expected');
+}
+
+# recommended number of backslashes by the spec, not ambiguous
+my $exec_1 = <<'EOS';
+echo "hello \\$PWD \\"and\\" more"
+EOS
+my $want_1 = <<'EOS';
+exec "echo \"hello \\$PWD \\\"and\\\" more\""
+EOS
+verify_exec($exec_1, $want_1);
+
+# permitted, but ambiguous
+my $exec_2 = <<'EOS';
+echo "hello \$PWD \"and\" more"
+EOS
+my $want_2 = <<'EOS';
+exec "echo \"hello \\$PWD \\\"and\\\" more\""
+EOS
+verify_exec($exec_2, $want_2);
+
+# electrum
+my $exec_3 = <<'EOS';
+sh -c "PATH=\"\\$HOME/.local/bin:\\$PATH\"; electrum %u"
+EOS
+my $want_3 = <<'EOS';
+exec "sh -c \"PATH=\\\"\\$HOME/.local/bin:\\$PATH\\\"; electrum \""
+EOS
+verify_exec($exec_3, $want_3);
+
+# complicated emacsclient command
+my $exec_4 = <<'EOS';
+sh -c "if [ -n \\"\\$*\\" ]; then exec emacsclient --alternate-editor= --display=\\"\\$DISPLAY\\" \\"\\$@\\"; else exec emacsclient --alternate-editor= --create-frame; fi" placeholder %F
+EOS
+my $want_4 = <<'EOS';
+exec "sh -c \"if [ -n \\\"\\$*\\\" ]; then exec emacsclient --alternate-editor= --display=\\\"\\$DISPLAY\\\" \\\"\\$@\\\"; else exec emacsclient --alternate-editor= --create-frame; fi\" placeholder "
+EOS
+verify_exec($exec_4, $want_4);
+
+# permitted, but unusual to quote the first arg
+my $exec_5 = <<'EOS';
+"electrum" arg
+EOS
+my $want_5 = <<'EOS';
+exec "\"electrum\" arg"
+EOS
+verify_exec($exec_5, $want_5);
+
+done_testing;