diff --git a/static/js/cli.js b/static/js/cli.js index 9725db791c..f81c2c8443 100644 --- a/static/js/cli.js +++ b/static/js/cli.js @@ -185,9 +185,9 @@ async function executeCommands(dbid, pre, input, commands, animate) { try { const { replies } = await execute(commands, dbid); for (const [i, command] of commands.entries()) { - const { error, value } = replies[i]; + const { error, value, status } = replies[i]; try { - await writeLines(pre, input, command, error ? `(error) ${value}` : formatReply(value), animate, false); + await writeLines(pre, input, command, error ? `(error) ${value}` : formatReply(value, '', status), animate, false); } catch (err) { console.error(err); await writeLines(pre, input, command, `(fatal error) ${err.message}`, animate); @@ -239,14 +239,54 @@ async function execute(commands, dbid = '') { return reply; } -function formatReply(reply, indent = '') { +// Quote and escape a bulk string exactly like redis-cli's sdscatrepr: operate +// on the raw UTF-8 bytes, use the named escapes \\ " \n \r \t \a \b, leave +// printable ASCII (0x20-0x7e) literal, and emit \xHH (lowercase) for every +// other byte. This mirrors the native CLI byte-for-byte, e.g. a NUL renders as +// "\x00" (not JSON's "\u0000") and "é" as "\xc3\xa9". +function reprString(str) { + const bytes = new TextEncoder().encode(str); + let out = '"'; + for (const b of bytes) { + switch (b) { + case 0x5c: out += '\\\\'; break; + case 0x22: out += '\\"'; break; + case 0x0a: out += '\\n'; break; + case 0x0d: out += '\\r'; break; + case 0x09: out += '\\t'; break; + case 0x07: out += '\\a'; break; + case 0x08: out += '\\b'; break; + default: + out += (b >= 0x20 && b <= 0x7e) + ? String.fromCharCode(b) + : `\\x${b.toString(16).padStart(2, '0')}`; + } + } + return out + '"'; +} + +function formatReply(reply, indent = '', status = false) { if (reply === null) { return '(nil)'; } + // Out-of-range 64-bit integers are tagged by the backend (safe_json_integers) + // as {$int: ""} so they survive JSON without precision loss and stay + // distinct from numeric bulk strings. Render them as a plain integer reply. + if (typeof reply === 'object' && !Array.isArray(reply) + && typeof reply.$int === 'string') { + return `(integer) ${reply.$int}`; + } + const type = typeof reply; if (type === 'string') { - return `"${reply}"`; + // RESP simple string / status reply (e.g. PONG, OK): rendered without quotes + if (status) { + return reply; + } + // Bulk string: quote and escape it byte-for-byte the way redis-cli does + // (e.g. a JSON.GET payload renders as "{\"a\":1}" and a NUL byte as "\x00"). + return reprString(reply); } else if (type === 'number') { return `(integer) ${reply}`; } else if (Array.isArray(reply)) { @@ -343,4 +383,4 @@ document.addEventListener('DOMContentLoaded', () => { for (const cli of document.querySelectorAll('form.redis-cli')) { createCli(cli); } -}); \ No newline at end of file +});