From 45aa8d2f1464c8bc4f3a036607527965c724089b Mon Sep 17 00:00:00 2001 From: Travis Bischel Date: Fri, 24 Apr 2026 14:46:19 -0600 Subject: [PATCH 1/2] fix(frontend): preserve \u00XX escapes in Avro JSON viewer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avro's JSON encoding represents bytes/fixed as ISO-8859-1 code points (\u00XX for anything above 0x7F). Running the normalized payload through JSON.parse then JSON.stringify for display launders those escapes into literal Unicode glyphs (e.g. Û -> U+00DB -> "Û"), so "Copy Value" placed UTF-8 bytes on the clipboard instead of the original byte. Scoped to payload.encoding === 'avro' only. KowlJsonView now re-escapes code points in 0x80-0xFF back to \u00XX before display when called from an avro payload view, which is lossless for both Avro bytes fields (recovers the exact byte) and legitimate Latin-1 strings (valid JSON escape for the same Unicode character). Non-avro encodings (proto, json, text, etc.) are unchanged. Fixes #2421. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/misc/kowl-json-view.tsx | 21 ++++++++++++++----- .../message-display/payload-component.tsx | 5 ++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/misc/kowl-json-view.tsx b/frontend/src/components/misc/kowl-json-view.tsx index c6df2f1ff1..8abda24b35 100644 --- a/frontend/src/components/misc/kowl-json-view.tsx +++ b/frontend/src/components/misc/kowl-json-view.tsx @@ -49,15 +49,26 @@ const READ_ONLY_EDITOR_OPTIONS = { scrollBeyondLastLine: false, } as const; -export const KowlJsonView = (props: { srcObj: object | string | null | undefined; style?: CSSProperties }) => { +export const KowlJsonView = (props: { + srcObj: object | string | null | undefined; + style?: CSSProperties; + // escapeLatin1 re-escapes code points in 0x80-0xFF as \u00XX so bytes from + // Avro's JSON bytes encoding survive copy-paste out of the viewer. Without + // this, JSON.stringify emits the code point as a literal glyph and the + // clipboard receives the UTF-8 encoding, not the original byte. + escapeLatin1?: boolean; +}) => { const containerRef = useRef(null); const editorRef = useRef(null); const frameRef = useRef(null); const lastSizeRef = useRef({ width: 0, height: 0 }); - const str = useMemo( - () => (typeof props.srcObj === 'string' ? props.srcObj : JSON.stringify(props.srcObj, undefined, 4)), - [props.srcObj] - ); + const str = useMemo(() => { + const raw = typeof props.srcObj === 'string' ? props.srcObj : JSON.stringify(props.srcObj, undefined, 4); + if (!props.escapeLatin1) { + return raw; + } + return raw.replace(/[\u0080-\u00ff]/g, (c) => `\\u00${c.charCodeAt(0).toString(16).padStart(2, '0')}`); + }, [props.srcObj, props.escapeLatin1]); const scheduleLayout = useCallback(() => { if (frameRef.current !== null) { diff --git a/frontend/src/components/pages/topics/Tab.Messages/message-display/payload-component.tsx b/frontend/src/components/pages/topics/Tab.Messages/message-display/payload-component.tsx index 78418637b1..a270f0e48e 100644 --- a/frontend/src/components/pages/topics/Tab.Messages/message-display/payload-component.tsx +++ b/frontend/src/components/pages/topics/Tab.Messages/message-display/payload-component.tsx @@ -182,7 +182,10 @@ export const PayloadComponent = (p: { payload: Payload; loadLargeMessage: () => ); } if (renderData.type === 'json') { - return ; + // Avro JSON encodes bytes fields as \u00XX escape sequences. Re-escape + // Latin-1 code points in the viewer so copy-paste yields the original + // bytes rather than their UTF-8 encoding. + return ; } return Error in RenderExpandedMessage: {renderData.content}; }; From f810407070fababf3d66e98e92f7a76e3292dca7 Mon Sep 17 00:00:00 2001 From: Travis Bischel Date: Mon, 27 Apr 2026 09:47:05 -0600 Subject: [PATCH 2/2] test(frontend): cover escapeLatin1 round-trip in KowlJsonView Lock in the regression fix from #2421: with escapeLatin1, Latin-1 glyphs are written as \u00XX in the editor value (recoverable via copy-paste); without it, the glyph passes through. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/misc/kowl-json-view.test.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/frontend/src/components/misc/kowl-json-view.test.tsx b/frontend/src/components/misc/kowl-json-view.test.tsx index 9460626f23..c49e6a999f 100644 --- a/frontend/src/components/misc/kowl-json-view.test.tsx +++ b/frontend/src/components/misc/kowl-json-view.test.tsx @@ -161,4 +161,24 @@ describe('KowlJsonView', () => { expect(editorLayoutSpy).toHaveBeenLastCalledWith({ width: 800, height: 480 }); }); + + test('escapeLatin1 re-escapes Latin-1 code points so copy-paste recovers the original byte', async () => { + render(); + + await waitFor(() => { + const value = editorPropsSpy.mock.lastCall?.[0].value as string; + expect(value).toContain('\\u00db'); + expect(value).not.toContain('Û'); + }); + }); + + test('without escapeLatin1, Latin-1 glyphs pass through to the editor unchanged', async () => { + render(); + + await waitFor(() => { + const value = editorPropsSpy.mock.lastCall?.[0].value as string; + expect(value).toContain('Û'); + expect(value).not.toContain('\\u00db'); + }); + }); });