Uploaded filenames are decoded as latin1, so every non-ASCII name arrives as mojibake
まだ誰も着手していません。
評価
- 難易度
- 2/5
- 見積もり時間
- 1〜3時間
- 初心者へのやさしさ
- 82/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- node.js, typescript
- 領域
- backend
調査の方向性
まず src/backend/content-routes.ts の1315行目と1329行目付近にあるBusboyの呼び出しとファイル名の処理を確認し、次に利用可能であればアップロードルートのテストを読みます。multipartのファイル名パラメーターがどのようにデコードされるかを確認し、アップロードストリームの経路とチャンク処理の経路を比較します。小さなmultipartアップロードでも非ASCIIの名前が保持されれば完了です。issueには、318行目付近の src/backend/download-routes.ts におけるContent-Dispositionの関連変更も記載されています。
索引モデルが issue の本文から書いたものです。
説明
Repository: Termix-SSH/Plugin-File-Manager
Version: plugin 1.1.1 / Termix 26.10.5
Where: src/backend/content-routes.ts L1315 and L1329; related: src/backend/download-routes.ts L318-319
What happens
Uploading a file whose name is not pure ASCII through the File Manager creates it on the remote
host under a garbled name. Uploading 中文文件.txt leaves 䏿–‡æ–‡ä»¶.txt; uploading
新建 文本文档.txt leaves æ°å»º ææ¬ææ¡£.txt.
That is exactly Buffer.from(name, "utf8").toString("latin1") — every UTF-8 byte becomes its own
character. Hebrew, Cyrillic, Greek, accented Latin, Vietnamese and emoji names are all affected.
Everything else in the plugin handles these names fine: the directory listing renders them, rename
works, create folder works. Only the multipart upload path corrupts them.
Why
// src/backend/content-routes.ts L1315 — route POST /uploadFileStream
const bb = Busboy({ headers: req.headers });
bb.on("file", (fieldname, fileStream, info) => {
fileName = info.filename; // L1329 — already mojibake here
...
const fullPath = remotePath.endsWith("/")
? remotePath + fileName
: remotePath + "/" + fileName;
Busboy is called without defParamCharset. From the busboy README:
defParamCharset- string - For multipart forms, the default character set to use for values
of part header parameters (e.g. filename) that are not extended parameters (that contain an
explicit charset). Default:'latin1'.
In busboy's source that becomes:
const paramDecoder = typeof cfg.defParamCharset === "string" && cfg.defParamCharset
? getDecoder(cfg.defParamCharset)
: nullDecoder; // nullDecoder returns the value unchanged
function nullDecoder(val, hint) { return val; }
The browser sends the part header as raw UTF-8 bytes with no filename*:
// src/frontend/api/ssh-file-operations-api.ts
const form = new FormData();
form.append("sessionId", sessionId);
form.append("path", path);
form.append("file", file, fileName); // filename= holds the UTF-8 bytes
await getFileManagerApiForSession(sessionId).postForm("/uploadFileStream", form, ...);
Decoding those bytes as latin1 is what produces the mojibake.
Fingerprint: large files get the name right
The chunked path is unaffected because the name travels as a query parameter, not a multipart
header:
const CHUNK_THRESHOLD_BYTES = 1.5 * 1024 * 1024 * 1024; // 1.5 GiB
if (file.size > CHUNK_THRESHOLD_BYTES) {
...
post("/uploadFileChunk", chunkBlob, {
params: { sessionId, path, fileName, offset: start, totalSize: file.size },
});
| File size | Route | How the name arrives | Result |
|---|---|---|---|
| ≤ 1.5 GiB | POST /uploadFileStream |
multipart header → latin1 | garbled |
| > 1.5 GiB | POST /uploadFileChunk |
query parameter | correct |
The same non-ASCII name comes out correct for a 2 GiB file and garbled for a 2 MB one. That was the
clue that pointed at the parser rather than at SSH or the filesystem.
Localised to one route
createFile, createFolder and renameItem read the name from req.body
(operation-routes.ts), not from a multipart header, and are unaffected. So the bug is exactly one
Busboy call.
Steps to reproduce
- Termix 26.10.5, File Manager enabled, any SSH host (reproduced on Debian and on Windows OpenSSH).
- Upload a small file named
中文文件.txtthrough the UI. - Run
lson the host: the name is䏿–‡æ–‡ä»¶.txt. - Upload the same name on a file larger than 1.5 GiB: the name is correct.
- Rename or create a file with the same name through the UI: correct.
Byte-level confirmation:
Buffer.from("中文文件.txt", "utf8").toString("latin1") // "䏿–‡æ–‡ä»¶.txt"
What I expected
The file appears with the name that was uploaded.
Suggested fix
- const bb = Busboy({ headers: req.headers });
+ const bb = Busboy({ headers: req.headers, defParamCharset: "utf8" });
Browsers always send part-header parameters as UTF-8 and never attach a charset to them, so
defParamCharset: "utf8" is correct for this API. preservePath stays false as before.
Related: the download side sends a non-standard Content-Disposition
// src/backend/download-routes.ts L318
res.setHeader(
"Content-Disposition",
`attachment; filename="${encodeURIComponent(fileName)}"`,
);
RFC 6266 does not allow percent-encoded text in a bare filename=. The standard form is
filename*=UTF-8''<pct-encoded>. Chromium deliberately percent-decodes this legacy shape, so Chrome
and Edge save the file correctly, but curl, several download managers and some mobile clients save
it literally as %E4%B8%AD%E6%96%87%E6%96%87%E4%BB%B6.txt. Emitting both parameters keeps every
client happy:
res.setHeader(
"Content-Disposition",
`attachment; filename="download"; filename*=UTF-8''${encodeURIComponent(fileName)}`,
);
filename="download" stays as a plain ASCII fallback for pre-RFC-6266 clients.
Environment
- Termix 26.10.5, Plugin File Manager 1.1.1 — still present on
mainat the lines quoted above - Docker on Linux,
ghcr.io/termix-ssh/termix:latest, SQLite - Remote hosts: Debian (OpenSSH) and Windows Server (OpenSSH) — same behaviour on both
- Clients: Chrome and Firefox on Windows and Linux, plus the macOS desktop app
Anything else
Found it in the running container's bundled dist/backend.js, then confirmed the same code on
main. Happy to open a PR with both changes.
- 主要言語
- TypeScript
- スター
- 1
- フォーク
- 3
- 平均マージ
- 1日 4時間
- マージ済み PR(30日)
- 6
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
Termix-SSH/Plugin-File-Manager のほかの issue
-
bug platform-desktop
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
Termix-SSH/Plugin-File-Manager#19 · コメント 2 件 ·
-
bug platform-desktop platform-docker
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
Termix-SSH/Plugin-File-Manager#16 · コメント 2 件 ·
-
bug platform-desktop platform-docker
難易度 2/5 1〜3時間 初心者へのやさしさ 66/100
Termix-SSH/Plugin-File-Manager#15 · コメント 2 件 ·
-
bug platform-docker
難易度 4/5 3〜5日 初心者へのやさしさ 45/100
Termix-SSH/Plugin-File-Manager#18 · コメント 1 件 ·
-
bug
難易度 3/5 1〜2日 初心者へのやさしさ 60/100
Termix-SSH/Plugin-File-Manager#14 · コメント 1 件 ·
Termix-SSH/Plugin-File-Manager の issue をすべて見る
似ている issue
-
area:plugins area:security area:skills bug has repro
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
anthropics/claude-code#105911 ·
メンテナーはふだん 1 日以内に返信
-
openspec-apply-change skill: when blocked, may select a ready artifact unrelated to missingArtifactsオープン
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
Fission-AI/OpenSpec#2165 ·
メンテナーはふだん 1 日以内に返信
-
🐞 bug 🧐 unconfirmed
難易度 2/5 1〜3時間 初心者へのやさしさ 66/100
Tencent/tdesign-vue-next#7032 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
gnosis/gnosis_vpn#661 ·
メンテナーはふだん 1 日以内に返信
-
content-freshness: staleOwnedContent doc comments still say 'complete receipt' (follow-up to #7912)オープンtriage:bug-verified
難易度 1/5 1時間未満 初心者へのやさしさ 92/100
メンテナーはふだん 1 日以内に返信