From 2fb8e284afdec7dd1f9497ade6921f76cfb81481 Mon Sep 17 00:00:00 2001 From: Aleksei Bavshin Date: Wed, 29 Mar 2023 03:03:32 -0700 Subject: [PATCH] sway: escape commands from launcher before sending to IPC `swaymsg exec` does not preserve quoting or field splitting of the arguments. Additional escaping is necessary to be able to pass fields with special characters, such as spaces or quotes. --- scripts/sway/sway-ipc-exec | 61 ++++++++++++++++++++++++++++++++++++++ sway/config.in | 2 ++ tests/run-test-exec | 15 ++++++++++ tests/test helper | 24 +++++++++++++++ 4 files changed, 102 insertions(+) create mode 100755 scripts/sway/sway-ipc-exec create mode 100755 tests/run-test-exec create mode 100755 tests/test helper diff --git a/scripts/sway/sway-ipc-exec b/scripts/sway/sway-ipc-exec new file mode 100755 index 0000000..b628a18 --- /dev/null +++ b/scripts/sway/sway-ipc-exec @@ -0,0 +1,61 @@ +#!/usr/bin/python3 +""" +Pass the arguments to the `exec` i3 IPC command with proper escaping. +The script could be used as a replacement for `swaymsg exec --`. + +See also: https://github.com/swaywm/sway/issues/5931 +Usage: rofi -run-command "this-script {cmd}" +""" + +import json +import os +import socket +import struct +import sys + + +IPC_COMMAND = 0x0 +MAGIC = "i3-ipc".encode("utf-8") +# IPC message format: in native byte order +IPC_HEADER = f"={len(MAGIC)}sII" +IPC_HEADER_SIZE = struct.calcsize(IPC_HEADER) + +SWAYSOCK = os.environ.get("SWAYSOCK", os.environ.get("I3SOCK", None)) +TABLE = str.maketrans({c: "\\" + c for c in " $'\"\\(),;\t"}) + + +def quote(field: str): + return field.translate(TABLE) + + +def ipc_send(sock: socket.socket, msg: int, payload: str): + data = payload.encode("utf-8") + data = struct.pack(IPC_HEADER, MAGIC, len(data), msg) + data + + sock.sendall(data) + + data = sock.recv(IPC_HEADER_SIZE) + if len(data) != IPC_HEADER_SIZE: + return False + + magic, msg_len, msg_type = struct.unpack(IPC_HEADER, data) + if magic != MAGIC or msg_type != msg: + return False + + data = sock.recv(msg_len) + if len(data) != msg_len: + return False + + result = json.loads(data) + return result[0].get("success", False) + + +if SWAYSOCK is None: + sys.exit(1) + +with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect(SWAYSOCK) + args = map(quote, sys.argv[1:]) + if not ipc_send(sock, IPC_COMMAND, "exec " + " ".join(args)): + sys.exit(1) + sock.shutdown(socket.SHUT_RDWR) diff --git a/sway/config.in b/sway/config.in index 6e57ce5..c320c4f 100644 --- a/sway/config.in +++ b/sway/config.in @@ -21,6 +21,8 @@ set $term foot # on the original workspace that the command was run on. # Recommends: rofi-wayland set $rofi_cmd rofi \ + -run-command '$LIBEXECDIR/sway/sway-ipc-exec {cmd}' \ + -run-shell-command '$LIBEXECDIR/sway/sway-ipc-exec {terminal} -e {cmd}' \ -terminal '$term' # Shows a combined list of the applications with desktop files and # executables from PATH. diff --git a/tests/run-test-exec b/tests/run-test-exec new file mode 100755 index 0000000..bafd694 --- /dev/null +++ b/tests/run-test-exec @@ -0,0 +1,15 @@ +#!/bin/sh +# Entrypoint for exec wrapper tests + +DIRNAME=$(realpath "$0") +DIRNAME=$(dirname "$DIRNAME") +DIRNAME=$(dirname "$DIRNAME") + +for WRAPPER in "" "$DIRNAME/scripts/sway/sway-ipc-exec"; do + $WRAPPER "$DIRNAME/tests/test helper" --lor\'em \ + 'ipsum $dolor sit" amet;' \ + "consectetur adipiscing\' elit," \ + "(sed do eiusmod tempor) [incididunt ut labore]" \ + et\ dolore\ magna\[\ aliqua. +done +echo "Check the second test result in Sway output (journal or stdout)" diff --git a/tests/test helper b/tests/test helper new file mode 100755 index 0000000..04953ee --- /dev/null +++ b/tests/test helper @@ -0,0 +1,24 @@ +#!/usr/bin/python3 + +import sys + +TEST = [ + "--lor'em", + 'ipsum $dolor sit" amet;', + "consectetur\tadipiscing\\' elit,", + "(sed do eiusmod tempor) [incididunt ut labore]", + "et dolore magna[ aliqua.", +] + +status = "passed" + +if len(sys.argv) != len(TEST) + 1: + status = "failed" + +for left, right in zip(sys.argv[1:], TEST): + if left != right: + print(f"'{left}' != '{right}'") + status = "failed" + +print(f"Test status: {status}") +sys.exit(0 if status == "passed" else 1)