From 5de47a0d93f5fdc187e4f8a7489580a107a2fd49 Mon Sep 17 00:00:00 2001 From: Stephan Garland Date: Tue, 27 Jan 2026 13:59:53 -0500 Subject: [PATCH] fix: Network map: escape double-quotes and backslashes in device attributes (#30746) Co-authored-by: Koen Kanters --- lib/extension/networkMap.ts | 4 +++- test/extensions/networkMap.test.ts | 33 ++++++++++++++++++++---------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/lib/extension/networkMap.ts b/lib/extension/networkMap.ts index 0a2958178..01a49a813 100644 --- a/lib/extension/networkMap.ts +++ b/lib/extension/networkMap.ts @@ -92,6 +92,8 @@ export default class NetworkMap extends Extension { } labels.push(lastSeen); + // Escape backslashes and double-quotes to avoid breaking Graphviz .dot files. + const escapedLabels = labels.map((label) => label.replace(/\\/g, "\\\\").replace(/"/g, '\\"')); // Shape the record according to device type if (node.type === "Coordinator") { @@ -103,7 +105,7 @@ export default class NetworkMap extends Extension { } // Add the device with its labels to the graph as a node. - text += ` "${node.ieeeAddr}" [${style}, label="{${labels.join("|")}}"];\n`; + text += ` "${node.ieeeAddr}" [${style}, label="{${escapedLabels.join("|")}}"];\n`; /** * Add an edge between the device and its child to the graph diff --git a/test/extensions/networkMap.test.ts b/test/extensions/networkMap.test.ts index df24a0177..698ca1f6d 100644 --- a/test/extensions/networkMap.test.ts +++ b/test/extensions/networkMap.test.ts @@ -367,23 +367,34 @@ describe("Extension: NetworkMap", () => { it("Output graphviz networkmap", async () => { mock(); - const device = devices.bulb_color; - device.lastSeen = undefined; - const endpoint = device.getEndpoint(1); - const data = {modelID: "test"}; - const payload = {data, cluster: "genOnOff", device, endpoint, type: "readResponse", linkquality: 10}; - await mockZHEvents.message(payload); - mockMQTTEvents.message("zigbee2mqtt/bridge/request/networkmap", stringify({type: "graphviz", routes: true})); - await flushPromises(); - expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1); - expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bridge/response/networkmap"); + const z2mDevice = controller.zigbee.resolveEntity(devices.bulb_color.ieeeAddr); + const originalDef = z2mDevice.definition; + z2mDevice.definition = { + ...originalDef, + description: `${originalDef.description} 6"`, + }; + + try { + const device = devices.bulb_color; + device.lastSeen = undefined; + const endpoint = device.getEndpoint(1); + const data = {modelID: "test"}; + const payload = {data, cluster: "genOnOff", device, endpoint, type: "readResponse", linkquality: 10}; + await mockZHEvents.message(payload); + mockMQTTEvents.message("zigbee2mqtt/bridge/request/networkmap", stringify({type: "graphviz", routes: true})); + await flushPromises(); + expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1); + expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/bridge/response/networkmap"); + } finally { + z2mDevice.definition = originalDef; + } const expected = `digraph G { node[shape=record]; "0x00124b00120144ae" [style="bold, filled", fillcolor="#e04e5d", fontcolor="#ffffff", label="{Coordinator|0x00124b00120144ae (0x0000)|0 seconds ago}"]; "0x000b57fffec6a5b2" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb|0x000b57fffec6a5b2 (0x9db1)|IKEA TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm (LED1545G12)|9 seconds ago}"]; "0x000b57fffec6a5b2" -> "0x00124b00120144ae" [penwidth=2, weight=1, color="#009900", label="92 (routes: 0x198c)"] - "0x000b57fffec6a5b3" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb_color|0x000b57fffec6a5b3 (0x9dcf)|Philips Hue Go (7146060PH)|unknown}"]; + "0x000b57fffec6a5b3" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb_color|0x000b57fffec6a5b3 (0x9dcf)|Philips Hue Go 6\\" (7146060PH)|unknown}"]; "0x000b57fffec6a5b3" -> "0x00124b00120144ae" [penwidth=0.5, weight=0, color="#994444", label="120"] "0x000b57fffec6a5b3" -> "0x000b57fffec6a5b2" [penwidth=0.5, weight=0, color="#994444", label="110"] "0x0017880104e45521" [style="rounded, dashed, filled", fillcolor="#fff8ce", fontcolor="#000000", label="{button_double_key|0x0017880104e45521 (0x198a)|Aqara Wireless remote switch (double rocker), 2016 model (WXKG02LM_rev1)|9 seconds ago}"];