17 Commits

Author SHA1 Message Date
Caleb J. Gardner 8002e745e0 Fixed using the wrong decompression function for gzip 2026-03-17 22:17:11 -05:00
Caleb J. Gardner b5742bc282 Updated flags to new use_zig_decomp 2026-03-17 21:50:22 -05:00
Caleb J. Gardner 51305a1a80 Separated out zlib-ng compilation to separate package 2026-03-11 19:21:07 -05:00
Caleb J. Gardner 1dc85c62fc Include zstd as submodule 2026-03-10 03:16:06 -05:00
Caleb J. Gardner 8b8c9a772f Added -Ddebug build option
Re-added errors from settings xattr values
2026-03-09 00:16:19 -05:00
Caleb Gardner f563648b20 Merge pull request #4 from CalebQ42/inode_finish
Re-do much of extraction
2026-03-08 22:36:37 -05:00
Caleb Gardner 8d4f3d72f8 Merge branch 'main' into inode_finish 2026-03-08 22:34:50 -05:00
Caleb J. Gardner 0b6129f1ae Switch to zlib-ng 2026-03-08 22:15:28 -05:00
Caleb J. Gardner 7308faad36 Moved kv free's in setMetadata 2026-03-05 12:54:25 -06:00
Caleb J. Gardner c9499251f8 Moved lookup tables into separate struct to fix some race conditions
Fixed lingering issues due to zero work size InodeFinish
Fixed xattrs not applying due to the keys sometimes not being
null-terminated.
Updated performance numbers
2026-03-05 12:20:30 -06:00
Caleb J. Gardner d470ca98e3 Added --force to unsquashfs
Fixing race condition bugs (yay)
2026-03-05 07:04:24 -06:00
Caleb J. Gardner a606f5e11a Move extract logic to util/extract.zig to make it easier to read. 2026-03-05 03:03:37 -06:00
Caleb J. Gardner a4e23a840d Updated performance values in README.
Added ability to ignore xattrs & permissions.
Ignore setting xattr errors due to an unknown issues.
2026-03-05 03:03:34 -06:00
Caleb J. Gardner 3a10572953 Updated performance values in README.
Added ability to ignore xattrs & permissions.
Ignore setting xattr errors due to an unknown issues.
2026-03-04 13:28:29 -06:00
Caleb J. Gardner edfe919c1b Expirementation with a new way to finish threads. Currently not working. 2026-03-04 06:39:44 -06:00
Caleb J. Gardner 4515610082 Added xattr support (currently untested)
Use stack fallback allocator on extraction for better performance.
2026-03-04 03:24:30 -06:00
Caleb J. Gardner beca6a2ae6 Small tweaks 2026-03-01 02:27:54 -06:00
18 changed files with 950 additions and 546 deletions
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
- name: Build normal version - name: Build normal version
run: zig build --release=fast -Dversion=${{ github.ref_name }} run: zig build --release=fast -Dversion=${{ github.ref_name }}
- name: Move normal build out - name: Move normal build out
run: mv zig-out/bin/unsquashfs ./unsquashfs-x86_64 run: mv zig-out/bin/unsquashfs ./unsquashfs-x86_64-zig-libs
- name: Rebuild with C libraries - name: Rebuild with C libraries
run: zig build --release=fast -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 - name: Move C build out
@@ -27,5 +27,5 @@ jobs:
with: with:
prerelease: true prerelease: true
files: | files: |
unsquashfs-x86_64 unsquashfs-x86_64-zig-libs
unsquashfs-x86_64-c-libs unsquashfs-x86_64-c-libs
+3
View File
@@ -0,0 +1,3 @@
[submodule "extern/zstd"]
path = extern/zstd
url = https://github.com/facebook/zstd
+25
View File
@@ -0,0 +1,25 @@
// 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", "-Duse_c_libs=true", "-Ddebug=true"],
},
"program": "zig-out/bin/unsquashfs",
"args": [
"--force",
"-d",
"testing/TestExtractUnsquashfs",
"testing/LinuxPATest.sfs",
],
},
]
+14 -12
View File
@@ -18,9 +18,9 @@ Instead of using Zig's standard library for decompression, use the system's C li
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`. 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`.
> `-Dvalgrind=true` > `-Ddebug=true`
Just sets the valgrind build option. Sets various build options that make debugging easier. Specifically, debug optimization is forced, valgrind support is enabled, error tracing is enabled, stipping is disabled, and copmilation uses LLVM (this is due to some linking issues when on Debug optimization and is required for debugging tools such as `lldb`. In the future this may be removed from the debug flag).
> `-Dversion=0.0.0` > `-Dversion=0.0.0`
@@ -30,7 +30,6 @@ Sets the version of `unsquashfs` shown when `--version` is passed.
Most features are present except for the following: Most features are present except for the following:
* 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 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. * 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.
@@ -38,17 +37,20 @@ Most features are present except for the following:
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`. 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) Currently, my only performance checks are checking execution time, nothing deeper.
* 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: * Under ideal circumstances, my library is ~70% slower (.12s vs .20s).
* Using Zig decompression libraries *significantly* increases decompression time by ~600%. Under ideal circumstances.
* Performance improvements/regressions will be common. I'm still learning Zig.
* *unsquashfs*: .11s Example Times:
* *C-libs, single-threaded*: .18s
* *C-libs, multi-threaded*: .57s * *unsquashfs, multi-threaded*: .12s
* *Zig-libs, single-threaded*: 5.87s * *unsquashfs, single-threaded*: .13s
* *Zig-libs, multi-threaded*: 1.10s * *C-libs, single-threaded*: .45s
* *C-libs, multi-threaded*: .20s
* *Zig-libs, single-threaded*: 5.78s
* *Zig-libs, multi-threaded*: 1.08s
## Build considerations ## Build considerations
+91 -17
View File
@@ -1,32 +1,44 @@
const std = @import("std"); const std = @import("std");
const Compile = std.Build.Step.Compile;
const ResolvedTarget = std.Build.ResolvedTarget;
const OptimizeMode = std.builtin.OptimizeMode;
const Module = std.Build.Module;
pub fn build(b: *std.Build) !void { 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 use_zig_decomp = b.option(bool, "use_zig_decomp", "Use Zig standard library for decompression instead of C libraries.") orelse false;
const allow_lzo = b.option(bool, "allow_lzo", "Compile with lzo support"); const allow_lzo = b.option(bool, "allow_lzo", "Compile with lzo support") orelse false;
const valgrind = b.option(bool, "valgrind", "Compile with valgrind integration"); const debug = b.option(bool, "debug", "Enable options to make debugging easier.");
const version_string_option = b.option([]const u8, "version", "Version of the library/binary"); const version_string_option = b.option([]const u8, "version", "Version of the library/binary");
const zig_squashfs_options = b.addOptions(); const zig_squashfs_options = b.addOptions();
zig_squashfs_options.addOption(bool, "use_c_libs", use_c_libs_option orelse false); zig_squashfs_options.addOption(bool, "use_zig_decomp", use_zig_decomp);
zig_squashfs_options.addOption(bool, "allow_lzo", allow_lzo orelse false); zig_squashfs_options.addOption(bool, "allow_lzo", allow_lzo);
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); 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,
.optimize = optimize, .optimize = if (debug == true) .Debug else optimize,
.link_libc = use_c_libs_option, .link_libc = !use_zig_decomp,
.valgrind = valgrind, .valgrind = debug,
.error_tracing = debug,
.strip = if (debug == true) false else null,
}); });
mod.addOptions("config", zig_squashfs_options); mod.addOptions("config", zig_squashfs_options);
if (use_c_libs_option == true) {
mod.linkSystemLibrary("zlib", .{}); if (!use_zig_decomp) {
mod.linkSystemLibrary("lzma", .{}); var zlib = b.dependency("zlib_ng", .{});
mod.linkLibrary(zlib.artifact("zng"));
mod.linkSystemLibrary("lzma", .{ .preferred_link_mode = .static });
if (allow_lzo == true) if (allow_lzo == true)
mod.linkSystemLibrary("minilzo", .{}); mod.linkSystemLibrary("minilzo", .{ .preferred_link_mode = .static });
mod.linkSystemLibrary("lz4", .{}); mod.linkSystemLibrary("lz4", .{ .preferred_link_mode = .static });
mod.linkSystemLibrary("zstd", .{});
const zstd_lib = buildZstdLibrary(b, target, optimize, debug);
mod.linkLibrary(zstd_lib);
mod.addIncludePath(b.path("extern/zstd/lib/"));
} }
var version = version_string_option orelse "0.0.0-testing"; var version = version_string_option orelse "0.0.0-testing";
@@ -41,22 +53,26 @@ pub fn build(b: *std.Build) !void {
var exe_mod = b.createModule(.{ var exe_mod = b.createModule(.{
.root_source_file = b.path("src/bin/unsquashfs.zig"), .root_source_file = b.path("src/bin/unsquashfs.zig"),
.target = target, .target = target,
.optimize = optimize, .optimize = if (debug == true) .Debug else optimize,
.link_libc = use_c_libs_option, .link_libc = !use_zig_decomp,
.imports = &.{ .imports = &.{
.{ .name = "zig_squashfs", .module = mod }, .{ .name = "zig_squashfs", .module = mod },
}, },
.valgrind = valgrind, .valgrind = debug,
.error_tracing = debug,
.strip = if (debug == true) false else null,
}); });
exe_mod.addOptions("config", unsquashfs_options); exe_mod.addOptions("config", unsquashfs_options);
const exe = b.addExecutable(.{ const exe = b.addExecutable(.{
.name = "unsquashfs", .name = "unsquashfs",
.root_module = exe_mod, .root_module = exe_mod,
.use_llvm = debug,
}); });
const lib = b.addLibrary(.{ const lib = b.addLibrary(.{
.name = "squashfs", .name = "squashfs",
.root_module = mod, .root_module = mod,
.use_llvm = debug,
}); });
b.installArtifact(lib); b.installArtifact(lib);
@@ -87,3 +103,61 @@ pub fn build(b: *std.Build) !void {
check.dependOn(&lib_check.step); check.dependOn(&lib_check.step);
check.dependOn(&exe_check.step); check.dependOn(&exe_check.step);
} }
fn buildZstdLibrary(b: *std.Build, target: ResolvedTarget, optimize: OptimizeMode, debug: ?bool) *Compile {
var zstd_lib = b.addLibrary(.{
.name = "zstd",
.linkage = .static,
.root_module = b.createModule(.{
.target = target,
.optimize = if (debug == true) .Debug else optimize,
.link_libc = true,
}),
.use_llvm = debug,
});
zstd_lib.root_module.addCSourceFiles(.{
.root = b.path("extern/zstd/lib/"),
.files = &.{
"common/debug.c",
"common/entropy_common.c",
"common/error_private.c",
"common/fse_decompress.c",
"common/pool.c",
"common/threading.c",
"common/xxhash.c",
"common/zstd_common.c",
"compress/fse_compress.c",
"compress/hist.c",
"compress/huf_compress.c",
"compress/zstd_compress.c",
"compress/zstd_compress_literals.c",
"compress/zstd_compress_sequences.c",
"compress/zstd_compress_superblock.c",
"compress/zstd_double_fast.c",
"compress/zstd_fast.c",
"compress/zstd_lazy.c",
"compress/zstd_ldm.c",
"compress/zstdmt_compress.c",
"compress/zstd_opt.c",
"compress/zstd_preSplit.c",
"decompress/huf_decompress.c",
"decompress/zstd_ddict.c",
"decompress/zstd_decompress_block.c",
"decompress/zstd_decompress.c",
"dictBuilder/cover.c",
"dictBuilder/divsufsort.c",
"dictBuilder/fastcover.c",
"dictBuilder/zdict.c",
},
});
zstd_lib.root_module.addCSourceFiles(.{
.root = b.path("extern/zstd/lib/decompress"),
.files = &.{"huf_decompress_amd64.S"},
});
zstd_lib.installHeadersDirectory(b.path("extern/zstd/lib/"), &.{}, .{});
zstd_lib.installHeadersDirectory(b.path("extern/zstd/lib/common/"), &.{}, .{});
zstd_lib.installHeadersDirectory(b.path("extern/zstd/lib/compress/"), &.{}, .{});
zstd_lib.installHeadersDirectory(b.path("extern/zstd/lib/dictBuilder/"), &.{}, .{});
zstd_lib.installHeadersDirectory(b.path("extern/zstd/lib/"), &.{}, .{});
return zstd_lib;
}
+6 -35
View File
@@ -1,43 +1,14 @@
.{ .{
.name = .squashfs, .name = .squashfs,
.version = "0.0.1", .version = "0.0.6",
.fingerprint = 0x37ba29474b87f145, // Changing this has security and trust implications. .fingerprint = 0x37ba29474b87f145, // Changing this has security and trust implications.
.minimum_zig_version = "0.15.2", .minimum_zig_version = "0.15.2",
// This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`.
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.dependencies = .{ .dependencies = .{
// See `zig fetch --save <url>` for a command-line interface for adding dependencies. .zlib_ng = .{
//.example = .{ // .url = "https://github.com/CalebQ42/zig-zlib-ng/archive/refs/tags/2.3.3.tar.gz",
// // When updating this field to a new URL, be sure to delete the corresponding // .hash = "zlib_ng-2.3.3-2HYS4Bw_AADjgv7tkrqjjz2fVz5kRTvha8wN9LEcjYNp",
// // `hash`, otherwise you are communicating that you expect to find the old hash at .path = "../zig-zlib-ng",
// // the new URL. If the contents of a URL change this will result in a hash mismatch },
// // which will prevent zig from using it.
// .url = "https://example.com/foo.tar.gz",
//
// // This is computed from the file contents of the directory of files that is
// // obtained after fetching `url` and applying the inclusion rules given by
// // `paths`.
// //
// // This field is the source of truth; packages do not come from a `url`; they
// // come from a `hash`. `url` is just one of many possible mirrors for how to
// // obtain a package matching this `hash`.
// //
// // Uses the [multihash](https://multiformats.io/multihash/) format.
// .hash = "...",
//
// // When this is provided, the package is found in a directory relative to the
// // build root. In this case the package's hash is irrelevant and therefore not
// // computed. This field and `url` are mutually exclusive.
// .path = "foo",
//
// // When this is set to `true`, a package is declared to be lazily
// // fetched. This makes the dependency only get fetched if it is
// // actually used.
// .lazy = false,
//},
}, },
.paths = .{ .paths = .{
"build.zig", "build.zig",
Vendored Submodule
+1
Submodule extern/zstd added at f8745da6ff
+15 -25
View File
@@ -12,23 +12,16 @@ const InodeRef = Inode.Ref;
const BlockSize = @import("inode_data/file.zig").BlockSize; const BlockSize = @import("inode_data/file.zig").BlockSize;
const SfsFile = @import("file.zig"); const SfsFile = @import("file.zig");
const Superblock = @import("super.zig").Superblock; const Superblock = @import("super.zig").Superblock;
const Table = @import("table.zig").Table; const Tables = @import("tables.zig");
const MetadataReader = @import("util/metadata.zig"); const MetadataReader = @import("util/metadata.zig");
const OffsetFile = @import("util/offset_file.zig"); const OffsetFile = @import("util/offset_file.zig");
const XattrTable = @import("xattr.zig");
const config = if (builtin.is_test) .{ const config = if (builtin.is_test) .{
.use_c_libs = true, .use_zig_decomp = builtin.link_libc != true,
.allow_lzo = false, .allow_lzo = false,
} else @import("config"); } else @import("config");
/// Information about a fragment section. Multiple fragments are contained in the block described by a single FragEntry.
/// The offset into the block and fragment size is stored in the file's inode.
pub const FragEntry = packed struct {
start: u64,
size: BlockSize,
_: u32,
};
const Archive = @This(); const Archive = @This();
alloc: std.mem.Allocator, alloc: std.mem.Allocator,
@@ -38,9 +31,7 @@ decomp: Decomp.DecompFn,
super: Superblock, super: Superblock,
frag_table: Table(FragEntry) = undefined, tables: ?Tables = null,
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. /// 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, offset: u64) !Archive { pub fn init(alloc: std.mem.Allocator, fil: File, offset: u64) !Archive {
@@ -54,29 +45,24 @@ pub fn init(alloc: std.mem.Allocator, fil: File, offset: u64) !Archive {
.lzma => Decomp.lzmaDecompress, .lzma => Decomp.lzmaDecompress,
.xz => Decomp.xzDecompress, .xz => Decomp.xzDecompress,
.zstd => Decomp.zstdDecompress, .zstd => Decomp.zstdDecompress,
.lz4 => if (config.use_c_libs) Decomp.cLz4 else return error.Lz4Unsupported, .lz4 => if (!config.use_zig_decomp) Decomp.cLz4 else return error.Lz4Unsupported,
.lzo => if (config.use_c_libs and config.allow_lzo) Decomp.lzoDecompress else return error.LzoUnsupported, .lzo => if (!config.use_zig_decomp and config.allow_lzo) Decomp.lzoDecompress else return error.LzoUnsupported,
}; };
return .{ return .{
.alloc = alloc, .alloc = alloc,
.fil = off_fil, .fil = off_fil,
.decomp = decomp, .decomp = decomp,
.super = super, .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 { pub fn deinit(self: *Archive) void {
self.frag_table.deinit(); if (self.tables != null)
self.export_table.deinit(); self.tables.?.deinit();
self.id_table.deinit();
} }
pub fn inode(self: *Archive, alloc: std.mem.Allocator, num: u32) !Inode { pub fn inode(self: *Archive, alloc: std.mem.Allocator, num: u32) !Inode {
if (self.tables == null)
self.tables = try .init(alloc, self);
const ref = try self.export_table.get(num - 1); 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 rdr = try self.fil.readerAt(ref.block_start + self.super.inode_start, &[0]u8{});
var meta: MetadataReader = .init(alloc, &rdr.interface, &self.decomp); var meta: MetadataReader = .init(alloc, &rdr.interface, &self.decomp);
@@ -85,6 +71,8 @@ pub fn inode(self: *Archive, alloc: std.mem.Allocator, num: u32) !Inode {
} }
pub fn root(self: *Archive, alloc: std.mem.Allocator) !SfsFile { pub fn root(self: *Archive, alloc: std.mem.Allocator) !SfsFile {
if (self.tables == null)
self.tables = try .init(alloc, self);
var rdr = try self.fil.readerAt(self.super.root_ref.block_start + self.super.inode_start, &[0]u8{}); var rdr = try self.fil.readerAt(self.super.root_ref.block_start + self.super.inode_start, &[0]u8{});
var meta: MetadataReader = .init(alloc, &rdr.interface, self.decomp); var meta: MetadataReader = .init(alloc, &rdr.interface, self.decomp);
try meta.interface.discardAll(self.super.root_ref.block_offset); try meta.interface.discardAll(self.super.root_ref.block_offset);
@@ -93,12 +81,14 @@ pub fn root(self: *Archive, alloc: std.mem.Allocator) !SfsFile {
} }
pub fn open(self: *Archive, alloc: std.mem.Allocator, path: []const u8) !SfsFile { pub fn open(self: *Archive, alloc: std.mem.Allocator, path: []const u8) !SfsFile {
if (self.tables == null)
self.tables = try .init(alloc, self);
var root_fil = try self.root(alloc); var root_fil = try self.root(alloc);
defer if (!SfsFile.pathIsSelf(path)) root_fil.deinit(); defer if (!SfsFile.pathIsSelf(path)) root_fil.deinit();
return root_fil.open(path); return root_fil.open(path);
} }
pub fn extract(self: *Archive, alloc: std.mem.Allocator, path: []const u8, options: ExtractionOptions) !void { 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 rdr = try self.fil.readerAt(self.super.root_ref.block_start + self.super.inode_start, &[0]u8{});
var meta: MetadataReader = .init(self.alloc, &rdr.interface, self.decomp); var meta: MetadataReader = .init(self.alloc, &rdr.interface, self.decomp);
try meta.interface.discardAll(self.super.root_ref.block_offset); try meta.interface.discardAll(self.super.root_ref.block_offset);
+20
View File
@@ -14,10 +14,14 @@ const help_mgs =
\\ -d <location> Extract to the given location instead of "squashfs-root" \\ -d <location> Extract to the given location instead of "squashfs-root"
\\ \\
\\ -o <offset> Start reading the archive at the given offset. \\ -o <offset> Start reading the archive at the given offset.
\\ -dx Don't set xattr values
\\ -dp Don't set permissions (includes setting uid & gid owner)
\\ \\
\\ -p <threads> Specify how many threads to use. If no present or zero, 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 \\ -v Verbose
\\ \\
\\ --force Force extraction. If the destination already exists, it will be deleted.
\\
\\ --help Display this messages \\ --help Display this messages
\\ --version Display the version \\ --version Display the version
\\ \\
@@ -30,6 +34,9 @@ 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; var verbose: bool = false;
var ignore_xattrs: bool = false;
var ignore_permissions: bool = false;
var force: bool = false;
pub fn main() !void { pub fn main() !void {
const alloc = std.heap.smp_allocator; const alloc = std.heap.smp_allocator;
@@ -50,7 +57,11 @@ pub fn main() !void {
.threads = if (threads == 0) try std.Thread.getCpuCount() else threads, .threads = if (threads == 0) try std.Thread.getCpuCount() else threads,
.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_permissions = ignore_permissions,
}; };
if (force)
try std.fs.cwd().deleteTree(extLoc);
try arc.extract(alloc, extLoc, options); //TODO: Handle error gracefully. try arc.extract(alloc, extLoc, options); //TODO: Handle error gracefully.
} }
@@ -92,6 +103,15 @@ fn handleArgs(alloc: std.mem.Allocator, out: *Writer) !void {
} else if (std.mem.eql(u8, arg, "-v")) { } else if (std.mem.eql(u8, arg, "-v")) {
verbose = true; verbose = true;
continue; continue;
} else if (std.mem.eql(u8, arg, "-dx")) {
ignore_xattrs = true;
continue;
} 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")) { } else if (std.mem.eql(u8, arg, "--version")) {
try out.print("zig-unsquashfs v", .{}); try out.print("zig-unsquashfs v", .{});
try config.version.format(out); try config.version.format(out);
+10 -9
View File
@@ -6,12 +6,12 @@ const Reader = std.Io.Reader;
const builtin = @import("builtin"); const builtin = @import("builtin");
const config = if (builtin.is_test) .{ const config = if (builtin.is_test) .{
.use_c_libs = builtin.link_libc == true, .use_zig_decomp = builtin.link_libc != true,
.allow_lzo = false, // Change once LZO compilation is fixed .allow_lzo = false, // Change once LZO compilation is fixed
} else @import("config"); } else @import("config");
const c = @cImport({ const c = @cImport({
@cInclude("zlib.h"); @cInclude("zlib-ng.h");
@cInclude("lzma.h"); @cInclude("lzma.h");
@cInclude("lz4.h"); @cInclude("lz4.h");
@cInclude("zstd.h"); @cInclude("zstd.h");
@@ -31,7 +31,7 @@ pub const CompressionType = enum(u16) {
pub const DecompFn = *const fn (alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize; // TODO: replace anyerror to definitive error types. pub const DecompFn = *const fn (alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize; // TODO: replace anyerror to definitive error types.
pub const gzipDecompress = if (config.use_c_libs) cGzip else zigGzip; pub const gzipDecompress = if (!config.use_zig_decomp) cGzip else zigGzip;
fn zigGzip(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize { fn zigGzip(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
var rdr: Reader = .fixed(in); var rdr: Reader = .fixed(in);
@@ -43,7 +43,7 @@ fn zigGzip(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
fn cGzip(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize { fn cGzip(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
_ = alloc; _ = alloc;
var out_len: usize = out.len; var out_len: usize = out.len;
const res = c.uncompress(out.ptr, &out_len, in.ptr, in.len); const res = c.zng_uncompress(out.ptr, &out_len, in.ptr, in.len);
return switch (res) { return switch (res) {
c.Z_OK => out_len, c.Z_OK => out_len,
c.Z_MEM_ERROR => error.NotEnoughMemory, c.Z_MEM_ERROR => error.NotEnoughMemory,
@@ -53,7 +53,7 @@ fn cGzip(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
}; };
} }
pub const lzmaDecompress = if (config.use_c_libs) cLzma else zigLzma; pub const lzmaDecompress = if (!config.use_zig_decomp) cLzma else zigLzma;
fn zigLzma(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize { fn zigLzma(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
var rdr: Reader = .fixed(in); var rdr: Reader = .fixed(in);
@@ -90,7 +90,7 @@ fn cLzma(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
}; };
} }
// pub const lzoDecompress = if (config.use_c_libs) cLzo else zigLzo; // pub const lzoDecompress = if (!config.use_zig_decomp) cLzo else zigLzo;
// fn zigLzo(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize { // fn zigLzo(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
// _ = alloc; // _ = alloc;
@@ -123,7 +123,7 @@ pub fn cLzo(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
}; };
} }
pub const xzDecompress = if (config.use_c_libs) cXz else zigXz; pub const xzDecompress = if (!config.use_zig_decomp) cXz else zigXz;
fn zigXz(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize { fn zigXz(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
var rdr: Reader = .fixed(in); var rdr: Reader = .fixed(in);
@@ -137,6 +137,7 @@ fn cXz(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
.avail_in = in.len, .avail_in = in.len,
.next_out = out.ptr, .next_out = out.ptr,
.avail_out = out.len, .avail_out = out.len,
// .allocator = _, TODO: create a custom allocator based on alloc,
}; };
var res = c.lzma_stream_decoder(&stream, in.len * 2, 0); var res = c.lzma_stream_decoder(&stream, in.len * 2, 0);
switch (res) { switch (res) {
@@ -160,7 +161,7 @@ fn cXz(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
}; };
} }
// pub const lz4Decompress = if (config.use_c_libs) cLz4 else zigLz4; // pub const lz4Decompress = if (!config.use_zig_decomp) cLz4 else zigLz4;
// fn zigLz4(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize { // fn zigLz4(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
// _ = alloc; // _ = alloc;
@@ -175,7 +176,7 @@ pub fn cLz4(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
return error.Lz4DecompressFailed; return error.Lz4DecompressFailed;
} }
pub const zstdDecompress = if (config.use_c_libs) cZstd else zigZstd; pub const zstdDecompress = if (!config.use_zig_decomp) cZstd else zigZstd;
pub fn zigZstd(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize { pub fn zigZstd(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
var rdr: Reader = .fixed(in); var rdr: Reader = .fixed(in);
+43 -393
View File
@@ -12,8 +12,12 @@ const ExtractionOptions = @import("options.zig");
const dir = @import("inode_data/dir.zig"); const dir = @import("inode_data/dir.zig");
const file = @import("inode_data/file.zig"); const file = @import("inode_data/file.zig");
const misc = @import("inode_data/misc.zig"); const misc = @import("inode_data/misc.zig");
const Tables = @import("tables.zig");
const DataReader = @import("util/data.zig"); const DataReader = @import("util/data.zig");
const ThreadedDataReader = @import("util/data_threaded.zig"); const ThreadedDataReader = @import("util/data_threaded.zig");
const InodeExtract = @import("util/extract.zig");
const InodeFinish = @import("util/inode_finish.zig");
const FinishUnion = InodeFinish.FinishUnion;
const MetadataReader = @import("util/metadata.zig"); const MetadataReader = @import("util/metadata.zig");
pub const Ref = packed struct { pub const Ref = packed struct {
@@ -93,7 +97,7 @@ pub fn read(alloc: std.mem.Allocator, rdr: *Reader, block_size: u32) !Inode {
}, },
}; };
} }
pub fn readFromEntry(alloc: std.mem.Allocator, archive: *Archive, entry: DirEntry) !Inode { pub fn readFromEntry(alloc: std.mem.Allocator, archive: Archive, entry: DirEntry) !Inode {
var rdr = try archive.fil.readerAt(archive.super.inode_start + entry.block_start, &[0]u8{}); var rdr = try archive.fil.readerAt(archive.super.inode_start + entry.block_start, &[0]u8{});
var meta: MetadataReader = .init(alloc, &rdr.interface, archive.decomp); var meta: MetadataReader = .init(alloc, &rdr.interface, archive.decomp);
try meta.interface.discardAll(entry.block_offset); try meta.interface.discardAll(entry.block_offset);
@@ -111,31 +115,17 @@ pub fn deinit(self: Inode, alloc: std.mem.Allocator) void {
} }
/// Get the data reader for a file inode. /// Get the data reader for a file inode.
pub fn dataReader(self: Inode, alloc: std.mem.Allocator, archive: *Archive) !DataReader { pub fn dataReader(self: Inode, alloc: std.mem.Allocator, archive: Archive, tables: *Tables) !DataReader {
return switch (self.hdr.inode_type) { return switch (self.hdr.inode_type) {
.file => readerFromData(alloc, archive, self.data.file), .file => readerFromData(alloc, archive, tables, self.data.file),
.ext_file => readerFromData(alloc, archive, self.data.ext_file), .ext_file => readerFromData(alloc, archive, tables, self.data.ext_file),
else => error.NotRegularFile, else => error.NotRegularFile,
}; };
} }
fn readerFromData(alloc: std.mem.Allocator, archive: *Archive, data: anytype) !DataReader { fn readerFromData(alloc: std.mem.Allocator, archive: Archive, tables: *Tables, data: anytype) !DataReader {
var out: DataReader = .init(alloc, archive.*, data.block_sizes, data.block_start, data.size); var out: DataReader = .init(alloc, archive, data.block_sizes, data.block_start, data.size);
if (data.frag_idx != 0xFFFFFFFF) if (data.frag_idx != 0xFFFFFFFF)
out.addFragment(try archive.frag_table.get(data.frag_idx), data.frag_block_offset); out.addFragment(try tables.frag_table.get(data.frag_idx), data.frag_block_offset);
return out;
}
/// Get a threaded data reader for a file inode.
pub fn threadedDataReader(self: Inode, alloc: std.mem.Allocator, archive: *Archive) !ThreadedDataReader {
return switch (self.hdr.inode_type) {
.file => threadedReaderFromData(alloc, archive, self.data.file),
.ext_file => threadedReaderFromData(alloc, archive, self.data.ext_file),
else => error.NotRegularFile,
};
}
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_table.get(data.frag_idx), data.frag_block_offset);
return out; return out;
} }
@@ -154,386 +144,46 @@ fn entriesFromData(alloc: std.mem.Allocator, archive: Archive, data: anytype) ![
return DirEntry.readDir(alloc, &meta.interface, data.size); return DirEntry.readDir(alloc, &meta.interface, data.size);
} }
/// Extract the inode to the given path. Single threaded. /// Returns the xattr index for the given inode. If the inode isn't an extended variant or doesn't have any, the u32 max is returned (0xFFFFFFFF).
pub fn extractTo(self: Inode, alloc: std.mem.Allocator, archive: *Archive, path: []const u8, options: ExtractionOptions) !void { pub fn xattrIdx(self: Inode) u32 {
if (options.threads > 1) return self.extractToThreaded(alloc, archive, path, options); return switch (self.data) {
switch (self.hdr.inode_type) { .ext_dir => |d| d.xattr_id,
.dir, .ext_dir => { .ext_file => |f| f.xattr_idx,
// Removing any trailing separators since that's the easiest path forward. .ext_symlink => |s| s.xattr_idx,
if (path[path.len - 1] == '/') return self.extractTo(alloc, archive, path[0 .. path.len - 1], options); .ext_block_dev, .ext_char_dev => |d| d.xattr_idx,
std.fs.cwd().makeDir(path) catch |err| { .ext_fifo, .ext_socket => |i| i.xattr_idx,
if (err != std.fs.Dir.MakeError.PathAlreadyExists) return err; else => 0xFFFFFFFF,
};
const entries = try self.dirEntries(alloc, archive.*);
defer {
for (entries) |entry| entry.deinit(alloc);
alloc.free(entries);
}
for (entries) |entry| {
var new_path = try alloc.alloc(u8, path.len + 1 + entry.name.len);
@memcpy(new_path[0..path.len], path);
@memcpy(new_path[path.len + 1 ..], entry.name);
new_path[path.len] = '/';
defer alloc.free(new_path);
var inode: Inode = try readFromEntry(alloc, archive, entry);
defer inode.deinit(alloc);
try inode.extractTo(alloc, archive, new_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 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.
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(allocator, archive, path[0 .. path.len - 1], options);
// Fixed Allocator
// const mem_buf = archive.allocator().alloc(u8, 2 * 1024 * 1024 * 1024);
// defer archive.allocator().free(mem_buf);
// var fixed_alloc: std.heap.FixedBufferAllocator = .init(mem_buf);
// const alloc = fixed_alloc.threadSafeAllocator();
// Arena 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 = .{};
// 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 = 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, null);
pool.waitAndWork(&wg);
if (out_err != null) return out_err.?;
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 => {
var pool: Pool = undefined;
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, .{});
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));
}
},
.symlink, .ext_symlink => try self.extractSymlink(path),
else => try self.extractDevice(archive, path, options),
}
}
fn extractThreadEntry(
entry: DirEntry,
alloc: std.mem.Allocator,
archive: *Archive,
path: []const u8,
options: ExtractionOptions,
wg: *WaitGroup,
pool: *Pool,
out_err: *?anyerror,
parent: ?*Parent,
) void {
var new_path = alloc.alloc(u8, path.len + entry.name.len + 1) catch |err| {
wg.finish();
out_err.* = err;
return;
}; };
@memcpy(new_path[0..path.len], path);
@memcpy(new_path[path.len + 1 ..], entry.name);
new_path[path.len] = '/';
var inode = readFromEntry(alloc, archive, entry) catch |err| {
out_err.* = err;
wg.finish();
return;
};
inode.extractThread(alloc, archive, new_path, options, wg, pool, out_err, parent);
} }
/// Extract threadedly the inode to the path. /// Applies the Inode's metadata to the given File.
fn extractThread( /// Mod time is always set, but permissions and xattrs are set based on the given ExtractionOptions.
self: Inode, pub fn setMetadata(self: Inode, alloc: std.mem.Allocator, tables: *Tables, fil: std.fs.File, options: ExtractionOptions) !void {
alloc: std.mem.Allocator,
archive: *Archive,
path: []const u8,
options: ExtractionOptions,
wg: *WaitGroup,
pool: *Pool,
out_err: *?anyerror,
parent: ?*Parent,
) void {
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;
};
wg.startMany(entries.len);
// 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, p);
continue;
}
pool.spawn(
extractThreadEntry,
.{
entry,
alloc,
archive,
path,
options,
wg,
pool,
out_err,
p,
},
) catch |err| {
wg.finish();
if (options.verbose)
options.verbose_writer.?.print("Error starting extraction thread: {}\n", .{err}) catch {};
out_err.* = err;
continue;
};
}
},
.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;
};
},
}
}
/// Creates and writes the inode file contents to the given path.
/// Optionally set owner & permissions.
///
/// 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 {
var fil = try std.fs.cwd().createFile(path, .{ .exclusive = true });
defer fil.close();
var wrt = fil.writer(&[0]u8{});
var dat_rdr = try self.dataReader(alloc, archive);
defer dat_rdr.deinit();
_ = try dat_rdr.interface.streamRemaining(&wrt.interface);
try wrt.interface.flush();
const time = @as(i128, self.hdr.mod_time) * 1000000000; const time = @as(i128, self.hdr.mod_time) * 1000000000;
try fil.updateTimes(time, 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_table.get(self.hdr.uid_idx), try archive.id_table.get(self.hdr.gid_idx)); try fil.chown(try tables.id_table.get(self.hdr.uid_idx), try tables.id_table.get(self.hdr.gid_idx));
}
}
/// Extract the inode file contents to the given path threadedly.
/// pool is used to spawn threads.
///
/// 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 {
var fil = try std.fs.cwd().createFile(path, .{});
defer fil.close();
var data = try self.threadedDataReader(alloc, archive);
try data.extractThreaded(fil, pool);
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));
}
}
/// Creates the symlink described by the inode.
///
/// Assumes the inode is a symlink or ext_symlink type.
fn extractSymlink(self: Inode, path: []const u8) !void {
const target = switch (self.data) {
.symlink => |s| s.target,
.ext_symlink => |s| s.target,
else => unreachable,
};
try std.fs.cwd().symLink(target, path, .{});
}
/// Creates the device described by the inode.
///
/// Optionally set owner & permissions.
/// Assumes the inode is a char_dev, block_dev, fifo, socket, or their extended counterparts.
fn extractDevice(self: Inode, archive: *Archive, path: []const u8, options: ExtractionOptions) !void {
var mode: u32 = undefined;
var dev: u32 = 0;
switch (self.data) {
.char_dev => |d| {
mode = std.posix.S.IFCHR;
dev = d.dev;
},
.ext_char_dev => |d| {
mode = std.posix.S.IFCHR;
dev = d.dev;
},
.block_dev => |d| {
mode = std.posix.S.IFBLK;
dev = d.dev;
},
.ext_block_dev => |d| {
mode = std.posix.S.IFBLK;
dev = d.dev;
},
.fifo, .ext_fifo => mode = std.posix.S.IFIFO,
.socket, .ext_socket => mode = std.posix.S.IFSOCK,
else => unreachable,
}
const res: std.os.linux.E = @enumFromInt(std.os.linux.mknod(@ptrCast(path), mode, dev));
switch (res) {
.SUCCESS => {},
.ACCES => return std.fs.Dir.MakeError.AccessDenied,
.DQUOT => return std.fs.Dir.MakeError.DiskQuota,
.EXIST => return std.fs.Dir.MakeError.PathAlreadyExists,
.FAULT, .NOENT => return std.fs.Dir.MakeError.BadPathName,
.LOOP => return std.fs.Dir.MakeError.SymLinkLoop,
.NAMETOOLONG => return std.fs.Dir.MakeError.NameTooLong,
.NOMEM => return std.fs.Dir.MakeError.SystemResources,
.NOSPC => return std.fs.Dir.MakeError.NoSpaceLeft,
.NOTDIR => return std.fs.Dir.MakeError.NotDir,
.PERM => return std.fs.Dir.MakeError.PermissionDenied,
.ROFS => return std.fs.Dir.MakeError.ReadOnlyFileSystem,
else => return blk: {
std.debug.print("unhandled mknod result: {}\n", .{res});
break :blk std.fs.Dir.MakeError.Unexpected;
},
}
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));
} }
if (!options.ignore_xattr) { if (!options.ignore_xattr) {
// TODO const idx = self.xattrIdx();
if (idx == 0xFFFFFFFF) return;
const xattrs = try tables.xattr_table.get(alloc, idx);
defer alloc.free(xattrs);
for (xattrs) |kv| {
const res = std.os.linux.fsetxattr(fil.handle, kv.key, kv.value.ptr, kv.value.len, 0);
alloc.free(kv.key);
alloc.free(kv.value);
if (res != 0) {
if (options.verbose)
options.verbose_writer.?.print("fsetxattr has result of: {}\n", .{res}) catch {};
return error.SetXattr;
}
}
} }
} }
/// Extract the inode to the given path.
pub fn extractTo(self: Inode, alloc: std.mem.Allocator, archive: Archive, path: []const u8, options: ExtractionOptions) !void {
return InodeExtract.extractTo(alloc, self, archive, path, options);
}
+34 -3
View File
@@ -1,14 +1,45 @@
const std = @import("std"); const std = @import("std");
const Mutex = std.Thread.Mutex; const Mutex = std.Thread.Mutex;
const Archive = @import("archive.zig");
const DecompFn = @import("decomp.zig").DecompFn; const DecompFn = @import("decomp.zig").DecompFn;
const BlockSize = @import("inode_data/file.zig").BlockSize;
const InodeRef = @import("inode.zig").Ref;
const Superblock = @import("super.zig").Superblock;
const MetadataReader = @import("util/metadata.zig"); const MetadataReader = @import("util/metadata.zig");
const OffsetFile = @import("util/offset_file.zig"); const OffsetFile = @import("util/offset_file.zig");
const XattrTable = @import("xattr.zig");
const TableError = error{ /// Information about a fragment section. Multiple fragments are contained in the block described by a single FragEntry.
InvalidIndex, /// The offset into the block and fragment size is stored in the file's inode.
pub const FragEntry = packed struct {
start: u64,
size: BlockSize,
_: u32,
}; };
const Tables = @This();
frag_table: Table(FragEntry),
id_table: Table(u16),
export_table: Table(InodeRef),
xattr_table: XattrTable,
pub fn init(alloc: std.mem.Allocator, archive: Archive) !Tables {
return .{
.frag_table = try .init(alloc, archive.fil, archive.decomp, archive.super.frag_start, archive.super.frag_count),
.id_table = try .init(alloc, archive.fil, archive.decomp, archive.super.id_start, archive.super.id_count),
.export_table = try .init(alloc, archive.fil, archive.decomp, archive.super.export_start, archive.super.inode_count),
.xattr_table = try .init(alloc, archive.fil, archive.decomp, archive.super.xattr_start),
};
}
pub fn deinit(self: *Tables) void {
self.frag_table.deinit();
self.id_table.deinit();
self.export_table.deinit();
self.xattr_table.deinit();
}
/// A two-layer metadata table. /// A two-layer metadata table.
pub fn Table(T: anytype) type { pub fn Table(T: anytype) type {
return struct { return struct {
@@ -47,7 +78,7 @@ pub fn Table(T: anytype) type {
} }
pub fn get(self: *Self, idx: u32) !T { pub fn get(self: *Self, idx: u32) !T {
if (idx >= self.values) return TableError.InvalidIndex; if (idx >= self.values) return error.InvalidIndex;
const block_num = idx / VALS_PER_BLOCK; const block_num = idx / VALS_PER_BLOCK;
const idx_offset = idx - (block_num * VALS_PER_BLOCK); const idx_offset = idx - (block_num * VALS_PER_BLOCK);
if (self.tab.contains(block_num)) { if (self.tab.contains(block_num)) {
+1 -1
View File
@@ -6,9 +6,9 @@ const Writer = std.Io.Writer;
const Limit = std.Io.Limit; const Limit = std.Io.Limit;
const Archive = @import("../archive.zig"); const Archive = @import("../archive.zig");
const FragEntry = Archive.FragEntry;
const DecompFn = @import("../decomp.zig").DecompFn; const DecompFn = @import("../decomp.zig").DecompFn;
const BlockSize = @import("../inode_data/file.zig").BlockSize; const BlockSize = @import("../inode_data/file.zig").BlockSize;
const FragEntry = @import("../tables.zig").FragEntry;
const OffsetFile = @import("offset_file.zig"); const OffsetFile = @import("offset_file.zig");
const DataReader = @This(); const DataReader = @This();
+73 -49
View File
@@ -8,9 +8,10 @@ const WaitGroup = std.Thread.WaitGroup;
const Pool = std.Thread.Pool; const Pool = std.Thread.Pool;
const Archive = @import("../archive.zig"); const Archive = @import("../archive.zig");
const FragEntry = Archive.FragEntry;
const DecompFn = @import("../decomp.zig").DecompFn; const DecompFn = @import("../decomp.zig").DecompFn;
const BlockSize = @import("../inode_data/file.zig").BlockSize; const BlockSize = @import("../inode_data/file.zig").BlockSize;
const FragEntry = @import("../tables.zig").FragEntry;
const InodeFinish = @import("inode_finish.zig");
const OffsetFile = @import("offset_file.zig"); const OffsetFile = @import("offset_file.zig");
const ThreadedDataReader = @This(); const ThreadedDataReader = @This();
@@ -25,6 +26,7 @@ blocks: []BlockSize,
frag: ?FragEntry = null, // TODO: do something better? frag: ?FragEntry = null, // TODO: do something better?
frag_offset: u32 = 0, frag_offset: u32 = 0,
size: u64, size: u64,
num_blocks: usize,
start_offset: u64, start_offset: u64,
@@ -34,8 +36,12 @@ pub fn init(alloc: std.mem.Allocator, archive: Archive, blocks: []BlockSize, sta
.fil = archive.fil, .fil = archive.fil,
.decomp = archive.decomp, .decomp = archive.decomp,
.block_size = archive.super.block_size, .block_size = archive.super.block_size,
.blocks = blocks, .blocks = blocks,
.size = size, .size = size,
.num_blocks = blocks.len,
.start_offset = start, .start_offset = start,
}; };
} }
@@ -43,141 +49,159 @@ pub fn init(alloc: std.mem.Allocator, archive: Archive, blocks: []BlockSize, sta
pub fn addFragment(self: *ThreadedDataReader, entry: FragEntry, frag_offset: u32) void { pub fn addFragment(self: *ThreadedDataReader, entry: FragEntry, frag_offset: u32) void {
self.frag = entry; self.frag = entry;
self.frag_offset = frag_offset; self.frag_offset = frag_offset;
} self.num_blocks = self.blocks.len + 1;
fn numBlocks(self: ThreadedDataReader) usize {
var res = self.blocks.len;
if (self.frag != null) res += 1;
return res;
} }
/// Extract the data to the file threadedly, using pool to spawn threads. /// Extract the data to the file threadedly, using pool to spawn threads.
/// If multiple errors occur, thread spawning errors will have, then the last decompression error that occurs; /// If errors occur, they are set to finish.out_err.
/// pub fn extractThreaded(self: ThreadedDataReader, file: std.fs.File, pool: *Pool, finish: *InodeFinish) void {
/// The function must be called from an unused DataReader. The DataReader is still usable afterwards.
/// If only extractThreaded is used, there is no need to call deinit() afterwards.
///
/// The file will always be written to starting at 0.
pub fn extractThreaded(self: ThreadedDataReader, file: std.fs.File, pool: *Pool) !void {
var wg: WaitGroup = .{};
wg.startMany(self.numBlocks());
var out_err: ?anyerror = null;
var cur_write_offset: u64 = 0; var cur_write_offset: u64 = 0;
var cur_read_offset: u64 = self.start_offset; var cur_read_offset: u64 = self.start_offset;
for (0..self.blocks.len) |i| { for (0..self.blocks.len) |i| {
const cur_block_size = if (i == self.numBlocks() - 1) self.size % self.block_size else self.block_size; 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, &wg, &out_err }); 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_write_offset += cur_block_size;
cur_read_offset += self.blocks[i].size; cur_read_offset += self.blocks[i].size;
} }
if (self.frag != null) { if (self.frag != null)
try pool.spawn(workThreadFragment, .{ self, file, cur_write_offset, &wg, &out_err }); pool.spawn(workThreadFragment, .{ self, file, cur_write_offset, finish }) catch |res_err| {
} finish.logError("Can't spawn pool task: {}", .{res_err});
pool.waitAndWork(&wg); finish.out_err.* = res_err;
if (out_err != null) return out_err.?; finish.finish();
};
} }
fn workThreadBlocks(self: ThreadedDataReader, fil: std.fs.File, write_offset: u64, read_offset: u64, block: BlockSize, cur_block_size: u64, wg: *WaitGroup, out_err: *?anyerror) void { fn workThreadBlocks(
defer wg.finish(); self: ThreadedDataReader,
fil: std.fs.File,
write_offset: u64,
read_offset: u64,
block: BlockSize,
cur_block_size: u64,
finish: *InodeFinish,
) void {
defer finish.finish();
var wrt = fil.writer(&[0]u8{}); var wrt = fil.writer(&[0]u8{});
wrt.seekTo(write_offset) catch |err| { wrt.seekTo(write_offset) catch |err| {
out_err.* = err; finish.logError("Error seeking file writer: {}", .{err});
finish.out_err.* = err;
return; return;
}; };
defer wrt.interface.flush() catch |err| { defer wrt.interface.flush() catch |err| {
out_err.* = err; finish.logError("Error flushing file writer: {}", .{err});
finish.out_err.* = err;
}; };
if (block.size == 0) { if (block.size == 0) {
wrt.interface.splatByteAll(0, cur_block_size) catch |err| { wrt.interface.splatByteAll(0, cur_block_size) catch |err| {
out_err.* = err; finish.logError("Error writing zeroes: {}", .{err});
finish.out_err.* = err;
return; return;
}; };
return; return;
} }
var rdr = self.fil.readerAt(read_offset, &[0]u8{}) catch |err| { var rdr = self.fil.readerAt(read_offset, &[0]u8{}) catch |err| {
out_err.* = err; finish.logError("Error creating file reader: {}", .{err});
finish.out_err.* = err;
return; return;
}; };
if (block.uncompressed) { if (block.uncompressed) {
rdr.interface.streamExact(&wrt.interface, block.size) catch |err| { rdr.interface.streamExact(&wrt.interface, block.size) catch |err| {
out_err.* = err; finish.logError("Error streaming data: {}", .{err});
finish.out_err.* = err;
return; return;
}; };
return; return;
} }
// TODO: shared buffers // TODO: shared buffers
const read_buf = self.alloc.alloc(u8, block.size) catch |err| { const read_buf = self.alloc.alloc(u8, block.size) catch |err| {
out_err.* = err; finish.logError("Error creating reader buffer: {}", .{err});
finish.out_err.* = err;
return; return;
}; };
defer self.alloc.free(read_buf); defer self.alloc.free(read_buf);
rdr.interface.readSliceAll(read_buf) catch |err| { rdr.interface.readSliceAll(read_buf) catch |err| {
out_err.* = err; finish.logError("Error reading data into reader buffer: {}", .{err});
finish.out_err.* = err;
return; return;
}; };
// TODO: shared buffers // TODO: shared buffers
const res_buf = self.alloc.alloc(u8, cur_block_size) catch |err| { const res_buf = self.alloc.alloc(u8, cur_block_size) catch |err| {
out_err.* = err; finish.logError("Error creating result buffer: {}", .{err});
finish.out_err.* = err;
return; return;
}; };
defer self.alloc.free(res_buf); defer self.alloc.free(res_buf);
_ = self.decomp(self.alloc, read_buf, res_buf) catch |err| { _ = self.decomp(self.alloc, read_buf, res_buf) catch |err| {
out_err.* = err; finish.logError("Error decompressing data block: {}", .{err});
finish.out_err.* = err;
return; return;
}; };
wrt.interface.writeAll(res_buf) catch |err| { wrt.interface.writeAll(res_buf) catch |err| {
out_err.* = err; finish.logError("Error writing to file: {}", .{err});
finish.out_err.* = err;
return; return;
}; };
} }
fn workThreadFragment(self: ThreadedDataReader, fil: std.fs.File, write_offset: u64, wg: *WaitGroup, out_err: *?anyerror) void { fn workThreadFragment(self: ThreadedDataReader, fil: std.fs.File, write_offset: u64, finish: *InodeFinish) void {
defer wg.finish(); defer finish.finish();
var wrt = fil.writer(&[0]u8{}); var wrt = fil.writer(&[0]u8{});
wrt.seekTo(write_offset) catch |err| { wrt.seekTo(write_offset) catch |err| {
out_err.* = err; finish.logError("Error seeking file writer for file fragment: {}", .{err});
finish.out_err.* = err;
return; return;
}; };
defer wrt.interface.flush() catch |err| { defer wrt.interface.flush() catch |err| {
out_err.* = err; finish.out_err.* = err;
}; };
var rdr = self.fil.readerAt(self.frag.?.start, &[0]u8{}) catch |err| { var rdr = self.fil.readerAt(self.frag.?.start, &[0]u8{}) catch |err| {
out_err.* = err; finish.logError("Error creating file reader for file fragment: {}", .{err});
finish.out_err.* = err;
return; return;
}; };
if (self.frag.?.size.uncompressed) { if (self.frag.?.size.uncompressed) {
rdr.interface.discardAll(self.frag_offset) catch |err| { rdr.interface.discardAll(self.frag_offset) catch |err| {
out_err.* = err; finish.logError("Error discarding useless fragment data: {}", .{err});
finish.out_err.* = err;
return; return;
}; };
rdr.interface.streamExact(&wrt.interface, self.size % self.block_size) catch |err| { rdr.interface.streamExact(&wrt.interface, self.size % self.block_size) catch |err| {
out_err.* = err; finish.logError("Error streaming fragment data: {}", .{err});
finish.out_err.* = err;
return; return;
}; };
return; return;
} }
const tmp_buf = self.alloc.alloc(u8, self.frag.?.size.size) catch |err| { const tmp_buf = self.alloc.alloc(u8, self.frag.?.size.size) catch |err| {
out_err.* = err; finish.logError("Error creating a temporary buffer for a file fragment: {}", .{err});
finish.out_err.* = err;
return; return;
}; };
defer self.alloc.free(tmp_buf); defer self.alloc.free(tmp_buf);
rdr.interface.readSliceAll(tmp_buf) catch |err| { rdr.interface.readSliceAll(tmp_buf) catch |err| {
out_err.* = err; finish.logError("Error reading data into fragment buffer: {}", .{err});
finish.out_err.* = err;
return; return;
}; };
const needed_block = self.alloc.alloc(u8, self.block_size) catch |err| { const needed_block = self.alloc.alloc(u8, self.block_size) catch |err| {
out_err.* = err; finish.logError("Error allocating fragment decompression results: {}", .{err});
finish.out_err.* = err;
return; return;
}; };
defer self.alloc.free(needed_block); defer self.alloc.free(needed_block);
_ = self.decomp(self.alloc, tmp_buf, needed_block) catch |err| { _ = self.decomp(self.alloc, tmp_buf, needed_block) catch |err| {
out_err.* = err; finish.logError("Error decompressing fragment: {}", .{err});
finish.out_err.* = err;
return; return;
}; };
wrt.interface.writeAll(needed_block[self.frag_offset .. self.frag_offset + (self.size % self.block_size)]) catch |err| { wrt.interface.writeAll(needed_block[self.frag_offset .. self.frag_offset + (self.size % self.block_size)]) catch |err| {
out_err.* = err; finish.logError("Error writing fragment: {}", .{err});
finish.out_err.* = err;
return; return;
}; };
} }
+353
View File
@@ -0,0 +1,353 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const Pool = std.Thread.Pool;
const WaitGroup = std.Thread.WaitGroup;
const Archive = @import("../archive.zig");
const DirEntry = @import("../dir_entry.zig");
const Inode = @import("../inode.zig");
const ExtractionOptions = @import("../options.zig");
const Tables = @import("../tables.zig");
const InodeFinish = @import("inode_finish.zig");
const FinishUnion = InodeFinish.FinishUnion;
const ThreadedDataReader = @import("data_threaded.zig");
// 1 MB
const STACK_ALLOC_SIZE = 1024 * 1024;
pub fn extractTo(
allocator: Allocator,
inode: Inode,
archive: Archive,
path: []const u8,
options: ExtractionOptions,
) !void {
if (path[path.len - 1] == '/')
return extractTo(allocator, inode, archive, path[0 .. path.len - 2], options);
var stack_alloc = std.heap.stackFallback(STACK_ALLOC_SIZE, allocator);
var arena: std.heap.ArenaAllocator = .init(stack_alloc.get());
defer arena.deinit();
if (options.threads <= 1) {
const alloc = arena.allocator();
var tables: Tables = try .init(alloc, archive);
return extractSingleThread(arena.allocator(), inode, archive, &tables, path, options);
}
var thread_alloc = std.heap.ThreadSafeAllocator{ .child_allocator = arena.allocator() };
const alloc = thread_alloc.allocator();
var tables: Tables = try .init(alloc, archive);
var pool_alloc = std.heap.stackFallback(10 * 1024, alloc);
var pool: Pool = undefined;
try pool.init(.{ .allocator = pool_alloc.get(), .n_jobs = options.threads - 1 });
var wg: WaitGroup = .{};
var err: ?anyerror = null;
wg.start();
try pool.spawn(extractMultiThread, .{
alloc,
inode,
archive,
&tables,
path,
options,
&pool,
FinishUnion{ .wg = &wg },
&err,
});
pool.waitAndWork(&wg);
if (err != null) return err.?;
}
fn extractSingleThread(
alloc: Allocator,
inode: Inode,
archive: Archive,
tables: *Tables,
path: []const u8,
options: ExtractionOptions,
) !void {
switch (inode.hdr.inode_type) {
.dir, .ext_dir => {
_ = std.fs.cwd().makeDir(path) catch |err| switch (err) {
std.fs.Dir.MakeError.PathAlreadyExists => {},
else => return err,
};
// Currently we are ignoring any deinit or free calls since we know we are under an ArenaAllocator.
// Possibly in the future, do some simple math to see if it would be safe to ONLY deinit via Arena,
// otherwise be more conscientious about freeing memory.
// For now, this is good enough.
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 u8{ path, "/", ent.name });
try extractSingleThread(alloc, sub_inode, archive, tables, new_path, options);
}
const fil = try std.fs.cwd().openFile(path, .{});
defer fil.close();
try inode.setMetadata(alloc, tables, fil, options);
},
.file, .ext_file => {
var fil = try std.fs.cwd().createFile(path, .{ .exclusive = true });
defer fil.close();
var wrt = fil.writer(&[0]u8{});
var dat_rdr = try inode.dataReader(alloc, archive, tables);
defer dat_rdr.deinit();
_ = try dat_rdr.interface.streamRemaining(&wrt.interface);
try wrt.interface.flush();
try inode.setMetadata(alloc, tables, fil, options);
},
.symlink, .ext_symlink => return extractSymlink(inode, path),
else => return extractDeviceAndIPC(inode, alloc, tables, path, options),
}
}
fn extractMultiThread(
alloc: Allocator,
inode: Inode,
archive: Archive,
tables: *Tables,
path: []const u8,
options: ExtractionOptions,
pool: *Pool,
fin: FinishUnion,
err: *?anyerror,
) void {
if (err.* != null) {
fin.finish();
return;
}
switch (inode.hdr.inode_type) {
.dir, .ext_dir => {
_ = std.fs.cwd().makeDir(path) catch |res_err| switch (res_err) {
std.fs.Dir.MakeError.PathAlreadyExists => {},
else => {
err.* = res_err;
fin.finish();
return;
},
};
// Currently we are ignoring any deinit or free calls since we know we are under an ArenaAllocator.
// Possibly in the future, do some simple math to see if it would be safe to ONLY deinit via Arena,
// otherwise be more conscientious about freeing memory.
// For now, this is good enough.
const entries = inode.dirEntries(alloc, archive) catch |res_err| {
err.* = res_err;
fin.finish();
return;
};
if (entries.len == 0) {
fin.finish();
return;
}
var dir_fin = InodeFinish.create(
alloc,
inode,
path,
tables,
options,
fin,
err,
null,
entries.len,
) catch |res_err| {
err.* = res_err;
fin.finish();
return;
};
for (entries) |ent| {
if (ent.inode_type == .dir) {
extractEntry(
alloc,
ent,
archive,
tables,
path,
options,
pool,
.{ .fin = dir_fin },
err,
);
continue;
}
pool.spawn(
extractEntry,
.{ alloc, ent, archive, tables, path, options, pool, FinishUnion{ .fin = dir_fin }, err },
) catch |res_err| {
err.* = res_err;
dir_fin.finish();
return;
};
}
},
.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, tables) 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;
};
if (data_rdr == null) {
inode.setMetadata(alloc, tables, fil, options) catch |res_err| {
if (options.verbose)
options.verbose_writer.?.print("Can't set metadata to {s}: {}\n", .{ path, res_err }) catch {};
err.* = res_err;
};
fin.finish();
return;
}
const file_fin = InodeFinish.create(
alloc,
inode,
path,
tables,
options,
fin,
err,
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;
};
data_rdr.?.extractThreaded(fil, pool, file_fin);
},
.symlink, .ext_symlink => {
extractSymlink(inode, path) catch |res_err| {
err.* = res_err;
};
fin.finish();
},
else => {
extractDeviceAndIPC(inode, alloc, tables, path, options) catch |res_err| {
err.* = res_err;
};
fin.finish();
},
}
}
fn extractEntry(
alloc: Allocator,
ent: DirEntry,
archive: Archive,
tables: *Tables,
path: []const u8,
options: ExtractionOptions,
pool: *Pool,
fin: FinishUnion,
err: *?anyerror,
) void {
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| {
err.* = res_err;
fin.finish();
return;
};
extractMultiThread(alloc, inode, archive, tables, new_path, options, pool, fin, err);
}
/// Get a threaded data reader for a file inode.
fn threadedDataReader(self: Inode, alloc: std.mem.Allocator, archive: Archive, tables: *Tables) !?ThreadedDataReader {
return switch (self.hdr.inode_type) {
.file => threadedReaderFromData(alloc, archive, tables, self.data.file),
.ext_file => threadedReaderFromData(alloc, archive, tables, self.data.ext_file),
else => error.NotRegularFile,
};
}
fn threadedReaderFromData(alloc: std.mem.Allocator, archive: Archive, tables: *Tables, data: anytype) !?ThreadedDataReader {
if (data.block_sizes.len == 0 and data.frag_idx == 0xFFFFFFFF) return null;
var out: ThreadedDataReader = .init(alloc, archive, data.block_sizes, data.block_start, data.size);
if (data.frag_idx != 0xFFFFFFFF)
out.addFragment(try tables.frag_table.get(data.frag_idx), data.frag_block_offset);
return out;
}
/// Creates the symlink described by the inode.
/// Sets metadata.
fn extractSymlink(self: Inode, path: []const u8) !void {
const target = switch (self.data) {
.symlink => |s| s.target,
.ext_symlink => |s| s.target,
else => unreachable,
};
try std.fs.cwd().symLink(target, path, .{});
}
/// Creates the device described by the inode.
/// Sets metadata.
fn extractDeviceAndIPC(self: Inode, alloc: std.mem.Allocator, tables: *Tables, path: []const u8, options: ExtractionOptions) !void {
var mode: u32 = undefined;
var dev: u32 = 0;
switch (self.data) {
.char_dev => |d| {
mode = std.posix.S.IFCHR;
dev = d.dev;
},
.ext_char_dev => |d| {
mode = std.posix.S.IFCHR;
dev = d.dev;
},
.block_dev => |d| {
mode = std.posix.S.IFBLK;
dev = d.dev;
},
.ext_block_dev => |d| {
mode = std.posix.S.IFBLK;
dev = d.dev;
},
.fifo, .ext_fifo => mode = std.posix.S.IFIFO,
.socket, .ext_socket => mode = std.posix.S.IFSOCK,
else => unreachable,
}
const res: std.os.linux.E = @enumFromInt(std.os.linux.mknod(@ptrCast(path), mode, dev));
switch (res) {
.SUCCESS => {},
.ACCES => return std.fs.Dir.MakeError.AccessDenied,
.DQUOT => return std.fs.Dir.MakeError.DiskQuota,
.EXIST => return std.fs.Dir.MakeError.PathAlreadyExists,
.FAULT, .NOENT => return std.fs.Dir.MakeError.BadPathName,
.LOOP => return std.fs.Dir.MakeError.SymLinkLoop,
.NAMETOOLONG => return std.fs.Dir.MakeError.NameTooLong,
.NOMEM => return std.fs.Dir.MakeError.SystemResources,
.NOSPC => return std.fs.Dir.MakeError.NoSpaceLeft,
.NOTDIR => return std.fs.Dir.MakeError.NotDir,
.PERM => return std.fs.Dir.MakeError.PermissionDenied,
.ROFS => return std.fs.Dir.MakeError.ReadOnlyFileSystem,
else => return blk: {
std.debug.print("unhandled mknod result: {}\n", .{res});
break :blk std.fs.Dir.MakeError.Unexpected;
},
}
var fil = try std.fs.cwd().openFile(path, .{});
defer fil.close();
try self.setMetadata(alloc, tables, fil, options);
}
+101
View File
@@ -0,0 +1,101 @@
const std = @import("std");
const WaitGroup = std.Thread.WaitGroup;
const Mutex = std.Thread.Mutex;
const Archive = @import("../archive.zig");
const Inode = @import("../inode.zig");
const ExtractionOptions = @import("../options.zig");
const Tables = @import("../tables.zig");
const InodeFinish = @This();
const FinishEnum = enum {
wg,
fin,
};
pub const FinishUnion = union(FinishEnum) {
wg: *WaitGroup,
fin: *InodeFinish,
pub fn finish(self: FinishUnion) void {
switch (self) {
.wg => |wg| wg.finish(),
.fin => |fin| fin.finish(),
}
}
};
alloc: std.mem.Allocator,
inode: Inode,
path: []const u8,
tables: *Tables,
options: ExtractionOptions,
parent_finish: FinishUnion,
fil: ?std.fs.File,
out_err: *?anyerror,
wg: WaitGroup = .{},
mut: Mutex = .{},
pub fn create(
alloc: std.mem.Allocator,
inode: Inode,
path: []const u8,
tables: *Tables,
options: ExtractionOptions,
parent_finish: FinishUnion,
out_err: *?anyerror,
fil: ?std.fs.File,
work_size: usize,
) !*InodeFinish {
if (work_size == 0)
return error.InvalidWorkSize;
const out = try alloc.create(InodeFinish);
errdefer alloc.destroy(out);
out.* = .{
.alloc = alloc,
.inode = inode,
.path = path,
.tables = tables,
.options = options,
.parent_finish = parent_finish,
.out_err = out_err,
.fil = fil,
};
out.wg.startMany(work_size);
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();
{
defer self.mut.unlock();
self.wg.finish();
if (!self.wg.isDone()) return;
}
defer {
self.parent_finish.finish();
self.alloc.destroy(self);
}
if (self.fil == null)
self.fil = std.fs.cwd().openFile(self.path, .{}) catch |err| {
if (self.options.verbose)
self.options.verbose_writer.?.print("Error opening {s} to set metadata: {}\n", .{ self.path, err }) catch {};
self.out_err.* = err;
return;
};
defer self.fil.?.close();
self.inode.setMetadata(self.alloc, self.tables, self.fil.?, self.options) catch |err| {
if (self.options.verbose)
self.options.verbose_writer.?.print("Error setting metadata to {s}: {}\n", .{ self.path, err }) catch {};
self.out_err.* = err;
return;
};
}
+41
View File
@@ -0,0 +1,41 @@
const std = @import("std");
const OffsetFile = @import("offset_file.zig");
const MetadataCache = @This();
alloc: std.mem.Allocator,
buf: []u8,
fixed_alloc: std.heap.FixedBufferAllocator,
cache: std.AutoArrayHashMap(u64, [8192]u8),
mut: std.Thread.Mutex = .{},
cache_mut: std.AutoArrayHashMap(u64, std.Thread.Mutex),
fil: OffsetFile,
pub fn init(alloc: std.mem.Allocator, cache_size: u64) !MetadataCache {}
pub fn deinit(self: *MetadataCache) void {
self.mut.lock();
defer self.mut.unlock();
self.cache.deinit();
self.cache_mut.deinit();
self.alloc.free(self.buf);
}
pub fn getChunk(self: *MetadataCache, offset: u64) ![8192]u8 {
var res = self.cache.get(offset);
if (res != null) return res.?;
var offset_mut = blk: {
self.mut.lock();
defer self.mut.unlock();
const mut = try self.cache_mut.getOrPut(offset);
if (!mut.found_existing)
mut.value_ptr.* = .{};
break :blk mut.value_ptr;
};
offset_mut.lock();
defer offset_mut.unlock();
}
+117
View File
@@ -0,0 +1,117 @@
const std = @import("std");
const DecompFn = @import("decomp.zig").DecompFn;
const Table = @import("tables.zig").Table;
const MetadataReader = @import("util/metadata.zig");
const OffsetFile = @import("util/offset_file.zig");
const Ref = packed struct {
block_offset: u16,
block_start: u32,
_: u16,
};
const Entry = packed struct {
ref: Ref,
count: u32,
size: u32,
};
const KeyPrefix = enum(u8) {
user,
trusted,
security,
};
const KeyRaw = packed struct {
type: packed struct {
prefix: KeyPrefix,
out_of_line: bool,
_: u7,
},
name_size: u16,
};
pub const KeyValue = struct {
key: [:0]u8,
value: []u8,
};
const XattrTable = @This();
alloc: std.mem.Allocator,
fil: OffsetFile,
decomp: DecompFn,
count: u32,
start: u64,
table: Table(Entry),
pub fn init(alloc: std.mem.Allocator, fil: OffsetFile, decomp: DecompFn, table_start: u64) !XattrTable {
var info = packed struct {
start: u64 = undefined,
count: u32 = undefined,
_: u32 = undefined,
}{};
var rdr = try fil.readerAt(table_start, &[0]u8{});
try rdr.interface.readSliceEndian(@TypeOf(info), @ptrCast(&info), .little);
return .{
.alloc = alloc,
.fil = fil,
.decomp = decomp,
.count = info.count,
.start = info.start,
.table = try .init(alloc, fil, decomp, table_start + 16, info.count),
};
}
pub fn deinit(self: *XattrTable) void {
self.table.deinit();
}
pub fn get(self: *XattrTable, alloc: std.mem.Allocator, idx: u32) ![]KeyValue {
const entry: Entry = try self.table.get(idx);
const out = try alloc.alloc(KeyValue, entry.count);
for (out) |*kv| {
var rdr = try self.fil.readerAt(self.start + entry.ref.block_start, &[0]u8{});
var meta: MetadataReader = .init(alloc, &rdr.interface, self.decomp);
try meta.interface.discardAll(entry.ref.block_offset);
var key_raw: KeyRaw = undefined;
try meta.interface.readSliceEndian(KeyRaw, @ptrCast(&key_raw), .little);
switch (key_raw.type.prefix) {
.user => {
kv.key = @ptrCast(try alloc.alloc(u8, key_raw.name_size + 5 + 1));
@memcpy(kv.key[0..5], "user.");
try meta.interface.readSliceAll(kv.key[5 .. kv.key.len - 1]);
kv.key[kv.key.len - 1] = 0;
},
.security => {
kv.key = @ptrCast(try alloc.alloc(u8, key_raw.name_size + 9 + 1));
@memcpy(kv.key[0..9], "security.");
try meta.interface.readSliceAll(kv.key[9 .. kv.key.len - 1]);
kv.key[kv.key.len - 1] = 0;
},
.trusted => {
kv.key = @ptrCast(try alloc.alloc(u8, key_raw.name_size + 8 + 1));
@memcpy(kv.key[0..8], "trusted.");
try meta.interface.readSliceAll(kv.key[8 .. kv.key.len - 1]);
kv.key[kv.key.len - 1] = 0;
},
}
if (key_raw.type.out_of_line) {
try meta.interface.discardAll(4);
var ref: Ref = undefined;
try meta.interface.readSliceEndian(Ref, @ptrCast(&ref), .little);
rdr = try self.fil.readerAt(self.start + ref.block_start, &[0]u8{});
meta = .init(alloc, &rdr.interface, self.decomp);
try meta.interface.discardAll(ref.block_offset);
}
var value_size: u32 = undefined;
try meta.interface.readSliceEndian(u32, @ptrCast(&value_size), .little);
kv.value = try alloc.alloc(u8, value_size);
try meta.interface.readSliceAll(kv.value);
}
return out;
}