二進位外科手術 — 直接操作 ArrayBuffer
現在我們要當一次「二進位外科醫生」,直接切開 Blob,修改裡面的數字。
操作流程是:
Blob (封存的包裹) → ArrayBuffer (打開包裹放在桌上) → Uint8Array (戴上手套的手,直接修改)一、 手術工具介紹
| 工具 | 比喻 | 用途 |
|---|---|---|
| ArrayBuffer | 手術台 | 一塊固定大小的記憶體空間 |
| Uint8Array | 手術刀 | 用 0-255 的視角讀寫這塊記憶體 |
| DataView | 精密儀器 | 可以指定 endianness 讀取多 byte 數值 |
二、 實驗一:把文字當數字改 (字串變身術)
假設我們有一段文字 "ABC"。在 ASCII 編碼中:
- A = 65
- B = 66
- C = 67
如果我們把第一個數字 65 加 1,它就會變成 66,也就是 'B'。
我們完全不使用字串取代 (replace),而是直接改記憶體裡的數字:
javascript
async function modifyBinary() {
// 1. 準備原始 Blob
const originalBlob = new Blob(["ABC"], { type: "text/plain" });
// 2. 轉成 ArrayBuffer (取得記憶體控制權)
const buffer = await originalBlob.arrayBuffer();
// 3. 建立 "編輯視圖" (Uint8Array)
// 這讓我們可以像操作陣列一樣操作記憶體
const bytes = new Uint8Array(buffer);
console.log("修改前:", bytes); // Uint8Array(3) [65, 66, 67]
// --- 手術開始 ---
bytes[0] = bytes[0] + 1; // 把第一個 byte (A/65) 加 1
// 現在變成 66 (也就是 'B')
// --- 手術結束 ---
console.log("修改後:", bytes); // Uint8Array(3) [66, 66, 67]
// 4. 打包回 Blob
const newBlob = new Blob([buffer], { type: "text/plain" });
// 驗證結果
const newText = await newBlob.text();
console.log("最終文字:", newText); // "BBC"
}
modifyBinary();結論:電腦眼裡沒有文字,只有數字運算。我們剛才透過數學運算修改了文字。
三、 實驗二:讀取「檔案指紋」(Magic Numbers)
這是前端開發(尤其是做檔案上傳功能時)最實用的技巧。
問題情境
很多時候使用者會「騙」你:他把 virus.exe 改名成 photo.png 然後上傳。如果你只檢查副檔名 (.png),你就被騙了。
但是,檔案的頭幾個 Bytes (Magic Numbers) 是騙不了人的。
Magic Numbers 是什麼?
每種檔案格式在開頭都會放置固定的「簽名」,這就是 Magic Numbers。例如:
- PNG:開頭 8 個 bytes 是
89 50 4E 47 0D 0A 1A 0A - JPEG:開頭 2 個 bytes 是
FF D8 - PDF:開頭 4 個 bytes 是
25 50 44 46(就是%PDF的 ASCII)
驗證程式碼
javascript
async function checkIsRealPng(file) {
// 1. 我們只需要讀取前 8 個 bytes,不用讀整張圖 (節省效能)
const buffer = await file.slice(0, 8).arrayBuffer();
// 2. 轉成 Uint8Array
const bytes = new Uint8Array(buffer);
console.log("檔案的前 8 個數字:", bytes);
// PNG 的標準簽名 (Magic Numbers)
// 137, 80, 78, 71, 13, 10, 26, 10 (16進位轉成10進位)
const pngSignature = [137, 80, 78, 71, 13, 10, 26, 10];
// 3. 比對數字
const isPng = pngSignature.every((byte, index) => byte === bytes[index]);
if (isPng) {
console.log("✅ 驗證通過:這是一張真正的 PNG");
} else {
console.log("❌ 警告:這不是 PNG,可能是假冒的檔案!");
}
return isPng;
}
// 測試:建立一個假的 PNG
const fakeFile = new Blob(["這不是圖片"], { type: "image/png" });
checkIsRealPng(fakeFile); // ❌ 警告四、 常見檔案類型的 Magic Numbers 對照表
| 檔案類型 | Magic Numbers (Hex) | Magic Numbers (Decimal) | 位置 |
|---|---|---|---|
| PNG | 89 50 4E 47 0D 0A 1A 0A | [137, 80, 78, 71, 13, 10, 26, 10] | 前 8 bytes |
| JPEG | FF D8 FF | [255, 216, 255] | 前 3 bytes |
| GIF | 47 49 46 38 | [71, 73, 70, 56] | 前 4 bytes (GIF8) |
25 50 44 46 | [37, 80, 68, 70] | 前 4 bytes (%PDF) | |
| ZIP | 50 4B 03 04 | [80, 75, 3, 4] | 前 4 bytes (PK) |
| WebP | 52 49 46 46 | [82, 73, 70, 70] | 前 4 bytes (RIFF) |
五、 封裝通用驗證器
以下是一個可以直接用在專案中的檔案驗證器:
javascript
const FILE_SIGNATURES = {
"image/png": {
signature: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
offset: 0,
},
"image/jpeg": {
signature: [0xff, 0xd8, 0xff],
offset: 0,
},
"image/gif": {
signature: [0x47, 0x49, 0x46, 0x38], // GIF8
offset: 0,
},
"application/pdf": {
signature: [0x25, 0x50, 0x44, 0x46], // %PDF
offset: 0,
},
"image/webp": {
signature: [0x52, 0x49, 0x46, 0x46], // RIFF
offset: 0,
},
};
/**
* 驗證檔案是否符合其聲稱的 MIME Type
* @param {File} file - 要驗證的檔案
* @param {string} expectedType - 期望的 MIME Type
* @returns {Promise<boolean>}
*/
async function validateFileType(file, expectedType) {
const config = FILE_SIGNATURES[expectedType];
if (!config) {
console.warn(`未知的檔案類型: ${expectedType},跳過驗證`);
return true;
}
const { signature, offset } = config;
const length = signature.length;
// 只讀取需要的部分
const buffer = await file.slice(offset, offset + length).arrayBuffer();
const bytes = new Uint8Array(buffer);
return signature.every((byte, index) => byte === bytes[index]);
}
// 使用範例
async function handleUpload(file) {
const isValid = await validateFileType(file, file.type);
if (!isValid) {
alert("檔案類型不符,請上傳正確的檔案格式");
return;
}
// 繼續上傳流程...
}六、 為什麼這對 Web 開發很重要?
- 安全性 (Security):防止惡意軟體偽裝成圖片上傳
- 修復損壞的檔案:有些下載下來的 PDF 或圖片打不開,可能是因為伺服器傳輸時多塞了一些空白字元在檔頭。你可以用這個方法把多餘的 Byte 切掉
- 解析自定義格式:讀取二進位檔案的結構,例如解析音訊檔案的 metadata
七、 進階:使用 DataView 讀取多 byte 數值
當你需要讀取一個跨越多個 bytes 的數值(例如一個 32-bit 整數),就需要考慮 位元組序 (Endianness):
- Big Endian:高位 byte 在前(網路傳輸常用)
- Little Endian:低位 byte 在前(x86 處理器常用)
javascript
async function readFileHeader(file) {
const buffer = await file.slice(0, 16).arrayBuffer();
const view = new DataView(buffer);
// 讀取一個 32-bit 無符號整數 (4 bytes)
// 第二個參數 true = little endian, false = big endian
const value = view.getUint32(0, false); // Big Endian
console.log("檔頭數值:", value.toString(16));
}總結
| 概念 | 說明 |
|---|---|
| ArrayBuffer | 固定大小的記憶體空間(手術台) |
| Uint8Array | 用 0-255 視角操作記憶體(手術刀) |
| Magic Numbers | 檔案開頭的簽名,用來辨識真正的檔案類型 |
| DataView | 可指定 endianness 讀取多 byte 數值 |
透過直接操作這些數字,你就跳脫了 HTML/JS 的高階限制,進入了更底層的「資料處理」領域。