6 Commits

Author SHA1 Message Date
Caleb J. Gardner b0160e005b Some fixes 2026-02-17 05:54:32 -06:00
Caleb J. Gardner b7b99325da Slight improvement to how permissions are applied to folders
unsquashfs Verbose flag
2026-02-13 01:35:20 -06:00
Caleb J. Gardner b22e4d003d Update times 2026-02-12 08:57:36 -06:00
Caleb J. Gardner a41c37fef4 Updated unsquashfs version print out 2026-02-12 06:54:20 -06:00
Caleb J. Gardner 2d079d77f7 Exclusive file creation 2026-02-12 05:19:25 -06:00
Caleb J. Gardner 48f4235875 Fixed threads == 0 causing single threaded extraction.
Set exclusive file creation
2026-02-12 05:08:16 -06:00
5 changed files with 123 additions and 60 deletions
+5 -5
View File
@@ -17,15 +17,15 @@ jobs:
- name: Build normal version - name: Build normal version
run: zig build -Drelease=true -Dversion=${{ github.ref_name }} run: zig build -Drelease=true -Dversion=${{ github.ref_name }}
- name: Move normal build out - name: Move normal build out
run: mv zig-out/bin/unsquashfs ./ run: mv zig-out/bin/unsquashfs ./unsquashfs-x86_64
- name: Rebuild with C libraries - name: Rebuild with C libraries
run: zig build -Drelease=true -Duse_c_libs=true -Dversion="${{ github.ref_name }}" run: zig build --release=fast -Drelease=true -Duse_c_libs=true -Dversion="${{ github.ref_name }}"
- name: Move C build out - name: Move C build out
run: mv zig-out/bin/unsquashfs ./unsquashfs-c-libs run: mv zig-out/bin/unsquashfs ./unsquashfs-x86_64-c-libs
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
prerelease: true prerelease: true
files: | files: |
unsquashfs unsquashfs-x86_64
unsquashfs-c-libs unsquashfs-x86_64-c-libs
+1 -1
View File
@@ -10,7 +10,7 @@ pub fn build(b: *std.Build) !void {
zig_squashfs_options.addOption(bool, "allow_lzo", allow_lzo orelse false); zig_squashfs_options.addOption(bool, "allow_lzo", allow_lzo orelse false);
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseFast }); const optimize = b.standardOptimizeOption(.{});
const mod = b.addModule("zig_squashfs", .{ const mod = b.addModule("zig_squashfs", .{
.root_source_file = b.path("src/root.zig"), .root_source_file = b.path("src/root.zig"),
.target = target, .target = target,
+3 -4
View File
@@ -61,9 +61,8 @@ pub fn init(alloc: std.mem.Allocator, fil: File) !Archive {
try std.Thread.getCpuCount(), try std.Thread.getCpuCount(),
); );
} }
/// Create the Archive dictating the amount of threads & memory used. /// Create the Archive dictating the amount of threads used for extraction.
/// If trying to extract a full archive, a large memory size & thread count could help. /// If you're planning on only interacting with a small number of files, it should be fine to use few (or one) threads.
/// If you're planning on only interacting with a small number of files, it should be fine to use few threads and a small memory size.
pub fn initAdvanced(alloc: std.mem.Allocator, fil: File, offset: u64, threads: usize) !Archive { pub fn initAdvanced(alloc: std.mem.Allocator, fil: File, offset: u64, threads: usize) !Archive {
var super: Superblock = undefined; var super: Superblock = undefined;
const red = try fil.pread(@ptrCast(&super), offset); const red = try fil.pread(@ptrCast(&super), offset);
@@ -74,7 +73,7 @@ pub fn initAdvanced(alloc: std.mem.Allocator, fil: File, offset: u64, threads: u
.parent_alloc = alloc, .parent_alloc = alloc,
.alloc = .{ .child_allocator = alloc }, .alloc = .{ .child_allocator = alloc },
// .fixed_buf = fixed_buf, // .fixed_buf = fixed_buf,
.thread_count = threads, .thread_count = if (threads > 0) threads else try std.Thread.getCpuCount(),
.fil = .init(fil, offset), .fil = .init(fil, offset),
.decomp = switch (super.compression) { .decomp = switch (super.compression) {
.gzip => Decomp.gzipDecompress, .gzip => Decomp.gzipDecompress,
+9 -4
View File
@@ -15,7 +15,8 @@ const help_mgs =
\\ \\
\\ -o <offset> Start reading the archive at the given offset. \\ -o <offset> Start reading the archive at the given offset.
\\ \\
\\ -p <threads> Specify how many threads to use. If no present, the system's logical cores count is used. \\ -p <threads> Specify how many threads to use. If no present or zero, the system's logical cores count is used.
\\ -v Verbose
\\ \\
\\ --help Display this messages \\ --help Display this messages
\\ --version Display the version \\ --version Display the version
@@ -28,6 +29,7 @@ var archive: []const u8 = "";
var extLoc: []const u8 = "squashfs-root"; var extLoc: []const u8 = "squashfs-root";
var offset: u64 = 0; var offset: u64 = 0;
var threads: u32 = 0; var threads: u32 = 0;
var verbose: bool = false;
pub fn main() !void { pub fn main() !void {
const alloc = std.heap.smp_allocator; const alloc = std.heap.smp_allocator;
@@ -44,7 +46,7 @@ pub fn main() !void {
defer fil.close(); defer fil.close();
var arc: squashfs.Archive = try .initAdvanced(alloc, fil, offset, threads); //TODO: Update when memory size matters. //TODO: Handle error gracefully. var arc: squashfs.Archive = try .initAdvanced(alloc, fil, offset, threads); //TODO: Update when memory size matters. //TODO: Handle error gracefully.
defer arc.deinit(); defer arc.deinit();
try arc.extract(extLoc, .Default); //TODO: Handle error gracefully. try arc.extract(extLoc, if (verbose) .VerboseDefault(&out.interface) else .Default); //TODO: Handle error gracefully.
} }
fn handleArgs(alloc: std.mem.Allocator, out: *Writer) !void { fn handleArgs(alloc: std.mem.Allocator, out: *Writer) !void {
@@ -82,10 +84,13 @@ fn handleArgs(alloc: std.mem.Allocator, out: *Writer) !void {
return errors.InvalidArguments; return errors.InvalidArguments;
}; };
continue; continue;
} else if (std.mem.eql(u8, arg, "-v")) {
verbose = true;
continue;
} else if (std.mem.eql(u8, arg, "--version")) { } else if (std.mem.eql(u8, arg, "--version")) {
try out.print("zig-unsquashfs version ", .{}); try out.print("zig-unsquashfs v", .{});
try config.version.format(out); try config.version.format(out);
try out.print("\nBuilt using Zig {s} with {} backend in {} mode.\n", .{ builtin.zig_version_string, builtin.zig_backend, builtin.mode }); try out.print("\nBuilt using Zig {s} in {} mode\n", .{ builtin.zig_version_string, builtin.mode });
std.process.exit(0); std.process.exit(0);
return; return;
} else if (std.mem.eql(u8, arg, "--help")) { } else if (std.mem.eql(u8, arg, "--help")) {
+103 -44
View File
@@ -4,6 +4,7 @@ const std = @import("std");
const Reader = std.Io.Reader; const Reader = std.Io.Reader;
const WaitGroup = std.Thread.WaitGroup; const WaitGroup = std.Thread.WaitGroup;
const Pool = std.Thread.Pool; const Pool = std.Thread.Pool;
const Mutex = std.Thread.Mutex;
const Archive = @import("archive.zig"); const Archive = @import("archive.zig");
const DirEntry = @import("dir_entry.zig"); const DirEntry = @import("dir_entry.zig");
@@ -186,11 +187,57 @@ pub fn extractTo(self: Inode, archive: *Archive, path: []const u8, options: Extr
} }
} }
const Perms = struct { const Parent = struct {
alloc: std.mem.Allocator,
path: []const u8, path: []const u8,
uid: u16, uid: u16,
gid: u16, gid: u16,
perm: u16, perm: u16,
mod_time: u32,
ignore_permissions: bool,
ignore_xattr: bool,
wg: WaitGroup = .{},
mut: Mutex = .{},
fn create(alloc: std.mem.Allocator, hdr: Header, archive: *Archive, path: []const u8, options: ExtractionOptions, dir_size: usize) !*Parent {
const out = try alloc.create(Parent);
errdefer alloc.destroy(out);
out.* = .{
.alloc = alloc,
.path = path,
.uid = try archive.id(hdr.uid_idx),
.gid = try archive.id(hdr.gid_idx),
.perm = hdr.permissions,
.mod_time = hdr.mod_time,
.ignore_permissions = options.ignore_permissions,
.ignore_xattr = options.ignore_xattr,
};
out.wg.startMany(dir_size);
return out;
}
fn finish(p: *Parent) !void {
p.mut.lock();
{
defer p.mut.unlock();
p.wg.finish();
if (!p.wg.isDone()) return;
}
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);
}
}
}; };
/// Extract the inode to the given path. Multi-threaded. /// Extract the inode to the given path. Multi-threaded.
@@ -217,7 +264,6 @@ pub fn extractToThreaded(self: Inode, archive: *Archive, path: []const u8, optio
const alloc = thread_alloc.allocator(); const alloc = thread_alloc.allocator();
var wg: WaitGroup = .{}; var wg: WaitGroup = .{};
var perms: ?std.ArrayList(Perms) = if (options.ignore_permissions) null else try .initCapacity(alloc, 100);
// 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 = alloc, .n_jobs = threads - 1 }); try pool.init(.{ .allocator = alloc, .n_jobs = threads - 1 });
@@ -225,16 +271,17 @@ pub fn extractToThreaded(self: Inode, archive: *Archive, path: []const u8, optio
var out_err: ?anyerror = null; var out_err: ?anyerror = null;
wg.start(); wg.start();
self.extractThread(alloc, archive, path, options, &wg, &pool, &out_err, &perms); self.extractThread(alloc, archive, path, options, &wg, &pool, &out_err, null);
pool.waitAndWork(&wg); pool.waitAndWork(&wg);
if (out_err != null) return out_err.?; if (out_err != null) return out_err.?;
if (perms != null) { var fil = try std.fs.cwd().openFile(path, .{});
for (perms.?.items) |p| { defer fil.close();
var fil = try std.fs.cwd().openFile(p.path, .{}); const time = @as(i128, self.hdr.mod_time) * 1000000000;
try fil.chmod(p.perm); try fil.updateTimes(time, time);
try fil.chown(p.uid, p.gid); if (options.ignore_permissions) {
} try fil.chmod(self.hdr.permissions);
try fil.chown(try archive.id(self.hdr.uid_idx), try archive.id(self.hdr.gid_idx));
} }
}, },
.file, .ext_file => { .file, .ext_file => {
@@ -246,8 +293,11 @@ pub fn extractToThreaded(self: Inode, archive: *Archive, path: []const u8, optio
try self.extractRegFileThreaded(alloc, archive, path, options, &pool); try self.extractRegFileThreaded(alloc, archive, path, options, &pool);
if (!options.ignore_permissions) {
var fil = try std.fs.cwd().openFile(path, .{}); 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.chmod(self.hdr.permissions);
try fil.chown(try archive.id(self.hdr.uid_idx), try archive.id(self.hdr.gid_idx)); try fil.chown(try archive.id(self.hdr.uid_idx), try archive.id(self.hdr.gid_idx));
} }
@@ -266,7 +316,7 @@ fn extractThreadEntry(
wg: *WaitGroup, wg: *WaitGroup,
pool: *Pool, pool: *Pool,
out_err: *?anyerror, out_err: *?anyerror,
perms: *?std.ArrayList(Perms), parent: ?*Parent,
) void { ) void {
var new_path = alloc.alloc(u8, path.len + entry.name.len + 1) catch |err| { var new_path = alloc.alloc(u8, path.len + entry.name.len + 1) catch |err| {
wg.finish(); wg.finish();
@@ -281,7 +331,7 @@ fn extractThreadEntry(
wg.finish(); wg.finish();
return; return;
}; };
inode.extractThread(alloc, archive, new_path, options, wg, pool, out_err, perms); inode.extractThread(alloc, archive, new_path, options, wg, pool, out_err, parent);
} }
/// Extract threadedly the inode to the path. /// Extract threadedly the inode to the path.
@@ -294,20 +344,35 @@ fn extractThread(
wg: *WaitGroup, wg: *WaitGroup,
pool: *Pool, pool: *Pool,
out_err: *?anyerror, out_err: *?anyerror,
perms: *?std.ArrayList(Perms), parent: ?*Parent,
) void { ) void {
defer wg.finish(); if (options.verbose)
options.verbose_writer.?.print("Extracting inode #{} to {s}\n", .{ self.hdr.num, path }) catch {};
defer {
if (parent != null) parent.?.finish() catch |err| {
if (options.verbose)
options.verbose_writer.?.print("Error setting folder permission to {s}: {}\n", .{ path, err }) catch {};
out_err.* = err;
};
wg.finish();
}
if (out_err.* != null) return; if (out_err.* != null) return;
switch (self.hdr.inode_type) { switch (self.hdr.inode_type) {
.dir, .ext_dir => { .dir, .ext_dir => {
std.fs.cwd().makeDir(path) catch |err| { _ = std.fs.cwd().makePathStatus(path) catch |err| {
if (err != std.fs.Dir.MakeError.PathAlreadyExists) { if (options.verbose)
options.verbose_writer.?.print("Error creating {s}: {}\n", .{ path, err }) catch {};
out_err.* = err; out_err.* = err;
return; return;
}
}; };
const entries = self.dirEntries(alloc, archive.*) catch |err| { const entries = self.dirEntries(alloc, archive.*) catch |err| {
if (options.verbose)
options.verbose_writer.?.print("Error getting directory entries for inode #{} (extracting to {s}): {}\n", .{ self.hdr.num, path, err }) catch {};
out_err.* = err;
return;
};
const p = Parent.create(alloc, self.hdr, archive, path, options, entries.len) catch |err| {
out_err.* = err; out_err.* = err;
return; return;
}; };
@@ -315,7 +380,7 @@ fn extractThread(
// defer files.deinit(alloc); We don't need to do this due to ArenaAllocator // defer files.deinit(alloc); We don't need to do this due to ArenaAllocator
for (entries) |entry| { for (entries) |entry| {
if (entry.inode_type == .dir) { if (entry.inode_type == .dir) {
extractThreadEntry(entry, alloc, archive, path, options, wg, pool, out_err, perms); extractThreadEntry(entry, alloc, archive, path, options, wg, pool, out_err, p);
continue; continue;
} }
pool.spawn( pool.spawn(
@@ -329,45 +394,35 @@ fn extractThread(
wg, wg,
pool, pool,
out_err, out_err,
perms, p,
}, },
) catch |err| { ) catch |err| {
wg.finish(); wg.finish();
if (options.verbose)
options.verbose_writer.?.print("Error starting extraction thread: {}\n", .{err}) catch {};
out_err.* = err; out_err.* = err;
continue; continue;
}; };
} }
if (!options.ignore_permissions) {
const new_val = perms.*.?.addOne(alloc) catch |err| {
out_err.* = err;
return;
};
new_val.* = .{
.path = path,
.uid = archive.id(self.hdr.uid_idx) catch |err| {
out_err.* = err;
return;
},
.gid = archive.id(self.hdr.gid_idx) catch |err| {
out_err.* = err;
return;
},
.perm = self.hdr.permissions,
};
}
}, },
.file, .ext_file => { .file, .ext_file => {
self.extractRegFileThreaded(alloc, archive, path, options, pool) catch |err| { self.extractRegFileThreaded(alloc, archive, path, options, pool) catch |err| {
if (options.verbose)
options.verbose_writer.?.print("Error extracting file inode #{} to {s}: {}\n", .{ self.hdr.num, path, err }) catch {};
out_err.* = err; out_err.* = err;
}; };
}, },
.symlink, .ext_symlink => { .symlink, .ext_symlink => {
self.extractSymlink(path) catch |err| { self.extractSymlink(path) catch |err| {
if (options.verbose)
options.verbose_writer.?.print("Error extracting symlink inode #{} to {s}: {}\n", .{ self.hdr.num, path, err }) catch {};
out_err.* = err; out_err.* = err;
}; };
}, },
else => { else => {
self.extractDevice(archive, path, options) catch |err| { self.extractDevice(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; out_err.* = err;
}; };
}, },
@@ -378,16 +433,16 @@ fn extractThread(
/// ///
/// Assumes the inode is a file or ext_file type. /// Assumes the inode is a file or ext_file type.
fn extractRegFile(self: Inode, alloc: std.mem.Allocator, archive: *Archive, path: []const u8, options: ExtractionOptions) !void { fn extractRegFile(self: Inode, alloc: std.mem.Allocator, archive: *Archive, path: []const u8, options: ExtractionOptions) !void {
var fil = try std.fs.cwd().createFile(path, .{}); var fil = try std.fs.cwd().createFile(path, .{ .exclusive = true });
defer fil.close(); defer fil.close();
var wrt = fil.writer(&[0]u8{}); var wrt = fil.writer(&[0]u8{});
var dat_rdr = try self.dataReader(alloc, archive); var dat_rdr = try self.dataReader(alloc, archive);
defer dat_rdr.deinit(); defer dat_rdr.deinit();
_ = try dat_rdr.interface.streamRemaining(&wrt.interface); _ = try dat_rdr.interface.streamRemaining(&wrt.interface);
try wrt.interface.flush(); try wrt.interface.flush();
// updateTime is in nanoseconds (a billionth of a second). mod_time is in seconds.
// TODO: fix const time = @as(i128, self.hdr.mod_time) * 1000000000;
// try fil.updateTimes(self.hdr.mod_time, self.hdr.mod_time); try fil.updateTimes(time, time);
if (!options.ignore_permissions) { if (!options.ignore_permissions) {
try fil.chmod(self.hdr.permissions); try fil.chmod(self.hdr.permissions);
try fil.chown(try archive.id(self.hdr.uid_idx), try archive.id(self.hdr.gid_idx)); try fil.chown(try archive.id(self.hdr.uid_idx), try archive.id(self.hdr.gid_idx));
@@ -399,8 +454,12 @@ fn extractRegFile(self: Inode, alloc: std.mem.Allocator, archive: *Archive, path
/// Assumes the inode is a file or ext_file type. /// Assumes the inode is a file or ext_file type.
fn extractRegFileThreaded(self: Inode, alloc: std.mem.Allocator, archive: *Archive, path: []const u8, options: ExtractionOptions, pool: *Pool) !void { fn extractRegFileThreaded(self: Inode, alloc: std.mem.Allocator, archive: *Archive, path: []const u8, options: ExtractionOptions, pool: *Pool) !void {
var fil = try std.fs.cwd().createFile(path, .{}); var fil = try std.fs.cwd().createFile(path, .{});
defer fil.close();
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 fil.updateTimes(time, time);
if (!options.ignore_permissions) { if (!options.ignore_permissions) {
try fil.chmod(self.hdr.permissions); try fil.chmod(self.hdr.permissions);
try fil.chown(try archive.id(self.hdr.uid_idx), try archive.id(self.hdr.gid_idx)); try fil.chown(try archive.id(self.hdr.uid_idx), try archive.id(self.hdr.gid_idx));
@@ -465,9 +524,9 @@ 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, .{});
// updateTime is in nanoseconds (a billionth of a second). mod_time is in seconds. defer fil.close();
// TODO: fix const time = @as(i128, self.hdr.mod_time) * 1000000000;
// try fil.updateTimes(self.hdr.mod_time, self.hdr.mod_time); try fil.updateTimes(time, time);
if (!options.ignore_permissions) { if (!options.ignore_permissions) {
try fil.chmod(self.hdr.permissions); try fil.chmod(self.hdr.permissions);
try fil.chown(try archive.id(self.hdr.uid_idx), try archive.id(self.hdr.gid_idx)); try fil.chown(try archive.id(self.hdr.uid_idx), try archive.id(self.hdr.gid_idx));