Files
omarchy/plugins/io.github.elevate08.qs-bitwarden-cli/tests/stream-limits.test.js
T
asepharyana 1cdb82a76f Sync config from arch
- hypr/apps.lua
- hypr/autostart.lua
- hypr/envs.lua
- hypr/hyprland.lua
- hypr/hyprsunset.conf
- hypr/input.lua
- hypr/looknfeel.lua
- hypr/omasettings.lua
- hypr/xdph.conf
- omarchy/branding/about.txt
- omarchy/branding/screensaver.txt
- omarchy/extensions/omarchy-menu.jsonc
- omarchy/hooks/battery-low.d/play-warning-sound.sample
- omarchy/hooks/font-set.d/show-font-notification.sample
- omarchy/hooks/post-boot.d/weather.sample
- omarchy/hooks/post-update.d/install-voxtype.hook
- omarchy/hooks/post-update.d/setup-agent.hook
- omarchy/hooks/post-update.d/setup-fingerprint.hook
- omarchy/hooks/post-update.d/show-update-notification.sample
- omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample
- omarchy/hooks/theme-set.d/show-theme-notification.sample
- omarchy/shell.json
- omarchy/shell.toml
- omarchy/theme.name
- omarchy/themes/azure-glow/README.md
- omarchy/themes/azure-glow/alacritty.toml
- omarchy/themes/azure-glow/btop.theme
- omarchy/themes/azure-glow/hyprland.conf
- omarchy/themes/azure-glow/hyprlock.conf
- omarchy/themes/azure-glow/icons.theme
- … 269 more
2026-09-23 15:19:12 +07:00

259 lines
11 KiB
JavaScript

#!/usr/bin/env node
// Verifies that all data streams read by the long-lived shell process
// are capped on the producer side to prevent unbounded buffering.
const fs = require("fs")
const path = require("path")
const os = require("os")
const { execFileSync, spawnSync } = require("child_process")
const Model = {}
const code = fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8")
.replace(/^\.pragma library\s*$/m, "")
new Function("exports", code + `
exports.listCommand = listCommand
exports.getItemCommand = getItemCommand
exports.listSendsCommand = listSendsCommand
exports.listFoldersCommand = listFoldersCommand
exports.listOrganizationsCommand = listOrganizationsCommand
exports.listOrgCollectionsCommand = listOrgCollectionsCommand
exports.getTotpCommand = getTotpCommand
exports.statusCommand = statusCommand
exports.generateCommand = generateCommand
exports.generateServeRequestCommand = generateServeRequestCommand
exports.createSendCommand = createSendCommand
exports.createItemCommand = createItemCommand
exports.editItemCommand = editItemCommand
exports.deleteItemCommand = deleteItemCommand
exports.createFolderCommand = createFolderCommand
exports.attachmentDownloadCommand = attachmentDownloadCommand
exports.sessionHandoffReadCommand = sessionHandoffReadCommand
exports.associationsReadCommand = associationsReadCommand
exports.keyringLookupCommand = keyringLookupCommand
exports.keyringLookupMasterPasswordCommand = keyringLookupMasterPasswordCommand
exports.pinUnlockCommand = pinUnlockCommand
exports.dependencyCheckCommand = dependencyCheckCommand
exports.buildCappedCommand = buildCappedCommand
exports.syncCommand = syncCommand
exports.deleteSendCommand = deleteSendCommand
exports.settingWriteCommand = settingWriteCommand
`)(Model)
let pass = 0
const failures = []
const check = (label, ok, detail) => {
if (ok) {
pass++
} else {
failures.push(`${label}\n ${detail}`)
}
}
const flat = (cmd) => (Array.isArray(cmd) ? cmd.join(" ") : String(cmd))
// 1. Vault item list stream is capped
const listCmd = Model.listCommand()
check("listCommand produces bash pipeline with head -c byte cap",
flat(listCmd).includes("bw list items") && flat(listCmd).includes("head -c 16777216"),
flat(listCmd))
check("listCommand caps diagnostic stderr stream",
flat(listCmd).includes("exec 2> >(head -c 8192 >&2)"),
flat(listCmd))
// 2. Vault item detail stream is capped
const getItemCmd = Model.getItemCommand("12345-abc")
check("getItemCommand caps item detail output to 4MB",
flat(getItemCmd).includes("bw get item -- 12345-abc") && flat(getItemCmd).includes("head -c 4194304"),
flat(getItemCmd))
check("getItemCommand caps stderr stream",
flat(getItemCmd).includes("exec 2> >(head -c 8192 >&2)"),
flat(getItemCmd))
// 3. Bitwarden send list stream is capped
const sendsCmd = Model.listSendsCommand()
check("listSendsCommand caps send list output to 8MB",
flat(sendsCmd).includes("bw send list") && flat(sendsCmd).includes("head -c 8388608"),
flat(sendsCmd))
// 4. Folder list stream is capped
const foldersCmd = Model.listFoldersCommand()
check("listFoldersCommand caps folder list output to 2MB",
flat(foldersCmd).includes("bw list folders") && flat(foldersCmd).includes("head -c 2097152"),
flat(foldersCmd))
// 5. Organization list stream is capped
const orgsCmd = Model.listOrganizationsCommand()
check("listOrganizationsCommand caps org list output to 2MB",
flat(orgsCmd).includes("bw list organizations") && flat(orgsCmd).includes("head -c 2097152"),
flat(orgsCmd))
// 6. Organization collections stream is capped
const orgColsCmd = Model.listOrgCollectionsCommand("org-99")
check("listOrgCollectionsCommand caps collections output to 2MB",
flat(orgColsCmd).includes("bw list org-collections --organizationid org-99") && flat(orgColsCmd).includes("head -c 2097152"),
flat(orgColsCmd))
// 7. Status and unlock streams are capped
const statusCmd = Model.statusCommand()
check("statusCommand caps status json output to 64KB",
flat(statusCmd).includes("bw status") && flat(statusCmd).includes("head -c 65536"),
flat(statusCmd))
// 8. TOTP code stream is capped
const totpCmd = Model.getTotpCommand("item-55")
check("getTotpCommand caps totp output to 4KB",
flat(totpCmd).includes("bw get totp --raw -- item-55") && flat(totpCmd).includes("head -c 4096"),
flat(totpCmd))
// 9. Session handoff file reader is size-bounded
const handoffCmd = Model.sessionHandoffReadCommand(true)
check("sessionHandoffReadCommand bounds file reading with head -c 4096",
flat(handoffCmd).includes("head -c 4096") && !flat(handoffCmd).includes("cat \"$f\""),
flat(handoffCmd))
// 10. Associations file reader is size-bounded
const assocCmd = Model.associationsReadCommand()
check("associationsReadCommand bounds file reading with head -c 1048576",
flat(assocCmd).includes("head -c 1048576") && !flat(assocCmd).includes("cat \"$ASSOC_FILE\""),
flat(assocCmd))
// 11. Keyring lookups and PIN unlock are size-bounded
const keyringCmd = Model.keyringLookupCommand()
check("keyringLookupCommand bounds secret-tool output to 4KB",
flat(keyringCmd).includes("head -c 4096") && flat(keyringCmd).includes("head -c 128"),
flat(keyringCmd))
const pinCmd = Model.pinUnlockCommand()
check("pinUnlockCommand bounds both ciphertext lookup and decrypted password",
flat(pinCmd).includes("head -c 8192") && flat(pinCmd).includes("head -c 4096"),
flat(pinCmd))
// 12. Password generator output is capped
const genPassCmd = Model.generateCommand({ length: 32 })
check("generateCommand caps password output to 4KB",
flat(genPassCmd).includes("bw generate") && flat(genPassCmd).includes("head -c 4096"),
flat(genPassCmd))
// 12b. Generator serve request stream is capped on the producer side
const serveReqCmd = Model.generateServeRequestCommand({ length: 24 })
check("generateServeRequestCommand bounds loopback response stream with head -c 65536",
flat(serveReqCmd).includes("curl -q -s -S") && flat(serveReqCmd).includes("head -c 65536"),
flat(serveReqCmd))
// 13. Create/Edit/Delete commands are capped
const createFolderCmd = Model.createFolderCommand("test")
check("createFolderCommand caps stderr and response",
flat(createFolderCmd).includes("exec 2> >(head -c 8192 >&2)") && flat(createFolderCmd).includes("head -c 65536"),
flat(createFolderCmd))
// The save commands cap their response the way sanitizedListCommand does:
// read one byte past the ceiling, then refuse anything that reached it. A
// bare `head -c <max>` cannot tell a stream that fit from one that was cut,
// and these two now carry a sanitising stage whose output must be whole or
// discarded. See the same idiom asserted for the item list in ssh-items.
const capsResponse = cmd =>
flat(cmd).includes("head -c 65537") && flat(cmd).includes('-gt 65536')
const createItemCmd = Model.createItemCommand({ organizationId: "org-1" })
check("createItemCommand caps stderr and response",
flat(createItemCmd).includes("exec 2> >(head -c 8192 >&2)") && capsResponse(createItemCmd),
flat(createItemCmd))
const editItemCmd = Model.editItemCommand("item-1")
check("editItemCommand caps stderr and response",
flat(editItemCmd).includes("exec 2> >(head -c 8192 >&2)") && capsResponse(editItemCmd),
flat(editItemCmd))
const deleteItemCmd = Model.deleteItemCommand("item-1")
check("deleteItemCommand caps stderr and response",
flat(deleteItemCmd).includes("exec 2> >(head -c 8192 >&2)") && flat(deleteItemCmd).includes("head -c 65536"),
flat(deleteItemCmd))
// 14. Live execution check: verify head -c truncation behaviour on huge stream
const hugeScript = "yes 'unbounded streaming line' | head -c 1024"
const hugeOut = execFileSync("bash", ["-c", hugeScript], { encoding: "utf8" })
check("head -c strictly bounds incoming stream to exact byte count",
Buffer.byteLength(hugeOut, "utf8") === 1024,
`Expected 1024 bytes, got ${Buffer.byteLength(hugeOut, "utf8")}`)
// 15. Live execution check: verify stderr bounding does not corrupt stdout
const stderrScript = "exec 2> >(head -c 100 >&2); echo 'stdout data'; (echo 'short stderr error' >&2)"
const proc = execFileSync("bash", ["-c", stderrScript], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
check("capped stderr does not leak into stdout",
proc.trim() === "stdout data",
`stdout was: ${JSON.stringify(proc)}`)
// 16. A cap must not swallow the producer's exit status. `head -c` closes the
// pipe and exits 0, so without `pipefail` every failing bw command would reach
// the panel as a success and the UI would report "Item deleted" for a delete
// that never happened.
const cappedBuilders = [
["listCommand", Model.listCommand()],
["getItemCommand", Model.getItemCommand("x")],
["deleteItemCommand", Model.deleteItemCommand("x")],
["deleteSendCommand", Model.deleteSendCommand("x")],
["syncCommand", Model.syncCommand()],
["createItemCommand", Model.createItemCommand({})],
["editItemCommand", Model.editItemCommand("x")],
["createSendCommand", Model.createSendCommand()],
["createFolderCommand", Model.createFolderCommand("x")],
["settingWriteCommand", Model.settingWriteCommand("autoLockMinutes", 5, "int")],
]
for (const [name, cmd] of cappedBuilders) {
check(`${name} restores the producer's exit status with pipefail`,
flat(cmd).includes("set -o pipefail"), flat(cmd))
check(`${name} does not report truncation (SIGPIPE 141) as a failure`,
flat(cmd).includes('case "$__rc" in 141) __rc=0 ;; esac'), flat(cmd))
}
// 17. Live execution check, with a stub `bw`: a failing command must exit
// non-zero through the cap, and a stream large enough to hit the cap must not
// be mistaken for a failure.
const stubDir = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-stream-"))
fs.writeFileSync(path.join(stubDir, "bw"), [
"#!/bin/bash",
'case "$*" in',
' *boom*) echo "error: bad request" >&2; exit 1 ;;',
" *big*) yes '{\"x\":\"aaaaaaaaaaaaaaaaaaaa\"}' ;;",
" *) echo '{\"ok\":true}' ;;",
"esac",
"",
].join("\n"))
fs.chmodSync(path.join(stubDir, "bw"), 0o755)
const stubEnv = Object.assign({}, process.env, { PATH: stubDir + path.delimiter + process.env.PATH })
const runScript = (script) => {
const r = spawnSync("bash", ["-c", script], {
env: stubEnv, encoding: "utf8", maxBuffer: 64 * 1024 * 1024,
})
return { code: r.status, stdout: r.stdout || "", stderr: r.stderr || "" }
}
const failRun = runScript(Model.deleteItemCommand("boom")[2])
check("a failing bw command exits non-zero through the cap",
failRun.code === 1, `exit ${failRun.code}, stderr ${JSON.stringify(failRun.stderr)}`)
check("a failing bw command still delivers its stderr to the panel",
failRun.stderr.includes("bad request"), JSON.stringify(failRun.stderr))
const okRun = runScript(Model.deleteItemCommand("fine")[2])
check("a succeeding bw command exits zero through the cap",
okRun.code === 0, `exit ${okRun.code}`)
// `bw big` never stops printing: only the cap ends it, and the SIGPIPE that
// follows must not be reported as a failed vault read.
const truncRun = runScript(Model.getItemCommand("big")[2])
check("hitting the cap is not reported as a failure",
truncRun.code === 0, `exit ${truncRun.code}`)
check("hitting the cap truncates at exactly the limit",
Buffer.byteLength(truncRun.stdout, "utf8") === 4 * 1024 * 1024,
`got ${Buffer.byteLength(truncRun.stdout, "utf8")} bytes`)
fs.rmSync(stubDir, { recursive: true, force: true })
console.log(`${pass} passed, ${failures.length} failed`)
if (failures.length) {
console.error("\nFAILURES:\n " + failures.join("\n "))
process.exit(1)
}