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 類型」欄 |
|---|---|---|---|
.png | image/png | .json | application/json |
.jpg | image/jpeg | .csv | text/csv |
.jpeg | image/jpeg | .bin | application/octet-stream |
.txt | text/plain | .heic | 未知 |
.zip | application/x-zip-compressed | .log | 未知 |
.pdf | application/pdf | .7z | 未知 |
.md | text/markdown | .dat | 未知 |
.gif | image/gif | .xyz9 | 未知 |
.webp | image/webp | .html | text/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 類型」,不如叫「瀏覽器按副檔名給的那串」。
它到底讀了多少東西
把模板攤開看會更清楚:第 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 位元組,結尾不帶換行。