tar文件格式

Nine people can’t make a baby in a month.” (regarding the addition of more programmers to get a project completed faster)
九个人在一个月内不能生育。“(关于增加更多的程序员以更快地完成项目)

Rome was not built in a day。
传说特洛亚王子的后裔公主来茜西尔维娅被战神马尔斯所幸,生下了孪生兄弟罗马路斯和莱谟斯。当时的国王知道后杀死了他们的母亲,并把兄弟俩装进篮筐,扔进波涛翻滚的台伯尔河。是一只母狼救了他们,并用自己的乳汁哺育了兄弟两人。后来兄弟俩长大成人,为了替母亲报仇,他们想法杀死了国王,这两个天赋异禀的狼养大的孩子一夜之间建造了罗马城。
实际上,原文的意思是“罗马不是在一个白天建成的”,not “in a day”,应该是在晚上“in a night”
后来,随着时间的推移,这句话的意思慢慢转变,由于古罗马城建筑先进、繁复、建筑技术高超、设计精湛,后人用“罗马不是一天建成的”,表示很多先进技术、物质文明、甚至一个成就,都不是简单达成的,而是经由很多人、或者很多努力,才能够完成的。

tar文件格式解析

参考资料

https://www.gnu.org/software/tar/manual/html_node/Standard.html
http://en.academic.ru/dic.nsf/enwiki/11782062
http://www.mkssoftware.com/docs/man4/tar.4.asp
http://www.fileformat.info/format/tar/corion.htm

tar文件格式

http://www.moon-soft.com/program/FORMAT/comm/tar.htm

struct tar_header
{
    char name[100];//文件名
    char mode[8];
    char uid[8];
    char gid[8];
    char size[12]; //文件大小的八进制数的字符串形式
    char mtime[12];
    char chksum[8];
    char typeflag;
    char linkname[100];
    char magic[6];
    char version[2];
    char uname[32];
    char gname[32];
    char devmajor[8];
    char devminor[8];
    char prefix[155];
    char padding[12];
};

在tar文件中 文件信息的数据结构后跟着的就是文件的内容。文件内容以512字节为一个block进行分割,最后一个block不足部分以0补齐。直到所有文件都存储完了以后,最后存放一个全零的tar结构。

两个文件的合并成的tar包首先存放第一个文件的tar头结构,然后存储文件内容,接着存储第二个文件的tar头结构,然后存储文件内容,文件末尾还有一个全零的tar结构。

所有的tar文件大小应该都是512的倍数。

一个空文件打包后为512*3字节,包括一个tar结构头,一个全零的block存储文件内容,一个全零的tar结构。

size数组所能表示的大小问题

字符串以'\0'即为,意味着字符串只有11位八进制。
即为11111111111转化为十进制为8589934591,即size数组所能表示的文件大小字节数是8589934591 ~= 8GB
足够大,我们生活中绝大数使用完全可以满足。

问题1
为什么这些数据成员都是使用char类型而不是用int
提示:大小端对齐。

问题2
下面输出的结果是多少 ?为什么? 512

int main()
{
        printf("%u\n",sizeof(struct tar_header));
        return 0;
}

size为文件大小的八进制字节表示,例如文件大小为90个字节,那么这里就是八进制的90,即为132。
文件大小,修改时间,checksum都是存储的对应的八进制字符串,字符串最后一个字符为空格字符 。

checksum的计算方法为除去checksum字段其他所有的512-8共504个字节的ascii码相加的值再加上256(checksum当作八个空格,即8*0x20)。

没有数据的地方全部填充为'\0'。
往tar文件中添加一个空文件,那么tar文件将新增1024Byte大小(512Byte的tar_header加上512Byte的全0数据)

然而根据我们测试发现,在新版本tar工具中机制已经发生了一些变化。

pc@iZ25g2i2xsmZ:~$ tar --version
tar (GNU tar) 1.27.1
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Written by John Gilmore and Jay Fenlason.

在该版本的tar工具中 文件大小是10240Byte的整数倍。,文件大小只能是10240*n,n=0,1,2,3,4,...n

pc@iZ25g2i2xsmZ:~$ ll tt*

-rw-rw-r-- 1 pc pc     0 Feb  9 18:20 tt

pc@iZ25g2i2xsmZ:~$ tar cvf tt.tar tt tt tt tt tt tt tt tt tt tt tt tt tt

tt
tt
tt
tt
tt
tt
tt
tt
tt
tt
tt
tt
tt

pc@iZ25g2i2xsmZ:~$ ll tt*

-rw-rw-r-- 1 pc pc     0 Feb  9 18:20 tt
-rw-rw-r-- 1 pc pc 10240 Feb  9 18:24 tt.tar

空文件打包进tar文件的时候只占头512字节,无数据空间。

解包tar

解包更容易一些 直接有已经打包的tar文件即可进行。

int main()
{
    //printf("%lu\n",sizeof(struct tar_header));
    char buf[sizeof(struct tar_header)];
    FILE *fp = fopen("my.tar","rb");
    if( fp == NULL )
    {
        fprintf(stderr,"file not found");
        return 0;
    }
    fread(buf,1,sizeof(struct tar_header),fp);
    struct tar_header * head = (struct tar_header *)buf;
    printf("name %s,size %s\n",head->name,head->size);
    fclose(fp);
    return 0;
}

pc@iZ25g2i2xsmZ:~$ ll
-rw-rw-r-- 1 pc pc 716 Dec 16 14:15 main.c

pc@iZ25g2i2xsmZ:~$ tar cvf my.tar main.c
main.c
pc@iZ25g2i2xsmZ:~$ ls
a.out code main.c my.tar

pc@iZ25g2i2xsmZ:~$ ./a.out
name main.c,size 00000001314
pc@iZ25g2i2xsmZ:~$ ll
-rw-rw-r-- 1 pc pc 716 Dec 16 14:15 main.c
-rw-rw-r-- 1 pc pc 10240 Dec 16 14:16 my.tar

八进制的1314对应十进制的716。

在解包的案例中 我们只需要关注最核心的问题,文件名和文件大小。其余的属性待大家能完成这个基本功能之后再进行深入学习和理解。

那么问题来了 如何将8进制的字符串转化为十进制数呢?

int myatoi(const char * str)
{
    int ret = 0;
    int i ;
    // 每个位(数值*该位权值)之和,前11位为有效字符 最后为'\0'
    for (i = 0; i < 11; ++i)
        ret += (str[i]-'0') * (int)pow(8,11-i-1);
    return ret ;    
}

实现代码

#include <stdio.h>
#include <math.h>
#define HEAD_SIZE sizeof(struct tar_header)

int myatoi(const char * str,int fbase,int tobase)
{
    int ret = 0;
    int value = atoi(str);
    //先将该数转化为十进制 每个位(数值*该位权值)之和
    int i ;
    //前11位为有效字符 最后为'\0'
    for (i = 0; i < 11; ++i)
    {
        ret += (str[i]-'0') * (int)pow(fbase,11-i-1);
    }
    if(tobase == 10)
        return ret ;
    else //10 ---> n
    {
        fprintf(stderr,"function have't complete code");
        return 0;
    }
}

int main(int argc,char **argv)
{
    if( argc < 2 )
    {
        fprintf(stderr,"USEAGE %s filename",argv[0]);
        return 1;
    }
    //printf("%lu\n",sizeof(struct tar_header));
    char buf[sizeof(struct tar_header)];
    FILE *fp = fopen(argv[1],"rb");
    if( fp == NULL )
    {
        fprintf(stderr,"file not found");
        return 0;
    }
    unsigned int ret ;
    int need_write_len;
    while ( ret = fread(buf,1,sizeof(struct tar_header),fp) )
    {
        struct tar_header * head = (struct tar_header *)buf;
        if( head->name[0] == '\0')
        {
            printf("tar file END\n");
            break;
        }
        need_write_len = myatoi(head->size);
        printf("name %s,size 0%s is %d\n",head->name,head->size,need_write_len);
        FILE *data_file_p = fopen(head->name,"wb");
        if( data_file_p == NULL)
        {
            fprintf(stderr,"FILE %s can't write",head->name);
            continue;
        }
        while( need_write_len > 0)
        {
            ret = fread(buf,HEAD_SIZE,1,fp);
            if( ret == 0 )
                break;
            fwrite(buf,1,need_write_len > HEAD_SIZE ?HEAD_SIZE:need_write_len, data_file_p);
            printf("ret = %d need_write_len = %d",ret,need_write_len);
            need_write_len -= HEAD_SIZE;
        }
        fclose(data_file_p);
    }
    fclose(fp);
    return 0;
}

效果展示

pc@iZ25g2i2xsmZ:~/code$ gcc main2.c -lm
pc@iZ25g2i2xsmZ:~/code$ ls

a.out  main2.c  mmy.tar  my.tar  t2.tar  t.tar

pc@iZ25g2i2xsmZ:~/code$ ./a.out t2.tar

name t8,size 000000020000 is 8192
ret = 1 need_write_len = 8192ret = 1 need_write_len = 7680ret = 1 need_write_len = 7168ret = 1 need_write_len = 6656ret = 1 need_write_len = 6144ret = 1 need_write_len = 5632ret = 1 need_write_len = 5120ret = 1 need_write_len = 4608ret = 1 need_write_len = 4096ret = 1 need_write_len = 3584ret = 1 need_write_len = 3072ret = 1 need_write_len = 2560ret = 1 need_write_len = 2048ret = 1 need_write_len = 1536ret = 1 need_write_len = 1024ret = 1 need_write_len = 512name i,size 000000000012 is 10
ret = 1 need_write_len = 10tar file END

pc@iZ25g2i2xsmZ:~/code$ ll

total 100
drwxrwxr-x 2 pc pc  4096 Dec 17 23:07 ./
drwxr-xr-x 4 pc pc  4096 Dec 17 23:00 ../
-rwxrwxr-x 1 pc pc 16488 Dec 17 22:58 a.out*
-rw-rw-r-- 1 pc pc    10 Dec 17 23:07 i
-rw-rw-r-- 1 pc pc  1969 Dec 17 22:58 main2.c
-rw-rw-r-- 1 pc pc 10240 Dec 17 20:35 mmy.tar
-rw-rw-r-- 1 pc pc 10240 Dec 17 20:35 my.tar
-rw-rw-r-- 1 pc pc 20480 Dec 17 20:35 t2.tar
-rw-rw-r-- 1 pc pc  8192 Dec 17 23:07 t8
-rw-rw-r-- 1 pc pc 10240 Dec 17 20:35 t.tar

打包tar

了解了tar包的文件格式之后,打包tar就是一个较之于解包的逆向工程。

打包需要准备的参数更多 比如uid、gid、mtime、checksum 如果准备错误就会导致使用标准tar工具无法解包。
如果我们一开始就介入很多属性就无法让我们入手,我们还是只需要最常用的文件名和文件大小。

gzip

gzip不是一种算法

网上有人用“算法”来形容gzip,其实gzip根本不是一种算法,可以说它是一种压缩工具(software,见《Data Compression – The CompleteReference》 3.23节),或者说它是一种文件格式(file format,见RFC1952开篇),但绝对不是一种算法!!!这里澄清此概念。

我更倾向于将gzip作为一种文件格式来看待。因为毕竟对应的压缩结果是完全按照gzip压缩文件格式组织的,不管用什么软件去压,也不管用哪种实现库去压,只要最终结果是gzip的压缩结果,那么该结果肯定是按照gzip文件格式组织的!gzip文件格式可以理解为一只虾,一只虾分成三个部分:头、中间、尾巴,gzip也一样,有文件头,文件尾,中间保存被压缩之后的数据(参考文档:RFC1952)

本质上和zlib都是类似的按照自己的格式对deflate算法处理之后的数据进行的封装。GZIP中字节序是LSB方式,即Little-Endian小端字节序,与ZLIB中的相反。

1f8b 0800 8e0b 9858

58980b8e 换算成十进制 1486359438 是UNIX/LINUX时间戳
在线转换 http://tool.chinaz.com/Tools/unixtime.aspx : 2017/2/6 13:37:18

gzip文件格式分析

+------+------+------+-------+---+---+---+---+------+-----+
| ID1 | ID2 | CM | FLG | MTIME | XFL | OS |
+------+------+------+-------+---+---+---+---+------+-----+
if FLG.FEXTRA set
+---+---+=================================+
| XLEN | XLEN bytes of "extra field"...|
+---+---+=================================+
if FLG.FNAME set
+=========================================+
| original file name, zero-terminated... |
+=========================================+
if FLG.FCOMMENT set
+===================================+
| file comment, zero-terminated... |
+===================================+
if FLG.FHCRC set
+---+---+
| CRC16 |
+---+---+
+=======================+
|...compressed blocks...|
+=======================+
+---+---+---+---+---+---+---+---+
| CRC32 | ISIZE |
+---+---+---+---+---+---+---+---+

gzip文件格式标准文档

https://tools.ietf.org/html/rfc1952
http://www.faqs.org/rfcs/rfc1952.html
http://blog.itpub.net/10794571/viewspace-974302/
http://blog.csdn.net/jison_r_wang/article/details/52068607

CRC32(Cyclic Redundancy Check):用标准循环冗余校验算法对原始数据进行计算的结果。
ISIZE(InputSIZE):将原始数据大小对2^32取模的结果(因为只能用四个字节存结果,所以只能对2^32取模)。

zlib库简介

zlib库解析gzip文件

在contrib和test目录中有相应实现例子。contrib\minizip\中的minizip.c/miniunz.c实现压缩、解压ZIP文件。
test\minigzip.c实现压缩、解压gzip文件(用nmake运行win32\下的Makefile.msc可编译它)。
不同于zip格式,gzip格式(.gz)只用于压缩单个文件。有多个文件时,通常先将它们合并成一个tar文件,再用gzip进行压缩。contrib\untgz\untgz.c实现一次性解压.tar.gz(.tgz)文件。

源码zlib1211.tar.gz

解压 tar -zxvf zlib1211.tar.gz

pc@iZ25g2i2xsmZ:~/code/$ cd zlib-1.2.11/
pc@iZ25g2i2xsmZ:~/code/zlib-1.2.11$configure && make
pc@iZ25g2i2xsmZ:~/code/zlib-1.2.11/project$vim Makefile

all:
    gcc -o pcgzip gzjoin.c -I. -L. -lz

pc@iZ25g2i2xsmZ:~/code/zlib-1.2.11/project$ ls
gzjoin.c libz.a Makefile pcgzip zconf.h zlib.h

zlib中最简洁的两个函数

函数原型
int compress(Bytef *dest, uLongf *destLen, const Bytef *source, uLong sourceLen);

函数功能

compress函数将 source 缓冲区中的内容压缩到 dest 缓冲区。 sourceLen 表示source 缓冲区的字节大小。注意函数的第二个参数 destLen 是传址调用。当调用函数时,destLen表示 dest 缓冲区的大小,destLen > (sourceLen + 12)*100.1%。当函数退出后,destLen 表示压缩后缓冲区的实际大小。此时 destLen / sourceLen 正好是压缩率。

函数返回值

compress 若成功,则返回 Z_OK;若没有足够内存,则返回 Z_MEM_ERROR;若输出缓冲区不够大,则返回 Z_BUF_ERROR。

函数原型

int uncompress(Bytef *dest, uLongf *destLen, const Bytef *source, uLong sourceLen);

函数功能

uncompress 函数将 source 缓冲区的内容解压缩到 dest 缓冲区。sourceLen 是 source 缓冲区的大小(以字节计)。注意函数的第二个参数 destLen 是传址调用。当调用函数时,destLen 表示 dest 缓冲区的大小, dest 缓冲区要足以容下解压后的数据。在进行解压缩时,需要提前知道被压缩的数据解压出来会有多大。这就要求在进行压缩之前,保存原始数据的大小(也就是解压后的数据的大小)。这不是 zlib 函数库的功能,需要我们做额外的工作。当函数退出后, destLen 是解压出来的数据的实际大小。

函数返回值
uncompress 若成功,则返回 Z_OK ;若没有足够内存,则返回 Z_MEM_ERROR;若输出缓冲区不够大,则返回 Z_BUF_ERROR。若输入数据有误,则返回 Z_DATA_ERROR。

由于无法预测压缩包解压之后的大小,所以只能预先分配一段空间。
言外之意就是如果文件太大,就会导致解压失败(一般来说如果内存需求比较大,内存分配失败就会间接导致文件解压失败)。

pc@iZ25g2i2xsmZ:~/code/project$ gcc -o pcgzip 01test_compress.c -I. -L. -lz
pc@iZ25g2i2xsmZ:~/code/project$ ./pcgzip
compress OK
len:21,data:xNIMK
unpressing data
uncompress OK
len:13 data:abcdefghijkl

pc@iZ25g2i2xsmZ:~/code/project/tgzip$ ll
total 28
drwxrwxr-x 2 pc pc 4096 Feb 7 17:41 ./
drwxrwxr-x 3 pc pc 4096 Feb 7 17:02 ../
-rw-rw-r-- 1 pc pc 39 Feb 7 17:41 t.gz

头10字节+0字节拓展+21字节压缩数据+4字节CRC32+4字节文件大小

Linux压缩保留源文件的方法:

gzip –c filename > filename.gz

Linux解压缩保留源文件的方法:

gunzip –c filename.gz > filename

zlib库提供的读写gz文件的接口

  • gzFile定义

    typedef struct gzFile_s *gzFile;

  • 打开gz文件

    gzFile ZEXPORT gzopen OF((const char _path, const char _mode));

  • 读取gz文件

    int ZEXPORT gzread OF((gzFile file, voidp buf, unsigned len));

  • 写入gz文件

    int ZEXPORT gzwrite OF((gzFile file,voidpc buf, unsigned len));

  • 关闭gz文件

    int ZEXPORT gzclose OF((gzFile file));

重新认识地址和指针

指针的本质

int number = 0;
int *p = &number;

int **pp = &p;

如果有指针定义 int \*\*\*\*\*\*\*\* p ;呢?

char buf[512] = {0};
struct tar_header * head = (struct tar_header *)buf;
strcpy( head->name,"我是我,不一样的烟火");

获得结构体成员偏移量的方法

    struct n
    {
        int a;
        int b[10];
    };
    int main(void)
    {
        //说一说下面每一行代码的含义
        struct n *pnn,nn ;
        pnn = &nnl
        printf("%d \n %d \n",pnn->a,pnn->b);
        printf("%d \n %d \n",((struct n*)NULL)->a,((struct n*)NULL)->b);
        printf("%d \n %d \n",&((struct n*)NULL)->a,&((struct n*)NULL)->b);
        return 0;
    }
pnn->a == *((char *)pnn + offset(a))