zip.zig 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. const builtin = @import("builtin");
  2. const std = @import("std");
  3. const zip = @import("lib/zip.zig");
  4. fn oom(e: error{OutOfMemory}) noreturn {
  5. @panic(@errorName(e));
  6. }
  7. fn fatal(comptime fmt: []const u8, args: anytype) noreturn {
  8. std.log.err(fmt, args);
  9. std.process.exit(0xff);
  10. }
  11. fn usage() noreturn {
  12. std.io.getStdErr().writer().writeAll(
  13. "Usage: zip [-options] ZIP_FILE FILES/DIRS..\n",
  14. ) catch |e| @panic(@errorName(e));
  15. std.process.exit(1);
  16. }
  17. var windows_args_arena = if (builtin.os.tag == .windows)
  18. std.heap.ArenaAllocator.init(std.heap.page_allocator) else struct{}{};
  19. pub fn cmdlineArgs() [][*:0]u8 {
  20. if (builtin.os.tag == .windows) {
  21. const slices = std.process.argsAlloc(windows_args_arena.allocator()) catch |err| switch (err) {
  22. error.OutOfMemory => oom(error.OutOfMemory),
  23. //error.InvalidCmdLine => @panic("InvalidCmdLine"),
  24. error.Overflow => @panic("Overflow while parsing command line"),
  25. };
  26. const args = windows_args_arena.allocator().alloc([*:0]u8, slices.len - 1) catch |e| oom(e);
  27. for (slices[1..], 0..) |slice, i| {
  28. args[i] = slice.ptr;
  29. }
  30. return args;
  31. }
  32. return std.os.argv.ptr[1 .. std.os.argv.len];
  33. }
  34. pub fn main() !void {
  35. var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator);
  36. defer arena_instance.deinit();
  37. const arena = arena_instance.allocator();
  38. const cmd_args = blk: {
  39. const cmd_args = cmdlineArgs();
  40. var arg_index: usize = 0;
  41. var non_option_len: usize = 0;
  42. while (arg_index < cmd_args.len) : (arg_index += 1) {
  43. const arg = std.mem.span(cmd_args[arg_index]);
  44. if (!std.mem.startsWith(u8, arg, "-")) {
  45. cmd_args[non_option_len] = arg;
  46. non_option_len += 1;
  47. } else {
  48. fatal("unknown cmdline option '{s}'", .{arg});
  49. }
  50. }
  51. break :blk cmd_args[0 .. non_option_len];
  52. };
  53. if (cmd_args.len < 2) usage();
  54. const zip_file_arg = std.mem.span(cmd_args[0]);
  55. const paths_to_include = cmd_args[1..];
  56. // expand cmdline arguments to a list of files
  57. var file_entries: std.ArrayListUnmanaged(FileEntry) = .{};
  58. for (paths_to_include) |path_ptr| {
  59. const path = std.mem.span(path_ptr);
  60. const stat = std.fs.cwd().statFile(path) catch |err| switch (err) {
  61. error.FileNotFound => fatal("path '{s}' is not found", .{path}),
  62. else => |e| return e,
  63. };
  64. switch (stat.kind) {
  65. .directory => {
  66. @panic("todo: directories");
  67. },
  68. .file => {
  69. if (isBadFilename(path))
  70. fatal("filename '{s}' is invalid for zip files", .{path});
  71. try file_entries.append(arena, .{
  72. .path = path,
  73. .size = stat.size,
  74. });
  75. },
  76. .sym_link => fatal("todo: symlinks", .{}),
  77. .block_device,
  78. .character_device,
  79. .named_pipe,
  80. .unix_domain_socket,
  81. .whiteout,
  82. .door,
  83. .event_port,
  84. .unknown => fatal("file '{s}' is an unsupported type {s}", .{path, @tagName(stat.kind)}),
  85. }
  86. }
  87. const store = try arena.alloc(FileStore, file_entries.items.len);
  88. // no need to free
  89. {
  90. const zip_file = std.fs.cwd().createFile(zip_file_arg, .{}) catch |err|
  91. fatal("create file '{s}' failed: {s}", .{zip_file_arg, @errorName(err)});
  92. defer zip_file.close();
  93. try writeZip(zip_file, file_entries.items, store);
  94. }
  95. // go fix up the local file headers
  96. {
  97. const zip_file = std.fs.cwd().openFile(zip_file_arg, .{ .mode = .read_write }) catch |err|
  98. fatal("open file '{s}' failed: {s}", .{zip_file_arg, @errorName(err)});
  99. defer zip_file.close();
  100. for (file_entries.items, 0..) |file, i| {
  101. try zip_file.seekTo(store[i].file_offset);
  102. const hdr: zip.LocalFileHeader = .{
  103. .signature = zip.local_file_header_sig,
  104. .version_needed_to_extract = 10,
  105. .flags = .{ .encrypted = false, ._ = 0 },
  106. .compression_method = store[i].compression,
  107. .last_modification_time = 0,
  108. .last_modification_date = 0,
  109. .crc32 = store[i].crc32,
  110. .compressed_size = store[i].compressed_size,
  111. .uncompressed_size = @intCast(file.size),
  112. .filename_len = @intCast(file.path.len),
  113. .extra_len = 0,
  114. };
  115. try writeStructEndian(zip_file.writer(), hdr, .little);
  116. }
  117. }
  118. }
  119. const FileEntry = struct {
  120. path: []const u8,
  121. size: u64,
  122. };
  123. fn writeZip(
  124. out_zip: std.fs.File,
  125. file_entries: []const FileEntry,
  126. store: []FileStore,
  127. ) !void {
  128. var zipper = initZipper(out_zip.writer());
  129. for (file_entries, 0..) |file_entry, i| {
  130. const file_offset = zipper.counting_writer.bytes_written;
  131. const compression: zip.CompressionMethod = .deflate;
  132. try zipper.writeFileHeader(file_entry.path, compression);
  133. var file = try std.fs.cwd().openFile(file_entry.path, .{});
  134. defer file.close();
  135. var crc32: u32 = undefined;
  136. var compressed_size = file_entry.size;
  137. switch (compression) {
  138. .store => {
  139. var hash = std.hash.Crc32.init();
  140. var full_rw_buf: [std.mem.page_size]u8 = undefined;
  141. var remaining = file_entry.size;
  142. while (remaining > 0) {
  143. const buf = full_rw_buf[0 .. @min(remaining, full_rw_buf.len)];
  144. const read_len = try file.reader().read(buf);
  145. std.debug.assert(read_len == buf.len);
  146. hash.update(buf);
  147. try zipper.counting_writer.writer().writeAll(buf);
  148. remaining -= buf.len;
  149. }
  150. crc32 = hash.final();
  151. },
  152. .deflate => {
  153. const start_offset = zipper.counting_writer.bytes_written;
  154. var br = std.io.bufferedReader(file.reader());
  155. var cr = Crc32Reader(@TypeOf(br.reader())){ .underlying_reader = br.reader() };
  156. try std.compress.flate.deflate.compress(
  157. .raw,
  158. cr.reader(),
  159. zipper.counting_writer.writer(),
  160. .{ .level = .best },
  161. );
  162. if (br.end != br.start) fatal("deflate compressor didn't read all data", .{});
  163. compressed_size = zipper.counting_writer.bytes_written - start_offset;
  164. crc32 = cr.crc32.final();
  165. },
  166. else => @panic("codebug"),
  167. }
  168. store[i] = .{
  169. .file_offset = file_offset,
  170. .compression = compression,
  171. .uncompressed_size = @intCast(file_entry.size),
  172. .crc32 = crc32,
  173. .compressed_size = @intCast(compressed_size),
  174. };
  175. }
  176. for (file_entries, 0..) |file, i| {
  177. try zipper.writeCentralRecord(store[i], .{
  178. .name = file.path,
  179. });
  180. }
  181. try zipper.writeEndRecord();
  182. }
  183. pub fn Crc32Reader(comptime ReaderType: type) type {
  184. return struct {
  185. underlying_reader: ReaderType,
  186. crc32: std.hash.Crc32 = std.hash.Crc32.init(),
  187. pub const Error = ReaderType.Error;
  188. pub const Reader = std.io.Reader(*Self, Error, read);
  189. const Self = @This();
  190. pub fn read(self: *Self, dest: []u8) Error!usize {
  191. const len = try self.underlying_reader.read(dest);
  192. self.crc32.update(dest[0..len]);
  193. return len;
  194. }
  195. pub fn reader(self: *Self) Reader {
  196. return .{ .context = self };
  197. }
  198. };
  199. }
  200. fn isBadFilename(filename: []const u8) bool {
  201. if (std.mem.indexOfScalar(u8, filename, '\\')) |_|
  202. return true;
  203. if (filename.len == 0 or filename[0] == '/' or filename[0] == '\\')
  204. return true;
  205. var it = std.mem.splitAny(u8, filename, "/\\");
  206. while (it.next()) |part| {
  207. if (std.mem.eql(u8, part, ".."))
  208. return true;
  209. }
  210. return false;
  211. }
  212. // Used to store any data from writing a file to the zip archive that's needed
  213. // when writing the corresponding central directory record.
  214. pub const FileStore = struct {
  215. file_offset: u64,
  216. compression: zip.CompressionMethod,
  217. uncompressed_size: u32,
  218. crc32: u32,
  219. compressed_size: u32,
  220. };
  221. pub fn initZipper(writer: anytype) Zipper(@TypeOf(writer)) {
  222. return .{ .counting_writer = std.io.countingWriter(writer) };
  223. }
  224. fn Zipper(comptime Writer: type) type {
  225. return struct {
  226. counting_writer: std.io.CountingWriter(Writer),
  227. central_count: u64 = 0,
  228. first_central_offset: ?u64 = null,
  229. last_central_limit: ?u64 = null,
  230. const Self = @This();
  231. pub fn writeFileHeader(
  232. self: *Self,
  233. name: []const u8,
  234. compression: zip.CompressionMethod,
  235. ) !void {
  236. const writer = self.counting_writer.writer();
  237. const hdr: zip.LocalFileHeader = .{
  238. .signature = zip.local_file_header_sig,
  239. .version_needed_to_extract = 10,
  240. .flags = .{ .encrypted = false, ._ = 0 },
  241. .compression_method = compression,
  242. .last_modification_time = 0,
  243. .last_modification_date = 0,
  244. .crc32 = 0,
  245. .compressed_size = 0,
  246. .uncompressed_size = 0,
  247. .filename_len = @intCast(name.len),
  248. .extra_len = 0,
  249. };
  250. try writeStructEndian(writer, hdr, .little);
  251. try writer.writeAll(name);
  252. }
  253. pub fn writeCentralRecord(
  254. self: *Self,
  255. store: FileStore,
  256. opt: struct {
  257. name: []const u8,
  258. version_needed_to_extract: u16 = 10,
  259. },
  260. ) !void {
  261. if (self.first_central_offset == null) {
  262. self.first_central_offset = self.counting_writer.bytes_written;
  263. }
  264. self.central_count += 1;
  265. const hdr: zip.CentralDirectoryFileHeader = .{
  266. .signature = zip.central_file_header_sig,
  267. .version_made_by = 0,
  268. .version_needed_to_extract = opt.version_needed_to_extract,
  269. .flags = .{ .encrypted = false, ._ = 0 },
  270. .compression_method = store.compression,
  271. .last_modification_time = 0,
  272. .last_modification_date = 0,
  273. .crc32 = store.crc32,
  274. .compressed_size = store.compressed_size,
  275. .uncompressed_size = @intCast(store.uncompressed_size),
  276. .filename_len = @intCast(opt.name.len),
  277. .extra_len = 0,
  278. .comment_len = 0,
  279. .disk_number = 0,
  280. .internal_file_attributes = 0,
  281. .external_file_attributes = 0,
  282. .local_file_header_offset = @intCast(store.file_offset),
  283. };
  284. try writeStructEndian(self.counting_writer.writer(), hdr, .little);
  285. try self.counting_writer.writer().writeAll(opt.name);
  286. self.last_central_limit = self.counting_writer.bytes_written;
  287. }
  288. pub fn writeEndRecord(self: *Self) !void {
  289. const cd_offset = self.first_central_offset orelse 0;
  290. const cd_end = self.last_central_limit orelse 0;
  291. const hdr: zip.EndRecord = .{
  292. .signature = zip.end_record_sig,
  293. .disk_number = 0,
  294. .central_directory_disk_number = 0,
  295. .record_count_disk = @intCast(self.central_count),
  296. .record_count_total = @intCast(self.central_count),
  297. .central_directory_size = @intCast(cd_end - cd_offset),
  298. .central_directory_offset = @intCast(cd_offset),
  299. .comment_len = 0,
  300. };
  301. try writeStructEndian(self.counting_writer.writer(), hdr, .little);
  302. }
  303. };
  304. }
  305. const native_endian = @import("builtin").target.cpu.arch.endian();
  306. fn writeStructEndian(writer: anytype, value: anytype, endian: std.builtin.Endian) anyerror!void {
  307. // TODO: make sure this value is not a reference type
  308. if (native_endian == endian) {
  309. return writer.writeStruct(value);
  310. } else {
  311. var copy = value;
  312. byteSwapAllFields(@TypeOf(value), &copy);
  313. return writer.writeStruct(copy);
  314. }
  315. }
  316. pub fn byteSwapAllFields(comptime S: type, ptr: *S) void {
  317. switch (@typeInfo(S)) {
  318. .Struct => {
  319. inline for (std.meta.fields(S)) |f| {
  320. switch (@typeInfo(f.type)) {
  321. .Struct => |struct_info| if (struct_info.backing_integer) |Int| {
  322. @field(ptr, f.name) = @bitCast(@byteSwap(@as(Int, @bitCast(@field(ptr, f.name)))));
  323. } else {
  324. byteSwapAllFields(f.type, &@field(ptr, f.name));
  325. },
  326. .Array => byteSwapAllFields(f.type, &@field(ptr, f.name)),
  327. .Enum => {
  328. @field(ptr, f.name) = @enumFromInt(@byteSwap(@intFromEnum(@field(ptr, f.name))));
  329. },
  330. else => {
  331. @field(ptr, f.name) = @byteSwap(@field(ptr, f.name));
  332. },
  333. }
  334. }
  335. },
  336. .Array => {
  337. for (ptr) |*item| {
  338. switch (@typeInfo(@TypeOf(item.*))) {
  339. .Struct, .Array => byteSwapAllFields(@TypeOf(item.*), item),
  340. .Enum => {
  341. item.* = @enumFromInt(@byteSwap(@intFromEnum(item.*)));
  342. },
  343. else => {
  344. item.* = @byteSwap(item.*);
  345. },
  346. }
  347. }
  348. },
  349. else => @compileError("byteSwapAllFields expects a struct or array as the first argument"),
  350. }
  351. }