Read busctl as JSON so device names keep their characters
A phone named "Gib's iPhone" with a typographic apostrophe was shown as "Gib\342\200\231s iPhone". busctl's default text output escapes every non-ASCII byte in octal, and escapes it into the output rather than into a quoted string a shell-style parser can undo, so shlex handed back the escape sequences as literal characters and they went straight to the page. Apostrophes were only the visible case: accents, emoji, quotes and backslashes were all affected, and a name containing a quote could have split a field. Property and method reads now use --json=short, which returns real UTF-8, and the parsers read a document rather than splitting words. That removes the class rather than unescaping octal by hand. The fixtures were the reason this stayed invisible: every test fed the text form and passed against output the helper is no longer asking for. They now carry what busctl actually emits in the mode used, plus a case for a non-ASCII name and one asserting the old text form is refused rather than parsed wrongly. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
This commit is contained in:
@@ -7,7 +7,6 @@ from __future__ import annotations
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
@@ -88,33 +87,44 @@ def normalize_device_line(
|
||||
}
|
||||
|
||||
|
||||
# busctl properties are read with --json=short, not in its default text form.
|
||||
#
|
||||
# The text form escapes every non-ASCII byte in octal, and it escapes them into
|
||||
# the OUTPUT rather than into a quoted string a shell-style parser can undo -- so
|
||||
# a phone named "Gib's iPhone" with a typographic apostrophe arrived as the
|
||||
# literal characters "Gib\342\200\231s iPhone" and was displayed that way.
|
||||
# That is not specific to apostrophes: any name with an accent, an emoji, a
|
||||
# quote, or a backslash was affected the same way.
|
||||
#
|
||||
# The JSON form returns real UTF-8 and needs no unescaping, which is why these
|
||||
# parse a document rather than splitting words.
|
||||
def parse_property(output: str, expected: str) -> object | None:
|
||||
"""The value of a busctl --json=short property read, or None if it is not
|
||||
the type asked for."""
|
||||
try:
|
||||
payload = json.loads(output)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return None
|
||||
if not isinstance(payload, dict) or payload.get("type") != expected:
|
||||
return None
|
||||
return payload.get("data")
|
||||
|
||||
|
||||
def parse_loaded_plugins(output: str) -> list[str]:
|
||||
try:
|
||||
parts = shlex.split(output)
|
||||
except ValueError:
|
||||
data = parse_property(output, "as")
|
||||
if not isinstance(data, list):
|
||||
return []
|
||||
if len(parts) < 2 or parts[0] != "as":
|
||||
return []
|
||||
try:
|
||||
count = int(parts[1])
|
||||
except ValueError:
|
||||
return []
|
||||
return parts[2 : 2 + max(0, count)]
|
||||
return [str(item) for item in data]
|
||||
|
||||
|
||||
def parse_string_property(output: str) -> str:
|
||||
try:
|
||||
parts = shlex.split(output)
|
||||
except ValueError:
|
||||
return ""
|
||||
return parts[1] if len(parts) == 2 and parts[0] == "s" else ""
|
||||
data = parse_property(output, "s")
|
||||
return data if isinstance(data, str) else ""
|
||||
|
||||
|
||||
def parse_bool_property(output: str) -> bool | None:
|
||||
parts = output.split()
|
||||
if len(parts) != 2 or parts[0] != "b" or parts[1] not in {"true", "false"}:
|
||||
return None
|
||||
return parts[1] == "true"
|
||||
data = parse_property(output, "b")
|
||||
return data if isinstance(data, bool) else None
|
||||
|
||||
|
||||
def run_command(
|
||||
@@ -141,6 +151,7 @@ def loaded_plugins(device_id: str, runner: Runner = subprocess.run) -> list[str]
|
||||
[
|
||||
"busctl",
|
||||
"--user",
|
||||
"--json=short",
|
||||
"call",
|
||||
"org.kde.kdeconnect",
|
||||
device_object(device_id),
|
||||
@@ -157,6 +168,7 @@ def supported_plugins(device_id: str, runner: Runner = subprocess.run) -> list[s
|
||||
[
|
||||
"busctl",
|
||||
"--user",
|
||||
"--json=short",
|
||||
"get-property",
|
||||
"org.kde.kdeconnect",
|
||||
device_object(device_id),
|
||||
@@ -178,6 +190,7 @@ def reported_type(device_id: str, runner: Runner = subprocess.run) -> str:
|
||||
[
|
||||
"busctl",
|
||||
"--user",
|
||||
"--json=short",
|
||||
"get-property",
|
||||
"org.kde.kdeconnect",
|
||||
device_object(device_id),
|
||||
@@ -198,6 +211,7 @@ def device_property(
|
||||
[
|
||||
"busctl",
|
||||
"--user",
|
||||
"--json=short",
|
||||
"get-property",
|
||||
"org.kde.kdeconnect",
|
||||
device_object(device_id),
|
||||
|
||||
Reference in New Issue
Block a user