跳至主要內容
Skip to content

二進位外科手術 — 直接操作 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)位置
PNG89 50 4E 47 0D 0A 1A 0A[137, 80, 78, 71, 13, 10, 26, 10]前 8 bytes
JPEGFF D8 FF[255, 216, 255]前 3 bytes
GIF47 49 46 38[71, 73, 70, 56]前 4 bytes (GIF8)
PDF25 50 44 46[37, 80, 68, 70]前 4 bytes (%PDF)
ZIP50 4B 03 04[80, 75, 3, 4]前 4 bytes (PK)
WebP52 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 開發很重要?

  1. 安全性 (Security):防止惡意軟體偽裝成圖片上傳
  2. 修復損壞的檔案:有些下載下來的 PDF 或圖片打不開,可能是因為伺服器傳輸時多塞了一些空白字元在檔頭。你可以用這個方法把多餘的 Byte 切掉
  3. 解析自定義格式:讀取二進位檔案的結構,例如解析音訊檔案的 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 的高階限制,進入了更底層的「資料處理」領域。


← 上一章:互轉實戰 | 返回專題首頁 | 下一章:FormData 混合上傳 →