共计 5893 个字符,预计需要花费 15 分钟才能阅读完成。
先明确需要:用 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
参考文档
- https://linux.die.net/man/5/m…
- https://man7.org/linux/man-pa…
- https://www.cnblogs.com/gwind…