乐趣区

关于文件:判断文件为文本文件还是二进制文件C语言实现

先明确需要:用 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 个域指定。别离为offsettypetestmessage

  • 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 7Ubuntu20.04,都没有胜利将程序跑起来(gcc编译报告找不到 magic.h), 而我的需要是要求一种比拟通用的办法,不仅要求在Linux 上能够工作,还要在 WindowsAIX上有比拟好的体现,因而,希图实现一套相似 file 的原理的路行不通。
那么,就没有一种比拟通用的计划来实现对文件类型的判断吗?
网上很多材料说能够依据文件的字符来判断,如果文件中蕴含 \x00,则肯定是二进制或压缩文件,否则的话就是一般文本文件。
在大多数时候,这个规定是成立的。然而如果一般文本文件的编码是 UTF-16 或者 UTF-32,则又要哭晕在厕所了。
因而,这种计划不靠谱。

非凡文件的 header

libmagic的思路,说白了,就是依据文件头的编码进行判断,也就是说,只有咱们晓得某些非凡的文件头编码,对这些非凡的文件头进行匹配,如果能匹配上,就代表它是非凡文件,否则的话,就是一般文本文件,依照这个思路,也能实现 libmagic 库一样的成果。
在各种类型文件头规范编码这篇文章里,列举了一些常见的文件头编码。比方常见的 jar 包、rarzip压缩文件,都是以 504B0304 结尾,而 Linux 下的二进制文件,包含 .o,.a,.so,以及coredump 文件,都是属于 ELF 文件,文件头都是 7F454C46。然而windows 的可执行文件结尾却是 504B0304AIX 零碎简单些,但结尾三个字节根本都是 01DF00。因而,依据这些,就能够做很多辨别了。
事实上,对于 windows 零碎来说,依据后缀其实就能辨别进去,而 Unix 零碎的约定俗成的后缀规定也能辨别出很多的文件,比方一个文件后缀为 .rpm,你无论如何不会将其当成文本文件,看到.o 就晓得是二进制指标文件,.so是动态链接库。比拟有歧义的可能只是一些可执行文件,比方 lsgrepa.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…
退出移动版