fix(redis): preserve control bytes in copied values
This commit is contained in:
parent
330d27d718
commit
72997b88d8
|
|
@ -33,6 +33,7 @@ import {
|
|||
redisCollectionPageItems,
|
||||
redisJsonValueText,
|
||||
normalizeRedisJsonDraft,
|
||||
redisClipboardSafeText,
|
||||
redisMemberCopyText,
|
||||
redisValueCopyText,
|
||||
redisValueCollectionItems,
|
||||
|
|
@ -1667,7 +1668,7 @@ onBeforeUnmount(() => {
|
|||
<Pencil class="h-4 w-4" />
|
||||
{{ t("redis.editMember") }}
|
||||
</Button>
|
||||
<Button variant="outline" @click="copyText(detailTextForFormat(selectedMemberDetail, memberValueView))">
|
||||
<Button variant="outline" @click="copyText(redisClipboardSafeText(detailTextForFormat(selectedMemberDetail, memberValueView)))">
|
||||
<Copy class="h-4 w-4" />
|
||||
{{ t("redis.copyMember") }}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
getRedisMemberSelectionKey,
|
||||
normalizeRedisJsonDraft,
|
||||
preferredRedisValueFormat,
|
||||
redisClipboardSafeText,
|
||||
redisJsonValueText,
|
||||
redisMemberCopyText,
|
||||
redisValueCopyText,
|
||||
|
|
@ -29,6 +30,10 @@ describe("redisValuePresentation", () => {
|
|||
expect(sanitizeRedisDisplayText("line1\nline2\tvalue\r\n")).toBe("line1\nline2\tvalue\r\n");
|
||||
});
|
||||
|
||||
it("escapes clipboard-unsafe controls without changing normal UTF-8 whitespace", () => {
|
||||
expect(redisClipboardSafeText("普通文本\n下一行\t值\x00\x06\u0085结束")).toBe("普通文本\n下一行\t值\\x00\\x06\\x85结束");
|
||||
});
|
||||
|
||||
it("strips utf8 c1 control bytes for display", () => {
|
||||
expect(sanitizeRedisDisplayText("before\u0085after")).toBe("beforeafter");
|
||||
});
|
||||
|
|
@ -39,6 +44,10 @@ describe("redisValuePresentation", () => {
|
|||
expect(getRedisMemberSelectionKey("member", raw)).toBe(`member\n${raw}`);
|
||||
});
|
||||
|
||||
it("copies the complete Redis member when UTF-8 text contains NUL", () => {
|
||||
expect(redisMemberCopyText("before\x00after")).toBe("before\\x00after");
|
||||
});
|
||||
|
||||
it("can disambiguate duplicate stream fields with an explicit identity", () => {
|
||||
expect(getRedisMemberSelectionKey("event", "login", "stream:1:0")).not.toBe(getRedisMemberSelectionKey("event", "login", "stream:1:1"));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -249,7 +249,24 @@ export function canRenderRedisValueFormat(detail: RedisMemberDetail, format: Red
|
|||
}
|
||||
|
||||
export function redisMemberCopyText(value: unknown): string {
|
||||
return isRedisBlob(value) ? redisBlobRawText(value) : formatRedisMemberDetail(value).rawText;
|
||||
const text = isRedisBlob(value) ? redisBlobRawText(value) : formatRedisMemberDetail(value).rawText;
|
||||
return redisClipboardSafeText(text);
|
||||
}
|
||||
|
||||
export function redisClipboardSafeText(value: string): string {
|
||||
let output = "";
|
||||
for (const ch of value) {
|
||||
if (ch === "\n" || ch === "\r" || ch === "\t" || !isUtf8ControlCharacter(ch)) {
|
||||
output += ch;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Native clipboard text backends may treat embedded controls such as NUL as string terminators.
|
||||
// Keep ordinary whitespace intact, but copy other controls as visible byte-style escapes.
|
||||
const codePoint = ch.codePointAt(0)!;
|
||||
output += codePoint <= 0xff ? `\\x${codePoint.toString(16).padStart(2, "0")}` : `\\u{${codePoint.toString(16)}}`;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
export function getRedisMemberSelectionKey(title: string, value: unknown, identity = title): string {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
highlightRedisJsonDetail,
|
||||
parseRedisJsonDetail,
|
||||
preferredRedisValueFormat,
|
||||
redisClipboardSafeText,
|
||||
redisMemberCopyText,
|
||||
redisValueCopyText,
|
||||
} from "../../apps/desktop/src/lib/redis/redisValuePresentation.ts";
|
||||
|
|
@ -40,6 +41,16 @@ test("keeps plain Redis member strings unchanged", () => {
|
|||
assert.deepEqual(detail.availableFormats, ["utf8", "ascii", "binary", "hex", "base64"]);
|
||||
});
|
||||
|
||||
test("keeps normal text whitespace and unicode unchanged for Redis clipboard output", () => {
|
||||
assert.equal(redisClipboardSafeText("line 1\nline 2\t中文"), "line 1\nline 2\t中文");
|
||||
});
|
||||
|
||||
test("escapes clipboard-unsafe controls in Redis member copies without truncating the suffix", () => {
|
||||
const serialized = 'o:\\28:"JobMessage":1:{s:7:"payload";s:11:"before\x00after";}';
|
||||
|
||||
assert.equal(redisMemberCopyText(blobFromText(serialized)), 'o:\\28:"JobMessage":1:{s:7:"payload";s:11:"before\\x00after";}');
|
||||
});
|
||||
|
||||
test("formats JSON string values without changing plain strings", () => {
|
||||
assert.equal(formatRedisStringValue('{"id":1,"name":"Ada"}'), '{\n "id": 1,\n "name": "Ada"\n}');
|
||||
assert.equal(formatRedisStringValue("plain redis value"), "plain redis value");
|
||||
|
|
@ -223,6 +234,22 @@ test("copies collection values as readable content instead of blob transport obj
|
|||
assert.equal(redisValueCopyText(value), '[\n {\n "field": "name",\n "value": "Ada"\n }\n]');
|
||||
});
|
||||
|
||||
test("keeps whole-key JSON copies JSON-escaped when members contain NUL", () => {
|
||||
const value = {
|
||||
key_display: "users",
|
||||
key_raw: "users",
|
||||
ttl: -1,
|
||||
redis_type: "list",
|
||||
data: {
|
||||
kind: "list" as const,
|
||||
items: [{ value: blobFromText("before\x00after") }],
|
||||
total: 1,
|
||||
},
|
||||
};
|
||||
|
||||
assert.equal(redisValueCopyText(value), '[\n "before\\u0000after"\n]');
|
||||
});
|
||||
|
||||
test("copies stream entries without collapsing repeated field names", () => {
|
||||
const value = {
|
||||
key_display: "events",
|
||||
|
|
|
|||
Loading…
Reference in New Issue