From d470ca98e3def152e3fe4929319f977b4ea0473b Mon Sep 17 00:00:00 2001 From: "Caleb J. Gardner" Date: Thu, 5 Mar 2026 07:04:24 -0600 Subject: [PATCH] Added --force to unsquashfs Fixing race condition bugs (yay) --- .zed/debug.json | 30 +++++++++++++++++++++ build.zig | 1 + src/bin/unsquashfs.zig | 8 ++++++ src/util/data_threaded.zig | 35 +++++++++++++++++++++--- src/util/extract.zig | 55 +++++++++++++++++++++++++------------- src/util/inode_finish.zig | 5 ++++ 6 files changed, 112 insertions(+), 22 deletions(-) create mode 100644 .zed/debug.json diff --git a/.zed/debug.json b/.zed/debug.json new file mode 100644 index 0000000..198e0f6 --- /dev/null +++ b/.zed/debug.json @@ -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", + ], + }, +] diff --git a/build.zig b/build.zig index ca8f18d..d27f8f9 100644 --- a/build.zig +++ b/build.zig @@ -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(.{ diff --git a/src/bin/unsquashfs.zig b/src/bin/unsquashfs.zig index 1db474f..a945c01 100644 --- a/src/bin/unsquashfs.zig +++ b/src/bin/unsquashfs.zig @@ -20,6 +20,8 @@ const help_mgs = \\ -p 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); diff --git a/src/util/data_threaded.zig b/src/util/data_threaded.zig index f9f8860..784ed5d 100644 --- a/src/util/data_threaded.zig +++ b/src/util/data_threaded.zig @@ -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; }; diff --git a/src/util/extract.zig b/src/util/extract.zig index 20b1ec4..20ea222 100644 --- a/src/util/extract.zig +++ b/src/util/extract.zig @@ -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. diff --git a/src/util/inode_finish.zig b/src/util/inode_finish.zig index e162989..c6e57fc 100644 --- a/src/util/inode_finish.zig +++ b/src/util/inode_finish.zig @@ -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(); {