先明確需求:用C
語言實現判斷檔案是文字檔案還是二進位制檔案,或者其他壓縮格式檔案。
檔案的型別
Linux
系統下,萬物皆檔案。
為了將所有的東西都能當成檔案來管理,Linux
系統將檔案分成了七種型別,分別如下:
型別 | 簡寫 | S_IFMT | st_mode | 說明 |
---|---|---|---|---|
塊裝置 | b | S_IFBLK | S_ISBLK(m) | 系統存取資料的介面裝置,例如硬碟 |
字元裝置 | c | S_IFCHR | S_ISCHR(m) | 串列埠的介面裝置,例如鍵盤、滑鼠、印表機、tty 終端 |
目錄 | d | S_IFDIR | S_ISDIR(m) | 資料夾 |
連結檔案 | l | S_IFLNK | S_ISLNK(m) | 符號連結,分軟連結和硬連結 |
套接字 | s | S_IFSOCK | S_ISSOCK(m) | 用於網路通訊 |
普通檔案 | - | S_IFREG | S_ISREG(m) | 分純文字檔案和二進位制檔案 |
命名管道 | p | S_IFIFO | S_ISFIFO(m) | 命名管道檔案 |
上表中第三、第四列是Linux
下使用stat
函式判斷檔案型別提供的一些巨集定義。如判斷一個檔案是否屬於普通檔案,可以使用下面的程式碼:
stat(pathname, &sb);
if ((sb.st_mode & S_IFMT) == S_IFREG) {
/* Handle regular file */
}
或者直接使用:
stat(pathname, &sb);
if (S_ISREG(sb.st_mode)) {
/* Handle regular file */
}
但是我們的需求是判斷檔案是否屬於文字檔案還是二進位制檔案。而這兩種都屬於S_IFREG
普通檔案,因此無法使用上面的方法進行判斷。
萬能的file命令
file
命令是Linux
下用來檢測檔案型別的一個內建的命令。
大概原理就是讀取一個檔案的前面1024
個位元組,然後根據magic
(/etc/magic
或者 /usr/share/misc/magic
) 裡對應的規則分析出檔案頭,並列印到螢幕上。
使用也很簡單,直接file
後面跟上檔名即可:
[root@ck08 ~]# file anaconda-ks.cfg
anaconda-ks.cfg: ASCII text
[root@ck08 ~]# file tls.pcap
tls.pcap: tcpdump capture file (little-endian) - version 2.4 (Ethernet, capture length 262144)
[root@ck08 ~]# file zlib-1.2.11.tar.gz
zlib-1.2.11.tar.gz: gzip compressed data, was "zlib-1.2.11.tar", from Unix, last modified: Mon Jan 16 01:36:58 2017, max compression
[root@ck08 ~]# file /usr/bin/grep
/usr/bin/grep: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=bb5d89868c5a04ae48f76250559cb01fae1cd762, stripped
從上面的示例中,可以看出file
命令很強大,幾乎可以識別出檔案的詳細型別,甚至是編碼,壓縮格式,大小端等具體的資訊。
因此,這個命名是符合我們的需求的。
但是,我們需要的是C
語言實現,因此,不得不研究magic
的檔案頭規則。
magic檔案規則
檔案中的每行都指定了一個規則測試去檢驗檔案型別,這個規則由4
個域指定。分別為offset
、type
、test
、message
。
offset
- 指定由檔案起始的第幾個
byte
開始檢驗。
- 指定由檔案起始的第幾個
type
要進行檢驗的資料型別,即由
offset
那個byte
開始的那個資料型別是什麼。具體有哪些資料型別,可以參考magic(5)
。常用的資料型別有byte
:一個byte
的值short
:兩個byte
的值long
:四個byte
的值string
:字串
test
- 檢驗值。用於檢驗
offset
下的type
是否是這個test
值。使用C
語言的數值或字元表示形式。
- 檢驗值。用於檢驗
message
- 用於顯示檢驗結果的資訊顯示。
如果
type
為數值型別,那麼其後面可新增&value
,表示先與後面的test
值進行'與'操作,再進行比較。如果type
為字串型別,則其後可跟/[Bbc]*
,/b
表示忽略空格,/c
表示忽略字母大小寫。
如果test
的值為數值型別,可以數值前新增=
,<
,>
,&
,^
,~
,分別表示相等、小於、大於、與操作、異或操作、取反操作。
如果test
的值為字串型別,可以在其前新增=
、<
、>
。
例如,ELF檔案的magic表示為:# ELF #0string ELF ELF 0 string \177ELF ELF >4 byte 1 32-bit >4 byte 2 64-bit >5 byte 1 LSB >5 byte 2 MSB >16 short 0 unknown type >16 short 1 relocatable >16 short 2 executable >16 short 3 dynamic lib >16 short 4 core file >18 short 0 unknown machine >18 short 1 WE32100 >18 short 2 SPARC >18 short 3 80386 >18 short 4 M68000 >18 short 5 M88000 >20 long 1 Version 1 >36 long 1 MathCoPro/FPU/MAU Required
發現沒有,這個magic
實在不是人類能夠輕易讀懂的,Linux
核心提供了libmagic
庫用來解析magic
檔案,但是我嘗試了CentOS 7
和Ubuntu20.04
,都沒有成功將程式跑起來(gcc
編譯報告找不到magic.h
),而我的需求是要求一種比較通用的方法,不僅要求在Linux
上可以工作,還要在Windows
和AIX
上有比較好的表現,因此,企圖實現一套類似file
的原理的路行不通。
那麼,就沒有一種比較通用的方案來實現對檔案型別的判斷嗎?
網上很多資料說可以根據檔案的字元來判斷,如果檔案中包含\x00
,則一定是二進位制或壓縮檔案,否則的話就是普通文字檔案。
在大多數時候,這個規則是成立的。但是如果普通文字檔案的編碼是UTF-16
或者UTF-32
,則又要哭暈在廁所了。
因此,這種方案不靠譜。
特殊檔案的header
libmagic
的思路,說白了,就是根據檔案頭的編碼進行判斷,也就是說,只要我們知道某些特殊的檔案頭編碼,對這些特殊的檔案頭進行匹配,如果能匹配上,就代表它是特殊檔案,否則的話,就是普通文字檔案,按照這個思路,也能實現libmagic
庫一樣的效果。
在各種型別檔案頭標準編碼這篇文章裡,列舉了一些常見的檔案頭編碼。比如常見的jar
包、rar
、zip
壓縮檔案,都是以504B0304
開頭,而Linux
下的二進位制檔案,包括.o
,.a
,.so
,以及coredump
檔案,都是屬於ELF
檔案,檔案頭都是7F454C46
。但是windows
的可執行檔案開頭卻是504B0304
,AIX
系統複雜些,但開頭三個位元組基本都是01DF00
。因此,根據這些,就可以做很多區分了。
事實上,對於windows
系統來說,根據字尾其實就能區分出來,而Unix
系統的約定俗成的字尾規則也能區分出很多的檔案,比如一個檔案字尾為.rpm
,你無論如何不會將其當成文字檔案,看到.o
就知道是二進位制目標檔案,.so
是動態連結庫。比較有歧義的可能只是一些可執行檔案,比如ls
、grep
、a.out
這些字尾不代表實際意義的檔案。
因此,我們的思路也就明確了,分兩個步驟,首先可以大致根據檔案字尾區分出一些特別明顯的二進位制檔案、壓縮檔案,然後針對檔案的header
做進一步的區分。
C語言程式碼實現
程式碼實現如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef int boolean;
#define FALSE 0
#define TRUE 1
/*列舉一些常見的檔案頭,可以自行擴充套件,比如放到某個配置檔案中*/
static const char *with_suffix[] = {".gz", ".rar", ".exe", ".bz2",
".tar", ".xz", ".Z", ".rpm", ".zip",
".a", ".so", ".o", ".jar", ".dll",
".lib", ".deb", ".I", ".png",".jpg",
".mp3", ".mp4", ".m4a", ".flv", ".mkv",
".rmvb", ".avi", ".pcap", ".pdf", ".docx",
".xlsx", ".pptx", ".ram", ".mid", ".dwg",
NULL};
/*判斷某個字串是否擁有指定字尾*/
static boolean string_has_suffix(const char *str, const char *suffix) {
int n, m, i = 0;
char ch = '\0';
if (str == NULL || suffix == NULL)
{
return FALSE;
}
n = strlen(str);
m = strlen(suffix);
if (n < m) {
return FALSE;
}
for (i = m-1; i >= 0; i--) {
if (suffix[i] != str[n - m + i]) {
return FALSE;
}
}
return TRUE;
}
/*判斷檔案是否具有特殊字尾*/
static boolean file_has_spec_suffix(const char *fname) {
const char **suffix = NULL;
suffix = with_suffix;
while (*suffix)
{
if (string_has_suffix(fname, *suffix))
{
return TRUE;
}
suffix++;
}
return FALSE;
}
/*判斷檔案是否具有特殊檔案頭*/
static boolean file_has_spec_header(const char *fname) {
FILE *fp = NULL;
size_t len = 0;
char buf[16] = {0};
int i = 0;
boolean retval = FALSE;
if ((fp = fopen(fname, "r")) == NULL ){
return FALSE;
}
len = sizeof(buf) - 1;
if (fgets(buf, len, fp) == NULL ) {
return FALSE;
}
if (len < 4)
{
return FALSE;
}
#if defined(__linux__)
//ELF header
if (memcmp(buf, "\x7F\x45\x4C\x46", 4) == 0) {
return TRUE;
}
#elif defined(_AIX)
//executable binary
if (memcmp(buf, "\x01\xDF\x00", 3) == 0) {
return TRUE;
}
#elif defined(WIN32)
// standard exe file, actually, won't go into this case
if (memcmp(buf, "\x4D\x5A\x90\x00", 4) == 0)
{
return TRUE;
}
#endif
if (memcmp(buf, "\x50\x4B\x03\x04", 4) == 0) {
//maybe archive file, eg: jar zip rar sec.
return TRUE;
}
return FALSE;
}
/*測試程式
* 從命令列輸入一個檔案,返回該檔案的型別
*/
int main(int argc, const char **argv) {
if (argc < 2) {
printf("usgae: need target file\n");
exit(-1);
}
const char *fname = argv[1];
if (file_has_spec_suffix(fname)) {
printf("file %s have special suffix, maybe it's a binary or archive file\n", fname);
} else if (file_has_spec_header(fname)) {
printf("file %s have special header, maybe it's a binary or archive file\n", fname);
} else {
printf("file %s should be a text file\n", fname);
}
return 0;
}
執行結果如下所示,可以對比file命令,做一個參考:
[root@ck08 ctest]# gcc -o magic magic.c
[root@ck08 ctest]# ./magic ~/anaconda-ks.cfg
file /root/anaconda-ks.cfg should be a text file
[root@ck08 ctest]# ./magic ~/tls.pcap
file /root/tls.pcap have special suffix, maybe it's a binary or archive file
[root@ck08 ctest]# ./magic ~/zlib-1.2.11.tar.gz
file /root/zlib-1.2.11.tar.gz have special suffix, maybe it's a binary or archive file
[root@ck08 ctest]# ./magic /usr/bin/grep
file /usr/bin/grep have special header, maybe it's a binary or archive file
[root@ck08 ctest]# ./magic kafka_2.12-2.8.0.jar
file kafka_2.12-2.8.0.jar have special suffix, maybe it's a binary or archive file
[root@ck08 ctest]# ./magic kafka_2.12-2.8.0.jar.1
file kafka_2.12-2.8.0.jar.1 have special header, maybe it's a binary or archive file