如何在C++中解析tar文件

16

我想要做的是下载一个有多个目录和每个目录下有2个文件的.tar文件。问题是我找不到一种方法在不解压文件(使用tar)的情况下读取tar文件。

完美的解决方案应该是像这样的:

#include <easytar>

Tarfile tar("somefile.tar");
std::string currentFile, currentFileName;
for(int i=0; i<tar.size(); i++){
  file = tar.getFileText(i);
  currentFileName = tar.getFileName(i);
  // do stuff with it
}

我可能不得不自己编写这个,但任何想法都将不胜感激..


1
"man tar" 告诉我 "-t 将归档内容列表输出到标准输出。" 这是你想要的吗? - Potatoswatter
1
我实际想要的是相反的:从标准输入读取tar文件。 - Brendan Long
3个回答

35
我经过一番努力,自己搞清楚了这个问题。 tar文件规范 实际上告诉你所需的一切信息。
首先,每个文件都以512字节的头文件开头,因此您可以使用char[512]或指向较大char数组中某个位置的char*来表示它(例如,如果您将整个文件加载到一个数组中)。
头文件如下:
location  size  field
0         100   File name
100       8     File mode
108       8     Owner's numeric user ID
116       8     Group's numeric user ID
124       12    File size in bytes
136       12    Last modification time in numeric Unix time format
148       8     Checksum for header block
156       1     Link indicator (file type)
157       100   Name of linked file

如果您想获取文件名,可以使用 string filename(buffer[0], 100); 来获取。文件名是用空格填充的,因此您可以检查是否至少有一个空格,如果要节省空间,则可以省略大小。

现在我们要知道它是文件还是文件夹。"链接指示符"字段包含了这些信息,所以:

// Note that we're comparing to ascii numbers, not ints
switch(buffer[156]){
    case '0': // intentionally dropping through
    case '\0':
        // normal file
        break;
    case '1':
        // hard link
        break;
    case '2':
        // symbolic link
        break;
    case '3':
        // device file/special file
        break;
    case '4':
        // block device
        break;
    case '5':
        // directory
        break;
    case '6':
        // named pipe
        break;
}

此时,我们已经获取了有关目录的所有信息,但是我们还需要从普通文件中获得一个东西:实际的文件内容。
文件长度可以以两种不同的方式存储,一种是作为一个0或空格填充的空终止八进制字符串,另一种是“通过设置数值字段最左边字节的高位比特来表示的基256编码”。

使用ASCII数字将数值编码为八进制数,前导零。由于历史原因,应该使用最后的NUL或空格字符。因此,虽然有12个字节用于存储文件大小,但只能存储11个八进制数字。这给归档文件的最大文件大小为8 GB。为了克服这个限制,2001年星号引入了一种基256编码,它通过设置数值字段最左边字节的高位比特来表示。GNU-tar和BSD-tar遵循了这个想法。此外,在1988年第一个POSIX标准之前的tar版本中,使用空格而不是零填充值。

以下是如何读取八进制格式的方法,但我还没有为基256版本编写代码:
// in one function
int size_of_file = octal_string_to_int(&buffer[124], 11);

// elsewhere
int octal_string_to_int(char *current_char, unsigned int size){
    unsigned int output = 0;
    while(size > 0){
        output = output * 8 + *current_char - '0';
        current_char++;
        size--;
    }
    return output;
}

好的,现在我们已经拥有了除实际文件内容之外的所有内容。我们所要做的就是从tar文件中获取下一个size字节的数据,然后我们就可以得到我们的文件内容:

// Get to the next block after the header ends
location += 512;
file_contents = new char[size];
memcpy(file_contents, &buffer[location], size);
// Go to the next block by rounding up to 512
// This isn't necessarily the most efficient way to do this,
// but it's the most obvious.
location += (int)ceil(size / 512.0)

我目前正在使用你的代码,对于使用Gnome文件压缩器创建的tar文件,“sizeOfFile = octalStringToInt(...,11)”在“某些罕见情况下”似乎是错误的。你能指出第12个字节中省略了什么“魔法”吗? - rodrigob
@rodrigob 我真的不知道。如果你找到了,请告诉我。 - Brendan Long
注意,如果文件大小恰好为512字节,则“location = location + ((size / 512) + 1) * 512”将会错过下一个标头。 - Matvey Aksenov
@andy 我用了一种更简单的方法,这样我就不用再考虑边缘情况了。我假设使用这个方法的人可以自己想出更有效的四舍五入方式。 - Brendan Long
2
@rodrigob 这可能有点晚了,但显然有一些基于256位的格式,其大小字段的第一位是不同的。我计划稍后研究这个问题并编写一些解析代码。 - Brendan Long
显示剩余2条评论

12

你看过libtar吗?

从fink软件包信息中可以了解到:

libtar-1.2-1: Tar文件操纵API libtar是一个用于操作POSIX tar文件的C库。它处理添加和提取文件到/从tar归档文件。 libtar提供以下功能:
*灵活的API-您可以单独操纵文件或一次只提取整个归档。
*允许用户指定的read()和write()函数,例如zlib的gzread()和gzwrite()。
*支持POSIX 1003.1-1990和GNU tar文件格式。

不是c++本身,但你可以很容易地链接到c...


@BrendanLong "King of sucks" 是一个夸张的说法。 - Alex Huszagh

4

libarchive是一个开源库,用于解析tarball文件。它可以在不进行解压的情况下读取归档文件中的每个文件,并且还可以将数据写入以形成新的归档文件。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接