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:
@@ -89,8 +89,8 @@ class KdeConnectBridgeTest(unittest.TestCase):
|
||||
|
||||
def test_busctl_plugin_output_is_normalized(self) -> None:
|
||||
output = (
|
||||
'as 5 "kdeconnect_ping" "kdeconnect_share" '
|
||||
'"kdeconnect_clipboard" "kdeconnect_findmyphone" "unrelated"\n'
|
||||
'{"type":"as","data":["kdeconnect_ping","kdeconnect_share",'
|
||||
'"kdeconnect_clipboard","kdeconnect_findmyphone","unrelated"]}\n'
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
@@ -104,16 +104,42 @@ class KdeConnectBridgeTest(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_device_name_keeps_its_typographic_characters(self) -> None:
|
||||
"""A phone named "Gib's iPhone" is displayed that way.
|
||||
|
||||
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 the name arrived as the literal
|
||||
characters "Gib\\342\\200\\231s iPhone" and was shown on the Home &
|
||||
Phone page exactly like that. Apostrophes were only the visible case;
|
||||
accents, emoji, quotes and backslashes were all affected.
|
||||
"""
|
||||
output = '{"type":"s","data":"Gib\u2019s iPhone"}\n'
|
||||
|
||||
self.assertEqual(bridge.parse_string_property(output), "Gib\u2019s iPhone")
|
||||
|
||||
def test_octal_escaped_name_is_not_accepted_as_a_value(self) -> None:
|
||||
"""The old text form must not parse at all, rather than parse wrongly.
|
||||
|
||||
Reading it as a value is what produced the mangled name; refusing it
|
||||
means a future change back to text output fails loudly instead of
|
||||
displaying escape sequences to someone.
|
||||
"""
|
||||
self.assertEqual(bridge.parse_string_property('s "Gib\\342\\200\\231s iPhone"\n'), "")
|
||||
self.assertEqual(bridge.parse_bool_property("b true\n"), None)
|
||||
self.assertEqual(bridge.parse_loaded_plugins('as 1 "kdeconnect_ping"\n'), [])
|
||||
|
||||
def test_offline_device_falls_back_to_supported_plugins(self) -> None:
|
||||
def runner(command: list[str], **_kwargs: object) -> subprocess.CompletedProcess[str]:
|
||||
member = command[-1]
|
||||
if member == "loadedPlugins":
|
||||
return subprocess.CompletedProcess(command, 0, "as 0\n", "")
|
||||
return subprocess.CompletedProcess(command, 0, '{"type":"as","data":[]}\n', "")
|
||||
if member == "supportedPlugins":
|
||||
return subprocess.CompletedProcess(
|
||||
command,
|
||||
0,
|
||||
'as 3 "kdeconnect_share" "kdeconnect_clipboard" "kdeconnect_findmyphone"\n',
|
||||
'{"type":"as","data":["kdeconnect_share",'
|
||||
'"kdeconnect_clipboard","kdeconnect_findmyphone"]}\n',
|
||||
"",
|
||||
)
|
||||
raise AssertionError(command)
|
||||
@@ -139,17 +165,17 @@ class KdeConnectBridgeTest(unittest.TestCase):
|
||||
return subprocess.CompletedProcess(command, 0, "0 devices found\n", "")
|
||||
if command == ["busctl", "--user", "tree", "org.kde.kdeconnect"]:
|
||||
return subprocess.CompletedProcess(command, 0, f"└─ {device_path}\n", "")
|
||||
if command[:3] == ["busctl", "--user", "call"]:
|
||||
return subprocess.CompletedProcess(command, 0, "as 0\n", "")
|
||||
if command[:3] == ["busctl", "--user", "get-property"]:
|
||||
if command[:4] == ["busctl", "--user", "--json=short", "call"]:
|
||||
return subprocess.CompletedProcess(command, 0, '{"type":"as","data":[]}\n', "")
|
||||
if command[:4] == ["busctl", "--user", "--json=short", "get-property"]:
|
||||
values = {
|
||||
"name": 's "Fixture iPhone"\n',
|
||||
"type": 's "phone"\n',
|
||||
"isPaired": "b true\n",
|
||||
"isReachable": "b false\n",
|
||||
"name": '{"type":"s","data":"Fixture iPhone"}\n',
|
||||
"type": '{"type":"s","data":"phone"}\n',
|
||||
"isPaired": '{"type":"b","data":true}\n',
|
||||
"isReachable": '{"type":"b","data":false}\n',
|
||||
"supportedPlugins": (
|
||||
'as 3 "kdeconnect_share" "kdeconnect_clipboard" '
|
||||
'"kdeconnect_findmyphone"\n'
|
||||
'{"type":"as","data":["kdeconnect_share",'
|
||||
'"kdeconnect_clipboard","kdeconnect_findmyphone"]}\n'
|
||||
),
|
||||
}
|
||||
return subprocess.CompletedProcess(command, 0, values[command[-1]], "")
|
||||
@@ -180,14 +206,14 @@ class KdeConnectBridgeTest(unittest.TestCase):
|
||||
if command == ["busctl", "--user", "tree", "org.kde.kdeconnect"]:
|
||||
path = f"/modules/kdeconnect/devices/{device_id}"
|
||||
return subprocess.CompletedProcess(command, 0, f"└─ {path}\n", "")
|
||||
if command[:3] == ["busctl", "--user", "call"]:
|
||||
return subprocess.CompletedProcess(command, 0, "as 0\n", "")
|
||||
if command[:3] == ["busctl", "--user", "get-property"]:
|
||||
if command[:4] == ["busctl", "--user", "--json=short", "call"]:
|
||||
return subprocess.CompletedProcess(command, 0, '{"type":"as","data":[]}\n', "")
|
||||
if command[:4] == ["busctl", "--user", "--json=short", "get-property"]:
|
||||
values = {
|
||||
"name": 's "Fixture iPhone"\n',
|
||||
"type": 's "phone"\n',
|
||||
"isPaired": "b true\n",
|
||||
"isReachable": "b false\n",
|
||||
"name": '{"type":"s","data":"Fixture iPhone"}\n',
|
||||
"type": '{"type":"s","data":"phone"}\n',
|
||||
"isPaired": '{"type":"b","data":true}\n',
|
||||
"isReachable": '{"type":"b","data":false}\n',
|
||||
}
|
||||
if command[-1] == "supportedPlugins":
|
||||
raise subprocess.TimeoutExpired(command, 8)
|
||||
@@ -217,12 +243,12 @@ class KdeConnectBridgeTest(unittest.TestCase):
|
||||
if command == ["busctl", "--user", "tree", "org.kde.kdeconnect"]:
|
||||
path = f"/modules/kdeconnect/devices/{device_id}"
|
||||
return subprocess.CompletedProcess(command, 0, f"└─ {path}\n", "")
|
||||
if command[:3] == ["busctl", "--user", "get-property"]:
|
||||
if command[:4] == ["busctl", "--user", "--json=short", "get-property"]:
|
||||
values = {
|
||||
"name": 's "Nearby Stranger"\n',
|
||||
"type": 's "phone"\n',
|
||||
"isPaired": "b false\n",
|
||||
"isReachable": "b true\n",
|
||||
"name": '{"type":"s","data":"Nearby Stranger"}\n',
|
||||
"type": '{"type":"s","data":"phone"}\n',
|
||||
"isPaired": '{"type":"b","data":false}\n',
|
||||
"isReachable": '{"type":"b","data":true}\n',
|
||||
}
|
||||
return subprocess.CompletedProcess(command, 0, values[command[-1]], "")
|
||||
raise AssertionError(command)
|
||||
@@ -240,16 +266,16 @@ class KdeConnectBridgeTest(unittest.TestCase):
|
||||
if command == ["busctl", "--user", "tree", "org.kde.kdeconnect"]:
|
||||
path = f"/modules/kdeconnect/devices/{dbus_id}"
|
||||
return subprocess.CompletedProcess(command, 0, f"└─ {path}\n", "")
|
||||
if command[:3] == ["busctl", "--user", "call"]:
|
||||
return subprocess.CompletedProcess(command, 0, "as 0\n", "")
|
||||
if command[:3] == ["busctl", "--user", "get-property"]:
|
||||
if command[:4] == ["busctl", "--user", "--json=short", "call"]:
|
||||
return subprocess.CompletedProcess(command, 0, '{"type":"as","data":[]}\n', "")
|
||||
if command[:4] == ["busctl", "--user", "--json=short", "get-property"]:
|
||||
is_dbus_device = dbus_id in command[4]
|
||||
values = {
|
||||
"name": 's "Fixture iPhone"\n',
|
||||
"type": 's "phone"\n' if is_dbus_device else 's "desktop"\n',
|
||||
"isPaired": "b true\n",
|
||||
"isReachable": "b false\n",
|
||||
"supportedPlugins": "as 0\n",
|
||||
"name": '{"type":"s","data":"Fixture iPhone"}\n',
|
||||
"type": '{"type":"s","data":"phone"}\n' if is_dbus_device else '{"type":"s","data":"desktop"}\n',
|
||||
"isPaired": '{"type":"b","data":true}\n',
|
||||
"isReachable": '{"type":"b","data":false}\n',
|
||||
"supportedPlugins": '{"type":"as","data":[]}\n',
|
||||
}
|
||||
return subprocess.CompletedProcess(command, 0, values[command[-1]], "")
|
||||
raise AssertionError(command)
|
||||
|
||||
Reference in New Issue
Block a user