From f48e751d904a307f68c599f7e42b8779a070c01c Mon Sep 17 00:00:00 2001 From: onenewcode Date: Sat, 4 Jul 2026 23:03:29 +0800 Subject: [PATCH] =?UTF-8?q?fix(redis):=20=E4=BF=AE=E5=A4=8D=20Redis=20set?= =?UTF-8?q?=20=E7=B1=BB=E5=9E=8B=E7=9A=84=E5=80=BC=E4=B8=AD=E6=9C=89?= =?UTF-8?q?=E4=B9=B1=E7=A0=81=20(#2554)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/redis/RedisValueViewer.vue | 35 ++++++++++--------- .../redis/redisValuePresentation.spec.ts | 32 +++++++++++++++++ .../src/lib/redis/redisValuePresentation.ts | 25 +++++++++++-- 3 files changed, 72 insertions(+), 20 deletions(-) create mode 100644 apps/desktop/src/lib/__tests__/redis/redisValuePresentation.spec.ts diff --git a/apps/desktop/src/components/redis/RedisValueViewer.vue b/apps/desktop/src/components/redis/RedisValueViewer.vue index 8eb04ff91..37adfae9d 100644 --- a/apps/desktop/src/components/redis/RedisValueViewer.vue +++ b/apps/desktop/src/components/redis/RedisValueViewer.vue @@ -213,11 +213,12 @@ function escapeHtml(value: string): string { const deleteDetails = computed(() => { const pending = pendingDelete.value; if (!pending) return ""; - if (pending.kind === "key") return t("dangerDialog.redisKeyDetails", { key: props.keyDisplay }); - if (pending.kind === "hash") return t("dangerDialog.redisHashFieldDetails", { key: props.keyDisplay, field: pending.field }); - if (pending.kind === "list") return t("dangerDialog.redisListItemDetails", { key: props.keyDisplay, index: pending.index }); - if (pending.kind === "zset") return t("dangerDialog.redisSetMemberDetails", { key: props.keyDisplay, member: pending.member }); - return t("dangerDialog.redisSetMemberDetails", { key: props.keyDisplay, member: pending.member }); + const key = formatValue(props.keyDisplay); + if (pending.kind === "key") return t("dangerDialog.redisKeyDetails", { key }); + if (pending.kind === "hash") return t("dangerDialog.redisHashFieldDetails", { key, field: formatValue(pending.field) }); + if (pending.kind === "list") return t("dangerDialog.redisListItemDetails", { key, index: pending.index }); + if (pending.kind === "zset") return t("dangerDialog.redisSetMemberDetails", { key, member: formatValue(pending.member) }); + return t("dangerDialog.redisSetMemberDetails", { key, member: formatValue(pending.member) }); }); const isBinaryStringValue = computed(() => data.value?.key_type === "string" && data.value?.value_is_binary); @@ -465,7 +466,7 @@ async function copyInsertStatement() { } function copyMember(value: unknown) { - void copyText(formatRedisMemberDetail(value).text); + void copyText(formatRedisMemberDetail(value).rawText); } function selectMember(title: string, value: unknown, context: RedisMemberContext) { @@ -581,12 +582,12 @@ function startResizeZsetColumns(event: PointerEvent) { } function startEditMember() { - memberEditValue.value = selectedMemberDetail.value.text; + memberEditValue.value = selectedMemberDetail.value.rawText; isEditingMember.value = true; } function cancelEditMember() { - memberEditValue.value = selectedMemberDetail.value.text; + memberEditValue.value = selectedMemberDetail.value.rawText; isEditingMember.value = false; } @@ -815,7 +816,7 @@ onBeforeUnmount(() => {
- {{ data.key_display }} + {{ formatValue(data.key_display) }} @@ -906,7 +907,7 @@ onBeforeUnmount(() => { @click="viewMember(`#${row.index}`, row.value, { kind: 'list', index: row.index })" >
{{ row.index }}
-
{{ row.value }}
+
{{ formatValue(row.value) }}
@@ -946,7 +947,7 @@ onBeforeUnmount(() => { :style="{ height: `${REDIS_COLLECTION_ROW_HEIGHT}px` }" @click="viewMember(t('redis.member'), row.value, { kind: 'set', member: String(row.value) })" > -
{{ row.value }}
+
{{ formatValue(row.value) }}
@@ -1004,8 +1005,8 @@ onBeforeUnmount(() => { :class="{ 'bg-accent/60': isSelectedMember(String(row.value.field), row.value.value) }" @click="viewMember(String(row.value.field), row.value.value, { kind: 'hash', field: String(row.value.field) })" > -
{{ row.value.field }}
-
{{ row.value.value }}
+
{{ formatValue(row.value.field) }}
+
{{ formatValue(row.value.value) }}
- diff --git a/apps/desktop/src/lib/__tests__/redis/redisValuePresentation.spec.ts b/apps/desktop/src/lib/__tests__/redis/redisValuePresentation.spec.ts new file mode 100644 index 000000000..f02fdf089 --- /dev/null +++ b/apps/desktop/src/lib/__tests__/redis/redisValuePresentation.spec.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; + +import { formatRedisMemberDetail, formatRedisStringValue, getRedisMemberSelectionKey, sanitizeRedisDisplayText } from "@/lib/redis/redisValuePresentation"; + +describe("redisValuePresentation", () => { + it("strips control bytes from display without mutating raw member text", () => { + const raw = "send_message_to_esb\x06\x16\x06\x16send_message_to_esb"; + + const detail = formatRedisMemberDetail(raw); + + expect(detail.text).toBe("send_message_to_esbsend_message_to_esb"); + expect(detail.rawText).toBe(raw); + }); + + it("preserves common whitespace in display text", () => { + expect(sanitizeRedisDisplayText("line1\nline2\tvalue\r\n")).toBe("line1\nline2\tvalue\r\n"); + }); + + it("strips utf8 c1 control bytes for display", () => { + expect(sanitizeRedisDisplayText("before\u0085after")).toBe("beforeafter"); + }); + + it("uses raw member text for selection keys", () => { + const raw = "send_message_to_esb\x06\x16"; + + expect(getRedisMemberSelectionKey("member", raw)).toBe(`member\n${raw}`); + }); + + it("formats string values for display without changing plain text", () => { + expect(formatRedisStringValue("plain-text")).toBe("plain-text"); + }); +}); diff --git a/apps/desktop/src/lib/redis/redisValuePresentation.ts b/apps/desktop/src/lib/redis/redisValuePresentation.ts index 326fa5328..d18a7b00d 100644 --- a/apps/desktop/src/lib/redis/redisValuePresentation.ts +++ b/apps/desktop/src/lib/redis/redisValuePresentation.ts @@ -30,7 +30,7 @@ export function clampRedisMemberDetailSheetWidth(width: number, viewportWidth: n export function formatRedisMemberDetail(value: unknown): RedisMemberDetail { if (typeof value === "string") { const json = parseRedisJsonDetail(value); - return json ? { text: json.formattedText, rawText: value, format: "json", json } : { text: value, rawText: value, format: "text" }; + return json ? { text: json.formattedText, rawText: value, format: "json", json } : { text: sanitizeRedisDisplayText(value), rawText: value, format: "text" }; } try { @@ -49,7 +49,7 @@ export function formatRedisMemberDetail(value: unknown): RedisMemberDetail { export function formatRedisStringValue(value: unknown): string { if (typeof value !== "string") return String(value ?? ""); - return formatRedisJsonString(value) ?? value; + return formatRedisJsonString(value) ?? sanitizeRedisDisplayText(value); } export function formatRedisCommandResult(value: unknown): string { @@ -117,7 +117,26 @@ function isJsonContainer(value: unknown): boolean { } export function getRedisMemberSelectionKey(title: string, value: unknown): string { - return `${title}\n${formatRedisMemberDetail(value).text}`; + const detail = formatRedisMemberDetail(value); + return `${title}\n${detail.format === "json" ? detail.text : detail.rawText}`; +} + +export function sanitizeRedisDisplayText(value: string): string { + let output = ""; + for (const ch of value) { + if (ch === "\n" || ch === "\r" || ch === "\t") { + output += ch; + continue; + } + if (ch >= " " && ch !== "\u007f" && !isUtf8ControlCharacter(ch)) { + output += ch; + } + } + return output; +} + +function isUtf8ControlCharacter(ch: string): boolean { + return /\p{Cc}/u.test(ch); } export function highlightRedisJsonDetail(json: string): string {