Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
Aleksei Bavshin
2fb8e284af
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.
2023-03-30 20:53:59 -07:00
4 changed files with 102 additions and 0 deletions

61
scripts/sway/sway-ipc-exec Executable file
View file

@ -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: <magic> <len> <type> <payload> 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)

View file

@ -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.

15
tests/run-test-exec Executable file
View file

@ -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)"

24
tests/test helper Executable file
View file

@ -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)