Added --force to unsquashfs

Fixing race condition bugs (yay)
This commit is contained in:
Caleb J. Gardner
2026-03-05 07:04:24 -06:00
parent a606f5e11a
commit d470ca98e3
6 changed files with 112 additions and 22 deletions
+30
View File
@@ -0,0 +1,30 @@
// Project-local debug tasks
//
// For more documentation on how to configure debug tasks,
// see: https://zed.dev/docs/debugger
[
{
"label": "Build & Run",
"adapter": "CodeLLDB",
"request": "launch",
"build": {
"command": "zig",
"args": [
"build",
"-Doptimize=Debug",
"-Duse_c_libs=true",
"-Dvalgrind=true",
],
},
"program": "zig-out/bin/unsquashfs",
"args": [
"--force",
"-d",
"testing/TestExtractUnsquashfs",
"testing/LinuxPATest.sfs",
],
},
]
+1
View File
@@ -52,6 +52,7 @@ pub fn build(b: *std.Build) !void {
const exe = b.addExecutable(.{
.name = "unsquashfs",
.root_module = exe_mod,
.use_llvm = true,
});
const lib = b.addLibrary(.{
+8
View File
@@ -20,6 +20,8 @@ const help_mgs =
\\ -p <threads> Specify how many threads to use. If no present or zero, the system's logical cores count is used.
\\ -v Verbose
\\
\\ --force Force extraction. If the destination already exists, it will be deleted.
\\
\\ --help Display this messages
\\ --version Display the version
\\
@@ -34,6 +36,7 @@ var threads: u32 = 0;
var verbose: bool = false;
var ignore_xattrs: bool = false;
var ignore_permissions: bool = false;
var force: bool = false;
pub fn main() !void {
const alloc = std.heap.smp_allocator;
@@ -57,6 +60,8 @@ pub fn main() !void {
.ignore_xattr = ignore_xattrs,
.ignore_permissions = ignore_permissions,
};
if (force)
try std.fs.cwd().deleteTree(extLoc);
try arc.extract(alloc, extLoc, options); //TODO: Handle error gracefully.
}
@@ -104,6 +109,9 @@ fn handleArgs(alloc: std.mem.Allocator, out: *Writer) !void {
} else if (std.mem.eql(u8, arg, "-dp")) {
ignore_permissions = true;
continue;
} else if (std.mem.eql(u8, arg, "--force")) {
force = true;
continue;
} else if (std.mem.eql(u8, arg, "--version")) {
try out.print("zig-unsquashfs v", .{});
try config.version.format(out);
+31 -4
View File
@@ -53,18 +53,26 @@ pub fn addFragment(self: *ThreadedDataReader, entry: FragEntry, frag_offset: u32
}
/// Extract the data to the file threadedly, using pool to spawn threads.
/// This function only returns an error if pool.spawn fails. For actual extraction errors finish.out_err will be set.
pub fn extractThreaded(self: ThreadedDataReader, file: std.fs.File, pool: *Pool, finish: *InodeFinish) !void {
/// If errors occur, they are set to finish.out_err.
pub fn extractThreaded(self: ThreadedDataReader, file: std.fs.File, pool: *Pool, finish: *InodeFinish) void {
var cur_write_offset: u64 = 0;
var cur_read_offset: u64 = self.start_offset;
for (0..self.blocks.len) |i| {
const cur_block_size = if (i == self.num_blocks - 1) self.size % self.block_size else self.block_size;
try pool.spawn(workThreadBlocks, .{ self, file, cur_write_offset, cur_read_offset, self.blocks[i], cur_block_size, finish });
pool.spawn(workThreadBlocks, .{ self, file, cur_write_offset, cur_read_offset, self.blocks[i], cur_block_size, finish }) catch |res_err| {
finish.logError("Can't spawn pool task: {}", .{res_err});
finish.out_err.* = res_err;
finish.finish();
};
cur_write_offset += cur_block_size;
cur_read_offset += self.blocks[i].size;
}
if (self.frag != null)
try pool.spawn(workThreadFragment, .{ self, file, cur_write_offset, finish });
pool.spawn(workThreadFragment, .{ self, file, cur_write_offset, finish }) catch |res_err| {
finish.logError("Can't spawn pool task: {}", .{res_err});
finish.out_err.* = res_err;
finish.finish();
};
}
fn workThreadBlocks(
@@ -79,25 +87,30 @@ fn workThreadBlocks(
defer finish.finish();
var wrt = fil.writer(&[0]u8{});
wrt.seekTo(write_offset) catch |err| {
finish.logError("Error seeking file writer: {}", .{err});
finish.out_err.* = err;
return;
};
defer wrt.interface.flush() catch |err| {
finish.logError("Error flushing file writer: {}", .{err});
finish.out_err.* = err;
};
if (block.size == 0) {
wrt.interface.splatByteAll(0, cur_block_size) catch |err| {
finish.logError("Error writing zeroes: {}", .{err});
finish.out_err.* = err;
return;
};
return;
}
var rdr = self.fil.readerAt(read_offset, &[0]u8{}) catch |err| {
finish.logError("Error creating file reader: {}", .{err});
finish.out_err.* = err;
return;
};
if (block.uncompressed) {
rdr.interface.streamExact(&wrt.interface, block.size) catch |err| {
finish.logError("Error streaming data: {}", .{err});
finish.out_err.* = err;
return;
};
@@ -105,25 +118,30 @@ fn workThreadBlocks(
}
// TODO: shared buffers
const read_buf = self.alloc.alloc(u8, block.size) catch |err| {
finish.logError("Error creating reader buffer: {}", .{err});
finish.out_err.* = err;
return;
};
defer self.alloc.free(read_buf);
rdr.interface.readSliceAll(read_buf) catch |err| {
finish.logError("Error reading data into reader buffer: {}", .{err});
finish.out_err.* = err;
return;
};
// TODO: shared buffers
const res_buf = self.alloc.alloc(u8, cur_block_size) catch |err| {
finish.logError("Error creating result buffer: {}", .{err});
finish.out_err.* = err;
return;
};
defer self.alloc.free(res_buf);
_ = self.decomp(self.alloc, read_buf, res_buf) catch |err| {
finish.logError("Error decompressing data block: {}", .{err});
finish.out_err.* = err;
return;
};
wrt.interface.writeAll(res_buf) catch |err| {
finish.logError("Error writing to file: {}", .{err});
finish.out_err.* = err;
return;
};
@@ -133,6 +151,7 @@ fn workThreadFragment(self: ThreadedDataReader, fil: std.fs.File, write_offset:
var wrt = fil.writer(&[0]u8{});
wrt.seekTo(write_offset) catch |err| {
finish.logError("Error seeking file writer for file fragment: {}", .{err});
finish.out_err.* = err;
return;
};
@@ -141,39 +160,47 @@ fn workThreadFragment(self: ThreadedDataReader, fil: std.fs.File, write_offset:
};
var rdr = self.fil.readerAt(self.frag.?.start, &[0]u8{}) catch |err| {
finish.logError("Error creating file reader for file fragment: {}", .{err});
finish.out_err.* = err;
return;
};
if (self.frag.?.size.uncompressed) {
rdr.interface.discardAll(self.frag_offset) catch |err| {
finish.logError("Error discarding useless fragment data: {}", .{err});
finish.out_err.* = err;
return;
};
rdr.interface.streamExact(&wrt.interface, self.size % self.block_size) catch |err| {
finish.logError("Error streaming fragment data: {}", .{err});
finish.out_err.* = err;
return;
};
return;
}
const tmp_buf = self.alloc.alloc(u8, self.frag.?.size.size) catch |err| {
finish.logError("Error creating a temporary buffer for a file fragment: {}", .{err});
finish.out_err.* = err;
return;
};
defer self.alloc.free(tmp_buf);
rdr.interface.readSliceAll(tmp_buf) catch |err| {
finish.logError("Error reading data into fragment buffer: {}", .{err});
finish.out_err.* = err;
return;
};
const needed_block = self.alloc.alloc(u8, self.block_size) catch |err| {
finish.logError("Error allocating fragment decompression results: {}", .{err});
finish.out_err.* = err;
return;
};
defer self.alloc.free(needed_block);
_ = self.decomp(self.alloc, tmp_buf, needed_block) catch |err| {
finish.logError("Error decompressing fragment: {}", .{err});
finish.out_err.* = err;
return;
};
wrt.interface.writeAll(needed_block[self.frag_offset .. self.frag_offset + (self.size % self.block_size)]) catch |err| {
finish.logError("Error writing fragment: {}", .{err});
finish.out_err.* = err;
return;
};
+37 -18
View File
@@ -28,7 +28,7 @@ pub fn extractTo(
var arena: std.heap.ArenaAllocator = .init(stack_alloc.get());
defer arena.deinit();
if (options.threads <= 1)
return extractToSingle(arena.allocator(), inode, archive, path, options);
return extractSingleThread(arena.allocator(), inode, archive, path, options);
var thread_alloc = std.heap.ThreadSafeAllocator{ .child_allocator = arena.allocator() };
const alloc = thread_alloc.allocator();
@@ -39,21 +39,22 @@ pub fn extractTo(
var wg: WaitGroup = .{};
var err: ?anyerror = null;
extractToMulti(
wg.start();
try pool.spawn(extractMultiThread, .{
alloc,
inode,
archive,
path,
options,
&pool,
.{ .wg = &wg },
FinishUnion{ .wg = &wg },
&err,
);
});
pool.waitAndWork(&wg);
if (err != null) return err.?;
}
fn extractToSingle(
fn extractSingleThread(
alloc: Allocator,
inode: Inode,
archive: *Archive,
@@ -72,14 +73,14 @@ fn extractToSingle(
// otherwise be more conscientious about freeing memory.
// For now, this is good enough.
const entries = try inode.dirEntries(alloc, archive);
const entries = try inode.dirEntries(alloc, archive.*);
for (entries) |ent| {
const sub_inode: Inode = try .readFromEntry(alloc, archive, ent);
const new_path = try std.mem.concat(alloc, u8, []const []const u8{ path, "/", ent.name });
extractToSingle(alloc, sub_inode, archive, new_path, options);
const new_path = try std.mem.concat(alloc, u8, &[_][]const u8{ path, "/", ent.name });
try extractSingleThread(alloc, sub_inode, archive, new_path, options);
}
const fil = try std.fs.cwd().openFile(path);
const fil = try std.fs.cwd().openFile(path, .{});
defer fil.close();
try inode.setMetadata(alloc, archive, fil, options);
},
@@ -99,7 +100,7 @@ fn extractToSingle(
}
}
fn extractToMulti(
fn extractMultiThread(
alloc: Allocator,
inode: Inode,
archive: *Archive,
@@ -109,7 +110,7 @@ fn extractToMulti(
fin: FinishUnion,
err: *?anyerror,
) void {
if (err != null) {
if (err.* != null) {
fin.finish();
return;
}
@@ -129,7 +130,7 @@ fn extractToMulti(
// otherwise be more conscientious about freeing memory.
// For now, this is good enough.
const entries = try inode.dirEntries(alloc, archive) catch |res_err| {
const entries = inode.dirEntries(alloc, archive.*) catch |res_err| {
err.* = res_err;
fin.finish();
return;
@@ -153,9 +154,21 @@ fn extractToMulti(
for (entries) |ent| {
if (ent.inode_type == .dir)
extractEntry(alloc, ent, archive, path, options, pool, dir_fin, err);
extractEntry(
alloc,
ent,
archive,
path,
options,
pool,
.{ .fin = dir_fin },
err,
);
pool.spawn(extractEntry, .{ alloc, ent, archive, path, options, pool, dir_fin, err }) catch |res_err| {
pool.spawn(
extractEntry,
.{ alloc, ent, archive, path, options, pool, FinishUnion{ .fin = dir_fin }, err },
) catch |res_err| {
err.* = res_err;
dir_fin.finish();
return;
@@ -164,12 +177,16 @@ fn extractToMulti(
},
.file, .ext_file => {
const fil = std.fs.cwd().createFile(path, .{ .exclusive = true }) catch |res_err| {
if (options.verbose)
options.verbose_writer.?.print("Can't create file at {s}: {}\n", .{ path, res_err }) catch {};
err.* = res_err;
fin.finish();
return;
};
var data_rdr = threadedDataReader(inode, alloc, archive) catch |res_err| {
if (options.verbose)
options.verbose_writer.?.print("Can't create data reader for inode #{} (extracting to {s}): {}\n", .{ inode.hdr.num, path, res_err }) catch {};
err.* = res_err;
fin.finish();
return;
@@ -185,6 +202,8 @@ fn extractToMulti(
fil,
data_rdr.num_blocks,
) catch |res_err| {
if (options.verbose)
options.verbose_writer.?.print("Can't create callback for inode #{} (extracting to {s}): {}\n", .{ inode.hdr.num, path, res_err }) catch {};
err.* = res_err;
fin.finish();
return;
@@ -207,7 +226,7 @@ fn extractToMulti(
}
}
inline fn extractEntry(
fn extractEntry(
alloc: Allocator,
ent: DirEntry,
archive: *Archive,
@@ -217,18 +236,18 @@ inline fn extractEntry(
fin: FinishUnion,
err: *?anyerror,
) void {
const new_path = std.mem.concat(alloc, u8, []const []const u8{ path, "/", ent.name }) catch |res_err| {
const new_path = std.mem.concat(alloc, u8, &[_][]const u8{ path, "/", ent.name }) catch |res_err| {
err.* = res_err;
fin.finish();
return;
};
const inode: Inode = .readFromEntry(alloc, archive, ent) catch |res_err| {
const inode = Inode.readFromEntry(alloc, archive, ent) catch |res_err| {
err.* = res_err;
fin.finish();
return;
};
extractToMulti(alloc, inode, archive, new_path, options, pool, fin, err);
extractMultiThread(alloc, inode, archive, new_path, options, pool, fin, err);
}
/// Get a threaded data reader for a file inode.
+5
View File
@@ -65,6 +65,11 @@ pub fn create(
return out;
}
pub fn logError(self: *InodeFinish, comptime fmt: []const u8, args: anytype) void {
if (self.options.verbose)
self.options.verbose_writer.?.print(fmt, args) catch {};
}
pub fn finish(self: *InodeFinish) void {
self.mut.lock();
{