標題:「二進位」和「文字」檔案之間的區別
發表:真實
描述:探索「二進位」和「文字」檔案之間的差異。
標籤: 二進位, 文字, 編碼, unix
本文探討「二進位」和「文本」文件的主題。兩者有什麼區別(如果有的話)?對於「二進位」或「文字」檔案的構成是否有明確的定義?
我們從兩個候選文件開始我們的旅程,我們將其內容直觀地分別分類為「文字」和「二進位」資料:
echo "hello 🌍" > message
convert -size 1x1 xc:white png:white
我們建立了兩個檔案:一個名為message
的文件,其中包含文字內容「hello 🌍」 (包括 Unicode 符號「Earth Globe Europe-Africa」 ),以及一個帶有名為white
的單一白色像素的PNG 圖像。檔案副檔名被故意省略。
為了證明某些程式區分「文字」和「二進位」文件,請查看grep
如何更改其行為:
▶ grep -R hello
message:hello 🌍
▶ grep -R PNG
Binary file white matches
diff
做了類似的事情:
▶ echo "hello world" > other-message
▶ diff other-message message
1c1
< hello world
---
> hello 🌍
▶ convert -size 1x1 xc:black png:black
▶ diff black white
Binary files black and white differ
這些程式如何區分「文字」和「二進位」檔案?
在回答這個問題之前,讓我們先試著給一個定義。顯然,在基本檔案系統層級上,每個檔案只是位元組的集合,因此可以被視為二進位資料。另一方面,「文字」和「非文字」(以下簡稱「二進位」)資料之間的差異似乎對grep
或diff
之類的程式很有幫助,只要不弄亂終端模擬器的輸出即可。
所以也許我們可以從定義「文字」資料開始。從將文字作為Unicode 程式碼點序列的抽象概念開始似乎是合理的。程式碼點的範例包括k
、 ä
或א
等字符,以及%
、 ☢
或🙈
等特殊符號。要將給定的文字儲存為位元組序列,我們需要選擇一種編碼。如果我們希望能夠表示整個 Unicode 範圍,我們通常會選擇 UTF-8,有時會選擇 UTF-16 或 UTF-32。從歷史上看,僅支援當今 Unicode 一部分的編碼也很重要。最突出的是 US-ASCII 和 Latin1 (ISO 8859-1),但還有更多。所有這些在字節級別上看起來都不同。
僅給出文件的內容(而不是其建立方式的歷史記錄),因此我們可以嘗試以下定義:
如果檔案的內容由 Unicode 程式碼點的編碼序列組成,則該檔案稱為「文字檔案」。
這個定義有兩個實際問題。首先,我們需要所有可能的編碼的清單。其次,為了測試文件的內容是否以給定的編碼進行編碼,我們必須解碼文件的全部內容並查看是否成功。整個過程會非常緩慢。
事實證明,有一種更快的方法來區分文字和二進位文件,但它是以精確度為代價的。
要了解其工作原理,讓我們回到兩個候選文件並探索它們的位元組級內容。我使用hexyl
作為十六進位檢視器,但您也可以使用hexdump -C
:
請注意,這兩個檔案都包含 ASCII 範圍 ( 00
… 7f
) 以內和之外的位元組。例如, message
檔案中的四個位元組f0 9f 8c 8d
是 Unicode 程式碼點U+1F30D
(🌍) 的 UTF-8 編碼版本。另一方面, white
圖像開頭的位元組50 4e 47
是字元PNG
² 的簡單 ASCII 編碼版本。
很明顯,查看 ASCII 範圍之外的位元組不能用作檢測「二進位」檔案的方法。但是,這兩個文件之間存在差異。圖像檔案包含大量 NULL 位元組 ( 00
),而短文字訊息則不包含。事實證明,這可以變成一種簡單的啟發式方法來檢測二進位文件,因為許多編碼文字資料不包含任何 NULL 位元組(即使它可能是合法的)。
事實上,這正是diff
和grep
用來偵測「二進位」檔案的方法。以下巨集包含在diff
的原始碼 ( src/io.c
)中:
#define binary_file_p(buf, size) (memchr (buf, 0, size) != 0)
這裡, memchr(const void *s, int c, size_t n)
函數用於搜尋從buf
開始的記憶體區域的初始size
字節,以查找字元0
。為了進一步加快此過程,通常僅將檔案的前幾個位元組讀入緩衝區buf
(例如 1024 位元組)。總而言之, grep
和diff
使用以下啟發式方法:
如果檔案內容的前 1024 位元組不包含任何 NULL 位元組,則該檔案很可能是「文字檔案」。
請注意,有一些失敗的反例。例如,即使不太可能,UTF-8 編碼的文字也可以合法地包含 NULL 位元組。相反,某些特定的二進位格式(例如二進位PGM )不包含 NULL 位元組。此方法通常還將 UTF-16 和 UTF-32 編碼文字分類為“二進位”,因為它們使用 NULL 位元組對常見的 Latin-1 程式碼點進行編碼:
▶ iconv -f UTF-8 -t UTF-16 message > message-utf16
▶ hexdump -C message-utf16
00000000 ff fe 68 00 65 00 6c 00 6c 00 6f 00 20 00 3c d8 |..h.e.l.l.o. .<.|
00000010 0d df 0a 00 |....|
00000014
▶ grep . message-utf16
Binary file message-utf16 matches
然而,這種啟發式方法非常有用。我用 Rust 編寫了一個小型庫,它使用此方法的稍微改進的版本來快速確定給定檔案是否包含「二進位」或「文字」資料。它在我的程式bat
中用於防止“二進位”檔案轉儲到終端:
1 請注意,有些編碼會在檔案開頭寫入所謂的位元組順序標記(BOM),以指示編碼類型。例如,UTF-32 的小尾數變體使用ff fe 00 00
。這些 BOM 將有助於解決第二點,因為我們不需要解碼檔案的全部內容。不幸的是,加入 BOM 是可選的,並且許多編碼都沒有指定 BOM。
² 50 4e 47
是 PNG 格式的幻數的一部份。幻數與 BOM 類似,許多二進位格式在檔案開頭使用幻數來表示其類型。使用幻數來檢測某些類型的「二進位」檔案是file
工具使用的一種方法。