Added mutli-threaded extraction

This commit is contained in:
Caleb Gardner
2026-06-06 02:54:35 -05:00
parent 62f98cccaf
commit 039e517183
8 changed files with 729 additions and 357 deletions
+5 -2
View File
@@ -84,7 +84,10 @@ pub fn extract(self: *Archive, alloc: std.mem.Allocator, io: Io, ext_loc: []cons
self.super.root_ref, self.super.root_ref,
); );
return Extract.extractSingleThreaded(alloc, io, root_inode, &self.cache, self.super, ext_loc, options); return if (options.single_threaded)
Extract.extractSingleThreaded(alloc, io, root_inode, &self.cache, self.super, ext_loc, options)
else
Extract.extractAsync(alloc, io, root_inode, &self.cache, self.super, ext_loc, options);
} }
// Superblock // Superblock
@@ -188,7 +191,7 @@ test "FullExtraction" {
var arc: Archive = try .init(alloc, io, archive_file); var arc: Archive = try .init(alloc, io, archive_file);
defer arc.deinit(io); defer arc.deinit(io);
try arc.extract(alloc, io, TestFullExtractLocation, .default); try arc.extract(alloc, io, TestFullExtractLocation, .default_single_threaded);
} }
const LinuxPATestCorrectSuperblock: Superblock = .{ const LinuxPATestCorrectSuperblock: Superblock = .{
+13 -3
View File
@@ -57,18 +57,28 @@ pub fn main(init: std.process.Init) !void {
defer fil.close(io); defer fil.close(io);
var arc: squashfs.Archive = try .initAdvanced(alloc, io, fil, offset, 0); //TODO: Update when memory size matters. //TODO: Handle error gracefully. var arc: squashfs.Archive = try .initAdvanced(alloc, io, fil, offset, 0); //TODO: Update when memory size matters. //TODO: Handle error gracefully.
defer arc.deinit(io); defer arc.deinit(io);
const options: squashfs.ExtractionOptions = .{ const options: squashfs.ExtractionOptions = .{
.single_threaded = threads == 1,
.verbose = verbose, .verbose = verbose,
.verbose_writer = if (verbose) &out.interface else null, .verbose_writer = if (verbose) &out.interface else null,
.ignore_xattr = ignore_xattrs, .ignore_xattr = ignore_xattrs,
.ignore_permissions = ignore_permissions, .ignore_permissions = ignore_permissions,
}; };
if (force) if (force)
try Io.Dir.cwd().deleteTree(io, ext_loc); try Io.Dir.cwd().deleteTree(io, ext_loc);
if (threads != 1) {
try arc.extract(alloc, io, ext_loc, options); //TODO: Handle error gracefully. if (threads != 0 and threads != 1) {
var limited_threaded: Io.Threaded = .init(alloc, .{
.argv0 = .init(init.minimal.args),
.async_limit = @enumFromInt(threads),
.concurrent_limit = @enumFromInt(threads),
.environ = init.minimal.environ,
});
try arc.extract(alloc, limited_threaded.io(), ext_loc, options); //TODO: Handle error gracefully.
} else { } else {
try arc.extract(alloc, Io.Threaded.global_single_threaded.io(), ext_loc, options); try arc.extract(alloc, io, ext_loc, options); //TODO: Handle error gracefully.
} }
} }
+404
View File
@@ -0,0 +1,404 @@
const std = @import("std");
const Io = std.Io;
const Atomic = std.atomic.Value;
const DataExtractor = @import("data/extractor.zig");
const DecompCache = @import("decomp_cache.zig");
const Directory = @import("directory.zig");
const ExtractionOptions = @import("options.zig");
const Inode = @import("inode.zig");
const Lookup = @import("lookup.zig");
const Superblock = @import("archive.zig").Superblock;
const XattrTable = @import("xattr.zig");
pub fn extractAsync(
alloc: std.mem.Allocator,
io: Io,
inode: Inode,
cache: *DecompCache,
super: Superblock,
ext_loc: []const u8,
options: ExtractionOptions,
) !void {
const path = std.mem.trim(u8, ext_loc, "/");
var id_table: Lookup.Table(u16) = .init(alloc, cache, super.id_start, super.id_count);
defer id_table.deinit(io);
var xattr_table: XattrTable = try .init(alloc, cache, super.xattr_start);
defer xattr_table.deinit(io);
var sel_buffer: [100]ReturnUnion = undefined;
var sel: Io.Select(ReturnUnion) = .init(io, &sel_buffer);
defer {
while (sel.cancel()) |ret| switch (ret) {
.file_ret => |f| {
const res: FileReturn = f catch continue;
if (res.parent != null)
alloc.free(res.path);
},
.dir_ret => |d| {
const res: DirReturn = d catch continue;
if (res.parent != null)
alloc.free(res.path);
alloc.destroy(res.children);
},
.num_ret => {},
};
}
var concurrent = true;
var loop = io.concurrent(returnLoop, .{ alloc, io, &sel, &id_table, &xattr_table, options, inode.hdr.num }) catch blk: {
concurrent = false;
break :blk io.async(returnLoop, .{ alloc, io, &sel, &id_table, &xattr_table, options, inode.hdr.num });
};
var frag_table: Lookup.Table(Lookup.FragmentEntry) = .init(alloc, cache, super.frag_start, super.frag_count);
defer frag_table.deinit(io);
try extractReal(alloc, io, cache, super, &frag_table, &sel, concurrent, null, path, inode);
try loop.await(io);
}
fn extractReal(
alloc: std.mem.Allocator,
io: Io,
cache: *DecompCache,
super: Superblock,
frag_table: *Lookup.Table(Lookup.FragmentEntry),
sel: *Io.Select(ReturnUnion),
concurrent: bool,
parent: ?*Atomic(u32),
path: []const u8,
inode: Inode,
) !void {
errdefer if (parent != null) {
alloc.free(path);
inode.deinit(alloc);
};
switch (inode.hdr.type) {
.dir, .ext_dir => if (concurrent)
try sel.concurrent(.dir_ret, extractDir, .{ alloc, io, cache, super, frag_table, sel, concurrent, parent, path, inode })
else
sel.async(.dir_ret, extractDir, .{ alloc, io, cache, super, frag_table, sel, concurrent, parent, path, inode }),
.file, .ext_file => if (concurrent)
try sel.concurrent(.file_ret, extractFile, .{ alloc, io, cache, frag_table, super.block_size, parent, path, inode })
else
sel.async(.file_ret, extractFile, .{ alloc, io, cache, frag_table, super.block_size, parent, path, inode }),
.symlink, .ext_symlink => if (concurrent)
try sel.concurrent(.num_ret, extractSymlink, .{ alloc, io, parent, path, inode })
else
sel.async(.num_ret, extractSymlink, .{ alloc, io, parent, path, inode }),
else => if (concurrent)
try sel.concurrent(.file_ret, extractNod, .{ alloc, parent, path, inode })
else
sel.async(.file_ret, extractNod, .{ alloc, parent, path, inode }),
}
}
fn extractDir(
alloc: std.mem.Allocator,
io: Io,
cache: *DecompCache,
super: Superblock,
frag_table: *Lookup.Table(Lookup.FragmentEntry),
sel: *Io.Select(ReturnUnion),
concurrent: bool,
parent: ?*Atomic(u32),
path: []const u8,
inode: Inode,
) Error!DirReturn {
errdefer if (parent != null) alloc.free(path);
var dir = inode.directory(alloc, io, cache, super.dir_start) catch |err| switch (err) {
error.NotDirectory => unreachable,
else => |e| return e,
};
defer dir.deinit(alloc);
const children = try alloc.create(Atomic(usize));
errdefer alloc.destroy(children);
children.* = .init(dir.entries.len);
for (dir.entries) |entry| {
const new_inode: Inode = try .initDirEntry(alloc, io, cache, super.inode_start, super.block_size, entry);
const new_path = std.mem.concat(alloc, u8, &.{ path, "/", entry.name }) catch |err| {
new_inode.deinit(alloc);
return err;
};
try extractReal(alloc, io, cache, super, frag_table, sel, concurrent, parent, new_path, new_inode);
}
return .{
.hdr = inode.hdr,
.path = path,
.parent = parent,
.children = children,
.xattr_idx = switch (inode.data) {
.ext_dir => |d| if (d.xattr_idx != 0xFFFFFFFF)
d.xattr_idx
else
null,
else => null,
},
};
}
fn extractFile(
alloc: std.mem.Allocator,
io: Io,
cache: *DecompCache,
frag_table: *Lookup.Table(Lookup.FragmentEntry),
block_size: u32,
parent: ?*Atomic(u32),
path: []const u8,
inode: Inode,
) Error!FileReturn {
defer if (parent != null) {
inode.deinit(alloc);
_ = parent.?.fetchSub(1, .acquire);
};
errdefer alloc.free(path);
var atomic = try Io.Dir.cwd().createFileAtomic(io, path, .{});
defer atomic.deinit(io);
var ret: FileReturn = .{
.hdr = inode.hdr,
.path = path,
.parent = parent,
};
var data: DataExtractor = switch (inode.data) {
.file => |f| blk: {
var data: DataExtractor = .init(cache, block_size, f.size, f.data_start, f.blocks);
if (f.frag_idx != 0xFFFFFFFF) {
const entry: Lookup.FragmentEntry = try frag_table.get(io, f.frag_idx);
if (entry.size.uncompressed) {
data.addFragment(cache.map.memory[entry.start..][0..entry.size.size], f.frag_offset);
} else {
const block = try cache.get(io, entry.start, entry.size.size, block_size);
data.addFragment(block, f.frag_offset);
}
}
break :blk data;
},
.ext_file => |f| blk: {
var data: DataExtractor = .init(cache, block_size, f.size, f.data_start, f.blocks);
if (f.frag_idx != 0xFFFFFFFF) {
const entry: Lookup.FragmentEntry = try frag_table.get(io, f.frag_idx);
if (entry.size.uncompressed) {
data.addFragment(cache.map.memory[entry.start..][0..entry.size.size], f.frag_offset);
} else {
const block = try cache.get(io, entry.start, entry.size.size, block_size);
data.addFragment(block, f.frag_offset);
}
}
if (f.xattr_idx != 0xFFFFFFFF)
ret.xattr_idx = f.xattr_idx;
break :blk data;
},
else => unreachable,
};
try data.asyncExtract(io, atomic.file);
try atomic.link(io);
return ret;
}
fn extractSymlink(alloc: std.mem.Allocator, io: Io, parent: ?*Atomic(u32), path: []const u8, inode: Inode) Error!u32 {
defer {
alloc.free(path);
if (parent != null) {
inode.deinit(alloc);
_ = parent.?.fetchSub(1, .acquire);
}
}
const target = switch (inode.data) {
.symlink => |s| s.target,
.ext_symlink => |s| s.target,
else => unreachable,
};
try Io.Dir.cwd().symLink(io, target, path, .{});
return inode.hdr.num;
}
fn extractNod(alloc: std.mem.Allocator, parent: ?*Atomic(u32), path: []const u8, inode: Inode) Error!FileReturn {
errdefer if (parent != null) alloc.free(path);
var ret: FileReturn = .{
.hdr = inode.hdr,
.path = path,
.parent = parent,
};
const DT = std.os.linux.DT;
var dev: u32 = 0;
var mode: u32 = undefined;
switch (inode.data) {
.char_dev => |d| {
dev = d.device;
mode = DT.CHR;
},
.ext_char_dev => |d| {
dev = d.device;
mode = DT.CHR;
if (d.xattr_idx != 0xFFFFFFFF)
ret.xattr_idx = d.xattr_idx;
},
.block_dev => |d| {
dev = d.device;
mode = DT.BLK;
},
.ext_block_dev => |d| {
dev = d.device;
mode = DT.BLK;
if (d.xattr_idx != 0xFFFFFFFF)
ret.xattr_idx = d.xattr_idx;
},
.fifo => mode = DT.FIFO,
.ext_fifo => |f| {
mode = DT.FIFO;
if (f.xattr_idx != 0xFFFFFFFF)
ret.xattr_idx = f.xattr_idx;
},
.socket => mode = DT.SOCK,
.ext_socket => |s| {
mode = DT.SOCK;
if (s.xattr_idx != 0xFFFFFFFF)
ret.xattr_idx = s.xattr_idx;
},
else => unreachable,
}
const sentinel_path = try std.mem.concatWithSentinel(alloc, u8, &.{path}, 0);
defer alloc.free(sentinel_path);
const res = std.os.linux.mknod(sentinel_path, mode, dev);
if (res != 0)
return Error.MknodError;
return ret;
}
fn returnLoop(
alloc: std.mem.Allocator,
io: Io,
sel: *Io.Select(ReturnUnion),
id_table: *Lookup.Table(u16),
xattr_table: *XattrTable,
options: ExtractionOptions,
origin_num: u32,
) !void {
var origin_finished = false;
while (!origin_finished) {
const ret = try sel.await();
switch (ret) {
.file_ret => |f| {
const res: FileReturn = try f;
if (res.hdr.num == origin_num)
origin_finished = true;
try res.finish(alloc, io, id_table, xattr_table, options);
},
.dir_ret => |d| {
const res: DirReturn = try d;
if (res.children.load(.unordered) > 0) {
try sel.queue.putOne(io, .{ .dir_ret = res });
continue;
}
if (res.hdr.num == origin_num)
origin_finished = true;
try res.finish(alloc, io, id_table, xattr_table, options);
},
.num_ret => |n| {
const res: u32 = try n;
if (res == origin_num)
origin_finished = true;
},
}
}
}
fn setMetadata(alloc: std.mem.Allocator, io: Io, id_table: *Lookup.Table(u16), xattr_table: *XattrTable, options: ExtractionOptions, hdr: Inode.Header, xattr_idx: ?u32, path: []const u8) Error!void {
if (options.ignore_permissions and (options.ignore_xattr or xattr_idx == null)) return;
const file = try Io.Dir.cwd().openFile(io, path, .{});
defer file.close(io);
if (!options.ignore_permissions) {
try file.setTimestamps(io, .{ .modify_timestamp = .init(Io.Timestamp.fromNanoseconds(@as(i96, @intCast(hdr.mod_time)) * std.time.ns_per_s)) });
try file.setPermissions(io, @enumFromInt(hdr.permission));
try file.setOwner(io, try id_table.get(io, hdr.uid_idx), try id_table.get(io, hdr.gid_idx));
}
if (!options.ignore_permissions and xattr_idx != null) {
var xattrs = try xattr_table.get(alloc, io, xattr_idx.?);
defer xattrs.deinit(alloc);
for (xattrs.xattrs) |x| {
const res = std.os.linux.fsetxattr(file.handle, x.key, x.value.ptr, x.value.len, 0);
if (res != 0)
return Error.SetXattrError;
}
}
}
// Types
const Error = error{ Canceled, MknodError, SetXattrError, ConcurrencyUnavailable } || Directory.Error || Io.Dir.CreateFileAtomicError ||
Io.File.Atomic.LinkError || Io.Dir.SymLinkError || Io.Dir.CreateDirPathError || Io.Reader.StreamError || Io.File.SetPermissionsError ||
Io.File.Writer.Error || DataExtractor.Error;
const ReturnUnion = union(enum) {
file_ret: Error!FileReturn,
dir_ret: Error!DirReturn,
num_ret: Error!u32,
};
const FileReturn = struct {
hdr: Inode.Header,
path: []const u8,
parent: ?*Atomic(u32),
xattr_idx: ?u32 = null,
fn finish(self: FileReturn, alloc: std.mem.Allocator, io: Io, id_table: *Lookup.Table(u16), xattr_table: *XattrTable, options: ExtractionOptions) Error!void {
defer if (self.parent != null) {
alloc.free(self.path);
_ = self.parent.?.fetchSub(1, .acquire);
};
return setMetadata(alloc, io, id_table, xattr_table, options, self.hdr, self.xattr_idx, self.path);
}
};
const DirReturn = struct {
hdr: Inode.Header,
path: []const u8,
parent: ?*Atomic(u32),
children: *Atomic(usize),
xattr_idx: ?u32 = null,
fn finish(self: DirReturn, alloc: std.mem.Allocator, io: Io, id_table: *Lookup.Table(u16), xattr_table: *XattrTable, options: ExtractionOptions) Error!void {
std.debug.assert(self.children.load(.unordered) == 0);
defer {
if (self.parent != null) {
alloc.free(self.path);
_ = self.parent.?.fetchSub(1, .acquire);
}
alloc.destroy(self.children);
}
return setMetadata(alloc, io, id_table, xattr_table, options, self.hdr, self.xattr_idx, self.path);
}
};
+287
View File
@@ -0,0 +1,287 @@
const std = @import("std");
const Io = std.Io;
const Atomic = std.atomic.Value;
const DataReader = @import("data/reader.zig");
const DecompCache = @import("decomp_cache.zig");
const Directory = @import("directory.zig");
const ExtractionOptions = @import("options.zig");
const Inode = @import("inode.zig");
const Lookup = @import("lookup.zig");
const Superblock = @import("archive.zig").Superblock;
const XattrTable = @import("xattr.zig");
pub fn extractSingleThreaded(alloc: std.mem.Allocator, io: Io, inode: Inode, cache: *DecompCache, super: Superblock, ext_loc: []const u8, options: ExtractionOptions) !void {
const path = std.mem.trim(u8, ext_loc, "/");
var id_table: Lookup.Table(u16) = .init(alloc, cache, super.id_start, super.id_count);
defer id_table.deinit(io);
var xattr_table: XattrTable = try .init(alloc, cache, super.xattr_start);
defer xattr_table.deinit(io);
var frag_table: Lookup.Table(Lookup.FragmentEntry) = .init(alloc, cache, super.frag_start, super.frag_count);
defer frag_table.deinit(io);
try extractReal(alloc, io, cache, super, &frag_table, &id_table, &xattr_table, options, path, inode, true);
}
fn extractReal(
alloc: std.mem.Allocator,
io: Io,
cache: *DecompCache,
super: Superblock,
frag_table: *Lookup.Table(Lookup.FragmentEntry),
id_table: *Lookup.Table(u16),
xattr_table: *XattrTable,
options: ExtractionOptions,
path: []const u8,
inode: Inode,
origin: bool,
) Error!void {
defer if (!origin) {
inode.deinit(alloc);
alloc.free(path);
};
switch (inode.data) {
.dir, .ext_dir => {
const res = try extractDir(alloc, io, cache, super, frag_table, id_table, xattr_table, options, path, inode);
try res.finish(alloc, io, id_table, xattr_table, options);
},
.file, .ext_file => {
const res = try extractFile(io, cache, super.block_size, frag_table, path, inode);
try res.finish(alloc, io, id_table, xattr_table, options);
},
.symlink, .ext_symlink => try extractSymlink(io, path, inode),
else => {
const res = try extractNod(alloc, path, inode);
try res.finish(alloc, io, id_table, xattr_table, options);
},
}
}
fn extractDir(
alloc: std.mem.Allocator,
io: Io,
cache: *DecompCache,
super: Superblock,
frag_table: *Lookup.Table(Lookup.FragmentEntry),
id_table: *Lookup.Table(u16),
xattr_table: *XattrTable,
options: ExtractionOptions,
path: []const u8,
inode: Inode,
) Error!DirReturn {
try Io.Dir.cwd().createDirPath(io, path);
const dir = inode.directory(alloc, io, cache, super.dir_start) catch |err| switch (err) {
error.NotDirectory => unreachable,
else => |e| return e,
};
defer dir.deinit(alloc);
const ret: DirReturn = .{
.hdr = inode.hdr,
.path = path,
.xattr_idx = switch (inode.data) {
.ext_dir => |d| if (d.xattr_idx != 0xFFFFFFFF) d.xattr_idx else null,
else => null,
},
};
for (dir.entries) |entry| {
const new_inode: Inode = try .initDirEntry(alloc, io, cache, super.inode_start, super.block_size, entry);
const new_path = std.mem.concat(alloc, u8, &.{ path, "/", entry.name }) catch |err| {
new_inode.deinit(alloc);
return err;
};
try extractReal(
alloc,
io,
cache,
super,
frag_table,
id_table,
xattr_table,
options,
new_path,
new_inode,
false,
);
}
return ret;
}
fn extractFile(
io: Io,
cache: *DecompCache,
block_size: u32,
frag_table: *Lookup.Table(Lookup.FragmentEntry),
path: []const u8,
inode: Inode,
) Error!FileReturn {
var atomic = try Io.Dir.cwd().createFileAtomic(io, path, .{});
defer atomic.deinit(io);
var ret: FileReturn = .{
.hdr = inode.hdr,
.path = path,
};
var data: DataReader = switch (inode.data) {
.file => |f| blk: {
var data: DataReader = .init(io, cache, block_size, f.size, f.data_start, f.blocks);
if (f.frag_idx != 0xFFFFFFFF) {
const entry: Lookup.FragmentEntry = try frag_table.get(io, f.frag_idx);
if (entry.size.uncompressed) {
data.addFragment(cache.map.memory[entry.start..][0..entry.size.size], f.frag_offset);
} else {
const block = try cache.get(io, entry.start, entry.size.size, block_size);
data.addFragment(block, f.frag_offset);
}
}
break :blk data;
},
.ext_file => |f| blk: {
var data: DataReader = .init(io, cache, block_size, f.size, f.data_start, f.blocks);
if (f.frag_idx != 0xFFFFFFFF) {
const entry: Lookup.FragmentEntry = try frag_table.get(io, f.frag_idx);
if (entry.size.uncompressed) {
data.addFragment(cache.map.memory[entry.start..][0..entry.size.size], f.frag_offset);
} else {
const block = try cache.get(io, entry.start, entry.size.size, block_size);
data.addFragment(block, f.frag_offset);
}
}
if (f.xattr_idx != 0xFFFFFFFF)
ret.xattr_idx = f.xattr_idx;
break :blk data;
},
else => unreachable,
};
defer data.deinit();
var wrt_buf: [1024]u8 = undefined;
var wrt = atomic.file.writer(io, &wrt_buf);
_ = try data.interface.streamRemaining(&wrt.interface);
try wrt.flush();
try atomic.link(io);
return ret;
}
fn extractSymlink(io: Io, path: []const u8, inode: Inode) Error!void {
const target = switch (inode.data) {
.symlink => |s| s.target,
.ext_symlink => |s| s.target,
else => unreachable,
};
try Io.Dir.cwd().symLink(io, target, path, .{});
}
fn extractNod(alloc: std.mem.Allocator, path: []const u8, inode: Inode) Error!FileReturn {
var ret: FileReturn = .{
.hdr = inode.hdr,
.path = path,
};
const DT = std.os.linux.DT;
var dev: u32 = 0;
var mode: u32 = undefined;
switch (inode.data) {
.char_dev => |d| {
dev = d.device;
mode = DT.CHR;
},
.ext_char_dev => |d| {
dev = d.device;
mode = DT.CHR;
if (d.xattr_idx != 0xFFFFFFFF)
ret.xattr_idx = d.xattr_idx;
},
.block_dev => |d| {
dev = d.device;
mode = DT.BLK;
},
.ext_block_dev => |d| {
dev = d.device;
mode = DT.BLK;
if (d.xattr_idx != 0xFFFFFFFF)
ret.xattr_idx = d.xattr_idx;
},
.fifo => mode = DT.FIFO,
.ext_fifo => |f| {
mode = DT.FIFO;
if (f.xattr_idx != 0xFFFFFFFF)
ret.xattr_idx = f.xattr_idx;
},
.socket => mode = DT.SOCK,
.ext_socket => |s| {
mode = DT.SOCK;
if (s.xattr_idx != 0xFFFFFFFF)
ret.xattr_idx = s.xattr_idx;
},
else => unreachable,
}
const sentinel_path = try std.mem.concatWithSentinel(alloc, u8, &.{path}, 0);
defer alloc.free(sentinel_path);
const res = std.os.linux.mknod(sentinel_path, mode, dev);
if (res != 0)
return Error.MknodError;
return ret;
}
fn setMetadata(alloc: std.mem.Allocator, io: Io, id_table: *Lookup.Table(u16), xattr_table: *XattrTable, options: ExtractionOptions, hdr: Inode.Header, xattr_idx: ?u32, path: []const u8) Error!void {
if (options.ignore_permissions and (options.ignore_xattr or xattr_idx == null)) return;
const file = try Io.Dir.cwd().openFile(io, path, .{});
defer file.close(io);
if (!options.ignore_permissions) {
try file.setTimestamps(io, .{ .modify_timestamp = .init(Io.Timestamp.fromNanoseconds(@as(i96, @intCast(hdr.mod_time)) * std.time.ns_per_s)) });
try file.setPermissions(io, @enumFromInt(hdr.permission));
try file.setOwner(io, try id_table.get(io, hdr.uid_idx), try id_table.get(io, hdr.gid_idx));
}
if (!options.ignore_permissions and xattr_idx != null) {
var xattrs = try xattr_table.get(alloc, io, xattr_idx.?);
defer xattrs.deinit(alloc);
for (xattrs.xattrs) |x| {
const res = std.os.linux.fsetxattr(file.handle, x.key, x.value.ptr, x.value.len, 0);
if (res != 0)
return Error.SetXattrError;
}
}
}
const Error = error{ Canceled, MknodError, SetXattrError } || Directory.Error || Io.Dir.CreateFileAtomicError || Io.File.Atomic.LinkError ||
Io.Dir.SymLinkError || Io.Dir.CreateDirPathError || Io.Reader.StreamError || Io.File.SetPermissionsError || Io.File.Writer.Error;
const FileReturn = struct {
hdr: Inode.Header,
path: []const u8,
xattr_idx: ?u32 = null,
fn finish(self: FileReturn, alloc: std.mem.Allocator, io: Io, id_table: *Lookup.Table(u16), xattr_table: *XattrTable, options: ExtractionOptions) Error!void {
return setMetadata(alloc, io, id_table, xattr_table, options, self.hdr, self.xattr_idx, self.path);
}
};
const DirReturn = struct {
hdr: Inode.Header,
path: []const u8,
xattr_idx: ?u32 = null,
fn finish(self: DirReturn, alloc: std.mem.Allocator, io: Io, id_table: *Lookup.Table(u16), xattr_table: *XattrTable, options: ExtractionOptions) Error!void {
return setMetadata(alloc, io, id_table, xattr_table, options, self.hdr, self.xattr_idx, self.path);
}
};
+12 -266
View File
@@ -1,287 +1,33 @@
const std = @import("std"); const std = @import("std");
const Io = std.Io; const Io = std.Io;
const Atomic = std.atomic.Value;
const DataReader = @import("data/reader.zig");
const DecompCache = @import("decomp_cache.zig"); const DecompCache = @import("decomp_cache.zig");
const Directory = @import("directory.zig");
const ExtractionOptions = @import("options.zig"); const ExtractionOptions = @import("options.zig");
const Inode = @import("inode.zig"); const Inode = @import("inode.zig");
const Lookup = @import("lookup.zig"); const MultiThreaded = @import("extract-multi.zig");
const SingleThreaded = @import("extract-single.zig");
const Superblock = @import("archive.zig").Superblock; const Superblock = @import("archive.zig").Superblock;
const XattrTable = @import("xattr.zig");
pub fn extractSingleThreaded(alloc: std.mem.Allocator, io: Io, inode: Inode, cache: *DecompCache, super: Superblock, ext_loc: []const u8, options: ExtractionOptions) !void { pub fn extractSingleThreaded(
const path = std.mem.trim(u8, ext_loc, "/");
var id_table: Lookup.Table(u16) = .init(alloc, cache, super.id_start, super.id_count);
defer id_table.deinit(io);
var xattr_table: XattrTable = try .init(alloc, cache, super.xattr_start);
defer xattr_table.deinit(io);
var frag_table: Lookup.Table(Lookup.FragmentEntry) = .init(alloc, cache, super.frag_start, super.frag_count);
defer frag_table.deinit(io);
try extractReal(alloc, io, cache, super, &frag_table, &id_table, &xattr_table, options, path, inode, true);
}
fn extractReal(
alloc: std.mem.Allocator, alloc: std.mem.Allocator,
io: Io, io: Io,
inode: Inode,
cache: *DecompCache, cache: *DecompCache,
super: Superblock, super: Superblock,
frag_table: *Lookup.Table(Lookup.FragmentEntry), ext_loc: []const u8,
id_table: *Lookup.Table(u16),
xattr_table: *XattrTable,
options: ExtractionOptions, options: ExtractionOptions,
path: []const u8, ) !void {
inode: Inode, return SingleThreaded.extractSingleThreaded(alloc, io, inode, cache, super, ext_loc, options);
origin: bool,
) Error!void {
defer if (!origin) {
inode.deinit(alloc);
alloc.free(path);
};
switch (inode.data) {
.dir, .ext_dir => {
const res = try extractDir(alloc, io, cache, super, frag_table, id_table, xattr_table, options, path, inode);
try res.finish(alloc, io, id_table, xattr_table, options);
},
.file, .ext_file => {
const res = try extractFile(io, cache, super.block_size, frag_table, path, inode);
try res.finish(alloc, io, id_table, xattr_table, options);
},
.symlink, .ext_symlink => try extractSymlink(io, path, inode),
else => {
const res = try extractNod(alloc, path, inode);
try res.finish(alloc, io, id_table, xattr_table, options);
},
}
} }
fn extractDir( pub fn extractAsync(
alloc: std.mem.Allocator, alloc: std.mem.Allocator,
io: Io, io: Io,
inode: Inode,
cache: *DecompCache, cache: *DecompCache,
super: Superblock, super: Superblock,
frag_table: *Lookup.Table(Lookup.FragmentEntry), ext_loc: []const u8,
id_table: *Lookup.Table(u16),
xattr_table: *XattrTable,
options: ExtractionOptions, options: ExtractionOptions,
path: []const u8, ) !void {
inode: Inode, return MultiThreaded.extractAsync(alloc, io, inode, cache, super, ext_loc, options);
) Error!DirReturn {
try Io.Dir.cwd().createDirPath(io, path);
const dir = inode.directory(alloc, io, cache, super.dir_start) catch |err| switch (err) {
error.NotDirectory => unreachable,
else => |e| return e,
};
defer dir.deinit(alloc);
const ret: DirReturn = .{
.hdr = inode.hdr,
.path = path,
.xattr_idx = switch (inode.data) {
.ext_dir => |d| if (d.xattr_idx != 0xFFFFFFFF) d.xattr_idx else null,
else => null,
},
};
for (dir.entries) |entry| {
const new_inode: Inode = try .initDirEntry(alloc, io, cache, super.inode_start, super.block_size, entry);
const new_path = std.mem.concat(alloc, u8, &.{ path, "/", entry.name }) catch |err| {
new_inode.deinit(alloc);
return err;
};
try extractReal(
alloc,
io,
cache,
super,
frag_table,
id_table,
xattr_table,
options,
new_path,
new_inode,
false,
);
} }
return ret;
}
fn extractFile(
io: Io,
cache: *DecompCache,
block_size: u32,
frag_table: *Lookup.Table(Lookup.FragmentEntry),
path: []const u8,
inode: Inode,
) Error!FileReturn {
var atomic = try Io.Dir.cwd().createFileAtomic(io, path, .{});
defer atomic.deinit(io);
var ret: FileReturn = .{
.hdr = inode.hdr,
.path = path,
};
var data: DataReader = switch (inode.data) {
.file => |f| blk: {
var data: DataReader = .init(io, cache, block_size, f.size, f.data_start, f.blocks);
if (f.frag_idx != 0xFFFFFFFF) {
const entry: Lookup.FragmentEntry = try frag_table.get(io, f.frag_idx);
if (entry.size.uncompressed) {
data.addFragment(cache.map.memory[entry.start..][0..entry.size.size], f.frag_offset);
} else {
const block = try cache.get(io, entry.start, entry.size.size, block_size);
data.addFragment(block, f.frag_offset);
}
}
break :blk data;
},
.ext_file => |f| blk: {
var data: DataReader = .init(io, cache, block_size, f.size, f.data_start, f.blocks);
if (f.frag_idx != 0xFFFFFFFF) {
const entry: Lookup.FragmentEntry = try frag_table.get(io, f.frag_idx);
if (entry.size.uncompressed) {
data.addFragment(cache.map.memory[entry.start..][0..entry.size.size], f.frag_offset);
} else {
const block = try cache.get(io, entry.start, entry.size.size, block_size);
data.addFragment(block, f.frag_offset);
}
}
if (f.xattr_idx != 0xFFFFFFFF)
ret.xattr_idx = f.xattr_idx;
break :blk data;
},
else => unreachable,
};
defer data.deinit();
var wrt_buf: [1024]u8 = undefined;
var wrt = atomic.file.writer(io, &wrt_buf);
_ = try data.interface.streamRemaining(&wrt.interface);
try wrt.flush();
try atomic.link(io);
return ret;
}
fn extractSymlink(io: Io, path: []const u8, inode: Inode) Error!void {
const target = switch (inode.data) {
.symlink => |s| s.target,
.ext_symlink => |s| s.target,
else => unreachable,
};
try Io.Dir.cwd().symLink(io, target, path, .{});
}
fn extractNod(alloc: std.mem.Allocator, path: []const u8, inode: Inode) Error!FileReturn {
var ret: FileReturn = .{
.hdr = inode.hdr,
.path = path,
};
const DT = std.os.linux.DT;
var dev: u32 = 0;
var mode: u32 = undefined;
switch (inode.data) {
.char_dev => |d| {
dev = d.device;
mode = DT.CHR;
},
.ext_char_dev => |d| {
dev = d.device;
mode = DT.CHR;
if (d.xattr_idx != 0xFFFFFFFF)
ret.xattr_idx = d.xattr_idx;
},
.block_dev => |d| {
dev = d.device;
mode = DT.BLK;
},
.ext_block_dev => |d| {
dev = d.device;
mode = DT.BLK;
if (d.xattr_idx != 0xFFFFFFFF)
ret.xattr_idx = d.xattr_idx;
},
.fifo => mode = DT.FIFO,
.ext_fifo => |f| {
mode = DT.FIFO;
if (f.xattr_idx != 0xFFFFFFFF)
ret.xattr_idx = f.xattr_idx;
},
.socket => mode = DT.SOCK,
.ext_socket => |s| {
mode = DT.SOCK;
if (s.xattr_idx != 0xFFFFFFFF)
ret.xattr_idx = s.xattr_idx;
},
else => unreachable,
}
const sentinel_path = try std.mem.concatWithSentinel(alloc, u8, &.{path}, 0);
defer alloc.free(sentinel_path);
const res = std.os.linux.mknod(sentinel_path, mode, dev);
if (res != 0)
return Error.MknodError;
return ret;
}
fn setMetadata(alloc: std.mem.Allocator, io: Io, id_table: *Lookup.Table(u16), xattr_table: *XattrTable, options: ExtractionOptions, hdr: Inode.Header, xattr_idx: ?u32, path: []const u8) Error!void {
if (options.ignore_permissions and (options.ignore_xattr or xattr_idx == null)) return;
const file = try Io.Dir.cwd().openFile(io, path, .{});
defer file.close(io);
if (!options.ignore_permissions) {
try file.setTimestamps(io, .{ .modify_timestamp = .init(Io.Timestamp.fromNanoseconds(@as(i96, @intCast(hdr.mod_time)) * std.time.ns_per_s)) });
try file.setPermissions(io, @enumFromInt(hdr.permission));
try file.setOwner(io, try id_table.get(io, hdr.uid_idx), try id_table.get(io, hdr.gid_idx));
}
if (!options.ignore_permissions and xattr_idx != null) {
var xattrs = try xattr_table.get(alloc, io, xattr_idx.?);
defer xattrs.deinit(alloc);
for (xattrs.xattrs) |x| {
const res = std.os.linux.fsetxattr(file.handle, x.key, x.value.ptr, x.value.len, 0);
if (res != 0)
return Error.SetXattrError;
}
}
}
const Error = error{ Canceled, MknodError, SetXattrError } || Directory.Error || Io.Dir.CreateFileAtomicError || Io.File.Atomic.LinkError ||
Io.Dir.SymLinkError || Io.Dir.CreateDirPathError || Io.Reader.StreamError || Io.File.SetPermissionsError;
const FileReturn = struct {
hdr: Inode.Header,
path: []const u8,
xattr_idx: ?u32 = null,
fn finish(self: FileReturn, alloc: std.mem.Allocator, io: Io, id_table: *Lookup.Table(u16), xattr_table: *XattrTable, options: ExtractionOptions) Error!void {
return setMetadata(alloc, io, id_table, xattr_table, options, self.hdr, self.xattr_idx, self.path);
}
};
const DirReturn = struct {
hdr: Inode.Header,
path: []const u8,
xattr_idx: ?u32 = null,
fn finish(self: DirReturn, alloc: std.mem.Allocator, io: Io, id_table: *Lookup.Table(u16), xattr_table: *XattrTable, options: ExtractionOptions) Error!void {
return setMetadata(alloc, io, id_table, xattr_table, options, self.hdr, self.xattr_idx, self.path);
}
};
+5 -2
View File
@@ -102,6 +102,9 @@ pub fn open(self: *SfsFile, alloc: std.mem.Allocator, io: Io, filepath: []const
return found_file.open(alloc, io, path[first_element.len..]); return found_file.open(alloc, io, path[first_element.len..]);
} }
pub fn extract(self: *SfsFile, alloc: std.mem.Allocator, io: Io, ext_dir: []const u8, options: ExtractionOptions) !void { pub fn extract(self: *SfsFile, alloc: std.mem.Allocator, io: Io, ext_loc: []const u8, options: ExtractionOptions) !void {
return Extract.extractSingleThreaded(alloc, io, self.inode, &self.archive.cache, self.archive.super, ext_dir, options); return if (options.single_threaded)
Extract.extractSingleThreaded(alloc, io, self.inode, &self.archive.cache, self.archive.super, ext_loc, options)
else
Extract.extractAsync(alloc, io, self.inode, &self.archive.cache, self.archive.super, ext_loc, options);
} }
+3
View File
@@ -5,6 +5,8 @@ const Writer = std.Io.Writer;
const ExtractionOptions = @This(); const ExtractionOptions = @This();
/// Force single-threaded extraction.
single_threaded: bool = false,
/// Don't set the file's owner & permissions after extraction /// Don't set the file's owner & permissions after extraction
ignore_permissions: bool = false, ignore_permissions: bool = false,
/// Don't set xattr values. Currently xattrs are never set anyway. /// Don't set xattr values. Currently xattrs are never set anyway.
@@ -17,6 +19,7 @@ verbose: bool = false,
verbose_writer: ?*Writer = null, verbose_writer: ?*Writer = null,
pub const default: ExtractionOptions = .{}; pub const default: ExtractionOptions = .{};
pub const default_single_threaded: ExtractionOptions = .{ .single_threaded = true };
pub fn VerboseDefault(wrt: *Writer) !ExtractionOptions { pub fn VerboseDefault(wrt: *Writer) !ExtractionOptions {
return .{ return .{
.verbose = true, .verbose = true,
-84
View File
@@ -1,84 +0,0 @@
const std = @import("std");
const stuff = @import("builtin");
const Archive = @import("archive.zig");
const Superblock = @import("super.zig").Superblock;
const TestArchive = "testing/LinuxPATest.sfs";
test "Basics" {
var fil = try std.fs.cwd().openFile(TestArchive, .{});
defer fil.close();
var sfs: Archive = try .init(std.testing.allocator, fil);
defer sfs.deinit();
if (sfs.super != LinuxPATestCorrectSuperblock) {
std.debug.print("Superblock wrong\nShould be: {}\n\nis: {}\n", .{ LinuxPATestCorrectSuperblock, sfs.super });
return error.BadSuperblock;
}
}
const TestFile = "Start.exe";
const TestFileExtractLocation = "testing/Start.exe";
test "ExtractSingleFile" {
std.fs.cwd().deleteFile(TestFileExtractLocation) catch {};
var fil = try std.fs.cwd().openFile(TestArchive, .{});
defer fil.close();
var sfs: Archive = try .init(std.testing.allocator, fil);
defer sfs.deinit();
var test_fil = try sfs.open(TestFile);
defer test_fil.deinit();
try test_fil.extract(TestFileExtractLocation, .Default);
//TODO: validate extracted file.
}
const TestFullExtractLocation = "testing/TestExtract";
test "ExtractCompleteArchive" {
std.fs.cwd().deleteTree(TestFullExtractLocation) catch {};
var fil = try std.fs.cwd().openFile(TestArchive, .{});
defer fil.close();
var sfs: Archive = try .init(std.testing.allocator, fil);
defer sfs.deinit();
try sfs.extract(TestFullExtractLocation, .Default);
}
const LinuxPATestCorrectSuperblock: Superblock = .{
.magic = std.mem.readInt(u32, "hsqs", .little),
.inode_count = 2974,
.mod_time = 1632696724,
.block_size = 131072,
.frag_count = 264,
.compression = .zstd,
.block_log = 17,
.flags = .{
.inode_uncompressed = false,
.data_uncompressed = false,
.check = false,
.frag_uncompressed = false,
.fragment_never = false,
.fragment_always = false,
.duplicates = true,
.exportable = true,
.xattr_uncompressed = false,
.xattr_never = false,
.compression_options = false,
.ids_uncompressed = false,
._ = 0,
},
.id_count = 1,
.ver_maj = 4,
.ver_min = 0,
.root_ref = .{
.block_offset = 1363,
.block_start = 29237,
._ = 0,
},
.size = 106841744,
.id_start = 106841632,
.xattr_start = 106841720,
.inode_start = 106778274,
.dir_start = 106807998,
.frag_start = 106837747,
.export_start = 106841602,
};