fix(deeplink): decode URL-safe Base64 in import confirmations

The renderer only fed input to `atob`, which rejects the URL-safe
alphabet (RFC 4648 §5). The backend, meanwhile, tries STANDARD,
STANDARD_NO_PAD, URL_SAFE and URL_SAFE_NO_PAD in turn, so a link whose
payload used `-`/`_` decoded fine on the way in but not on the way to
the screen.

`decodeBase64Utf8` swallows its own failure and returns the input
unchanged, so the mismatch was silent:

  - usage script  -> the confirmation showed opaque Base64
  - prompt        -> same
  - MCP config    -> `JSON.parse` threw, the catch returned null, and
                     the dialog rendered "0 servers" with an empty list

The MCP case defeated the server/argument display added earlier: a
one-character substitution made the whole list disappear while the
backend still imported the real `mcpServers` entry.

Normalize `-` to `+` and `_` to `/` before decoding, in both the primary
path and the last-resort fallback, so the two sides agree on what a
payload says. Standard Base64 contains neither character, so this cannot
misread standard input.

Adds the first tests for this shared decoder. They exercise the real
implementation rather than an injected stub, and assert their own
premise -- a payload whose standard encoding happens to contain no `+`
or `/` makes the URL-safe conversion a no-op and the test vacuous.
This commit is contained in:
Jason
2026-07-29 00:04:07 +08:00
parent a443eae95a
commit 19bf236e58
2 changed files with 120 additions and 4 deletions
+18 -4
View File
@@ -1,3 +1,18 @@
/**
* 把 URL-safe Base64RFC 4648 §5)归一到 `atob` 认识的标准字母表,
* 并还原被 URL 解析吃掉的 `+`(会变成空格)。
*
* 标准 Base64 的字母表里不存在 `-` 与 `_`,因此这两条替换不会误伤标准输入。
*
* ⚠️ 这里必须与后端 `deeplink/utils.rs::decode_base64_param` 的语义保持一致。
* 后端依次尝试 STANDARD / STANDARD_NO_PAD / URL_SAFE / URL_SAFE_NO_PAD 四种引擎;
* 前端若只认标准字母表,同一条链接就会出现「确认框显示不透明 Base64 或空列表、
* 后端却解码并保存了真实内容」的错位——即用户在看不见 payload 的情况下点了确认。
*/
function toStandardBase64Alphabet(value: string): string {
return value.replace(/ /g, "+").replace(/-/g, "+").replace(/_/g, "/");
}
/**
* Decode Base64 encoded UTF-8 string
*
@@ -5,15 +20,14 @@
* Base64 strings are passed through URLs:
* - Spaces (URL parsing may convert '+' to space)
* - Missing padding ('=' characters)
* - Different Base64 variants
* - Standard and URL-safe alphabets (RFC 4648 §4 and §5)
*
* @param str - Base64 encoded string
* @returns Decoded UTF-8 string
*/
export function decodeBase64Utf8(str: string): string {
try {
// Clean up the input: replace spaces with + (URL parsing may convert + to space)
let cleaned = str.trim().replace(/ /g, "+");
let cleaned = toStandardBase64Alphabet(str.trim());
// Try to decode with standard Base64 first
try {
@@ -34,7 +48,7 @@ export function decodeBase64Utf8(str: string): string {
console.error("Base64 decode error:", e, "Input:", str);
// Last resort fallback using deprecated but sometimes working method
try {
return decodeURIComponent(escape(atob(str.replace(/ /g, "+"))));
return decodeURIComponent(escape(atob(toStandardBase64Alphabet(str))));
} catch {
// If all else fails, return original string
return str;