雜記 › 技術筆記 › 70 個位元組沒動,MIME 類型換了 13 次:副檔名才是唯一輸入

70 個位元組沒動,MIME 類型換了 13 次:副檔名才是唯一輸入

70 個位元組沒動,類型換了 13 次

先放一個不太體面的現場:同一份 70 位元組的 PNG 檔頭,我把它複製成 fi_x.png、fi_x.jpg、fi_x.gif……一共 18 個副檔名,內容一個位元組都沒改,逐份經真實的檔案選擇器餵進 file-info。頁面給出的「MIME 類型」欄有 13 種不同寫法,剩下 5 個副檔名落到「未知」。那 70 個位元組裡明明寫著 \x89PNG\r\n\x1a\n,這是任何一本格式書都會告訴你的 PNG 魔數,可這一欄對它的態度是——看都不看。

這件事的麻煩不在於「顯示得不好看」。我們把這類頁面當一個「檔案資訊檢視器」用,預設它讀過檔案、預設它給出的類型是從內容推出來的,然後在下游做判斷:是圖片就直接渲染,是壓縮包就送去解壓,是未知就退回人工。而 file-info 的「MIME 類型」欄其實只是把瀏覽器按副檔名給的那串轉抄到表格裡——副檔名是唯一的輸入,魔數全程沒有參與。

把 18 個副檔名攤開看

完整對照如下,左欄是我改出來的副檔名,右欄是頁面那一欄給出的字串。整份實驗裡唯一的變數就是檔名最後那幾個字母:

副檔名頁面「MIME 類型」欄副檔名頁面「MIME 類型」欄
.pngimage/png.jsonapplication/json
.jpgimage/jpeg.csvtext/csv
.jpegimage/jpeg.binapplication/octet-stream
.txttext/plain.heic未知
.zipapplication/x-zip-compressed.log未知
.pdfapplication/pdf.7z未知
.mdtext/markdown.dat未知
.gifimage/gif.xyz9未知
.webpimage/webp.htmltext/html

13 種類型加 5 個「未知」,湊滿 18 行。.jpg 與 .jpeg 給了同一個字串,說明這層映射本來就帶合併;而 .heic、.log、.7z、.dat、.xyz9 這 5 個副檔名一律落到「未知」——前 4 個並不是什麼冷門格式,iPhone 照片和日誌檔案都算常見,只是這臺機器上的瀏覽器沒有為它們準備映射。換個環境跑同一批檔名,落到「未知」的那幾個可能會變,這也是我把 資訊檔讀取 當成對照臺而不是結論的原因之一。

魔數與副檔名打架,贏的總是副檔名

光看同向實驗還不夠,真正能定口徑的是把兩種格式的指紋故意錯開:內容是一種格式、名字是另一種。我做了 4 組,頁面給出的類型每次都跟著名字走:

JPEG 檔頭 FF D8 FF E0        命名 fi_photo.png   → image/png   22 B
ZIP 檔頭  50 4B 03 04        命名 fi_archive.jpg → image/jpeg  39 B
GIF 檔頭  47 49 46 38 39 61  改名 .zip           → application/x-zip-compressed  10 B
PNG 檔頭  89 50 4E 47 0D 0A  改名 .gif           → image/gif   70 B

一個位元組都沒改,改的只是檔名。那一欄的取值口徑在模板裡只有一處:第 139 行的 type: f.type || '未知'——直接把 File 物件上的字串抄進表格;而 FileReader、readAs、ArrayBuffer、DataView 這些真正會碰到內容的介面,在整份模板裡的命中次數各為 0,壓根不存在一條可以「嗅探」的分支。所以與其叫它「MIME 類型」,不如叫「瀏覽器按副檔名給的那串」。

18 個副檔名的落點 內容是同一份 13 種有類型 5 個未知 同一份 70 位元組 PNG 檔頭 未知來自 .heic .log .7z .dat .xyz9 圖 1:副檔名換來 13 種類型 5 個未知

它到底讀了多少東西

把模板攤開看會更清楚:第 136 行把 file 物件存進陣列,第 137 到 140 行只取了 4 個欄位——name、size、type、lastModified,這個物件此後再也沒有被讀過一次。表格只有 4 欄不是設計上克制,而是輸入只有這 4 個。我按這個口徑去核對了幾件常被預設存在的事:

  • 行數、字元數、詞數、雜湊值、編碼判定、圖片寬高、建立時間、權限、完整路徑——這些欄在模板裡的命中次數都是 0。沒有這一欄,也就談不上「讀取失敗時顯示什麼」,問「MD5 那格為什麼是空的」是問一個不存在的格子。
  • 沒有格式白名單:accept= 出現 0 次,任何副檔名都進清單;同樣也沒有體積上限,實測 6442450944 位元組的稀疏檔案照常列出,顯示 6.00 GB,沒有任何拒絕分支或降級提示。
  • 沒有數量上限,也沒有去重:同一個 10 位元組的檔案餵 100 次,得到 100 行、總大小 1000 B,重名提示也沒有。想靠它做「這批檔案有沒有重複」是白費的。
  • 把整個目錄餵進去只得到 1 行:0 B 加「未知」,不遞迴、不合併子檔案大小,所以別拿它統計資料夾總大小——那條路徑上它只會給你一個 0。

大小那一欄走 1024,標籤卻寫 KB

閾值全在第 163 到 166 行,是 1024 的冪;模板裡 1000 出現 0 次、KiB 字樣 0 次,標籤卻寫 KB / MB / GB。於是標稱 1 GB 的 1000000000 位元組顯示 953.67 MB,與 1073741824 差 73741824 位元組,1000000 顯示 976.6 KB 還停在 KB 檔;進位點也在 1024,1023 仍顯示 1023 B。

小數位分 3 檔:B 檔 0 位、KB 檔 1 位、MB 與 GB 檔 2 位,toFixed 的進位讓讀數總比實際更大一點——2047 顯示 2.0 KB,1048575 顯示 1024.0 KB,差 1 個位元組就是進不了 MB 檔。第 166 行是最後一個 return,沒有 TB 檔,1099511627776 顯示 1024.00 GB。

三個當場會問到的細節

Q:為什麼我這邊的 .zip 顯示的不是 application/zip?

那一欄只是轉抄,不做正規化。本機 Chrome 加 Windows 的 .zip 給的是 application/x-zip-compressed,.tar.gz 只按最後一段副檔名給 application/x-gzip;顯式傳 application/zip 也照常原樣顯示,換一臺瀏覽器那串字樣就可能不同。

Q:類型和時間都讀不到時,格子裡放什麼?

三欄各走各的:type 為空字串才落到「未知」,application/octet-stream 原樣顯示;lastModified 為 0 不做預設值判斷,直接渲染成 1970-01-01 08:00:00;0 位元組顯示 0 B,沒有「空」這種寫法。

Q:匯出的文字會跟著螢幕排序走嗎?

不跟。第 245 行遍歷的是原始陣列:32 個檔案按修改時間升序後屏首是 rn_01.png,匯出序號 1 仍是 mx_gifmagic.zip;16 個檔案那批匯出 23 行、1098 個字元、1208 位元組,結尾不帶換行。

📎 相關文章

轉 JPG 時透明被強制填底色:91 個品質檔位在 PNG 面前不參與編碼 2026-10-23 「全部清除」後還剩 553 位元組,「只去掉位置信息」連縮圖都沒動 2026-10-23 序號位數只是下限、重名時 (2) 插在後綴之前:批次重新命名 2000 個檔案的實測口徑 2026-10-23 兩條憑證指紋一條 95 個字元一條 64 個:差的 31 個全是冒號,摘要物件是整張 DER 2026-10-23 TOTP 口令差 30 秒就換碼:±1 期反查只倒推方向,演算法也只過 SHA-1 一檔 2026-10-23