fix: correct UTF-8 encoding in base64 decode for file paths fix #20

The old decodeBase64 implementation tried atob() first which returns
garbled text for UTF-8 content (e.g., Chinese characters), and only
fell back to correct UTF-8 decoding when atob() threw an exception
(which it doesn't for valid base64).

Changes:
- Add encodeBase64/decodeBase64 utilities to web/src/lib/utils.ts
  using TextEncoder/TextDecoder for proper UTF-8 support
- Update file.tsx to use shared decodeBase64 instead of buggy local impl
- Update files.tsx to use shared encodeBase64 instead of deprecated
  escape/unescape approach
This commit is contained in:
weishu
2025-12-28 14:38:05 +08:00
parent d68b91fd07
commit 20c05a06ec
3 changed files with 27 additions and 22 deletions
+23
View File
@@ -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)
}
+1 -12
View File
@@ -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 ''
+3 -10
View File
@@ -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 },