首页 文章

C:创建存档文件头

提问于
浏览
4

我正在使用C中的POSIX API系统调用创建文件存档器/提取器(如tar) . 我已经完成了部分存档位 .

我想知道是否有人可以帮助我使用一些C源代码(使用上面的代码)为C中的文件创建文件头(其中头部充当索引),它描述了文件属性/元数据(名称,日期时间等) . 到目前为止我所做的只是理解(不确定是否正确)创建文件头它需要一个结构来保存元数据,并且需要lseek来寻找文件的开头/结尾,如:

FileName = file.txt FileSize = 0 FileDir = . / blah / blah FilePerms = 000 \ n \ n

程序的归档部分有这个过程:

  • 从命令行获取所有文件的列表 . (我可以做这部分)

  • 创建一个结构来保存有关每个文件的元数据:名称(255个字符),大小(64位整数),日期和时间以及权限 .

  • 对于每个文件,获取其统计信息 .

  • 将每个文件的统计信息存储在结构数组中 .

  • 打开存档进行编写 . (我可以做这部分)

  • 编写 Headers 结构 .

  • 对于每个文件,将其内容附加到存档文件(在每个文件的结尾/开头) .

  • 关闭存档文件 . (我可以做这部分)

即使我知道它需要做什么,我也难以创建整个头文件,正如我所说的那些位上面的编号点所述(2,3,4,6,7) .

任何帮助将不胜感激 . 谢谢 .

2 回答

  • 9

    正如ijw注意到,有几种方法可以创建存档文件头 . 如果跨平台可移植性将成为一个问题 - 或者如果您需要在同一平台上的32位和64位版本的软件之间切换,那么您需要确保尺寸和布局所有平台都完全理解这些字段 .

    每文件元数据

    一种方法是使用具有已知大小和字节序类型的固定格式二进制头 . 这是ijw建议的 . 但是,您需要处理长文件名,因此您需要存储长度(可能是2字节无符号整数),然后使用实际路径名 .

    替代的,通常现在有利的技术,是使用可打印的字段(通常称为ASCII格式,尽管这是用词不当) . 时间记录为自Epoch转换为字符串以来的十进制秒数等 . 这就是现代 ar 档案所使用的;这是GNU tar 的作用(或多或少;有一些历史怪癖使这更令人困惑);它是 cpio -c (这些天通常是默认值) . 字段可能由空值或空格分隔;有一种简单的方法可以检测 Headers 的结尾; Headers 包含有关文件名的信息(不一定是您希望或期望的直接信息,但同样,这通常是因为格式已经发展了多年),然后是实际数据 . 不知何故,您知道每个字段的大小以及标头描述的文件,以便您可以可靠地读取数据 .

    效率是一个红鲱鱼 . 与第一个磁盘访问相比,与文本格式的转换非常迅速,基本上没有可测量的性能问题 . 保证可移植性通常远远超过使用二进制数据格式的(微观)性能优势 - 当二进制数据必须在输入或输出上进行转换以使其成为体系结构中立格式时更是如此 .

    中央指数与分布式指数

    要考虑的另一个问题是归档中的文件索引是集中的(在前面还是在末尾)还是分布式的(每个文件的元数据紧接在文件的数据之前) . 每种格式都有一些优点 - 通常,系统使用分布式版本,因为您可以为每个文件编写信息,而无需知道总共要处理多少文件(例如,因为您递归地归档目录的内容) . 预先设置中央索引意味着您可以在不读取整个存档的情况下列出文件 - 分布式元数据意味着您必须读取整个文件 . 但是,中央索引使归档的构建变得复杂 .

    请注意,即使使用分布式索引,您通常也需要整个存档的 Headers ,以便您可以检测到文件的格式符合您的预期 . 通常,有某种标记信息( !<arch>\n 用于 ar 存档,通常; %PDF-1.2\n 在PDF文件的开头,等等)以确保文件包含您期望的内容 . 可能存在一些整体(存档级)元数据 . 然后,您将获得第一个文件元数据,后跟文件数据,重复直到存档结束(可能或可能不具有正式结束标记 - 更多元数据) .


    [H]我会在你建议的'固定格式二进制 Headers '中实现它 . 我在决定需要哪些命令/功能时遇到问题 .

    我打算建议你不要使用固定格式的二进制头文件;您应该使用基于文本的 Headers 格式 . 如果你能弄清楚如何做二进制格式,那就做我的客人(多年来我做过很多次 - 这并不意味着我认为这是一个好主意) .

    所以,这里有一些关于'text header'格式的指针 .

    对于文件元数据,您可以定义包括:

    • 大小

    • 模式(权限,类型)

    • 所有者

    • 修改时间

    • 名称长度

    • 名字

    您可以合理地确定您的文件大小限制为64位无符号整数,这意味着20个十进制数字 . 该模式可能打印为16位八进制数,需要6个八进制数字 . 所有者和组可能会打印为UID和GID值(而不是名称),在这种情况下,您可以使用10位数字 . 或者,您可以决定使用名称,但是您应该允许每个名称最多32个字符 . 请注意,名称通常比数字更便携 . 除非您以root身份提取数据,否则名称和数字都不会与接收计算机相关(但为什么要这样做呢?) . 修改时间通常是32位有符号整数,表示自Epoch(1970-01-01 00:00:00Z)以来的秒数 . 您应该通过允许秒数增长大于32位数量来允许Y2038错误;你可能会认为12个领先的数字将超过Y10K危机(大约4个左右),这已经足够了;你可能决定允许小数秒 . 总之,这表明时间戳的26个空格应该是矫枉过正的 . 您可以决定每个字段将通过空格与下一个字段分开(为了易读 - 请考虑“易于调试”!) . 您可以合理地确定所有文件名的总长度限制为4位小数 .

    您需要知道如何便携地格式化类型 - #include <inttypes.h> 是您的朋友 .

    然后,您可以设计用于打印(写入)文件元数据的格式字符串,以及用于扫描(读取)文件元数据的并行字符串 .

    印刷:

    "%20" PRIu64 " %06o %-.32s %-.32s %26" PRIu64 " %-4d %s\n"
    

    这也打印出名称 . 它用换行符终止 Headers . 总大小为127个字节加上文件名的长度 . 这可能是过度的,但你可以调整数字以适合自己 .

    扫描:

    "%" SCNu64 " %o %.32s %.32s %" SCNu64 "%d"
    

    这不会扫描名称;您需要仔细创建名称扫描程序,尤其是因为您需要读取名称中的空格 . 实际上,扫描用户名和组名的代码也都没有空格 . 如果这是不可接受的(即名称可能包含空格),则需要更复杂的扫描格式或 sscanf() 以外的其他内容来处理输入数据 .

    我假设时间字段为64位整数,而不是混合小数秒等,即使有足够的空间允许小数秒 . 你可能会在这里节省一些空间 .

  • 4

    使用stat()系统调用可以获取每个文件的信息 .

    对于 Headers 的写入,这里有两个解决方案 .

    琐碎而邪恶:

    struct file_header {
    ... data you want to put in 
    } fhdr;
    
    fwrite(file, fhdr, sizeof(fhdr));
    

    这是邪恶的,因为结构打包因机器而异,字节顺序和基本类型的大小如'int'也是如此 . 当程序在另一台机器上编译时,或者在某些情况下甚至在同一台机器上使用另一台编译器时,程序可能无法读取您的程序所写的文件 .

    非平凡但安全:

    char name[xxx];
    uint32_t length; /* Fixed byte length across architectures */
    ...
    
    fwrite(file, name, sizeof(name));
    length=htonl(length); /* Or something else that converts 
                             the length to a known endianness */
    fwrite(file, &length, sizeof(length);
    

    就个人而言,我不是htonl()和朋友的粉丝,我更喜欢使用移位运算符(可以使用移位运算符简单地编写)来编写将uint32_t转换为uchar [4]的东西,因为C没有确定内存中偶数整数的格式 . 实际上,你很难找到一些不会将uint32_t存储为8位4字节的东西,但这是需要考虑的事情 .

    上面列出的变量可以是结构中的结构成员 . 在阅读过程中颠倒过程留给读者作为练习 .

相关问题