diff --git a/src/archive.zig b/src/archive.zig index 61ef434..c19c635 100644 --- a/src/archive.zig +++ b/src/archive.zig @@ -15,6 +15,7 @@ const Superblock = @import("super.zig").Superblock; const Table = @import("table.zig").Table; const MetadataReader = @import("util/metadata.zig"); const OffsetFile = @import("util/offset_file.zig"); +const XattrTable = @import("xattr.zig"); const config = if (builtin.is_test) .{ .use_c_libs = true, @@ -38,9 +39,10 @@ decomp: Decomp.DecompFn, super: Superblock, -frag_table: Table(FragEntry) = undefined, -id_table: Table(u16) = undefined, -export_table: Table(InodeRef) = undefined, +frag_table: Table(FragEntry), +id_table: Table(u16), +export_table: Table(InodeRef), +xattr_table: XattrTable, /// Default settings using std.Thread.getCpuCount() threads and the minimum of 4gb or half of system memory for memory usage. pub fn init(alloc: std.mem.Allocator, fil: File, offset: u64) !Archive { @@ -59,15 +61,13 @@ pub fn init(alloc: std.mem.Allocator, fil: File, offset: u64) !Archive { }; return .{ .alloc = alloc, - .fil = off_fil, .decomp = decomp, - .super = super, - .frag_table = try .init(alloc, off_fil, decomp, super.frag_start, super.frag_count), .id_table = try .init(alloc, off_fil, decomp, super.id_start, super.id_count), .export_table = try .init(alloc, off_fil, decomp, super.export_start, super.inode_count), + .xattr_table = try .init(alloc, off_fil, decomp, super.xattr_start), }; } pub fn deinit(self: *Archive) void { diff --git a/src/inode.zig b/src/inode.zig index 5232ac2..ce71b40 100644 --- a/src/inode.zig +++ b/src/inode.zig @@ -15,6 +15,7 @@ const misc = @import("inode_data/misc.zig"); const DataReader = @import("util/data.zig"); const ThreadedDataReader = @import("util/data_threaded.zig"); const MetadataReader = @import("util/metadata.zig"); +const XattrTable = @import("xattr.zig"); pub const Ref = packed struct { block_offset: u16, @@ -154,7 +155,46 @@ fn entriesFromData(alloc: std.mem.Allocator, archive: Archive, data: anytype) ![ return DirEntry.readDir(alloc, &meta.interface, data.size); } -/// Extract the inode to the given path. Single threaded. +/// Returns the xattr index for the given inode. If the inode isn't an extended variant or doesn't have any, the returned value is the max u32 value (0xFFFFFFFF) +pub fn xattrIdx(self: Inode) u32 { + return switch (self.data) { + .ext_dir => |d| d.xattr_id, + .ext_file => |f| f.xattr_idx, + .ext_symlink => |s| s.xattr_idx, + .ext_block_dev, .ext_char_dev => |d| d.xattr_idx, + .ext_fifo, .ext_socket => |i| i.xattr_idx, + else => 0xFFFFFFFF, + }; +} + +inline fn setPermissionAndXattr(self: Inode, alloc: std.mem.Allocator, archive: *Archive, fil: std.fs.File, options: ExtractionOptions) !void { + const time = @as(i128, self.hdr.mod_time) * 1000000000; + try fil.updateTimes(time, time); + if (!options.ignore_permissions) { + try fil.chmod(self.hdr.permissions); + try fil.chown(try archive.id_table.get(self.hdr.uid_idx), try archive.id_table.get(self.hdr.gid_idx)); + } + if (!options.ignore_xattr) { + const idx = self.xattrIdx(); + if (idx == 0xFFFFFFFF) return; + const xattrs = try archive.xattr_table.get(alloc, idx); + defer alloc.free(xattrs); + for (xattrs) |kv| { + defer { + alloc.free(kv.key); + alloc.free(kv.value); + } + const res = std.os.linux.fsetxattr(fil.handle, @ptrCast(kv.key), @ptrCast(kv.value), kv.value.len, 0); + if (res != 0) { + if (options.verbose) + options.verbose_writer.?.print("fsetxattr has result of: {}\n", .{res}) catch {}; + return error.SetXattr; + } + } + } +} + +/// Extract the inode to the given path. pub fn extractTo(self: Inode, alloc: std.mem.Allocator, archive: *Archive, path: []const u8, options: ExtractionOptions) !void { if (options.threads > 1) return self.extractToThreaded(alloc, archive, path, options); switch (self.hdr.inode_type) { @@ -180,42 +220,38 @@ pub fn extractTo(self: Inode, alloc: std.mem.Allocator, archive: *Archive, path: defer inode.deinit(alloc); try inode.extractTo(alloc, archive, new_path, options); } + + var fil = try std.fs.cwd().openFile(path, .{}); + defer fil.close(); + try self.setPermissionAndXattr(alloc, archive, fil, options); }, .file, .ext_file => try self.extractRegFile(alloc, archive, path, options), .symlink, .ext_symlink => try self.extractSymlink(path), - else => try self.extractDevice(archive, path, options), + else => try self.extractDevice(alloc, archive, path, options), } } const Parent = struct { alloc: std.mem.Allocator, + inode: Inode, path: []const u8, - uid: u16, - gid: u16, - perm: u16, - mod_time: u32, - - ignore_permissions: bool, - ignore_xattr: bool, + archive: *Archive, + options: ExtractionOptions, wg: WaitGroup = .{}, mut: Mutex = .{}, - fn create(alloc: std.mem.Allocator, hdr: Header, archive: *Archive, path: []const u8, options: ExtractionOptions, dir_size: usize) !*Parent { + fn create(alloc: std.mem.Allocator, inode: Inode, path: []const u8, archive: *Archive, options: ExtractionOptions, dir_size: usize) !*Parent { const out = try alloc.create(Parent); errdefer alloc.destroy(out); out.* = .{ .alloc = alloc, + .inode = inode, .path = path, - .uid = try archive.id_table.get(hdr.uid_idx), - .gid = try archive.id_table.get(hdr.gid_idx), - .perm = hdr.permissions, - .mod_time = hdr.mod_time, - - .ignore_permissions = options.ignore_permissions, - .ignore_xattr = options.ignore_xattr, + .archive = archive, + .options = options, }; out.wg.startMany(dir_size); return out; @@ -231,33 +267,21 @@ const Parent = struct { defer p.alloc.destroy(p); var fil = try std.fs.cwd().openFile(p.path, .{}); defer fil.close(); - const time = @as(i128, p.mod_time) * 1000000000; - try fil.updateTimes(time, time); - if (p.ignore_permissions) { - try fil.chmod(p.perm); - try fil.chown(p.uid, p.gid); - } + try p.inode.setPermissionAndXattr(p.alloc, p.archive, fil, p.options); } }; /// Extract the inode to the given path. Multi-threaded. /// Functions identically to extractTo on all but regular files and directories. -/// -/// If threads <= 1, then this just calls extractTo. fn extractToThreaded(self: Inode, allocator: std.mem.Allocator, archive: *Archive, path: []const u8, options: ExtractionOptions) !void { switch (self.hdr.inode_type) { .dir, .ext_dir => { // Removing any trailing separators since that's the easiest path forward. if (path[path.len - 1] == '/') return self.extractToThreaded(allocator, archive, path[0 .. path.len - 1], options); - // Fixed Allocator - // const mem_buf = archive.allocator().alloc(u8, 2 * 1024 * 1024 * 1024); - // defer archive.allocator().free(mem_buf); - // var fixed_alloc: std.heap.FixedBufferAllocator = .init(mem_buf); - // const alloc = fixed_alloc.threadSafeAllocator(); - // Arena Allocator - var arena_alloc: std.heap.ArenaAllocator = .init(allocator); + var stack_alloc = std.heap.stackFallback(1024 * 1024, allocator); + var arena_alloc: std.heap.ArenaAllocator = .init(stack_alloc.get()); defer arena_alloc.deinit(); var thread_alloc: std.heap.ThreadSafeAllocator = .{ .child_allocator = arena_alloc.allocator() }; const alloc = thread_alloc.allocator(); @@ -265,7 +289,7 @@ fn extractToThreaded(self: Inode, allocator: std.mem.Allocator, archive: *Archiv var wg: WaitGroup = .{}; // defer if(!options.ignore_permissions) perms.?.deinit(alloc); We don't need to do this due to ArenaAllocator var pool: Pool = undefined; - try pool.init(.{ .allocator = allocator, .n_jobs = options.threads - 1 }); + try pool.init(.{ .allocator = alloc, .n_jobs = options.threads - 1 }); defer pool.deinit(); var out_err: ?anyerror = null; @@ -276,19 +300,16 @@ fn extractToThreaded(self: Inode, allocator: std.mem.Allocator, archive: *Archiv var fil = try std.fs.cwd().openFile(path, .{}); defer fil.close(); - const time = @as(i128, self.hdr.mod_time) * 1000000000; - try fil.updateTimes(time, time); - if (options.ignore_permissions) { - try fil.chmod(self.hdr.permissions); - try fil.chown(try archive.id_table.get(self.hdr.uid_idx), try archive.id_table.get(self.hdr.gid_idx)); - } + try self.setPermissionAndXattr(alloc, archive, fil, options); }, .file, .ext_file => { var pool: Pool = undefined; try pool.init(.{ .allocator = allocator, .n_jobs = options.threads - 1 }); defer pool.deinit(); - var arena_alloc: std.heap.ArenaAllocator = .init(allocator); + // Arena Allocator + var stack_alloc = std.heap.stackFallback(1024 * 1024, allocator); + var arena_alloc: std.heap.ArenaAllocator = .init(stack_alloc.get()); defer arena_alloc.deinit(); var thread_alloc: std.heap.ThreadSafeAllocator = .{ .child_allocator = arena_alloc.allocator() }; const alloc = thread_alloc.allocator(); @@ -297,15 +318,10 @@ fn extractToThreaded(self: Inode, allocator: std.mem.Allocator, archive: *Archiv var fil = try std.fs.cwd().openFile(path, .{}); defer fil.close(); - const time = @as(i128, self.hdr.mod_time) * 1000000000; - try fil.updateTimes(time, time); - if (!options.ignore_permissions) { - try fil.chmod(self.hdr.permissions); - try fil.chown(try archive.id_table.get(self.hdr.uid_idx), try archive.id_table.get(self.hdr.gid_idx)); - } + try self.setPermissionAndXattr(alloc, archive, fil, options); }, .symlink, .ext_symlink => try self.extractSymlink(path), - else => try self.extractDevice(archive, path, options), + else => try self.extractDevice(allocator, archive, path, options), } } @@ -374,7 +390,7 @@ fn extractThread( out_err.* = err; return; }; - const p = Parent.create(alloc, self.hdr, archive, path, options, entries.len) catch |err| { + const p = Parent.create(alloc, self, path, archive, options, entries.len) catch |err| { out_err.* = err; return; }; @@ -422,7 +438,7 @@ fn extractThread( }; }, else => { - self.extractDevice(archive, path, options) catch |err| { + self.extractDevice(alloc, archive, path, options) catch |err| { if (options.verbose) options.verbose_writer.?.print("Error extracting device/IPC inode #{} to {s}: {}\n", .{ self.hdr.num, path, err }) catch {}; out_err.* = err; @@ -443,12 +459,7 @@ fn extractRegFile(self: Inode, alloc: std.mem.Allocator, archive: *Archive, path _ = try dat_rdr.interface.streamRemaining(&wrt.interface); try wrt.interface.flush(); - const time = @as(i128, self.hdr.mod_time) * 1000000000; - try fil.updateTimes(time, time); - if (!options.ignore_permissions) { - try fil.chmod(self.hdr.permissions); - try fil.chown(try archive.id_table.get(self.hdr.uid_idx), try archive.id_table.get(self.hdr.gid_idx)); - } + try self.setPermissionAndXattr(alloc, archive, fil, options); } /// Extract the inode file contents to the given path threadedly. /// pool is used to spawn threads. @@ -460,12 +471,7 @@ fn extractRegFileThreaded(self: Inode, alloc: std.mem.Allocator, archive: *Archi var data = try self.threadedDataReader(alloc, archive); try data.extractThreaded(fil, pool); - const time = @as(i128, self.hdr.mod_time) * 1000000000; - try fil.updateTimes(time, time); - if (!options.ignore_permissions) { - try fil.chmod(self.hdr.permissions); - try fil.chown(try archive.id_table.get(self.hdr.uid_idx), try archive.id_table.get(self.hdr.gid_idx)); - } + try self.setPermissionAndXattr(alloc, archive, fil, options); } /// Creates the symlink described by the inode. /// @@ -482,7 +488,7 @@ fn extractSymlink(self: Inode, path: []const u8) !void { /// /// Optionally set owner & permissions. /// Assumes the inode is a char_dev, block_dev, fifo, socket, or their extended counterparts. -fn extractDevice(self: Inode, archive: *Archive, path: []const u8, options: ExtractionOptions) !void { +fn extractDevice(self: Inode, alloc: std.mem.Allocator, archive: *Archive, path: []const u8, options: ExtractionOptions) !void { var mode: u32 = undefined; var dev: u32 = 0; switch (self.data) { @@ -527,13 +533,5 @@ fn extractDevice(self: Inode, archive: *Archive, path: []const u8, options: Extr } var fil = try std.fs.cwd().openFile(path, .{}); defer fil.close(); - const time = @as(i128, self.hdr.mod_time) * 1000000000; - try fil.updateTimes(time, time); - if (!options.ignore_permissions) { - try fil.chmod(self.hdr.permissions); - try fil.chown(try archive.id_table.get(self.hdr.uid_idx), try archive.id_table.get(self.hdr.gid_idx)); - } - if (!options.ignore_xattr) { - // TODO - } + try self.setPermissionAndXattr(alloc, archive, fil, options); } diff --git a/src/xattr.zig b/src/xattr.zig new file mode 100644 index 0000000..1bffd00 --- /dev/null +++ b/src/xattr.zig @@ -0,0 +1,114 @@ +const std = @import("std"); + +const DecompFn = @import("decomp.zig").DecompFn; +const Table = @import("table.zig").Table; +const MetadataReader = @import("util/metadata.zig"); +const OffsetFile = @import("util/offset_file.zig"); + +const Ref = packed struct { + block_offset: u16, + block_start: u32, + _: u16, +}; +const Entry = packed struct { + ref: Ref, + count: u32, + size: u32, +}; +const KeyPrefix = enum(u8) { + user, + trusted, + security, +}; +const KeyRaw = packed struct { + type: packed struct { + prefix: KeyPrefix, + out_of_line: bool, + _: u7, + }, + name_size: u16, +}; + +pub const KeyValue = struct { + key: []u8, + value: []u8, +}; + +const XattrTable = @This(); + +alloc: std.mem.Allocator, +fil: OffsetFile, +decomp: DecompFn, + +count: u32, +start: u64, + +table: Table(Entry), + +pub fn init(alloc: std.mem.Allocator, fil: OffsetFile, decomp: DecompFn, table_start: u64) !XattrTable { + var info = packed struct { + start: u64 = undefined, + count: u32 = undefined, + _: u32 = undefined, + }{}; + var rdr = try fil.readerAt(table_start, &[0]u8{}); + try rdr.interface.readSliceEndian(@TypeOf(info), @ptrCast(&info), .little); + return .{ + .alloc = alloc, + .fil = fil, + .decomp = decomp, + .count = info.count, + .start = info.start, + .table = try .init(alloc, fil, decomp, table_start + 16, info.count), + }; +} +pub fn deinit(self: XattrTable) void { + self.table.deinit(); +} + +pub fn get(self: *XattrTable, alloc: std.mem.Allocator, idx: u32) ![]KeyValue { + const entry: Entry = try self.table.get(idx); + const out = try alloc.alloc(KeyValue, entry.count); + + for (out) |*kv| { + var rdr = try self.fil.readerAt(self.start + entry.ref.block_start, &[0]u8{}); + var meta: MetadataReader = .init(alloc, &rdr.interface, self.decomp); + try meta.interface.discardAll(entry.ref.block_offset); + + var key_raw: KeyRaw = undefined; + try meta.interface.readSliceEndian(KeyRaw, @ptrCast(&key_raw), .little); + + switch (key_raw.type.prefix) { + .user => { + kv.key = try alloc.alloc(u8, key_raw.name_size + 5); + @memcpy(kv.key[0..5], "user."); + try meta.interface.readSliceAll(kv.key[5..]); + }, + .security => { + kv.key = try alloc.alloc(u8, key_raw.name_size + 9); + @memcpy(kv.key[0..9], "security."); + try meta.interface.readSliceAll(kv.key[9..]); + }, + .trusted => { + kv.key = try alloc.alloc(u8, key_raw.name_size + 8); + @memcpy(kv.key[0..8], "trusted."); + try meta.interface.readSliceAll(kv.key[8..]); + }, + } + if (key_raw.type.out_of_line) { + try meta.interface.discardAll(4); + var ref: Ref = undefined; + try meta.interface.readSliceEndian(Ref, @ptrCast(&ref), .little); + + rdr = try self.fil.readerAt(self.start + ref.block_start, &[0]u8{}); + meta = .init(alloc, &rdr.interface, self.decomp); + try meta.interface.discardAll(ref.block_offset); + } + var value_size: u32 = undefined; + try meta.interface.readSliceEndian(u32, @ptrCast(&value_size), .little); + kv.value = try alloc.alloc(u8, value_size); + try meta.interface.readSliceAll(kv.value); + } + + return out; +}