diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index d70aa265..185b41e2 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -5,3 +5,26 @@ export function cn(...inputs: ClassValue[]): string { return twMerge(clsx(inputs)) } +/** + * Decode base64 string to UTF-8 text + */ +export function decodeBase64(value: string): { text: string; ok: boolean } { + try { + const binaryString = atob(value) + const bytes = Uint8Array.from(binaryString, (char) => char.charCodeAt(0)) + const text = new TextDecoder('utf-8').decode(bytes) + return { text, ok: true } + } catch { + return { text: '', ok: false } + } +} + +/** + * Encode UTF-8 text to base64 string + */ +export function encodeBase64(value: string): string { + const bytes = new TextEncoder().encode(value) + const binaryString = Array.from(bytes, (byte) => String.fromCharCode(byte)).join('') + return btoa(binaryString) +} + diff --git a/web/src/routes/sessions/file.tsx b/web/src/routes/sessions/file.tsx index afb710e3..300f005d 100644 --- a/web/src/routes/sessions/file.tsx +++ b/web/src/routes/sessions/file.tsx @@ -9,18 +9,7 @@ import { useAppGoBack } from '@/hooks/useAppGoBack' import { useCopyToClipboard } from '@/hooks/useCopyToClipboard' import { queryKeys } from '@/lib/query-keys' import { langAlias, useShikiHighlighter } from '@/lib/shiki' - -function decodeBase64(value: string): { text: string; ok: boolean } { - try { - return { text: atob(value), ok: true } - } catch { - try { - return { text: decodeURIComponent(escape(atob(value))), ok: true } - } catch { - return { text: '', ok: false } - } - } -} +import { decodeBase64 } from '@/lib/utils' function decodePath(value: string): string { if (!value) return '' diff --git a/web/src/routes/sessions/files.tsx b/web/src/routes/sessions/files.tsx index 8e34b67c..a54e3ff4 100644 --- a/web/src/routes/sessions/files.tsx +++ b/web/src/routes/sessions/files.tsx @@ -7,14 +7,7 @@ import { useAppGoBack } from '@/hooks/useAppGoBack' import { useGitStatusFiles } from '@/hooks/queries/useGitStatusFiles' import { useSession } from '@/hooks/queries/useSession' import { useSessionFileSearch } from '@/hooks/queries/useSessionFileSearch' - -function encodePath(value: string): string { - try { - return btoa(value) - } catch { - return btoa(unescape(encodeURIComponent(value))) - } -} +import { encodeBase64 } from '@/lib/utils' function BackIcon(props: { className?: string }) { return ( @@ -255,8 +248,8 @@ export default function FilesPage() { const handleOpenFile = useCallback((path: string, staged?: boolean) => { const search = staged === undefined - ? { path: encodePath(path) } - : { path: encodePath(path), staged } + ? { path: encodeBase64(path) } + : { path: encodeBase64(path), staged } navigate({ to: '/sessions/$sessionId/file', params: { sessionId },