You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

488 lines
16 KiB
Zig

//!
//! Extracted from https://github.com/mattnite/ezpkg.
//!
const std = @import("std");
const builtin = @import("builtin");
const testing = std.testing;
const Allocator = std.mem.Allocator;
// ustar tar implementation
pub const Header = extern struct {
name: [100]u8,
mode: [7:0]u8,
uid: [7:0]u8,
gid: [7:0]u8,
size: [11:0]u8,
mtime: [11:0]u8,
checksum: [7:0]u8,
typeflag: FileType,
linkname: [100]u8,
magic: [5:0]u8,
version: [2]u8,
uname: [31:0]u8,
gname: [31:0]u8,
devmajor: [7:0]u8,
devminor: [7:0]u8,
prefix: [155]u8,
pad: [12]u8,
comptime {
std.debug.assert(@sizeOf(Header) == 512);
}
const Self = @This();
const FileType = enum(u8) {
regular = '0',
hard_link = '1',
symbolic_link = '2',
character = '3',
block = '4',
directory = '5',
fifo = '6',
reserved = '7',
pax_global = 'g',
extended = 'x',
_,
};
const Options = struct {
typeflag: FileType,
path: []const u8,
size: u64,
mode: std.fs.File.Mode,
};
pub fn to_bytes(header: *const Header) *const [512]u8 {
return @ptrCast(header);
}
pub fn init(opts: Options) !Self {
var ret = std.mem.zeroes(Self);
ret.magic = [_:0]u8{ 'u', 's', 't', 'a', 'r' };
ret.version = [_:0]u8{ '0', '0' };
ret.typeflag = opts.typeflag;
try ret.setPath(opts.path);
try ret.setSize(opts.size);
try ret.setMtime(0);
try ret.setMode(opts.typeflag, opts.mode);
try ret.setUid(0);
try ret.setGid(0);
std.mem.copy(u8, &ret.uname, "root");
std.mem.copy(u8, &ret.gname, "root");
try ret.updateChecksum();
return ret;
}
pub fn setPath(self: *Self, path: []const u8) !void {
if (path.len > 100) {
var i: usize = 100;
while (i > 0) : (i -= 1) {
if (path[i] == '/' and i < 100)
break;
}
_ = try std.fmt.bufPrint(&self.prefix, "{s}", .{path[0..i]});
_ = try std.fmt.bufPrint(&self.name, "{s}", .{path[i + 1 ..]});
} else {
_ = try std.fmt.bufPrint(&self.name, "{s}", .{path});
}
}
pub fn setSize(self: *Self, size: u64) !void {
_ = try std.fmt.bufPrint(&self.size, "{o:0>11}", .{size});
}
pub fn get_size(header: Header) !u64 {
return std.fmt.parseUnsigned(u64, &header.size, 8);
}
pub fn setMtime(self: *Self, mtime: u32) !void {
_ = try std.fmt.bufPrint(&self.mtime, "{o:0>11}", .{mtime});
}
pub fn setMode(self: *Self, filetype: FileType, perm: std.fs.File.Mode) !void {
switch (filetype) {
.regular => _ = try std.fmt.bufPrint(&self.mode, "0{o:0>6}", .{perm}),
.directory => _ = try std.fmt.bufPrint(&self.mode, "0{o:0>6}", .{perm}),
else => return error.Unsupported,
}
}
pub fn get_mode(header: Header) !std.fs.File.Mode {
std.log.info("mode str: {s}", .{&header.mode});
return std.fmt.parseUnsigned(std.fs.File.Mode, &header.mode, 8);
}
fn setUid(self: *Self, uid: u32) !void {
_ = try std.fmt.bufPrint(&self.uid, "{o:0>7}", .{uid});
}
fn setGid(self: *Self, gid: u32) !void {
_ = try std.fmt.bufPrint(&self.gid, "{o:0>7}", .{gid});
}
pub fn updateChecksum(self: *Self) !void {
const offset = @offsetOf(Self, "checksum");
var checksum: usize = 0;
for (std.mem.asBytes(self), 0..) |val, i| {
checksum += if (i >= offset and i < offset + @sizeOf(@TypeOf(self.checksum)))
' '
else
val;
}
_ = try std.fmt.bufPrint(&self.checksum, "{o:0>7}", .{checksum});
}
pub fn fromStat(stat: std.fs.File.Stat, path: []const u8) !Header {
if (std.mem.indexOf(u8, path, "\\") != null) return error.NeedPosixPath;
if (std.fs.path.isAbsolute(path)) return error.NeedRelPath;
var ret = Self.init();
ret.typeflag = switch (stat.kind) {
.File => .regular,
.Directory => .directory,
else => return error.UnsupportedType,
};
try ret.setPath(path);
try ret.setSize(stat.size);
try ret.setMtime(@as(u32, @truncate(@as(u128, @bitCast(@divTrunc(stat.mtime, std.time.ns_per_s))))));
try ret.setMode(ret.typeflag, @as(u9, @truncate(stat.mode)));
try ret.setUid(0);
try ret.setGid(0);
std.mem.copy(u8, &ret.uname, "root");
std.mem.copy(u8, &ret.gname, "root");
try ret.updateChecksum();
return ret;
}
pub fn isBlank(self: *const Header) bool {
const block = std.mem.asBytes(self);
return for (block) |elem| {
if (elem != 0) break false;
} else true;
}
};
test "Header size" {
try testing.expectEqual(512, @sizeOf(Header));
}
pub fn instantiate(
allocator: Allocator,
dir: std.fs.Dir,
reader: anytype,
skip_depth: usize,
) !void {
var count: usize = 0;
while (true) {
const header = reader.readStruct(Header) catch |err| {
return if (err == error.EndOfStream)
if (count < 2) error.AbrubtEnd else break
else
err;
};
if (header.isBlank()) {
count += 1;
continue;
} else if (count > 0) {
return error.Format;
}
var size = try std.fmt.parseUnsigned(usize, &header.size, 8);
const block_size = ((size + 511) / 512) * 512;
var components = std.ArrayList([]const u8).init(allocator);
defer components.deinit();
var path_it = std.mem.tokenize(u8, &header.prefix, "/\x00");
if (header.prefix[0] != 0) {
while (path_it.next()) |component| {
try components.append(component);
}
}
path_it = std.mem.tokenize(u8, &header.name, "/\x00");
while (path_it.next()) |component| {
try components.append(component);
}
const tmp_path = try std.fs.path.join(allocator, components.items);
defer allocator.free(tmp_path);
if (skip_depth >= components.items.len) {
try reader.skipBytes(block_size, .{});
continue;
}
var i: usize = 0;
while (i < skip_depth) : (i += 1) {
_ = components.orderedRemove(0);
}
const file_path = try std.fs.path.join(allocator, components.items);
defer allocator.free(file_path);
switch (header.typeflag) {
.directory => try dir.makePath(file_path),
.pax_global => try reader.skipBytes(512, .{}),
.regular => {
const file = try dir.createFile(file_path, .{ .read = true, .truncate = true });
defer file.close();
const skip_size = block_size - size;
var buf: [std.mem.page_size]u8 = undefined;
while (size > 0) {
const buffered = try reader.read(buf[0..std.math.min(size, 512)]);
try file.writeAll(buf[0..buffered]);
size -= buffered;
}
try reader.skipBytes(skip_size, .{});
},
else => {},
}
}
}
pub fn builder(allocator: Allocator, writer: anytype) Builder(@TypeOf(writer)) {
return Builder(@TypeOf(writer)).init(allocator, writer);
}
pub fn Builder(comptime Writer: type) type {
return struct {
writer: Writer,
arena: std.heap.ArenaAllocator,
directories: std.StringHashMap(void),
const Self = @This();
pub fn init(allocator: Allocator, writer: Writer) Self {
return Self{
.arena = std.heap.ArenaAllocator.init(allocator),
.writer = writer,
.directories = std.StringHashMap(void).init(allocator),
};
}
pub fn deinit(self: *Self) void {
self.directories.deinit();
self.arena.deinit();
}
pub fn finish(self: *Self) !void {
try self.writer.writeByteNTimes(0, 1024);
}
fn maybeAddDirectories(
self: *Self,
path: []const u8,
) !void {
var i: usize = 0;
while (i < path.len) : (i += 1) {
while (path[i] != '/' and i < path.len) i += 1;
if (i >= path.len) break;
const dirpath = try self.arena.allocator().dupe(u8, path[0..i]);
if (self.directories.contains(dirpath)) continue else try self.directories.put(dirpath, {});
const stat = std.fs.File.Stat{
.inode = undefined,
.size = 0,
.mode = switch (builtin.os.tag) {
.windows => 0,
else => 0o755,
},
.kind = .Directory,
.atime = undefined,
.mtime = std.time.nanoTimestamp(),
.ctime = undefined,
};
const allocator = self.arena.child_allocator;
const posix_dirpath = try std.mem.replaceOwned(u8, allocator, dirpath, std.fs.path.sep_str_windows, std.fs.path.sep_str_posix);
defer allocator.free(posix_dirpath);
const header = try Header.fromStat(stat, posix_dirpath);
try self.writer.writeAll(std.mem.asBytes(&header));
}
}
/// prefix is a path to prepend subpath with
pub fn addFile(
self: *Self,
root: std.fs.Dir,
prefix: ?[]const u8,
subpath: []const u8,
) !void {
const allocator = self.arena.child_allocator;
const path = if (prefix) |prefix_path|
try std.fs.path.join(allocator, &[_][]const u8{ prefix_path, subpath })
else
subpath;
defer if (prefix != null) allocator.free(path);
const posix_path = try std.mem.replaceOwned(u8, allocator, path, std.fs.path.sep_str_windows, std.fs.path.sep_str_posix);
defer allocator.free(posix_path);
if (std.fs.path.dirname(posix_path)) |dirname|
try self.maybeAddDirectories(posix_path[0 .. dirname.len + 1]);
const subfile = try root.openFile(subpath, .{ .mode = .read_write });
defer subfile.close();
const stat = try subfile.stat();
const header = try Header.fromStat(stat, posix_path);
var buf: [std.mem.page_size]u8 = undefined;
try self.writer.writeAll(std.mem.asBytes(&header));
var counter = std.io.countingWriter(self.writer);
while (true) {
const n = try subfile.reader().read(&buf);
if (n == 0) break;
try counter.writer().writeAll(buf[0..n]);
}
const padding = blk: {
const mod = counter.bytes_written % 512;
break :blk if (mod > 0) 512 - mod else 0;
};
try self.writer.writeByteNTimes(0, @as(usize, @intCast(padding)));
}
/// add slice of bytes as file `path`
pub fn addSlice(self: *Self, slice: []const u8, path: []const u8) !void {
const allocator = self.arena.child_allocator;
const posix_path = try std.mem.replaceOwned(u8, allocator, path, std.fs.path.sep_str_windows, std.fs.path.sep_str_posix);
defer allocator.free(posix_path);
const stat = std.fs.File.Stat{
.inode = undefined,
.size = slice.len,
.mode = switch (builtin.os.tag) {
.windows => 0,
else => 0o644,
},
.kind = .File,
.atime = undefined,
.mtime = std.time.nanoTimestamp(),
.ctime = undefined,
};
var header = try Header.fromStat(stat, posix_path);
const padding = blk: {
const mod = slice.len % 512;
break :blk if (mod > 0) 512 - mod else 0;
};
try self.writer.writeAll(std.mem.asBytes(&header));
try self.writer.writeAll(slice);
try self.writer.writeByteNTimes(0, padding);
}
};
}
pub const PaxHeaderMap = struct {
text: []const u8,
map: std.StringHashMap([]const u8),
const Self = @This();
pub fn init(allocator: Allocator, reader: anytype) !Self {
// TODO: header verification
const header = try reader.readStruct(Header);
if (header.typeflag != .pax_global) return error.NotPaxGlobalHeader;
const size = try std.fmt.parseInt(usize, &header.size, 8);
const text = try allocator.alloc(u8, size);
errdefer allocator.free(text);
var i: usize = 0;
while (i < size) : (i = try reader.read(text[i..])) {}
var map = std.StringHashMap([]const u8).init(allocator);
errdefer map.deinit();
var it = std.mem.tokenize(u8, text, "\n");
while (it.next()) |line| {
const begin = (std.mem.indexOf(u8, line, " ") orelse return error.BadMapEntry) + 1;
const eql = std.mem.indexOf(u8, line[begin..], "=") orelse return error.BadMapEntry;
try map.put(line[begin .. begin + eql], line[begin + eql + 1 ..]);
}
return Self{
.text = text,
.map = map,
};
}
pub fn get(self: Self, key: []const u8) ?[]const u8 {
return self.map.get(key);
}
pub fn deinit(self: *Self) void {
self.map.allocator.free(self.text);
self.map.deinit();
}
};
pub fn fileExtractor(path: []const u8, reader: anytype) FileExtractor(@TypeOf(reader)) {
return FileExtractor(@TypeOf(reader)).init(path, reader);
}
pub fn FileExtractor(comptime ReaderType: type) type {
return struct {
path: []const u8,
internal: ReaderType,
len: ?usize,
const Self = @This();
pub fn init(path: []const u8, internal: ReaderType) Self {
return Self{
.path = path,
.internal = internal,
.len = null,
};
}
pub const Error = ReaderType.Error || error{ FileNotFound, EndOfStream } || std.fmt.ParseIntError;
pub const Reader = std.io.Reader(*Self, Error, read);
pub fn read(self: *Self, buf: []u8) Error!usize {
if (self.len == null) {
while (true) {
const header = try self.internal.readStruct(Header);
for (std.mem.asBytes(&header)) |c| {
if (c != 0) break;
} else return error.FileNotFound;
const size = try std.fmt.parseInt(usize, &header.size, 8);
const name = header.name[0 .. std.mem.indexOf(u8, &header.name, "\x00") orelse header.name.len];
if (std.mem.eql(u8, name, self.path)) {
self.len = size;
break;
} else if (size > 0) {
try self.internal.skipBytes(size + (512 - (size % 512)), .{});
}
}
}
const n = try self.internal.read(buf[0..std.math.min(self.len.?, buf.len)]);
self.len.? -= n;
return n;
}
pub fn reader(self: *Self) Reader {
return .{ .context = self };
}
};
}