Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 760e11df0b | |||
| 5f629df47c | |||
| b0160e005b | |||
| b7b99325da |
@@ -15,17 +15,17 @@ jobs:
|
||||
- name: Install deps
|
||||
run: sudo apt update && sudo apt install -y zlib1g-dev libzstd-dev liblzma-dev liblz4-dev liblzo2-dev
|
||||
- name: Build normal version
|
||||
run: zig build -Drelease=true -Dversion=${{ github.ref_name }}
|
||||
run: zig build --release=fast -Dversion=${{ github.ref_name }}
|
||||
- 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
|
||||
run: zig build -Drelease=true -Duse_c_libs=true -Dversion="${{ github.ref_name }}"
|
||||
run: zig build --release=fast -Duse_c_libs=true -Dversion="${{ github.ref_name }}"
|
||||
- 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
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
prerelease: true
|
||||
files: |
|
||||
unsquashfs
|
||||
unsquashfs-c-libs
|
||||
unsquashfs-x86_64
|
||||
unsquashfs-x86_64-c-libs
|
||||
|
||||
@@ -6,19 +6,23 @@ A library and application to decompress or view squashfs archives.
|
||||
|
||||
## Current State
|
||||
|
||||
Overall works, but currently is missing some features (see below). Extraction is a bit slow compared to the normal `unsquashfs` (from my _very_ basic testing it's about ~3x slower). Only properly work on Linux, any other OSes probably won't work fully and are untested.
|
||||
Overall works, but currently is missing some features ([see below](#capabilities)) and has significantly slow performance compared to `unsquashfs` ([see below](#performance)).
|
||||
|
||||
## Build options
|
||||
|
||||
> `-Duse_c_libs`
|
||||
> `-Duse_c_libs=true`
|
||||
|
||||
Instead of using Zig's standard library for decompression, use the system's C libraries. Has the benefit of being much faster and enabling LZO and LZ4 decompression.
|
||||
|
||||
> `-Dallow_lzo`
|
||||
> `-Dallow_lzo=true`
|
||||
|
||||
Enable compiling with LZO decompression support. The LZO library currently has some issues with Zig when imported so it's easier to just disable it by default. Only has an effect when using `-Duse_c_libs=true`.
|
||||
|
||||
> `-Dversion`
|
||||
> `-Dvalgrind=true`
|
||||
|
||||
Just sets the valgrind build option.
|
||||
|
||||
> `-Dversion=0.0.0`
|
||||
|
||||
Sets the version of `unsquashfs` shown when `--version` is passed.
|
||||
|
||||
@@ -26,11 +30,27 @@ Sets the version of `unsquashfs` shown when `--version` is passed.
|
||||
|
||||
Most features are present except for the following:
|
||||
|
||||
* mod_time is not set on extraction
|
||||
* xattrs are not applied on extraction
|
||||
* When using Zig decompression libraries then lzo and lz4 compression types are unavailable. I don't _currently_ plan on spending the time to find and validate a library since neither is popular.
|
||||
* When using C decompression libraries, lzo is not supported by default due to [some issues](#build-considerations). If it's needed it's trivial to fix, but it's easiest to just leave it disabled.
|
||||
|
||||
## Building considerations
|
||||
## Performance
|
||||
|
||||
This is some basic observation's I've made about this library's performance when compared to `unsquashfs`. Unless otherwise stated, most observations were made when extracting my test archive (which is fairly small and uses zstd compression) and with `--release=fast`.
|
||||
|
||||
* Under ideal circumstances, my library is ~70% slower (.11s vs .18s)
|
||||
* Mutli-threading on small archives noticably increases extraction times (when using C libraries) (.18s vs .57s). This should theoretically reverse on larger archives with many inodes, but I haven't tested that yet.
|
||||
* Using Zig libraries *significantly* increases decompression time by ~600% under ideal circumstances.
|
||||
|
||||
Times:
|
||||
|
||||
* *unsquashfs*: .11s
|
||||
* *C-libs, single-threaded*: .18s
|
||||
* *C-libs, multi-threaded*: .57s
|
||||
* *Zig-libs, single-threaded*: 5.87s
|
||||
* *Zig-libs, multi-threaded*: 1.10s
|
||||
|
||||
## Build considerations
|
||||
|
||||
Compilation without `use_c_libs` works completely fine, but Zig has issues with some symbols from the lzo library that needs to be manually fixed. In particular you need to fix the definitions for `lzo_bytep` and `lzo_voidp` to be `*u8` and `?*anyopaque` respectively. Due to this, you have to manually enable LZO decompression using `-Dallow_lzo=true` when building.
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ const std = @import("std");
|
||||
pub fn build(b: *std.Build) !void {
|
||||
const use_c_libs_option = b.option(bool, "use_c_libs", "Use C versions of decompression libraries instead of the Zig standard library ones");
|
||||
const allow_lzo = b.option(bool, "allow_lzo", "Compile with lzo support");
|
||||
const valgrind = b.option(bool, "valgrind", "Compile with valgrind integration");
|
||||
const version_string_option = b.option([]const u8, "version", "Version of the library/binary");
|
||||
|
||||
const zig_squashfs_options = b.addOptions();
|
||||
@@ -10,12 +11,13 @@ pub fn build(b: *std.Build) !void {
|
||||
zig_squashfs_options.addOption(bool, "allow_lzo", allow_lzo orelse false);
|
||||
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseFast });
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
const mod = b.addModule("zig_squashfs", .{
|
||||
.root_source_file = b.path("src/root.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.link_libc = if (use_c_libs_option == true) true else false,
|
||||
.link_libc = use_c_libs_option,
|
||||
.valgrind = valgrind,
|
||||
});
|
||||
mod.addOptions("config", zig_squashfs_options);
|
||||
if (use_c_libs_option == true) {
|
||||
@@ -40,10 +42,11 @@ pub fn build(b: *std.Build) !void {
|
||||
.root_source_file = b.path("src/bin/unsquashfs.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.link_libc = if (use_c_libs_option == true) true else false,
|
||||
.link_libc = use_c_libs_option,
|
||||
.imports = &.{
|
||||
.{ .name = "zig_squashfs", .module = mod },
|
||||
},
|
||||
.valgrind = valgrind,
|
||||
});
|
||||
exe_mod.addOptions("config", unsquashfs_options);
|
||||
const exe = b.addExecutable(.{
|
||||
@@ -58,13 +61,7 @@ pub fn build(b: *std.Build) !void {
|
||||
|
||||
b.installArtifact(lib);
|
||||
b.installArtifact(exe);
|
||||
const run_step = b.step("run", "Run the app");
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
const mod_tests = b.addTest(.{
|
||||
.root_module = mod,
|
||||
});
|
||||
@@ -76,4 +73,17 @@ pub fn build(b: *std.Build) !void {
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&run_mod_tests.step);
|
||||
test_step.dependOn(&run_exe_tests.step);
|
||||
|
||||
// zls build check steps
|
||||
const lib_check = b.addLibrary(.{
|
||||
.name = "squashfs",
|
||||
.root_module = mod,
|
||||
});
|
||||
const exe_check = b.addExecutable(.{
|
||||
.name = "unsquashfs",
|
||||
.root_module = exe_mod,
|
||||
});
|
||||
const check = b.step("check", "Check if unsquashfs compiles");
|
||||
check.dependOn(&lib_check.step);
|
||||
check.dependOn(&exe_check.step);
|
||||
}
|
||||
|
||||
+35
-96
@@ -31,138 +31,77 @@ pub const FragEntry = packed struct {
|
||||
|
||||
const Archive = @This();
|
||||
|
||||
// 4 Gigs
|
||||
const DEFAULT_MEM_SIZE = 4 * 1024 * 1024 * 1024;
|
||||
|
||||
parent_alloc: std.mem.Allocator,
|
||||
alloc: std.heap.ThreadSafeAllocator,
|
||||
// alloc: std.heap.FixedBufferAllocator,
|
||||
// fixed_buf: []u8,
|
||||
thread_count: usize,
|
||||
alloc: std.mem.Allocator,
|
||||
|
||||
fil: OffsetFile,
|
||||
decomp: Decomp.DecompFn,
|
||||
|
||||
super: Superblock,
|
||||
|
||||
setup: bool = false,
|
||||
|
||||
decomp: Decomp.DecompFn,
|
||||
|
||||
frag_table: Table(FragEntry) = undefined,
|
||||
id_table: Table(u16) = undefined,
|
||||
export_table: Table(InodeRef) = undefined,
|
||||
|
||||
/// 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) !Archive {
|
||||
return initAdvanced(
|
||||
alloc,
|
||||
fil,
|
||||
0,
|
||||
try std.Thread.getCpuCount(),
|
||||
);
|
||||
}
|
||||
/// Create the Archive dictating the amount of threads used for extraction.
|
||||
/// If you're planning on only interacting with a small number of files, it should be fine to use few (or one) threads.
|
||||
pub fn initAdvanced(alloc: std.mem.Allocator, fil: File, offset: u64, threads: usize) !Archive {
|
||||
pub fn init(alloc: std.mem.Allocator, fil: File, offset: u64) !Archive {
|
||||
var super: Superblock = undefined;
|
||||
const red = try fil.pread(@ptrCast(&super), offset);
|
||||
std.debug.assert(red == @sizeOf(Superblock));
|
||||
try super.validate();
|
||||
// const fixed_buf = try alloc.alloc(u8, mem);
|
||||
const off_fil: OffsetFile = .init(fil, offset);
|
||||
const decomp: Decomp.DecompFn = switch (super.compression) {
|
||||
.gzip => Decomp.gzipDecompress,
|
||||
.lzma => Decomp.lzmaDecompress,
|
||||
.xz => Decomp.xzDecompress,
|
||||
.zstd => Decomp.zstdDecompress,
|
||||
.lz4 => if (config.use_c_libs) Decomp.cLz4 else return error.Lz4Unsupported,
|
||||
.lzo => if (config.use_c_libs and config.allow_lzo) Decomp.lzoDecompress else return error.LzoUnsupported,
|
||||
};
|
||||
return .{
|
||||
.parent_alloc = alloc,
|
||||
.alloc = .{ .child_allocator = alloc },
|
||||
// .fixed_buf = fixed_buf,
|
||||
.thread_count = if (threads > 0) threads else try std.Thread.getCpuCount(),
|
||||
.fil = .init(fil, offset),
|
||||
.decomp = switch (super.compression) {
|
||||
.gzip => Decomp.gzipDecompress,
|
||||
.lzma => Decomp.lzmaDecompress,
|
||||
.xz => Decomp.xzDecompress,
|
||||
.zstd => Decomp.zstdDecompress,
|
||||
.lz4 => if (config.use_c_libs) Decomp.cLz4 else return error.Lz4Unsupported,
|
||||
.lzo => if (config.use_c_libs and config.allow_lzo) Decomp.lzoDecompress else return error.LzoUnsupported,
|
||||
},
|
||||
.alloc = alloc,
|
||||
|
||||
.fil = off_fil,
|
||||
.decomp = decomp,
|
||||
|
||||
.super = super,
|
||||
|
||||
.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),
|
||||
.export_table = try .init(alloc, off_fil, decomp, super.export_start, super.inode_count),
|
||||
};
|
||||
}
|
||||
pub fn deinit(self: *Archive) void {
|
||||
// self.parent_alloc.free(self.fixed_buf);
|
||||
if (self.setup) {
|
||||
self.frag_table.deinit();
|
||||
self.export_table.deinit();
|
||||
self.id_table.deinit();
|
||||
}
|
||||
self.frag_table.deinit();
|
||||
self.export_table.deinit();
|
||||
self.id_table.deinit();
|
||||
}
|
||||
|
||||
pub fn allocator(self: *Archive) std.mem.Allocator {
|
||||
return self.alloc.allocator();
|
||||
}
|
||||
|
||||
fn setupValues(self: *Archive) !void {
|
||||
const alloc = self.allocator();
|
||||
self.frag_table = try .init(alloc, self.fil, self.decomp, self.super.frag_start, self.super.frag_count);
|
||||
self.id_table = try .init(alloc, self.fil, self.decomp, self.super.id_start, self.super.id_count);
|
||||
self.export_table = try .init(alloc, self.fil, self.decomp, self.super.export_start, self.super.inode_count);
|
||||
self.setup = true;
|
||||
}
|
||||
|
||||
pub fn id(self: *Archive, idx: u32) !u16 {
|
||||
if (!self.setup) try self.setupValues();
|
||||
return self.id_table.get(idx);
|
||||
}
|
||||
|
||||
pub fn frag(self: *Archive, idx: u32) !FragEntry {
|
||||
if (!self.setup) try self.setupValues();
|
||||
return self.frag_table.get(idx);
|
||||
}
|
||||
|
||||
pub fn inode(self: *Archive, num: u32) !Inode {
|
||||
if (!self.setup) try self.setupValues();
|
||||
pub fn inode(self: *Archive, alloc: std.mem.Allocator, num: u32) !Inode {
|
||||
const ref = try self.export_table.get(num - 1);
|
||||
var rdr = try self.fil.readerAt(ref.block_start + self.super.inode_start, &[0]u8{});
|
||||
var meta: MetadataReader = .init(self.allocator(), &rdr.interface, &self.decomp);
|
||||
var meta: MetadataReader = .init(alloc, &rdr.interface, &self.decomp);
|
||||
try meta.interface.discardAll(ref.block_offset);
|
||||
return try .read(self.allocator(), &meta.interface, self.super.block_size);
|
||||
return try .read(alloc, &meta.interface, self.super.block_size);
|
||||
}
|
||||
|
||||
pub fn root(self: *Archive) !SfsFile {
|
||||
if (!self.setup) try self.setupValues();
|
||||
pub fn root(self: *Archive, alloc: std.mem.Allocator) !SfsFile {
|
||||
var rdr = try self.fil.readerAt(self.super.root_ref.block_start + self.super.inode_start, &[0]u8{});
|
||||
var meta: MetadataReader = .init(self.allocator(), &rdr.interface, self.decomp);
|
||||
var meta: MetadataReader = .init(alloc, &rdr.interface, self.decomp);
|
||||
try meta.interface.discardAll(self.super.root_ref.block_offset);
|
||||
const in: Inode = try .read(self.allocator(), &meta.interface, self.super.block_size);
|
||||
const in: Inode = try .read(alloc, &meta.interface, self.super.block_size);
|
||||
return .init(self, in, "");
|
||||
}
|
||||
|
||||
pub fn open(self: *Archive, path: []const u8) !SfsFile {
|
||||
if (!self.setup) try self.setupValues();
|
||||
var root_fil = try self.root();
|
||||
pub fn open(self: *Archive, alloc: std.mem.Allocator, path: []const u8) !SfsFile {
|
||||
var root_fil = try self.root(alloc);
|
||||
defer if (!SfsFile.pathIsSelf(path)) root_fil.deinit();
|
||||
return root_fil.open(path);
|
||||
}
|
||||
|
||||
pub fn extract(self: *Archive, path: []const u8, options: ExtractionOptions) !void {
|
||||
if (!self.setup) try self.setupValues();
|
||||
var alloc = self.allocator();
|
||||
var ext_path: []u8 = undefined;
|
||||
if (std.fs.cwd().statFile(path)) |stat| {
|
||||
if (stat.kind == .directory) {
|
||||
ext_path = @constCast(path);
|
||||
} else return error.ExtractionPathExists;
|
||||
} else |err| {
|
||||
if (err == error.FileNotFound) {
|
||||
ext_path = @constCast(path);
|
||||
} else {
|
||||
std.log.err("Error stat-ing extraction path {s}: {}\n", .{ path, err });
|
||||
return err;
|
||||
}
|
||||
}
|
||||
defer if (ext_path.len > path.len) alloc.free(ext_path);
|
||||
pub fn extract(self: *Archive, alloc: std.mem.Allocator, path: []const u8, options: ExtractionOptions) !void {
|
||||
var rdr = try self.fil.readerAt(self.super.root_ref.block_start + self.super.inode_start, &[0]u8{});
|
||||
var meta: MetadataReader = .init(self.allocator(), &rdr.interface, self.decomp);
|
||||
var meta: MetadataReader = .init(self.alloc, &rdr.interface, self.decomp);
|
||||
try meta.interface.discardAll(self.super.root_ref.block_offset);
|
||||
const in: Inode = try .read(self.allocator(), &meta.interface, self.super.block_size);
|
||||
try in.extractToThreaded(self, ext_path, options, self.thread_count);
|
||||
const in: Inode = try .read(self.alloc, &meta.interface, self.super.block_size);
|
||||
try in.extractTo(alloc, self, path, options);
|
||||
}
|
||||
|
||||
+12
-2
@@ -16,6 +16,7 @@ const help_mgs =
|
||||
\\ -o <offset> Start reading the archive at the given offset.
|
||||
\\
|
||||
\\ -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
|
||||
\\ --version Display the version
|
||||
@@ -28,6 +29,7 @@ var archive: []const u8 = "";
|
||||
var extLoc: []const u8 = "squashfs-root";
|
||||
var offset: u64 = 0;
|
||||
var threads: u32 = 0;
|
||||
var verbose: bool = false;
|
||||
|
||||
pub fn main() !void {
|
||||
const alloc = std.heap.smp_allocator;
|
||||
@@ -42,9 +44,14 @@ pub fn main() !void {
|
||||
}
|
||||
var fil: std.fs.File = try std.fs.cwd().openFile(archive, .{}); //TODO: Handle error gracefully.
|
||||
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 .init(alloc, fil, offset); //TODO: Update when memory size matters. //TODO: Handle error gracefully.
|
||||
defer arc.deinit();
|
||||
try arc.extract(extLoc, .Default); //TODO: Handle error gracefully.
|
||||
const options: squashfs.ExtractionOptions = .{
|
||||
.threads = if (threads == 0) try std.Thread.getCpuCount() else threads,
|
||||
.verbose = verbose,
|
||||
.verbose_writer = if (verbose) &out.interface else null,
|
||||
};
|
||||
try arc.extract(alloc, extLoc, options); //TODO: Handle error gracefully.
|
||||
}
|
||||
|
||||
fn handleArgs(alloc: std.mem.Allocator, out: *Writer) !void {
|
||||
@@ -82,6 +89,9 @@ fn handleArgs(alloc: std.mem.Allocator, out: *Writer) !void {
|
||||
return errors.InvalidArguments;
|
||||
};
|
||||
continue;
|
||||
} else if (std.mem.eql(u8, arg, "-v")) {
|
||||
verbose = true;
|
||||
continue;
|
||||
} else if (std.mem.eql(u8, arg, "--version")) {
|
||||
try out.print("zig-unsquashfs v", .{});
|
||||
try config.version.format(out);
|
||||
|
||||
+4
-32
@@ -96,7 +96,7 @@ pub fn iterate(self: SfsFile) !Iterator {
|
||||
}
|
||||
/// Open a sub-file/folder within a directory at the given path.
|
||||
/// If path is "", ".", "/", or "./", this File is returned.
|
||||
pub fn open(self: SfsFile, path: []const u8) !SfsFile {
|
||||
pub fn open(self: SfsFile, alloc: std.mem.Allocator, path: []const u8) !SfsFile {
|
||||
if (!self.isDir()) return FileError.NotDirectory;
|
||||
if (pathIsSelf(path)) return self;
|
||||
|
||||
@@ -109,7 +109,6 @@ pub fn open(self: SfsFile, path: []const u8) !SfsFile {
|
||||
if (std.mem.eql(u8, first_element, ".")) return self.open(path[idx + 1 ..]);
|
||||
const entries = try self.getEntries();
|
||||
defer {
|
||||
var alloc = self.archive.allocator();
|
||||
for (entries) |e| {
|
||||
e.deinit(alloc);
|
||||
}
|
||||
@@ -127,7 +126,7 @@ pub fn open(self: SfsFile, path: []const u8) !SfsFile {
|
||||
return fil;
|
||||
}
|
||||
defer fil.deinit();
|
||||
return fil.open(path[idx + 1 ..]);
|
||||
return fil.open(alloc, path[idx + 1 ..]);
|
||||
},
|
||||
.lt => cur_slice = cur_slice[0..split],
|
||||
.gt => cur_slice = cur_slice[split + 1 ..],
|
||||
@@ -170,35 +169,8 @@ pub fn devNum(self: SfsFile) !u32 {
|
||||
|
||||
/// Extract the given File to the path. If File is a regular file, the path must be a directory or not exist.
|
||||
/// If the gievn path is a folder, the File's contents will be extracted within.
|
||||
pub fn extract(self: *SfsFile, path: []const u8, options: ExtractionOptions) !void {
|
||||
var alloc = self.archive.allocator();
|
||||
var ext_path: []u8 = undefined;
|
||||
if (std.fs.cwd().statFile(path)) |stat| {
|
||||
if (stat.kind == .directory) {
|
||||
if (!self.isDir()) {
|
||||
const has_end_sep = path[path.len - 1] == '/';
|
||||
const alloc_size = if (has_end_sep)
|
||||
path.len + self.name.len
|
||||
else
|
||||
path.len + self.name.len + 1;
|
||||
ext_path = try alloc.alloc(u8, alloc_size);
|
||||
@memcpy(ext_path[0..path.len], path);
|
||||
@memcpy(ext_path[ext_path.len - self.name.len ..], self.name);
|
||||
if (!has_end_sep) ext_path[path.len] = '/';
|
||||
} else {
|
||||
ext_path = @constCast(path);
|
||||
}
|
||||
} else return FileError.ExtractionPathExists;
|
||||
} else |err| {
|
||||
if (err == error.FileNotFound) {
|
||||
ext_path = @constCast(path);
|
||||
} else {
|
||||
std.log.err("Error stat-ing extraction path {s}: {}\n", .{ path, err });
|
||||
return err;
|
||||
}
|
||||
}
|
||||
defer if (ext_path.len > path.len) alloc.free(ext_path);
|
||||
return self.inode.extractToThreaded(self.archive, path, options, self.archive.thread_count);
|
||||
pub fn extract(self: *SfsFile, alloc: std.mem.Allocator, path: []const u8, options: ExtractionOptions) !void {
|
||||
return self.inode.extractToThreaded(alloc, self.archive, path, options);
|
||||
}
|
||||
|
||||
/// Utility function.
|
||||
|
||||
+108
-55
@@ -4,6 +4,7 @@ const std = @import("std");
|
||||
const Reader = std.Io.Reader;
|
||||
const WaitGroup = std.Thread.WaitGroup;
|
||||
const Pool = std.Thread.Pool;
|
||||
const Mutex = std.Thread.Mutex;
|
||||
|
||||
const Archive = @import("archive.zig");
|
||||
const DirEntry = @import("dir_entry.zig");
|
||||
@@ -120,7 +121,7 @@ pub fn dataReader(self: Inode, alloc: std.mem.Allocator, archive: *Archive) !Dat
|
||||
fn readerFromData(alloc: std.mem.Allocator, archive: *Archive, data: anytype) !DataReader {
|
||||
var out: DataReader = .init(alloc, archive.*, data.block_sizes, data.block_start, data.size);
|
||||
if (data.frag_idx != 0xFFFFFFFF)
|
||||
out.addFragment(try archive.frag(data.frag_idx), data.frag_block_offset);
|
||||
out.addFragment(try archive.frag_table.get(data.frag_idx), data.frag_block_offset);
|
||||
return out;
|
||||
}
|
||||
/// Get a threaded data reader for a file inode.
|
||||
@@ -134,7 +135,7 @@ pub fn threadedDataReader(self: Inode, alloc: std.mem.Allocator, archive: *Archi
|
||||
fn threadedReaderFromData(alloc: std.mem.Allocator, archive: *Archive, data: anytype) !ThreadedDataReader {
|
||||
var out: ThreadedDataReader = .init(alloc, archive.*, data.block_sizes, data.block_start, data.size);
|
||||
if (data.frag_idx != 0xFFFFFFFF)
|
||||
out.addFragment(try archive.frag(data.frag_idx), data.frag_block_offset);
|
||||
out.addFragment(try archive.frag_table.get(data.frag_idx), data.frag_block_offset);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -154,15 +155,15 @@ fn entriesFromData(alloc: std.mem.Allocator, archive: Archive, data: anytype) ![
|
||||
}
|
||||
|
||||
/// Extract the inode to the given path. Single threaded.
|
||||
pub fn extractTo(self: Inode, 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);
|
||||
switch (self.hdr.inode_type) {
|
||||
.dir, .ext_dir => {
|
||||
// Removing any trailing separators since that's the easiest path forward.
|
||||
if (path[path.len - 1] == '/') return self.extractTo(archive, path[0 .. path.len - 1], options);
|
||||
if (path[path.len - 1] == '/') return self.extractTo(alloc, archive, path[0 .. path.len - 1], options);
|
||||
std.fs.cwd().makeDir(path) catch |err| {
|
||||
if (err != std.fs.Dir.MakeError.PathAlreadyExists) return err;
|
||||
};
|
||||
var alloc = archive.allocator();
|
||||
const entries = try self.dirEntries(alloc, archive.*);
|
||||
defer {
|
||||
for (entries) |entry| entry.deinit(alloc);
|
||||
@@ -177,32 +178,77 @@ pub fn extractTo(self: Inode, archive: *Archive, path: []const u8, options: Extr
|
||||
|
||||
var inode: Inode = try readFromEntry(alloc, archive, entry);
|
||||
defer inode.deinit(alloc);
|
||||
try inode.extractTo(archive, new_path, options);
|
||||
try inode.extractTo(alloc, archive, new_path, options);
|
||||
}
|
||||
},
|
||||
.file, .ext_file => try self.extractRegFile(archive.allocator(), archive, path, options),
|
||||
.file, .ext_file => try self.extractRegFile(alloc, archive, path, options),
|
||||
.symlink, .ext_symlink => try self.extractSymlink(path),
|
||||
else => try self.extractDevice(archive, path, options),
|
||||
}
|
||||
}
|
||||
|
||||
const Perms = struct {
|
||||
const Parent = struct {
|
||||
alloc: std.mem.Allocator,
|
||||
|
||||
path: []const u8,
|
||||
uid: u16,
|
||||
gid: 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_table.get(hdr.uid_idx),
|
||||
.gid = try archive.id_table.get(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.
|
||||
/// Functions identically to extractTo on all but regular files and directories.
|
||||
///
|
||||
/// If threads <= 1, then this just calls extractTo.
|
||||
pub fn extractToThreaded(self: Inode, archive: *Archive, path: []const u8, options: ExtractionOptions, threads: usize) !void {
|
||||
if (threads <= 1) return self.extractTo(archive, path, options);
|
||||
fn extractToThreaded(self: Inode, allocator: std.mem.Allocator, archive: *Archive, path: []const u8, options: ExtractionOptions) !void {
|
||||
switch (self.hdr.inode_type) {
|
||||
.dir, .ext_dir => {
|
||||
// Removing any trailing separators since that's the easiest path forward.
|
||||
if (path[path.len - 1] == '/') return self.extractToThreaded(archive, path[0 .. path.len - 1], options, threads);
|
||||
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);
|
||||
@@ -211,42 +257,42 @@ pub fn extractToThreaded(self: Inode, archive: *Archive, path: []const u8, optio
|
||||
// const alloc = fixed_alloc.threadSafeAllocator();
|
||||
|
||||
// Arena Allocator
|
||||
var arena_alloc: std.heap.ArenaAllocator = .init(archive.allocator());
|
||||
var arena_alloc: std.heap.ArenaAllocator = .init(allocator);
|
||||
defer arena_alloc.deinit();
|
||||
var thread_alloc: std.heap.ThreadSafeAllocator = .{ .child_allocator = arena_alloc.allocator() };
|
||||
const alloc = thread_alloc.allocator();
|
||||
|
||||
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
|
||||
var pool: Pool = undefined;
|
||||
try pool.init(.{ .allocator = alloc, .n_jobs = threads - 1 });
|
||||
try pool.init(.{ .allocator = allocator, .n_jobs = options.threads - 1 });
|
||||
defer pool.deinit();
|
||||
var out_err: ?anyerror = null;
|
||||
|
||||
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);
|
||||
if (out_err != null) return out_err.?;
|
||||
|
||||
if (perms != null) {
|
||||
for (perms.?.items) |p| {
|
||||
var fil = try std.fs.cwd().openFile(p.path, .{});
|
||||
defer fil.close();
|
||||
const time = @as(i128, self.hdr.mod_time) * 1000000000;
|
||||
try fil.updateTimes(time, time);
|
||||
try fil.chmod(p.perm);
|
||||
try fil.chown(p.uid, p.gid);
|
||||
}
|
||||
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.chown(try archive.id_table.get(self.hdr.uid_idx), try archive.id_table.get(self.hdr.gid_idx));
|
||||
}
|
||||
},
|
||||
.file, .ext_file => {
|
||||
const alloc = archive.allocator();
|
||||
|
||||
var pool: Pool = undefined;
|
||||
try pool.init(.{ .allocator = alloc, .n_jobs = threads });
|
||||
try pool.init(.{ .allocator = allocator, .n_jobs = options.threads - 1 });
|
||||
defer pool.deinit();
|
||||
|
||||
var arena_alloc: std.heap.ArenaAllocator = .init(allocator);
|
||||
defer arena_alloc.deinit();
|
||||
var thread_alloc: std.heap.ThreadSafeAllocator = .{ .child_allocator = arena_alloc.allocator() };
|
||||
const alloc = thread_alloc.allocator();
|
||||
|
||||
try self.extractRegFileThreaded(alloc, archive, path, options, &pool);
|
||||
|
||||
var fil = try std.fs.cwd().openFile(path, .{});
|
||||
@@ -255,7 +301,7 @@ pub fn extractToThreaded(self: Inode, archive: *Archive, path: []const u8, optio
|
||||
try fil.updateTimes(time, time);
|
||||
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));
|
||||
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),
|
||||
@@ -272,7 +318,7 @@ fn extractThreadEntry(
|
||||
wg: *WaitGroup,
|
||||
pool: *Pool,
|
||||
out_err: *?anyerror,
|
||||
perms: *?std.ArrayList(Perms),
|
||||
parent: ?*Parent,
|
||||
) void {
|
||||
var new_path = alloc.alloc(u8, path.len + entry.name.len + 1) catch |err| {
|
||||
wg.finish();
|
||||
@@ -287,7 +333,7 @@ fn extractThreadEntry(
|
||||
wg.finish();
|
||||
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.
|
||||
@@ -300,18 +346,35 @@ fn extractThread(
|
||||
wg: *WaitGroup,
|
||||
pool: *Pool,
|
||||
out_err: *?anyerror,
|
||||
perms: *?std.ArrayList(Perms),
|
||||
parent: ?*Parent,
|
||||
) 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;
|
||||
switch (self.hdr.inode_type) {
|
||||
.dir, .ext_dir => {
|
||||
_ = std.fs.cwd().makePathStatus(path) catch |err| {
|
||||
if (options.verbose)
|
||||
options.verbose_writer.?.print("Error creating {s}: {}\n", .{ path, err }) catch {};
|
||||
out_err.* = err;
|
||||
return;
|
||||
};
|
||||
|
||||
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;
|
||||
return;
|
||||
};
|
||||
@@ -319,7 +382,7 @@ fn extractThread(
|
||||
// defer files.deinit(alloc); We don't need to do this due to ArenaAllocator
|
||||
for (entries) |entry| {
|
||||
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;
|
||||
}
|
||||
pool.spawn(
|
||||
@@ -333,45 +396,35 @@ fn extractThread(
|
||||
wg,
|
||||
pool,
|
||||
out_err,
|
||||
perms,
|
||||
p,
|
||||
},
|
||||
) catch |err| {
|
||||
wg.finish();
|
||||
if (options.verbose)
|
||||
options.verbose_writer.?.print("Error starting extraction thread: {}\n", .{err}) catch {};
|
||||
out_err.* = err;
|
||||
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 => {
|
||||
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;
|
||||
};
|
||||
},
|
||||
.symlink, .ext_symlink => {
|
||||
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;
|
||||
};
|
||||
},
|
||||
else => {
|
||||
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;
|
||||
};
|
||||
},
|
||||
@@ -394,7 +447,7 @@ fn extractRegFile(self: Inode, alloc: std.mem.Allocator, archive: *Archive, path
|
||||
try fil.updateTimes(time, time);
|
||||
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));
|
||||
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.
|
||||
@@ -411,7 +464,7 @@ fn extractRegFileThreaded(self: Inode, alloc: std.mem.Allocator, archive: *Archi
|
||||
try fil.updateTimes(time, time);
|
||||
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));
|
||||
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.
|
||||
@@ -478,7 +531,7 @@ fn extractDevice(self: Inode, archive: *Archive, path: []const u8, options: Extr
|
||||
try fil.updateTimes(time, time);
|
||||
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));
|
||||
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
|
||||
|
||||
+10
-2
@@ -5,6 +5,8 @@ const Writer = std.Io.Writer;
|
||||
|
||||
const ExtractionOptions = @This();
|
||||
|
||||
/// The number of threads used for extraction. 0 implies single threaded.
|
||||
threads: usize = 1,
|
||||
/// Don't set the file's owner & permissions after extraction
|
||||
ignore_permissions: bool = false,
|
||||
/// Don't set xattr values. Currently xattrs are never set anyway.
|
||||
@@ -16,10 +18,16 @@ verbose: bool = false,
|
||||
/// Where to print verbose log.
|
||||
verbose_writer: ?*Writer = null,
|
||||
|
||||
pub const Default: ExtractionOptions = .{};
|
||||
pub fn VerboseDefault(wrt: *Writer) ExtractionOptions {
|
||||
pub const SingleThreadedDefault: ExtractionOptions = .{};
|
||||
pub fn Default() !ExtractionOptions {
|
||||
return .{
|
||||
.threads = try std.Thread.getCpuCount(),
|
||||
};
|
||||
}
|
||||
pub fn VerboseDefault(wrt: *Writer) !ExtractionOptions {
|
||||
return .{
|
||||
.verbose = true,
|
||||
.verbose_writer = wrt,
|
||||
.threads = try std.Thread.getCpuCount(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub const Archive = @import("archive.zig");
|
||||
pub const ExtractionOptions = @import("options.zig");
|
||||
|
||||
Reference in New Issue
Block a user