- safeClipboardTools now only dms + wl-copy (clipmanctl removed as niche optional history manager) - buildCopyCommand now returns pure argv arrays (no sh -c, no escaping, eliminates future injection risk) - Added getStrings() for centralised user-facing strings (prep for real i18n) - Updated all tests (TDD) and README - All 29 tests pass Written by AI agent working for @jtmorris. Model: Grok build-0.1.
This commit is contained in:
parent
e2a3315b08
commit
0b80f5cdc8
3 changed files with 52 additions and 20 deletions
|
|
@ -21,7 +21,7 @@ A lightweight widget plugin that shows Tailscale connectivity status on the Dank
|
||||||
|
|
||||||
- Dank Material Shell installed and running
|
- Dank Material Shell installed and running
|
||||||
- `tailscale` CLI available on `PATH`
|
- `tailscale` CLI available on `PATH`
|
||||||
- A clipboard tool (`dms`, `wl-copy`, or `clipmanctl`)
|
- A clipboard tool (`dms` or `wl-copy`)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,13 @@ function findActiveExitNode(peerMap) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
var safeClipboardTools = ["dms", "wl-copy", "clipmanctl"]
|
var safeClipboardTools = ["dms", "wl-copy"]
|
||||||
|
|
||||||
var clipboardCmdMap = {
|
// Direct argv form — no sh -c, no escaping, no future injection surface (#49)
|
||||||
"dms": "dms cl copy",
|
// clipmanctl intentionally dropped (niche optional history manager, not needed on DMS + Wayland)
|
||||||
"wl-copy": "wl-copy",
|
var clipboardTools = {
|
||||||
"clipmanctl": "clipmanctl copy"
|
"dms": { argv: ["dms", "cl", "copy"] },
|
||||||
|
"wl-copy": { argv: ["wl-copy"] }
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateClipboardTool(tool) {
|
function validateClipboardTool(tool) {
|
||||||
|
|
@ -44,11 +45,10 @@ function validateClipboardTool(tool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCopyCommand(text, tool) {
|
function buildCopyCommand(text, tool) {
|
||||||
if (!validateClipboardTool(tool)) return null
|
const entry = clipboardTools[tool]
|
||||||
var cmd = clipboardCmdMap[tool]
|
if (!entry) return null
|
||||||
if (!cmd) return null
|
// Pure argv — the Process will pass the string verbatim. No shell involved.
|
||||||
var escaped = text.replace(/'/g, "'\\''")
|
return [...entry.argv, text]
|
||||||
return ["sh", "-c", "printf '%s' '" + escaped + "' | " + cmd]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextClipboardTool(currentTool) {
|
function nextClipboardTool(currentTool) {
|
||||||
|
|
@ -61,6 +61,21 @@ function allClipboardTools() {
|
||||||
return safeClipboardTools.slice()
|
return safeClipboardTools.slice()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getStrings() {
|
||||||
|
return {
|
||||||
|
header: "Tailscale",
|
||||||
|
connected: "Connected",
|
||||||
|
disconnected: "Disconnected",
|
||||||
|
exitNodePrefix: "Exit node: ",
|
||||||
|
none: "None",
|
||||||
|
copied: function (text) { return "Copied " + text + " to clipboard" },
|
||||||
|
noClipboardTool: "No clipboard tool found",
|
||||||
|
invalidClipboardCommand: "Invalid clipboard command",
|
||||||
|
clearExitNode: "×",
|
||||||
|
setExitNode: "↗"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function parseStatusResult(jsonText) {
|
function parseStatusResult(jsonText) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(jsonText)
|
const data = JSON.parse(jsonText)
|
||||||
|
|
@ -93,5 +108,5 @@ function errorMessage(cmd) {
|
||||||
|
|
||||||
// CommonJS export for Node.js tests (ignored by QML)
|
// CommonJS export for Node.js tests (ignored by QML)
|
||||||
if (typeof module !== "undefined" && module.exports) {
|
if (typeof module !== "undefined" && module.exports) {
|
||||||
module.exports = { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardTool, buildCopyCommand, nextClipboardTool, allClipboardTools, buildToggleCommand, parseStatusResult }
|
module.exports = { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardTool, buildCopyCommand, nextClipboardTool, allClipboardTools, buildToggleCommand, parseStatusResult, getStrings }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { test } from "node:test"
|
import { test } from "node:test"
|
||||||
import assert from "node:assert"
|
import assert from "node:assert"
|
||||||
import lib from "../tailscalectl/lib.js"
|
import lib from "../tailscalectl/lib.js"
|
||||||
const { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardTool, buildCopyCommand, nextClipboardTool, allClipboardTools, buildToggleCommand, parseStatusResult } = lib
|
const { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardTool, buildCopyCommand, nextClipboardTool, allClipboardTools, buildToggleCommand, parseStatusResult, getStrings } = lib
|
||||||
|
|
||||||
test("parsePeers extracts exitNode from ExitNodeOption", () => {
|
test("parsePeers extracts exitNode from ExitNodeOption", () => {
|
||||||
const peerMap = {
|
const peerMap = {
|
||||||
|
|
@ -80,7 +80,7 @@ test("errorMessage returns generic message for unknown command", () => {
|
||||||
test("validateClipboardTool accepts whitelisted tool names", () => {
|
test("validateClipboardTool accepts whitelisted tool names", () => {
|
||||||
assert.strictEqual(validateClipboardTool("dms"), true)
|
assert.strictEqual(validateClipboardTool("dms"), true)
|
||||||
assert.strictEqual(validateClipboardTool("wl-copy"), true)
|
assert.strictEqual(validateClipboardTool("wl-copy"), true)
|
||||||
assert.strictEqual(validateClipboardTool("clipmanctl"), true)
|
// clipmanctl intentionally dropped (see #49)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("validateClipboardTool rejects malicious inputs", () => {
|
test("validateClipboardTool rejects malicious inputs", () => {
|
||||||
|
|
@ -101,8 +101,7 @@ test("validateClipboardTool rejects empty and falsy inputs", () => {
|
||||||
test("buildCopyCommand returns safe command for whitelisted clipboard tool", () => {
|
test("buildCopyCommand returns safe command for whitelisted clipboard tool", () => {
|
||||||
const cmd = buildCopyCommand("hello", "wl-copy")
|
const cmd = buildCopyCommand("hello", "wl-copy")
|
||||||
assert.ok(Array.isArray(cmd))
|
assert.ok(Array.isArray(cmd))
|
||||||
assert.strictEqual(cmd[0], "sh")
|
assert.deepStrictEqual(cmd, ["wl-copy", "hello"]) // direct argv, no sh -c (see #49)
|
||||||
assert.strictEqual(cmd[1], "-c")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("buildCopyCommand returns null for invalid clipboard tool", () => {
|
test("buildCopyCommand returns null for invalid clipboard tool", () => {
|
||||||
|
|
@ -120,8 +119,7 @@ test("buildCopyCommand safely handles text with special characters", () => {
|
||||||
|
|
||||||
test("nextClipboardTool cycles through tools", () => {
|
test("nextClipboardTool cycles through tools", () => {
|
||||||
assert.strictEqual(nextClipboardTool("dms"), "wl-copy")
|
assert.strictEqual(nextClipboardTool("dms"), "wl-copy")
|
||||||
assert.strictEqual(nextClipboardTool("wl-copy"), "clipmanctl")
|
assert.strictEqual(nextClipboardTool("wl-copy"), "dms") // clipmanctl dropped (#49)
|
||||||
assert.strictEqual(nextClipboardTool("clipmanctl"), "dms")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("nextClipboardTool falls back to first tool for unknown input", () => {
|
test("nextClipboardTool falls back to first tool for unknown input", () => {
|
||||||
|
|
@ -134,10 +132,29 @@ test("nextClipboardTool falls back to first tool for unknown input", () => {
|
||||||
test("allClipboardTools returns the list of safe tools", () => {
|
test("allClipboardTools returns the list of safe tools", () => {
|
||||||
const tools = allClipboardTools()
|
const tools = allClipboardTools()
|
||||||
assert.ok(Array.isArray(tools))
|
assert.ok(Array.isArray(tools))
|
||||||
assert.strictEqual(tools.length, 3)
|
assert.strictEqual(tools.length, 2)
|
||||||
assert.ok(tools.includes("dms"))
|
assert.ok(tools.includes("dms"))
|
||||||
assert.ok(tools.includes("wl-copy"))
|
assert.ok(tools.includes("wl-copy"))
|
||||||
assert.ok(tools.includes("clipmanctl"))
|
// clipmanctl intentionally removed (#49)
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- getStrings (for #34 i18n prep) ---
|
||||||
|
|
||||||
|
test("getStrings returns canonical UI strings for the widget", () => {
|
||||||
|
const s = getStrings()
|
||||||
|
assert.ok(s.header)
|
||||||
|
assert.ok(s.connected)
|
||||||
|
assert.ok(s.disconnected)
|
||||||
|
assert.ok(s.exitNodePrefix)
|
||||||
|
assert.ok(s.copied)
|
||||||
|
assert.ok(s.noClipboardTool)
|
||||||
|
assert.ok(s.invalidClipboardCommand)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("getStrings.copied interpolates the text", () => {
|
||||||
|
const s = getStrings()
|
||||||
|
const msg = s.copied("100.64.0.5")
|
||||||
|
assert.ok(msg.includes("100.64.0.5"))
|
||||||
})
|
})
|
||||||
|
|
||||||
// --- buildToggleCommand ---
|
// --- buildToggleCommand ---
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue