-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8068ac0
commit d52fde2
Showing
4 changed files
with
333 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,317 @@ | ||
const builtin = @import("builtin"); | ||
const std = @import("std"); | ||
const zip = @import("lib/zip.zig"); | ||
|
||
fn oom(e: error{OutOfMemory}) noreturn { | ||
@panic(@errorName(e)); | ||
} | ||
fn fatal(comptime fmt: []const u8, args: anytype) noreturn { | ||
std.log.err(fmt, args); | ||
std.process.exit(0xff); | ||
} | ||
|
||
fn usage() noreturn { | ||
std.io.getStdErr().writer().writeAll( | ||
"Usage: zip [-options] ZIP_FILE FILES/DIRS..\n", | ||
) catch |e| @panic(@errorName(e)); | ||
std.process.exit(1); | ||
} | ||
|
||
var windows_args_arena = if (builtin.os.tag == .windows) | ||
std.heap.ArenaAllocator.init(std.heap.page_allocator) else struct{}{}; | ||
pub fn cmdlineArgs() [][*:0]u8 { | ||
if (builtin.os.tag == .windows) { | ||
const slices = std.process.argsAlloc(windows_args_arena.allocator()) catch |err| switch (err) { | ||
error.OutOfMemory => oom(error.OutOfMemory), | ||
//error.InvalidCmdLine => @panic("InvalidCmdLine"), | ||
error.Overflow => @panic("Overflow while parsing command line"), | ||
}; | ||
const args = windows_args_arena.allocator().alloc([*:0]u8, slices.len - 1) catch |e| oom(e); | ||
for (slices[1..], 0..) |slice, i| { | ||
args[i] = slice.ptr; | ||
} | ||
return args; | ||
} | ||
return std.os.argv.ptr[1 .. std.os.argv.len]; | ||
} | ||
|
||
pub fn main() !void { | ||
var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator); | ||
defer arena_instance.deinit(); | ||
const arena = arena_instance.allocator(); | ||
|
||
const cmd_args = blk: { | ||
const cmd_args = cmdlineArgs(); | ||
var arg_index: usize = 0; | ||
var non_option_len: usize = 0; | ||
while (arg_index < cmd_args.len) : (arg_index += 1) { | ||
const arg = std.mem.span(cmd_args[arg_index]); | ||
if (!std.mem.startsWith(u8, arg, "-")) { | ||
cmd_args[non_option_len] = arg; | ||
non_option_len += 1; | ||
} else { | ||
fatal("unknown cmdline option '{s}'", .{arg}); | ||
} | ||
} | ||
break :blk cmd_args[0 .. non_option_len]; | ||
}; | ||
|
||
if (cmd_args.len < 2) usage(); | ||
const zip_file_arg = std.mem.span(cmd_args[0]); | ||
const paths_to_include = cmd_args[1..]; | ||
|
||
// expand cmdline arguments to a list of files | ||
var files: std.ArrayListUnmanaged(struct { | ||
path: []const u8, | ||
size: u64, | ||
}) = .{}; | ||
for (paths_to_include) |path_ptr| { | ||
const path = std.mem.span(path_ptr); | ||
const stat = std.fs.cwd().statFile(path) catch |err| switch (err) { | ||
error.FileNotFound => fatal("path '{s}' is not found", .{path}), | ||
else => |e| return e, | ||
}; | ||
switch (stat.kind) { | ||
.directory => { | ||
@panic("todo: directories"); | ||
}, | ||
.file => { | ||
if (zip.isBadFilename(path)) | ||
fatal("filename '{s}' is invalid for zip files", .{path}); | ||
try files.append(arena, .{ | ||
.path = path, | ||
.size = stat.size, | ||
}); | ||
}, | ||
.sym_link => fatal("todo: symlinks", .{}), | ||
.block_device, | ||
.character_device, | ||
.named_pipe, | ||
.unix_domain_socket, | ||
.whiteout, | ||
.door, | ||
.event_port, | ||
.unknown => fatal("file '{s}' is an unsupported type {s}", .{path, @tagName(stat.kind)}), | ||
} | ||
} | ||
|
||
const zip_file = std.fs.cwd().createFile(zip_file_arg, .{}) catch |err| | ||
fatal("open '{s}' failed: {s}", .{zip_file_arg, @errorName(err)}); | ||
defer zip_file.close(); | ||
|
||
const store = try arena.alloc(FileStore, files.items.len); | ||
// no need to free | ||
|
||
var zipper = initZipper(zip_file.writer()); | ||
for (files.items, 0..) |file_entry, i| { | ||
const file_offset = zipper.counting_writer.bytes_written; | ||
|
||
const compression: zip.CompressionMethod = .store; | ||
|
||
try zipper.writeFileHeader(file_entry.path, compression); | ||
|
||
var file = try std.fs.cwd().openFile(file_entry.path, .{}); | ||
defer file.close(); | ||
|
||
var hash = std.hash.Crc32.init(); | ||
var full_rw_buf: [std.mem.page_size]u8 = undefined; | ||
var remaining = file_entry.size; | ||
while (remaining > 0) { | ||
const buf = full_rw_buf[0 .. @min(remaining, full_rw_buf.len)]; | ||
const read_len = try file.reader().read(buf); | ||
std.debug.assert(read_len == buf.len); | ||
hash.update(buf); | ||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | ||
std.log.info("writing {} bytes of '{s}'", .{buf.len, file_entry.path}); | ||
try zipper.counting_writer.writer().writeAll(buf); | ||
remaining -= buf.len; | ||
} | ||
store[i] = .{ | ||
.file_offset = file_offset, | ||
.compression = compression, | ||
.uncompressed_size = @intCast(file_entry.size), | ||
.crc32 = hash.final(), | ||
.compressed_size = @intCast(file_entry.size), | ||
|
||
}; | ||
} | ||
for (files.items, 0..) |file, i| { | ||
try zipper.writeCentralRecord(store[i], .{ | ||
.name = file.path, | ||
}); | ||
} | ||
try zipper.writeEndRecord(); | ||
} | ||
|
||
// Used to store any data from writing a file to the zip archive that's needed | ||
// when writing the corresponding central directory record. | ||
pub const FileStore = struct { | ||
file_offset: u64, | ||
compression: zip.CompressionMethod, | ||
uncompressed_size: u32, | ||
crc32: u32, | ||
compressed_size: u32, | ||
}; | ||
|
||
pub fn initZipper(writer: anytype) Zipper(@TypeOf(writer)) { | ||
return .{ .counting_writer = std.io.countingWriter(writer) }; | ||
} | ||
|
||
fn Zipper(comptime Writer: type) type { | ||
return struct { | ||
counting_writer: std.io.CountingWriter(Writer), | ||
central_count: u64 = 0, | ||
first_central_offset: ?u64 = null, | ||
last_central_limit: ?u64 = null, | ||
|
||
const Self = @This(); | ||
|
||
pub fn writeFileHeader( | ||
self: *Self, | ||
name: []const u8, | ||
compression: zip.CompressionMethod, | ||
) !void { | ||
const writer = self.counting_writer.writer(); | ||
const hdr: zip.LocalFileHeader = .{ | ||
.signature = zip.local_file_header_sig, | ||
.version_needed_to_extract = 10, | ||
.flags = .{ .encrypted = false, .local_header_zero_fields = true }, | ||
.compression_method = compression, | ||
.last_modification_time = 0, | ||
.last_modification_date = 0, | ||
.crc32 = 0, | ||
.compressed_size = 0, | ||
.uncompressed_size = 0, | ||
.filename_len = @intCast(name.len), | ||
.extra_len = 0, | ||
}; | ||
try writeStructEndian(writer, hdr, .little); | ||
try writer.writeAll(name); | ||
|
||
// var compressed_size: u32 = undefined; | ||
// switch (opt.compression) { | ||
// .store => { | ||
// try writer.writeAll(opt.content); | ||
// compressed_size = @intCast(opt.content.len); | ||
// }, | ||
// .deflate => { | ||
// const offset = self.counting_writer.bytes_written; | ||
// var fbs = std.io.fixedBufferStream(opt.content); | ||
// try std.compress.flate.deflate.compress(.raw, fbs.reader(), writer, .{}); | ||
// std.debug.assert(fbs.pos == opt.content.len); | ||
// compressed_size = @intCast(self.counting_writer.bytes_written - offset); | ||
// }, | ||
// else => unreachable, | ||
// } | ||
// return .{ | ||
// .compression = opt.compression, | ||
// .file_offset = file_offset, | ||
// .crc32 = crc32, | ||
// .compressed_size = compressed_size, | ||
// .uncompressed_size = opt.content.len, | ||
// }; | ||
} | ||
|
||
pub fn writeCentralRecord( | ||
self: *Self, | ||
store: FileStore, | ||
opt: struct { | ||
name: []const u8, | ||
version_needed_to_extract: u16 = 10, | ||
}, | ||
) !void { | ||
if (self.first_central_offset == null) { | ||
self.first_central_offset = self.counting_writer.bytes_written; | ||
} | ||
self.central_count += 1; | ||
|
||
const hdr: zip.CentralDirectoryFileHeader = .{ | ||
.signature = zip.central_file_header_sig, | ||
.version_made_by = 0, | ||
.version_needed_to_extract = opt.version_needed_to_extract, | ||
.flags = .{ .encrypted = false, .local_header_zero_fields = true }, | ||
.compression_method = store.compression, | ||
.last_modification_time = 0, | ||
.last_modification_date = 0, | ||
.crc32 = store.crc32, | ||
.compressed_size = store.compressed_size, | ||
.uncompressed_size = @intCast(store.uncompressed_size), | ||
.filename_len = @intCast(opt.name.len), | ||
.extra_len = 0, | ||
.comment_len = 0, | ||
.disk_number = 0, | ||
.internal_file_attributes = 0, | ||
.external_file_attributes = 0, | ||
.local_file_header_offset = @intCast(store.file_offset), | ||
}; | ||
try writeStructEndian(self.counting_writer.writer(), hdr, .little); | ||
try self.counting_writer.writer().writeAll(opt.name); | ||
self.last_central_limit = self.counting_writer.bytes_written; | ||
} | ||
|
||
pub fn writeEndRecord(self: *Self) !void { | ||
const cd_offset = self.first_central_offset orelse 0; | ||
const cd_end = self.last_central_limit orelse 0; | ||
const hdr: zip.EndRecord = .{ | ||
.signature = zip.end_record_sig, | ||
.disk_number = 0, | ||
.central_directory_disk_number = 0, | ||
.record_count_disk = @intCast(self.central_count), | ||
.record_count_total = @intCast(self.central_count), | ||
.central_directory_size = @intCast(cd_end - cd_offset), | ||
.central_directory_offset = @intCast(cd_offset), | ||
.comment_len = 0, | ||
}; | ||
try writeStructEndian(self.counting_writer.writer(), hdr, .little); | ||
} | ||
}; | ||
} | ||
|
||
const native_endian = @import("builtin").target.cpu.arch.endian(); | ||
|
||
fn writeStructEndian(writer: anytype, value: anytype, endian: std.builtin.Endian) anyerror!void { | ||
// TODO: make sure this value is not a reference type | ||
if (native_endian == endian) { | ||
return writer.writeStruct(value); | ||
} else { | ||
var copy = value; | ||
byteSwapAllFields(@TypeOf(value), ©); | ||
return writer.writeStruct(copy); | ||
} | ||
} | ||
pub fn byteSwapAllFields(comptime S: type, ptr: *S) void { | ||
switch (@typeInfo(S)) { | ||
.Struct => { | ||
inline for (std.meta.fields(S)) |f| { | ||
switch (@typeInfo(f.type)) { | ||
.Struct => |struct_info| if (struct_info.backing_integer) |Int| { | ||
@field(ptr, f.name) = @bitCast(@byteSwap(@as(Int, @bitCast(@field(ptr, f.name))))); | ||
} else { | ||
byteSwapAllFields(f.type, &@field(ptr, f.name)); | ||
}, | ||
.Array => byteSwapAllFields(f.type, &@field(ptr, f.name)), | ||
.Enum => { | ||
@field(ptr, f.name) = @enumFromInt(@byteSwap(@intFromEnum(@field(ptr, f.name)))); | ||
}, | ||
else => { | ||
@field(ptr, f.name) = @byteSwap(@field(ptr, f.name)); | ||
}, | ||
} | ||
} | ||
}, | ||
.Array => { | ||
for (ptr) |*item| { | ||
switch (@typeInfo(@TypeOf(item.*))) { | ||
.Struct, .Array => byteSwapAllFields(@TypeOf(item.*), item), | ||
.Enum => { | ||
item.* = @enumFromInt(@byteSwap(@intFromEnum(item.*))); | ||
}, | ||
else => { | ||
item.* = @byteSwap(item.*); | ||
}, | ||
} | ||
} | ||
}, | ||
else => @compileError("byteSwapAllFields expects a struct or array as the first argument"), | ||
} | ||
} |