Added xattr support (currently untested)

Use stack fallback allocator on extraction for better performance.
This commit is contained in:
Caleb J. Gardner
2026-03-04 03:21:01 -06:00
parent beca6a2ae6
commit 4515610082
3 changed files with 189 additions and 77 deletions
+6 -6
View File
@@ -15,6 +15,7 @@ const Superblock = @import("super.zig").Superblock;
const Table = @import("table.zig").Table; const Table = @import("table.zig").Table;
const MetadataReader = @import("util/metadata.zig"); const MetadataReader = @import("util/metadata.zig");
const OffsetFile = @import("util/offset_file.zig"); const OffsetFile = @import("util/offset_file.zig");
const XattrTable = @import("xattr.zig");
const config = if (builtin.is_test) .{ const config = if (builtin.is_test) .{
.use_c_libs = true, .use_c_libs = true,
@@ -38,9 +39,10 @@ decomp: Decomp.DecompFn,
super: Superblock, super: Superblock,
frag_table: Table(FragEntry) = undefined, frag_table: Table(FragEntry),
id_table: Table(u16) = undefined, id_table: Table(u16),
export_table: Table(InodeRef) = undefined, 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. /// 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 { 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 .{ return .{
.alloc = alloc, .alloc = alloc,
.fil = off_fil, .fil = off_fil,
.decomp = decomp, .decomp = decomp,
.super = super, .super = super,
.frag_table = try .init(alloc, off_fil, decomp, super.frag_start, super.frag_count), .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), .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), .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 { pub fn deinit(self: *Archive) void {
+69 -71
View File
@@ -15,6 +15,7 @@ const misc = @import("inode_data/misc.zig");
const DataReader = @import("util/data.zig"); const DataReader = @import("util/data.zig");
const ThreadedDataReader = @import("util/data_threaded.zig"); const ThreadedDataReader = @import("util/data_threaded.zig");
const MetadataReader = @import("util/metadata.zig"); const MetadataReader = @import("util/metadata.zig");
const XattrTable = @import("xattr.zig");
pub const Ref = packed struct { pub const Ref = packed struct {
block_offset: u16, 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); 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 { 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); if (options.threads > 1) return self.extractToThreaded(alloc, archive, path, options);
switch (self.hdr.inode_type) { 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); defer inode.deinit(alloc);
try inode.extractTo(alloc, archive, new_path, options); 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), .file, .ext_file => try self.extractRegFile(alloc, archive, path, options),
.symlink, .ext_symlink => try self.extractSymlink(path), .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 { const Parent = struct {
alloc: std.mem.Allocator, alloc: std.mem.Allocator,
inode: Inode,
path: []const u8, path: []const u8,
uid: u16, archive: *Archive,
gid: u16, options: ExtractionOptions,
perm: u16,
mod_time: u32,
ignore_permissions: bool,
ignore_xattr: bool,
wg: WaitGroup = .{}, wg: WaitGroup = .{},
mut: Mutex = .{}, 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); const out = try alloc.create(Parent);
errdefer alloc.destroy(out); errdefer alloc.destroy(out);
out.* = .{ out.* = .{
.alloc = alloc, .alloc = alloc,
.inode = inode,
.path = path, .path = path,
.uid = try archive.id_table.get(hdr.uid_idx), .archive = archive,
.gid = try archive.id_table.get(hdr.gid_idx), .options = options,
.perm = hdr.permissions,
.mod_time = hdr.mod_time,
.ignore_permissions = options.ignore_permissions,
.ignore_xattr = options.ignore_xattr,
}; };
out.wg.startMany(dir_size); out.wg.startMany(dir_size);
return out; return out;
@@ -231,33 +267,21 @@ const Parent = struct {
defer p.alloc.destroy(p); defer p.alloc.destroy(p);
var fil = try std.fs.cwd().openFile(p.path, .{}); var fil = try std.fs.cwd().openFile(p.path, .{});
defer fil.close(); defer fil.close();
const time = @as(i128, p.mod_time) * 1000000000; try p.inode.setPermissionAndXattr(p.alloc, p.archive, fil, p.options);
try fil.updateTimes(time, time);
if (p.ignore_permissions) {
try fil.chmod(p.perm);
try fil.chown(p.uid, p.gid);
}
} }
}; };
/// Extract the inode to the given path. Multi-threaded. /// Extract the inode to the given path. Multi-threaded.
/// Functions identically to extractTo on all but regular files and directories. /// 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 { fn extractToThreaded(self: Inode, allocator: std.mem.Allocator, archive: *Archive, path: []const u8, options: ExtractionOptions) !void {
switch (self.hdr.inode_type) { switch (self.hdr.inode_type) {
.dir, .ext_dir => { .dir, .ext_dir => {
// Removing any trailing separators since that's the easiest path forward. // 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); 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 // 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(); defer arena_alloc.deinit();
var thread_alloc: std.heap.ThreadSafeAllocator = .{ .child_allocator = arena_alloc.allocator() }; var thread_alloc: std.heap.ThreadSafeAllocator = .{ .child_allocator = arena_alloc.allocator() };
const alloc = thread_alloc.allocator(); const alloc = thread_alloc.allocator();
@@ -265,7 +289,7 @@ fn extractToThreaded(self: Inode, allocator: std.mem.Allocator, archive: *Archiv
var wg: WaitGroup = .{}; var wg: WaitGroup = .{};
// defer if(!options.ignore_permissions) perms.?.deinit(alloc); We don't need to do this due to ArenaAllocator // defer if(!options.ignore_permissions) perms.?.deinit(alloc); We don't need to do this due to ArenaAllocator
var pool: Pool = undefined; 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(); defer pool.deinit();
var out_err: ?anyerror = null; 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, .{}); var fil = try std.fs.cwd().openFile(path, .{});
defer fil.close(); defer fil.close();
const time = @as(i128, self.hdr.mod_time) * 1000000000; try self.setPermissionAndXattr(alloc, archive, fil, options);
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));
}
}, },
.file, .ext_file => { .file, .ext_file => {
var pool: Pool = undefined; var pool: Pool = undefined;
try pool.init(.{ .allocator = allocator, .n_jobs = options.threads - 1 }); try pool.init(.{ .allocator = allocator, .n_jobs = options.threads - 1 });
defer pool.deinit(); 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(); defer arena_alloc.deinit();
var thread_alloc: std.heap.ThreadSafeAllocator = .{ .child_allocator = arena_alloc.allocator() }; var thread_alloc: std.heap.ThreadSafeAllocator = .{ .child_allocator = arena_alloc.allocator() };
const alloc = thread_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, .{}); var fil = try std.fs.cwd().openFile(path, .{});
defer fil.close(); defer fil.close();
const time = @as(i128, self.hdr.mod_time) * 1000000000; try self.setPermissionAndXattr(alloc, archive, fil, options);
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));
}
}, },
.symlink, .ext_symlink => try self.extractSymlink(path), .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; out_err.* = err;
return; 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; out_err.* = err;
return; return;
}; };
@@ -422,7 +438,7 @@ fn extractThread(
}; };
}, },
else => { else => {
self.extractDevice(archive, path, options) catch |err| { self.extractDevice(alloc, archive, path, options) catch |err| {
if (options.verbose) if (options.verbose)
options.verbose_writer.?.print("Error extracting device/IPC inode #{} to {s}: {}\n", .{ self.hdr.num, path, err }) catch {}; options.verbose_writer.?.print("Error extracting device/IPC inode #{} to {s}: {}\n", .{ self.hdr.num, path, err }) catch {};
out_err.* = err; 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 dat_rdr.interface.streamRemaining(&wrt.interface);
try wrt.interface.flush(); try wrt.interface.flush();
const time = @as(i128, self.hdr.mod_time) * 1000000000; try self.setPermissionAndXattr(alloc, archive, fil, options);
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));
}
} }
/// Extract the inode file contents to the given path threadedly. /// Extract the inode file contents to the given path threadedly.
/// pool is used to spawn threads. /// 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); var data = try self.threadedDataReader(alloc, archive);
try data.extractThreaded(fil, pool); try data.extractThreaded(fil, pool);
const time = @as(i128, self.hdr.mod_time) * 1000000000; try self.setPermissionAndXattr(alloc, archive, fil, options);
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));
}
} }
/// Creates the symlink described by the inode. /// Creates the symlink described by the inode.
/// ///
@@ -482,7 +488,7 @@ fn extractSymlink(self: Inode, path: []const u8) !void {
/// ///
/// Optionally set owner & permissions. /// Optionally set owner & permissions.
/// Assumes the inode is a char_dev, block_dev, fifo, socket, or their extended counterparts. /// 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 mode: u32 = undefined;
var dev: u32 = 0; var dev: u32 = 0;
switch (self.data) { 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, .{}); var fil = try std.fs.cwd().openFile(path, .{});
defer fil.close(); defer fil.close();
const time = @as(i128, self.hdr.mod_time) * 1000000000; try self.setPermissionAndXattr(alloc, archive, fil, options);
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
}
} }
+114
View File
@@ -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;
}