34 Commits

Author SHA1 Message Date
Caleb Gardner aa6b61fc9f Fixed a couple minor issues
Added benchmark.sh
2026-06-20 01:22:46 -05:00
Caleb Gardner 69a4599f78 Fixed (hopefully) the last lingering issues. 2026-06-17 23:46:11 -05:00
Caleb J. Gardner f47ef9ee46 Fixed a couple stupid mistakes 2026-06-17 22:30:53 -05:00
Caleb Gardner d4f069b57a Added option to extract individual files with unsquashfs. 2026-06-17 06:46:43 -05:00
Caleb Gardner 80c4c8df5e Bug fixes 2026-06-17 05:23:39 -05:00
Caleb J. Gardner 42a92853a4 Re-added tests
Fixed compile errors from tests
2026-06-16 22:01:44 -05:00
Caleb Gardner 9ecab7f2a4 Finished (?) mutli-threaded extraction 2026-06-14 02:43:21 -05:00
Caleb Gardner 8a9ff7bf5f Finished work (theoretically) on single threaded extraction
Started work on Multi-threaded extraction
Added decompression cache.
Added XattrTable
2026-06-13 06:06:27 -05:00
Caleb Gardner f3318e55a3 More foundational work on extraction 2026-06-11 06:03:45 -05:00
Caleb J. Gardner 0bac064252 Started working on lookup tables 2026-06-10 13:38:18 -05:00
Caleb Gardner 8087a59c5c Finishing up a few things.
Architecting out a few things (including extraction)
2026-06-10 05:46:12 -05:00
Caleb J. Gardner 5c04933520 Resetting. Again.
Getting the basics re-added
2026-06-09 22:23:54 -05:00
Caleb J. Gardner 4b2b7021c7 Moved & organized decompression
Fully implemented Decompressor vtable
2026-04-02 06:27:34 -05:00
Caleb J. Gardner a1b9828578 Finished (?) decompression restructuring 2026-03-26 06:40:17 -05:00
Caleb J. Gardner 8e4661c4c6 Moving decompression to a vtable interface 2026-03-22 06:35:25 -05:00
Caleb J. Gardner 54aaf30ea5 Working on re-doing decompression 2026-03-21 02:13:36 -05:00
Caleb J. Gardner df22cf6529 Started work on stateful decompression 2026-03-20 01:55:00 -05:00
Caleb J. Gardner 6b5c830234 Actually fix build 2026-03-18 06:17:07 -05:00
Caleb J. Gardner 116234cf9c Fixed build 2026-03-18 06:15:52 -05:00
Caleb J. Gardner 4601e8f323 Updated build 2026-03-18 06:13:19 -05:00
Caleb J. Gardner 50cae8b63d Use zig packed versions of zlib-ng, lz4, and zstd.
Changed use_c_libs to use_zig_decomp so c libraries are now default
2026-03-18 05:24:58 -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
40 changed files with 2529 additions and 2048 deletions
+6 -6
View File
@@ -13,13 +13,13 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
- uses: mlugg/setup-zig@v2 - uses: mlugg/setup-zig@v2
- name: Install deps - name: Install deps
run: sudo apt update && sudo apt install -y zlib1g-dev libzstd-dev liblzma-dev liblz4-dev liblzo2-dev run: sudo apt update && sudo apt install -y liblzma-dev liblzo2-dev
- name: Build normal version - name: Build normal version
run: zig build --release=fast -Dversion=${{ github.ref_name }} run: zig build --release=fast -Duse_zig_decomp=true -Dversion=${{ github.ref_name }}
- name: Move normal build out - name: Move zig 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 -Dversion="${{ github.ref_name }}"
- name: Move C build out - name: Move C build out
run: mv zig-out/bin/unsquashfs ./unsquashfs-x86_64-c-libs run: mv zig-out/bin/unsquashfs ./unsquashfs-x86_64-c-libs
- name: Release - name: Release
@@ -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
+1
View File
@@ -2,3 +2,4 @@ testing/
.zig-cache/ .zig-cache/
zig-out/ zig-out/
zig-pkg/
+46
View File
@@ -0,0 +1,46 @@
// 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", "-Ddebug=true"],
},
"program": "zig-out/bin/unsquashfs",
"args": [
"--force",
"-d",
"testing/TestExtractUnsquashfs",
"testing/LinuxPATest.sfs",
],
},
{
"label": "Build & Run Single-Threaded",
"adapter": "CodeLLDB",
"request": "launch",
"build": {
"command": "zig",
"args": ["build", "-Ddebug=true"],
},
"program": "zig-out/bin/unsquashfs",
"args": [
"--force",
"-p",
"1",
"-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
Executable
+22
View File
@@ -0,0 +1,22 @@
#! /usr/bin/env bash
ARCHIVE="testing/LinuxPATest.sfs"
REF_EXT_LOC="testing/LinuxPAReference"
PROG_EXT_LOC="testing/LinuxPABinTest"
echo "Building with ReleaseFast"
echo ""
zig build -Doptimize=ReleaseFast
echo "Testing Multi-threaded Performance"
echo ""
hyperfine --warmup 5 --prepare "rm -rf $REF_EXT_LOC && rm -rf $PROG_EXT_LOC" "unsquashfs -d $REF_EXT_LOC $ARCHIVE" "zig-out/bin/unsquashfs -d $PROG_EXT_LOC $ARCHIVE"
echo ""
echo "Testing Single-threaded Performance"
echo ""
hyperfine --warmup 5 --prepare "rm -rf $REF_EXT_LOC && rm -rf $PROG_EXT_LOC" "unsquashfs -p 1 -d $REF_EXT_LOC $ARCHIVE" "zig-out/bin/unsquashfs -p 1 -d $PROG_EXT_LOC $ARCHIVE"
+91 -55
View File
@@ -1,89 +1,125 @@
const std = @import("std"); const std = @import("std");
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 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();
zig_squashfs_options.addOption(bool, "use_c_libs", use_c_libs_option orelse false);
zig_squashfs_options.addOption(bool, "allow_lzo", allow_lzo orelse false);
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
const mod = b.addModule("zig_squashfs", .{ const target = b.standardTargetOptions(.{});
.root_source_file = b.path("src/root.zig"),
.target = target, const use_zig_decomp = b.option(bool, "use_zig_decomp", "Use zig standard library for decompression.") orelse false;
const allow_lzo = b.option(bool, "allow_lzo", "Compile with lzo support") orelse false;
const dynamic = b.option(bool, "dynamic", "Use dynamic instead of static linking.") orelse false;
const debug = b.option(bool, "debug", "Enable options to make debugging easier.") orelse false;
const version_string_option = b.option([]const u8, "version", "Version of the library/binary") orelse "0.0.0-testing";
const version: std.SemanticVersion = try .parse(version_string_option);
const options = b.addOptions();
options.addOption(bool, "use_zig_decomp", use_zig_decomp);
options.addOption(bool, "allow_lzo", allow_lzo);
options.addOption(std.SemanticVersion, "version", version);
const c = b.addTranslateC(.{
.optimize = optimize, .optimize = optimize,
.link_libc = use_c_libs_option, .target = target,
.valgrind = valgrind, .root_source_file = b.path("c.h"),
}); });
mod.addOptions("config", zig_squashfs_options); if (allow_lzo) c.defineCMacro("ALLOW_LZO", null);
if (use_c_libs_option == true) {
mod.linkSystemLibrary("zlib", .{}); if (!use_zig_decomp and dynamic) {
mod.linkSystemLibrary("lzma", .{}); c.linkSystemLibrary("z", .{});
if (allow_lzo == true) c.linkSystemLibrary("lzma", .{});
mod.linkSystemLibrary("minilzo", .{}); c.linkSystemLibrary("lz4", .{});
mod.linkSystemLibrary("lz4", .{}); c.linkSystemLibrary("zstd", .{});
mod.linkSystemLibrary("zstd", .{}); if (allow_lzo)
c.linkSystemLibrary("minilzo", .{});
} }
var version = version_string_option orelse "0.0.0-testing";
if (version[0] == 'v') version = version[1..];
const unsquashfs_options = b.addOptions();
unsquashfs_options.addOption(
std.SemanticVersion,
"version",
try std.SemanticVersion.parse(version),
);
var exe_mod = b.createModule(.{
.root_source_file = b.path("src/bin/unsquashfs.zig"),
.target = target,
.optimize = optimize,
.link_libc = use_c_libs_option,
.imports = &.{
.{ .name = "zig_squashfs", .module = mod },
},
.valgrind = valgrind,
});
exe_mod.addOptions("config", unsquashfs_options);
const exe = b.addExecutable(.{
.name = "unsquashfs",
.root_module = exe_mod,
});
const lib = b.addLibrary(.{ const lib = b.addLibrary(.{
.name = "squashfs", .name = "squashfs",
.root_module = mod, .use_llvm = debug,
.version = version,
.root_module = b.createModule(.{
.root_source_file = b.path("src/root.zig"),
.optimize = optimize,
.target = target,
.valgrind = debug,
.imports = &.{
.{ .name = "c", .module = c.createModule() },
.{ .name = "build_options", .module = options.createModule() },
},
}),
}); });
if (!use_zig_decomp and !dynamic) {
const zng = b.dependency("zlib_ng", .{ .optimize = optimize, .target = target, .zlib_compat = true });
lib.root_module.linkLibrary(zng.artifact("zng"));
const zstd = b.dependency("zstd", .{ .optimize = optimize, .target = target });
lib.root_module.linkLibrary(zstd.artifact("zstd"));
const lz4 = b.dependency("lz4", .{ .optimize = optimize, .target = target });
lib.root_module.linkLibrary(lz4.artifact("lz4"));
const xz = b.dependency("xz", .{ .optimize = optimize, .target = target });
lib.root_module.linkLibrary(xz.artifact("lzma"));
if (allow_lzo) {
const lzo = b.dependency("minilzo", .{ .optimize = optimize, .target = target });
lib.root_module.linkLibrary(lzo.artifact("minilzo"));
}
}
b.installArtifact(lib); b.installArtifact(lib);
const unsquashfs_options = b.addOptions();
unsquashfs_options.addOption(std.SemanticVersion, "version", version);
const exe = b.addExecutable(.{
.name = "unsquashfs",
.use_llvm = debug,
.version = version,
.root_module = b.createModule(.{
.root_source_file = b.path("src/bin/unsquashfs.zig"),
.optimize = optimize,
.target = target,
.valgrind = debug,
.imports = &.{
.{ .name = "squashfs", .module = lib.root_module },
.{ .name = "build", .module = unsquashfs_options.createModule() },
},
}),
});
b.installArtifact(exe); b.installArtifact(exe);
const mod_tests = b.addTest(.{ const lib_tests = b.addTest(.{
.root_module = mod, .root_module = lib.root_module,
.use_llvm = debug,
}); });
const run_mod_tests = b.addRunArtifact(mod_tests); const run_lib_tests = b.addRunArtifact(lib_tests);
const exe_tests = b.addTest(.{ const exe_tests = b.addTest(.{
.root_module = exe.root_module, .root_module = exe.root_module,
.use_llvm = debug,
}); });
const run_exe_tests = b.addRunArtifact(exe_tests); const run_exe_tests = b.addRunArtifact(exe_tests);
const test_step = b.step("test", "Run tests"); const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_mod_tests.step); test_step.dependOn(&run_lib_tests.step);
test_step.dependOn(&run_exe_tests.step); test_step.dependOn(&run_exe_tests.step);
// zls build check steps // zls build check steps
const lib_check = b.addLibrary(.{ const lib_check = b.addLibrary(.{
.name = "squashfs", .name = "squashfs",
.root_module = mod, .root_module = lib.root_module,
.use_llvm = debug,
}); });
const exe_check = b.addExecutable(.{ const exe_check = b.addExecutable(.{
.name = "unsquashfs", .name = "unsquashfs",
.root_module = exe_mod, .root_module = exe.root_module,
.use_llvm = debug,
}); });
const check = b.step("check", "Check if unsquashfs compiles"); const check = b.step("check", "Check if unsquashfs compiles");
check.dependOn(&lib_check.step); check.dependOn(&lib_check.step);
check.dependOn(&exe_check.step); check.dependOn(&exe_check.step);
test_step.dependOn(&lib_check.step);
test_step.dependOn(&exe_check.step);
} }
+22 -36
View File
@@ -1,43 +1,29 @@
.{ .{
.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.16.0",
// 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 = "git+https://github.com/CalebQ42/zig-zlib-ng#915db2dbd4a540d8a933336d67520b786f168f01",
// // When updating this field to a new URL, be sure to delete the corresponding .hash = "zlib_ng-2.3.3-pre1-2HYS4MlFAADc-eSZv4_xjkxZRCdGUVOYZKdtqhybstgk",
// // `hash`, otherwise you are communicating that you expect to find the old hash at },
// // the new URL. If the contents of a URL change this will result in a hash mismatch .zstd = .{
// // which will prevent zig from using it. .url = "git+https://github.com/allyourcodebase/zstd.git?ref=1.5.7-1#e1a501be57f42c541e8a5597e4b59a074dfd09a3",
// .url = "https://example.com/foo.tar.gz", .hash = "zstd-1.5.7-1-KEItkAMwAAD6OKY3m0OOmXG7aL-aLUfrDqbP5J5oYapU",
// },
// // This is computed from the file contents of the directory of files that is .lz4 = .{
// // obtained after fetching `url` and applying the inclusion rules given by .url = "git+https://github.com/allyourcodebase/lz4.git?ref=1.10.0-6#41f52ab227caf9d48cf88c89a4d2946caa12b102",
// // `paths`. .hash = "lz4-1.10.0-6-ewyzw-4NAAAWDpY4xpiqr4LQhZQAC0x_rGnW2iPh6jk2",
// // },
// // This field is the source of truth; packages do not come from a `url`; they .xz = .{
// // come from a `hash`. `url` is just one of many possible mirrors for how to .url = "git+https://github.com/akunaakwei/zig-xz.git#e2d389262c8291907e3e4c6fb119819141c16c0f",
// // obtain a package matching this `hash`. .hash = "xz-5.8.2-6v47_JYeAABSL-jonprpL5-E_YaaGc4B5xrbe93WsJ3G",
// // },
// // Uses the [multihash](https://multiformats.io/multihash/) format. .minilzo = .{
// .hash = "...", .url = "git+https://github.com/CalebQ42/zig-minilzo.git#f72a0e20a30fad38cd10104f393c8f3440478697",
// .hash = "minilzo-2.10.0-Ij7BO7YLAABd6oK2YdimXwvFTOC8wD1sNm0PHGoRNEwR",
// // 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",
+8
View File
@@ -0,0 +1,8 @@
#include <zlib.h>
#include <lzma.h>
#include <lz4.h>
#include <zstd.h>
#ifdef ALLOW_LZO
#include <lzo/minilzo.h>
#endif
-10
View File
@@ -1,10 +0,0 @@
#!/bin/sh
zig test \
-lc \
-lz \
-llzma \
-lminilzo \
-llz4 \
-lzstd \
src/test.zig
+105 -81
View File
@@ -1,107 +1,131 @@
//! A squashfs archive read from a file.
//! Can be used to directly access File's contents or extract to the filesystem.
const std = @import("std"); const std = @import("std");
const File = std.fs.File; const Io = std.Io;
const builtin = @import("builtin"); const MemoryMap = Io.File.MemoryMap;
const Decomp = @import("decomp.zig"); const Decomp = @import("decomp.zig");
const ExtractionOptions = @import("options.zig");
const Inode = @import("inode.zig"); const Inode = @import("inode.zig");
const InodeRef = Inode.Ref; const File = @import("file.zig");
const BlockSize = @import("inode_data/file.zig").BlockSize; const ExtractionOptions = @import("options.zig");
const SfsFile = @import("file.zig"); const Extract = @import("extract.zig");
const Superblock = @import("super.zig").Superblock;
const Table = @import("table.zig").Table;
const MetadataReader = @import("util/metadata.zig");
const OffsetFile = @import("util/offset_file.zig");
const config = if (builtin.is_test) .{
.use_c_libs = true,
.allow_lzo = false,
} 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,
fil: OffsetFile,
decomp: Decomp.DecompFn,
super: Superblock, super: Superblock,
frag_table: Table(FragEntry) = undefined, map: MemoryMap,
id_table: Table(u16) = undefined, decomp: Decomp.Fn,
export_table: Table(InodeRef) = undefined,
pub fn init(io: Io, file: Io.File, offset: u64) !Archive {
var rdr = file.reader(io, &[0]u8{});
try rdr.seekTo(offset);
/// 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 {
var super: Superblock = undefined; var super: Superblock = undefined;
const red = try fil.pread(@ptrCast(&super), offset); try rdr.interface.readSliceEndian(Superblock, @ptrCast(&super), .little);
std.debug.assert(red == @sizeOf(Superblock));
try super.validate(); try super.validate();
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 .{ return .{
.alloc = alloc,
.fil = off_fil,
.decomp = decomp,
.super = super, .super = super,
.frag_table = try .init(alloc, off_fil, decomp, super.frag_start, super.frag_count), .map = try file.createMemoryMap(io, .{
.id_table = try .init(alloc, off_fil, decomp, super.id_start, super.id_count), .offset = offset,
.export_table = try .init(alloc, off_fil, decomp, super.export_start, super.inode_count), .len = super.size,
.protection = .{ .read = true },
}),
.decomp = try Decomp.getFn(super.compression),
}; };
} }
pub fn deinit(self: *Archive) void { pub fn deinit(self: *Archive, io: Io) void {
self.frag_table.deinit(); self.map.destroy(io);
self.export_table.deinit();
self.id_table.deinit();
} }
pub fn inode(self: *Archive, alloc: std.mem.Allocator, num: u32) !Inode { pub fn root(self: Archive, alloc: std.mem.Allocator) !File {
const ref = try self.export_table.get(num - 1); return .initRef(alloc, self.super, self.map.memory, self.decomp, "", self.super.root_ref);
var rdr = try self.fil.readerAt(ref.block_start + self.super.inode_start, &[0]u8{});
var meta: MetadataReader = .init(alloc, &rdr.interface, &self.decomp);
try meta.interface.discardAll(ref.block_offset);
return try .read(alloc, &meta.interface, self.super.block_size);
} }
pub fn root(self: *Archive, alloc: std.mem.Allocator) !SfsFile { pub fn open(self: Archive, alloc: std.mem.Allocator, filepath: []const u8) !File {
var rdr = try self.fil.readerAt(self.super.root_ref.block_start + self.super.inode_start, &[0]u8{}); const root_file = try self.root(alloc);
var meta: MetadataReader = .init(alloc, &rdr.interface, self.decomp);
try meta.interface.discardAll(self.super.root_ref.block_offset); const path = std.mem.trim(u8, filepath, "/");
const in: Inode = try .read(alloc, &meta.interface, self.super.block_size);
return .init(self, in, ""); if (path.len == 0 or (path.len == 1 and path[0] == '.'))
return root_file;
defer root_file.deinit();
return root_file.open(alloc, filepath);
} }
pub fn open(self: *Archive, alloc: std.mem.Allocator, path: []const u8) !SfsFile { pub fn extract(self: Archive, alloc: std.mem.Allocator, io: Io, location: []const u8, options: ExtractionOptions) !void {
var root_fil = try self.root(alloc); const root_inode: Inode = try .initRef(
defer if (!SfsFile.pathIsSelf(path)) root_fil.deinit(); alloc,
return root_fil.open(path); self.map.memory,
self.decomp,
self.super.inode_start,
self.super.block_size,
self.super.root_ref,
);
defer root_inode.deinit(alloc);
return root_inode.extract(alloc, io, self.super, self.map.memory, self.decomp, location, options);
} }
pub fn extract(self: *Archive, alloc: std.mem.Allocator, path: []const u8, options: ExtractionOptions) !void { // Superblock
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); const SQUASHFS_MAGIC: u32 = std.mem.readInt(u32, "hsqs", .little);
try meta.interface.discardAll(self.super.root_ref.block_offset);
const in: Inode = try .read(self.alloc, &meta.interface, self.super.block_size); const SuperblockError = error{
try in.extractTo(alloc, self, path, options); InvalidMagic,
InvalidBlockLog,
InvalidVersion,
InvalidCheck,
};
/// A squashfs Superblock
pub const Superblock = extern struct {
magic: u32,
inode_count: u32,
mod_time: u32,
block_size: u32,
frag_count: u32,
compression: Decomp.Enum,
block_log: u16,
flags: packed struct(u16) {
inode_uncompressed: bool,
data_uncompressed: bool,
check: bool,
frag_uncompressed: bool,
fragment_never: bool,
fragment_always: bool,
duplicates: bool,
exportable: bool,
xattr_uncompressed: bool,
xattr_never: bool,
compression_options: bool,
ids_uncompressed: bool,
_: u4,
},
id_count: u16,
ver_maj: u16,
ver_min: u16,
root_ref: Inode.Ref,
size: u64,
id_start: u64,
xattr_start: u64,
inode_start: u64,
dir_start: u64,
frag_start: u64,
export_start: u64,
/// Validate the Superblock. If an error is returned, it's likely the archive is corrupted or not a squashfs archive.
pub fn validate(self: Superblock) !void {
if (self.magic != SQUASHFS_MAGIC)
return SuperblockError.InvalidMagic;
if (self.flags.check)
return SuperblockError.InvalidCheck;
if (self.ver_maj != 4 or self.ver_min != 0)
return SuperblockError.InvalidVersion;
if (std.math.log2(self.block_size) != self.block_log)
return SuperblockError.InvalidBlockLog;
} }
};
+97 -19
View File
@@ -1,9 +1,10 @@
const std = @import("std"); const std = @import("std");
const Writer = std.Io.Writer; const Io = std.Io;
const Writer = Io.Writer;
const builtin = @import("builtin"); const builtin = @import("builtin");
const config = @import("config"); const build = @import("build");
const squashfs = @import("zig_squashfs"); const squashfs = @import("squashfs");
//TODO: Add more options //TODO: Add more options
const help_mgs = const help_mgs =
@@ -12,12 +13,18 @@ const help_mgs =
\\ \\
\\Options: \\Options:
\\ -d <location> Extract to the given location instead of "squashfs-root" \\ -d <location> Extract to the given location instead of "squashfs-root"
\\ -f <filepath> Extract the files at the given filepath instead of the entire archive
\\ Can be given multiple times.
\\ \\
\\ -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
\\ \\
@@ -26,36 +33,82 @@ const help_mgs =
const errors = error{InvalidArguments}; const errors = error{InvalidArguments};
var archive: []const u8 = ""; var archive: []const u8 = "";
var extLoc: []const u8 = "squashfs-root"; var ext_loc: []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;
var files: [][:0]const u8 = &[0][:0]const u8{};
pub fn main() !void { var limited: ?Io.Threaded = null;
const alloc = std.heap.smp_allocator;
var stdout = std.fs.File.stdout(); pub fn main(init: std.process.Init) !void {
var out = stdout.writer(&[0]u8{}); var io = init.io;
const alloc = init.gpa;
var stdout = Io.File.stdout();
var out = stdout.writer(io, &[0]u8{});
defer out.interface.flush() catch {}; defer out.interface.flush() catch {};
try handleArgs(alloc, &out.interface);
try handleArgs(alloc, init.minimal.args, &out.interface);
if (archive.len == 0) { if (archive.len == 0) {
try out.interface.print("You must provide a squashfs archive\n", .{}); try out.interface.print("You must provide a squashfs archive\n", .{});
try out.interface.print(help_mgs, .{}); try out.interface.print(help_mgs, .{});
return; return;
} }
var fil: std.fs.File = try std.fs.cwd().openFile(archive, .{}); //TODO: Handle error gracefully.
defer fil.close(); var fil = try Io.Dir.cwd().openFile(io, archive, .{}); //TODO: Handle error gracefully.
var arc: squashfs.Archive = try .init(alloc, fil, offset); //TODO: Update when memory size matters. //TODO: Handle error gracefully. defer fil.close(io);
defer arc.deinit();
var arc: squashfs.Archive = try .init(io, fil, offset); //TODO: Update when memory size matters. //TODO: Handle error gracefully.
defer arc.deinit(io);
const options: squashfs.ExtractionOptions = .{ const options: squashfs.ExtractionOptions = .{
.threads = if (threads == 0) try std.Thread.getCpuCount() else threads, .single_threaded = (threads == 1),
.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,
}; };
try arc.extract(alloc, extLoc, options); //TODO: Handle error gracefully.
if (threads > 1) {
limited = Io.Threaded.init(alloc, .{
.argv0 = .init(init.minimal.args),
.async_limit = @enumFromInt(threads),
.concurrent_limit = @enumFromInt(threads),
.environ = init.minimal.environ,
});
io = limited.?.io();
} }
fn handleArgs(alloc: std.mem.Allocator, out: *Writer) !void { if (force)
var args = try std.process.argsWithAllocator(alloc); try Io.Dir.cwd().deleteTree(io, ext_loc);
if (files.len > 0) {
for (0..files.len) |i| {
var sfs_fil = try arc.open(alloc, files[i]);
defer sfs_fil.deinit();
const last_ind = std.mem.lastIndexOf(u8, files[i], "/");
const file_name = if (last_ind != null) files[i][last_ind.?..] else files[i];
const fil_ext_loc = if (ext_loc.len > 0)
try std.mem.concat(alloc, u8, &.{ ext_loc, "/", file_name })
else
file_name;
defer if (fil_ext_loc.len != file_name.len) alloc.free(fil_ext_loc);
try sfs_fil.extract(alloc, io, fil_ext_loc, options);
}
} else {
try arc.extract(alloc, io, ext_loc, options); //TODO: Handle error gracefully.
}
}
fn handleArgs(alloc: std.mem.Allocator, main_args: std.process.Args, out: *Writer) !void {
var args = main_args.iterate();
defer args.deinit(); defer args.deinit();
_ = args.next(); // args[0] is the application launch command. _ = args.next(); // args[0] is the application launch command.
while (args.next()) |arg| { while (args.next()) |arg| {
@@ -70,13 +123,29 @@ fn handleArgs(alloc: std.mem.Allocator, out: *Writer) !void {
return errors.InvalidArguments; return errors.InvalidArguments;
}; };
continue; continue;
} else if (std.mem.eql(u8, arg, "-f")) {
const nxt = args.next();
if (nxt == null or nxt.?.len == 0) {
try out.print("-d must be followed by a location\n", .{});
return errors.InvalidArguments;
}
if (!alloc.resize(files, files.len + 1)) {
const new_alloc = try alloc.alloc([:0]const u8, files.len + 1);
@memcpy(new_alloc[0..files.len], files);
alloc.free(files);
files = new_alloc;
} else {
files.len += 1;
}
files[files.len - 1] = nxt.?;
continue;
} else if (std.mem.eql(u8, arg, "-d")) { } else if (std.mem.eql(u8, arg, "-d")) {
const nxt = args.next(); const nxt = args.next();
if (nxt == null or nxt.?.len == 0) { if (nxt == null or nxt.?.len == 0) {
try out.print("-d must be followed by a location\n", .{}); try out.print("-d must be followed by a location\n", .{});
return errors.InvalidArguments; return errors.InvalidArguments;
} }
extLoc = nxt.?; ext_loc = std.mem.trim(u8, nxt.?, "/");
continue; continue;
} else if (std.mem.eql(u8, arg, "-p")) { } else if (std.mem.eql(u8, arg, "-p")) {
const nxt = args.next(); const nxt = args.next();
@@ -92,9 +161,18 @@ 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 build.version.format(out);
try out.print("\nBuilt using Zig {s} in {} mode\n", .{ builtin.zig_version_string, builtin.mode }); try out.print("\nBuilt using Zig {s} in {} mode\n", .{ builtin.zig_version_string, builtin.mode });
std.process.exit(0); std.process.exit(0);
return; return;
+56
View File
@@ -0,0 +1,56 @@
const std = @import("std");
const c = @import("c");
const Error = @import("decomp.zig").Error;
pub fn zlib(_: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
var stream: c.z_stream = .{
.next_in = in.ptr,
.avail_in = @truncate(in.len),
.next_out = out.ptr,
.avail_out = @truncate(out.len),
};
var res = c.inflateInit(&stream);
if (res != c.Z_OK)
return Error.DecompressionFailed;
res = c.inflate(&stream, c.Z_FULL_FLUSH);
if (res != c.Z_OK)
return Error.DecompressionFailed;
return stream.total_out;
}
pub fn lzma(_: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
var stream: c.lzma_stream = .{
.next_in = in.ptr,
.avail_in = in.len,
.next_out = out.ptr,
.avail_out = out.len,
};
var res = c.lzma_auto_decoder(&stream, 0, 0);
if (res != c.LZMA_OK)
return Error.DecompressionFailed;
while (res == c.LZMA_OK)
res = c.lzma_code(&stream, c.LZMA_RUN);
if (res != c.LZMA_STREAM_END)
return Error.DecompressionFailed;
return stream.total_out;
}
pub fn lz4(_: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
const res = c.LZ4_decompress_safe(
in.ptr,
out.ptr,
@intCast(in.len),
@intCast(out.len),
);
if (res < 0)
return Error.DecompressionFailed;
return @abs(res);
}
pub fn zstd(_: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
const res = c.ZSTD_decompress(out.ptr, out.len, in.ptr, in.len);
if (c.ZSTD_isError(res) != 0) {
std.debug.print("decompression failed: {s}\n", .{c.ZSTD_getErrorName(res)});
return Error.DecompressionFailed;
}
return res;
}
+125
View File
@@ -0,0 +1,125 @@
const std = @import("std");
const Io = std.Io;
const Decomp = @import("../decomp.zig");
const DataBlock = @import("../inode.zig").DataBlock;
const Cache = @import("../util/cache.zig");
const Extractor = @This();
data: []u8,
decomp: Decomp.Fn,
block_size: u32,
blocks: []DataBlock,
start: u64,
size: u64,
frag_data: ?[]u8 = null,
frag_offset: u32 = 0,
cache: ?*Cache = null,
pub fn init(data: []u8, decomp: Decomp.Fn, block_size: u32, blocks: []DataBlock, start: u64, size: u64) Extractor {
return .{
.data = data,
.decomp = decomp,
.block_size = block_size,
.blocks = blocks,
.start = start,
.size = size,
};
}
pub fn addFrag(self: *Extractor, frag_data: []u8, frag_offset: u32) void {
self.frag_data = frag_data;
self.frag_offset = frag_offset;
}
pub fn addCache(self: *Extractor, cache: *Cache) void {
self.cache = cache;
}
pub fn extractAsync(self: Extractor, alloc: std.mem.Allocator, io: Io, file: Io.File) Error!void {
if (self.size == 0) return;
try file.writePositionalAll(io, &[0]u8{}, self.size);
var map = try file.createMemoryMap(io, .{
.len = self.size,
.protection = .{ .write = true },
});
defer map.destroy(io);
var err: ?Error = null;
var group: Io.Group = .init;
var read_offset: u64 = self.start;
for (0.., self.blocks) |i, block| {
group.async(io, blockThread, .{ self, alloc, io, map.memory, read_offset, @truncate(i), &err });
read_offset += block.size;
}
if (self.frag_data != null)
group.async(io, fragThread, .{ self, map.memory });
try group.await(io);
if (err != null)
return err.?;
try map.write(io);
}
fn blockThread(self: Extractor, alloc: std.mem.Allocator, io: Io, map_data: []u8, read_offset: u64, block_idx: u32, err: *?Error) error{Canceled}!void {
const size = if (self.frag_data == null and block_idx == (self.size - 1 / self.block_size))
self.size % self.block_size
else
self.block_size;
const offset = block_idx * self.block_size;
const block = self.blocks[block_idx];
if (block.size == 0) {
@memset(map_data[offset..][0..size], 0);
return;
}
const data = self.data[read_offset..][0..block.size];
if (block.uncompressed) {
@memcpy(map_data[offset..][0..block.size], data);
return;
}
std.debug.print("offset: {} start: {} block: {any}\n", .{ read_offset, self.start, block });
if (self.cache != null) {
const decomp_block = self.cache.?.get(io, read_offset, block.size) catch |inner_err| {
std.debug.print("cache extractor err: {}\n", .{inner_err});
switch (inner_err) {
error.Canceled => return error.Canceled,
else => |e| err.* = e,
}
return;
};
std.debug.print("cached block size: {} should be {}\n", .{ decomp_block.len, size });
@memcpy(map_data[offset..][0..size], decomp_block[0..size]);
} else {
_ = self.decomp(alloc, data, map_data[offset..][0..size]) catch |inner_err| {
std.debug.print("data decomp err: {}\n", .{inner_err});
err.* = inner_err;
};
}
}
fn fragThread(self: Extractor, map_data: []u8) error{Canceled}!void {
const size = self.size % self.block_size;
const offset = self.blocks.len * self.block_size;
@memcpy(map_data[offset..][0..size], self.frag_data.?[self.frag_offset..][0..size]);
}
// Types
pub const Error = Io.File.WritePositionalError || Io.File.MemoryMap.CreateError || Decomp.Error || Cache.Error;
+168
View File
@@ -0,0 +1,168 @@
const std = @import("std");
const Io = std.Io;
const Decomp = @import("../decomp.zig");
const DataBlock = @import("../inode.zig").DataBlock;
const Cache = @import("../util/cache.zig");
const Reader = @This();
alloc: std.mem.Allocator,
data: []u8,
decomp: Decomp.Fn,
block_size: u32,
blocks: []DataBlock,
size: u64,
offset: u64,
block_idx: u32 = 0,
sparse_block: bool = false,
frag_data: ?[]u8 = null,
frag_offset: u32 = 0,
io: ?Io = null,
cache: ?*Cache = null,
block: [1024 * 1024]u8 = undefined,
interface: Io.Reader = .{
.buffer = &[0]u8{},
.end = 0,
.seek = 0,
.vtable = &.{
.stream = stream,
.discard = discard,
.readVec = readVec,
},
},
pub fn init(alloc: std.mem.Allocator, data: []u8, decomp: Decomp.Fn, block_size: u32, blocks: []DataBlock, size: u64, data_start: u64) Reader {
return .{
.alloc = alloc,
.data = data,
.decomp = decomp,
.block_size = block_size,
.blocks = blocks,
.size = size,
.offset = data_start,
};
}
pub fn addFrag(self: *Reader, frag_data: []u8, frag_offset: u32) void {
self.frag_data = frag_data;
self.frag_offset = frag_offset;
}
pub fn addCache(self: *Reader, io: Io, cache: *Cache) void {
self.io = io;
self.cache = cache;
}
fn advance(self: *Reader) Io.Reader.Error!void {
if (self.block_idx > self.blocks.len) return error.EndOfStream;
defer self.block_idx += 1;
self.interface.seek = 0;
errdefer self.interface.end = 0;
if (self.block_idx == self.blocks.len) {
if (self.frag_data == null) return error.EndOfStream;
self.sparse_block = false;
const size = self.size % self.block_size;
self.interface.buffer = self.frag_data.?[self.frag_offset..][0..size];
self.interface.end = size;
return;
}
const size = if (self.frag_data == null and self.block_idx == self.blocks.len - 1)
self.size % self.block_size
else
self.block_size;
const block = self.blocks[self.block_idx];
defer self.offset += block.size;
if (block.size == 0) {
self.sparse_block = true;
self.interface.end = size;
return;
} else {
self.sparse_block = false;
}
if (block.uncompressed) {
self.interface.buffer = self.data[self.offset..][0..block.size];
self.interface.end = block.size;
return;
}
if (self.cache == null) {
_ = self.decomp(self.alloc, self.data[self.offset..][0..block.size], self.block[0..size]) catch return error.ReadFailed;
self.interface.buffer = self.block[0..size];
self.interface.end = size;
} else {
self.interface.buffer = self.cache.?.get(self.io.?, self.offset, block.size) catch return error.ReadFailed;
self.interface.end = self.interface.buffer.len;
}
}
fn stream(r: *Io.Reader, w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize {
const self: *Reader = @fieldParentPtr("interface", r);
if (r.seek >= r.end)
try self.advance();
if (limit == .nothing) return 0;
const to_write = @min(@intFromEnum(limit), r.end - r.seek);
const wrote = if (self.sparse_block)
try w.splatByte(0, to_write)
else
try w.write(r.buffer[r.seek..][0..to_write]);
r.seek += wrote;
return wrote;
}
fn discard(r: *Io.Reader, limit: Io.Limit) Io.Reader.Error!usize {
if (r.seek >= r.end) {
const self: *Reader = @fieldParentPtr("interface", r);
try self.advance();
}
if (limit == .nothing) return 0;
const to_discard = @min(@intFromEnum(limit), r.end - r.seek);
r.seek += to_discard;
return to_discard;
}
fn readVec(r: *Io.Reader, vec: [][]u8) Io.Reader.Error!usize {
const self: *Reader = @fieldParentPtr("interface", r);
if (r.seek >= r.end)
try self.advance();
var copied: usize = 0;
for (vec) |v| {
const to_cpy = @min(v.len, r.end - r.seek);
if (self.sparse_block)
@memset(v[0..to_cpy], 0)
else
@memcpy(v[0..to_cpy], r.buffer[r.seek..][0..to_cpy]);
copied += to_cpy;
if (r.seek >= r.end) break;
}
return copied;
}
+21 -247
View File
@@ -1,26 +1,25 @@
//! Implementations for decompression.
//! TODO: change to vtable interface to allow for shared decompressors for better performance/resource usage.
const std = @import("std"); const std = @import("std");
const Reader = std.Io.Reader; const build = @import("build_options");
const builtin = @import("builtin");
const config = if (builtin.is_test) .{ const c = @import("c_decomp.zig");
.use_c_libs = builtin.link_libc == true, const zig = @import("zig_decomp.zig");
.allow_lzo = false, // Change once LZO compilation is fixed
} else @import("config");
const c = @cImport({ pub fn getFn(e: Enum) !Fn {
@cInclude("zlib.h"); return switch (e) {
@cInclude("lzma.h"); .gzip => if (build.use_zig_decomp) zig.zlib else c.zlib,
@cInclude("lz4.h"); .lzma => if (build.use_zig_decomp) zig.lzma else c.lzma,
@cInclude("zstd.h"); .lzo => if (build.use_zig_decomp or !build.allow_lzo) error.LzoUnsupported else c.lzo,
@cInclude("zstd_errors.h"); .xz => if (build.use_zig_decomp) zig.xz else c.lzma,
if (config.allow_lzo) .lz4 => if (build.use_zig_decomp) error.Lz4Unsupported else c.lz4,
@cInclude("lzo/minilzo.h"); .zstd => if (build.use_zig_decomp) zig.zstd else c.zstd,
}); };
}
pub const CompressionType = enum(u16) { // Types
pub const Fn = *const fn (std.mem.Allocator, in: []u8, out: []u8) Error!usize;
pub const Enum = enum(u16) {
gzip = 1, gzip = 1,
lzma, lzma,
lzo, lzo,
@@ -29,232 +28,7 @@ pub const CompressionType = enum(u16) {
zstd, zstd,
}; };
pub const DecompFn = *const fn (alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize; // TODO: replace anyerror to definitive error types. pub const Error = error{
OutOfMemory,
pub const gzipDecompress = if (config.use_c_libs) cGzip else zigGzip; DecompressionFailed,
fn zigGzip(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
var rdr: Reader = .fixed(in);
const buf = try alloc.alloc(u8, out.len);
defer alloc.free(buf);
var decomp = std.compress.flate.Decompress.init(&rdr, .zlib, buf);
return decomp.reader.readSliceShort(out);
}
fn cGzip(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
_ = alloc;
var out_len: usize = out.len;
const res = c.uncompress(out.ptr, &out_len, in.ptr, in.len);
return switch (res) {
c.Z_OK => out_len,
c.Z_MEM_ERROR => error.NotEnoughMemory,
c.Z_BUF_ERROR => error.OutBufferTooSmall,
c.Z_DATA_ERROR => error.BadData,
else => error.UnknownResult,
};
}
pub const lzmaDecompress = if (config.use_c_libs) cLzma else zigLzma;
fn zigLzma(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
var rdr: Reader = .fixed(in);
var decomp = try std.compress.lzma.decompress(alloc, rdr.adaptToOldInterface());
return decomp.read(out);
}
fn cLzma(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
_ = alloc;
var stream: c.lzma_stream = .{
.next_in = in.ptr,
.avail_in = in.len,
.next_out = out.ptr,
.avail_out = out.len,
};
var res = c.lzma_alone_decoder(&stream, in.len * 2);
switch (res) {
c.LZMA_OK => {},
c.LZMA_MEM_ERROR => return error.LzmaMemoryError,
c.LZMA_PROG_ERROR => return error.LzmaProgramError,
else => return error.UnknownResult,
}
defer c.lzma_end(&stream);
while (res == c.LZMA_OK)
res = c.lzma_code(&stream, c.LZMA_RUN);
return switch (res) {
c.LZMA_STREAM_END => stream.total_out,
c.LZMA_MEM_ERROR => error.LzmaMemoryError,
c.LZMA_MEMLIMIT_ERROR => error.LzmaMemoryLimit,
c.LZMA_FORMAT_ERROR => error.LzmaBadFormat,
c.LZMA_DATA_ERROR => error.LzmaDataCorrupt,
c.LZMA_BUF_ERROR => error.LzmaCannotProgress,
c.LZMA_PROG_ERROR => error.LzmaProgramError,
else => error.UnknownResult,
};
}
// pub const lzoDecompress = if (config.use_c_libs) cLzo else zigLzo;
// fn zigLzo(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
// _ = alloc;
// _ = in;
// _ = out;
// return error.LzoUnsupported;
// }
pub fn cLzo(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
_ = alloc;
var res = c.lzo_init();
if (res != 0) return error.LzoInitFailed;
var out_len: usize = out.len;
res = c.lzo1x_decompress(in.ptr, in.len, out.ptr, &out_len, null);
return switch (res) {
c.LZO_E_OK => out_len,
c.LZO_E_ERROR => error.LzoError,
c.LZO_E_OUT_OF_MEMORY => error.LzoOutOfMemory,
c.LZO_E_NOT_COMPRESSIBLE => error.LzoNotCompressible,
c.LZO_E_INPUT_OVERRUN => error.LzoInputOverrun,
c.LZO_E_OUTPUT_OVERRUN => error.LzoOutputOverrun,
c.LZO_E_LOOKBEHIND_OVERRUN => error.LzoLookbehindOverrun,
c.LZO_E_EOF_NOT_FOUND => error.LzoEofNotFound,
c.LZO_E_INPUT_NOT_CONSUMED => error.LzoInputNotConsumed,
c.LZO_E_NOT_YET_IMPLEMENTED => error.LzoNotYetImplemented,
c.LZO_E_INVALID_ARGUMENT => error.LzoInvalidArgument,
c.LZO_E_INVALID_ALIGNMENT => error.LzoInvalidAlignment,
c.LZO_E_OUTPUT_NOT_CONSUMED => error.LzoOutputNotConsumed,
c.LZO_E_INTERNAL_ERROR => error.LzoInternalError,
else => error.UnknownResult,
};
}
pub const xzDecompress = if (config.use_c_libs) cXz else zigXz;
fn zigXz(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
var rdr: Reader = .fixed(in);
var decomp = try std.compress.xz.decompress(alloc, rdr.adaptToOldInterface());
return decomp.read(out);
}
fn cXz(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
_ = alloc;
var stream: c.lzma_stream = .{
.next_in = in.ptr,
.avail_in = in.len,
.next_out = out.ptr,
.avail_out = out.len,
};
var res = c.lzma_stream_decoder(&stream, in.len * 2, 0);
switch (res) {
c.LZMA_OK => {},
c.LZMA_MEM_ERROR => return error.LzmaMemoryError,
c.LZMA_PROG_ERROR => return error.LzmaProgramError,
else => return error.UnknownResult,
}
defer c.lzma_end(&stream);
while (res == c.LZMA_OK)
res = c.lzma_code(&stream, c.LZMA_RUN);
return switch (res) {
c.LZMA_STREAM_END => stream.total_out,
c.LZMA_MEM_ERROR => error.LzmaMemoryError,
c.LZMA_MEMLIMIT_ERROR => error.LzmaMemoryLimit,
c.LZMA_FORMAT_ERROR => error.LzmaBadFormat,
c.LZMA_DATA_ERROR => error.LzmaDataCorrupt,
c.LZMA_BUF_ERROR => error.LzmaCannotProgress,
c.LZMA_PROG_ERROR => error.LzmaProgramError,
else => error.UnknownResult,
};
}
// pub const lz4Decompress = if (config.use_c_libs) cLz4 else zigLz4;
// fn zigLz4(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
// _ = alloc;
// _ = in;
// _ = out;
// return error.Lz4Unsupported;
// }
pub fn cLz4(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
_ = alloc;
const res = c.LZ4_decompress_safe(in.ptr, out.ptr, @intCast(in.len), @intCast(out.len));
if (res > 0) return @abs(res); // TODO: Find out what error values it can return.
return error.Lz4DecompressFailed;
}
pub const zstdDecompress = if (config.use_c_libs) cZstd else zigZstd;
pub fn zigZstd(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
var rdr: Reader = .fixed(in);
const buf = try alloc.alloc(u8, 1024 * 1024);
defer alloc.free(buf);
var decomp = std.compress.zstd.Decompress.init(&rdr, buf, .{});
return decomp.reader.readSliceShort(out) catch |err| {
return decomp.err orelse err;
};
}
fn cZstd(alloc: std.mem.Allocator, in: []u8, out: []u8) anyerror!usize {
_ = alloc;
const res = c.ZSTD_decompress(out.ptr, out.len, in.ptr, in.len);
if (c.ZSTD_isError(res) == 0) return res;
return switch (c.ZSTD_getErrorCode(res)) {
c.ZSTD_error_prefix_unknown => cZstdError.PrefixUnknown,
c.ZSTD_error_version_unsupported => cZstdError.VersionUnsupported,
c.ZSTD_error_frameParameter_unsupported => cZstdError.FrameParameterUnsupported,
c.ZSTD_error_frameParameter_windowTooLarge => cZstdError.FrameParameterWindowTooLarge,
c.ZSTD_error_corruption_detected => cZstdError.CorruptionDetected,
c.ZSTD_error_checksum_wrong => cZstdError.ChecksumWrong,
c.ZSTD_error_literals_headerWrong => cZstdError.LiteralsHeaderWrong,
c.ZSTD_error_dictionary_corrupted => cZstdError.DictionaryCorrupted,
c.ZSTD_error_dictionary_wrong => cZstdError.DictionaryWrong,
c.ZSTD_error_dictionaryCreation_failed => cZstdError.DictionaryCreationFailed,
c.ZSTD_error_parameter_unsupported => cZstdError.ParameterUnsupported,
c.ZSTD_error_parameter_combination_unsupported => cZstdError.ParameterCombinationUnsupported,
c.ZSTD_error_parameter_outOfBound => cZstdError.ParameterOutOfBound,
c.ZSTD_error_tableLog_tooLarge => cZstdError.TableLogTooLarge,
c.ZSTD_error_maxSymbolValue_tooLarge => cZstdError.MaxSymbolValueTooLarge,
c.ZSTD_error_maxSymbolValue_tooSmall => cZstdError.MaxSymbolValueTooSmall,
c.ZSTD_error_stabilityCondition_notRespected => cZstdError.StabilityConditionNotRespected,
c.ZSTD_error_stage_wrong => cZstdError.StageWrong,
c.ZSTD_error_init_missing => cZstdError.InitMissing,
c.ZSTD_error_memory_allocation => cZstdError.MemoryAllocation,
c.ZSTD_error_workSpace_tooSmall => cZstdError.WorkSpaceTooSmall,
c.ZSTD_error_dstSize_tooSmall => cZstdError.DstSizeTooSmall,
c.ZSTD_error_srcSize_wrong => cZstdError.SrcSizeWrong,
c.ZSTD_error_dstBuffer_null => cZstdError.DstBufferNull,
c.ZSTD_error_noForwardProgress_destFull => cZstdError.NoForwardProgressDestFull,
c.ZSTD_error_noForwardProgress_inputEmpty => cZstdError.NoForwardProgressInputEmpty,
else => cZstdError.Generic,
};
}
pub const cZstdError = error{
Generic,
PrefixUnknown,
VersionUnsupported,
FrameParameterUnsupported,
FrameParameterWindowTooLarge,
CorruptionDetected,
ChecksumWrong,
LiteralsHeaderWrong,
DictionaryCorrupted,
DictionaryWrong,
DictionaryCreationFailed,
ParameterUnsupported,
ParameterCombinationUnsupported,
ParameterOutOfBound,
TableLogTooLarge,
MaxSymbolValueTooLarge,
MaxSymbolValueTooSmall,
CannotProduceUncompressedBlock,
StabilityConditionNotRespected,
StageWrong,
InitMissing,
MemoryAllocation,
WorkSpaceTooSmall,
DstSizeTooSmall,
SrcSizeWrong,
DstBufferNull,
NoForwardProgressDestFull,
NoForwardProgressInputEmpty,
FrameIndexTooLarge,
SeekableIo,
DstBufferWrong,
SrcBufferWrong,
SequenceProducerFailed,
ExternalSequencesInvalid,
MaxCode,
}; };
-60
View File
@@ -1,60 +0,0 @@
//! Directory entry from the directory table.
const std = @import("std");
const Reader = std.Io.Reader;
const InodeType = @import("inode.zig").InodeType;
const Entry = @This();
const Header = extern struct { // use extern due to bad alignment with packed.
count: u32,
block_start: u32,
num: u32,
};
const RawEntry = packed struct {
offset: u16,
inode_offset: i16,
inode_type: InodeType,
name_size: u16,
};
block_start: u32,
block_offset: u16,
num: u32,
inode_type: InodeType,
name: []const u8,
pub fn readDir(alloc: std.mem.Allocator, rdr: *Reader, size: u32) ![]Entry {
var cur_red: u32 = 3; // start at 3 due to "." & ".." being counted in the dir size.
var hdr: Header = undefined;
var raw: RawEntry = undefined;
var out: std.ArrayList(Entry) = try .initCapacity(alloc, 100); // Start out with a decent capacity instead of needing to allocate per header.
errdefer out.deinit(alloc);
while (cur_red < size) {
try rdr.readSliceEndian(Header, @ptrCast(&hdr), .little);
cur_red += @sizeOf(Header);
try out.ensureUnusedCapacity(alloc, hdr.count + 1);
for (0..hdr.count + 1) |_| {
try rdr.readSliceEndian(RawEntry, @ptrCast(&raw), .little);
const name = try alloc.alloc(u8, raw.name_size + 1);
errdefer alloc.free(name);
try rdr.readSliceEndian(u8, name, .little);
const val = out.addOneAssumeCapacity();
val.* = .{
.block_start = hdr.block_start,
.block_offset = raw.offset,
.num = @abs(hdr.num + raw.offset),
.inode_type = raw.inode_type,
.name = name,
};
cur_red += @sizeOf(RawEntry) + raw.name_size + 1;
}
}
return out.toOwnedSlice(alloc);
}
pub fn deinit(self: Entry, alloc: std.mem.Allocator) void {
alloc.free(self.name);
}
+85
View File
@@ -0,0 +1,85 @@
const std = @import("std");
const Reader = std.Io.Reader;
const Inode = @import("inode.zig");
const Directory = @This();
entries: []Entry,
pub fn init(alloc: std.mem.Allocator, rdr: *Reader, size: u64) !Directory {
if (size <= 3) return Directory{ .entries = &[0]Entry{} };
var hdr: Header = undefined;
var raw: RawEntry = undefined;
var read: u64 = 3;
var out: std.ArrayList(Entry) = try .initCapacity(alloc, 50);
errdefer {
for (out.items) |entry|
entry.deinit(alloc);
out.deinit(alloc);
}
while (read < size) {
try rdr.readSliceEndian(Header, @ptrCast(&hdr), .little);
read += @sizeOf(Header);
try out.ensureUnusedCapacity(alloc, hdr.count + 1);
for (0..hdr.count + 1) |_| {
try rdr.readSliceEndian(RawEntry, @ptrCast(&raw), .little);
const name = try alloc.alloc(u8, raw.name_size + 1);
try rdr.readSliceEndian(u8, name, .little);
const entry = out.addOneAssumeCapacity();
entry.* = .{
.name = name,
.block_start = hdr.block_start,
.block_offset = raw.block_offset,
.type = raw.type,
};
read += @sizeOf(RawEntry) + raw.name_size + 1;
}
}
return .{ .entries = try out.toOwnedSlice(alloc) };
}
pub fn deinit(self: Directory, alloc: std.mem.Allocator) void {
for (self.entries) |entry|
entry.deinit(alloc);
alloc.free(self.entries);
}
// Types
pub const Error = error{OutOfMemory} || Reader.Error;
pub const Entry = struct {
name: []const u8,
block_start: u32,
block_offset: u16,
type: Inode.Type,
pub fn deinit(self: Entry, alloc: std.mem.Allocator) void {
alloc.free(self.name);
}
};
const Header = extern struct {
count: u32,
block_start: u32,
num: u32,
};
const RawEntry = extern struct {
block_offset: u16,
num_offset: i16,
type: Inode.Type,
name_size: u16,
};
+427
View File
@@ -0,0 +1,427 @@
const std = @import("std");
const Io = std.Io;
const DataExtractor = @import("data/extractor.zig");
const DataReader = @import("data/reader.zig");
const Decomp = @import("decomp.zig");
const Directory = @import("directory.zig");
const ExtractionOptions = @import("options.zig");
const Inode = @import("inode.zig");
const Lookup = @import("lookup.zig");
const MetadataReader = @import("meta_rdr.zig");
const Superblock = @import("archive.zig").Superblock;
const Cache = @import("util/cache.zig");
const XattrTable = @import("xattr.zig");
pub fn extract(
alloc: std.mem.Allocator,
io: Io,
super: Superblock,
data: []u8,
decomp: Decomp.Fn,
inode: Inode,
ext_loc: []const u8,
options: ExtractionOptions,
) !void {
const path = std.mem.trim(u8, ext_loc, "/");
var id_table: Lookup.Table(u16) = .init(alloc, data, decomp, super.id_start, super.id_count);
defer id_table.deinit();
var xattr_table: XattrTable = .init(alloc, data, decomp, super.xattr_start);
defer xattr_table.deinit();
var buf: [150]ReturnUnion = undefined;
var sel: Io.Select(ReturnUnion) = .init(io, &buf);
defer while (sel.cancel()) |res|
switch (res) {
.path => |p| {
const path_return = p catch continue;
if (path_return.path.len != path.len)
alloc.free(path_return.path);
},
else => {},
};
var loop = io.async(finishLoop, .{ alloc, io, &sel, &id_table, &xattr_table, inode.hdr.num, options });
var cache: Cache = .init(alloc, data, decomp);
defer cache.deinit();
var frag_table: Lookup.Table(Lookup.FragEntry) = .init(alloc, data, decomp, super.frag_start, super.frag_count);
defer frag_table.deinit();
switch (inode.hdr.type) {
.file, .ext_file => sel.async(
.path,
extractFile,
.{ alloc, io, data, decomp, super.block_size, &cache, &frag_table, inode, path, true },
),
.dir, .ext_dir => sel.async(
.path,
extractDir,
.{ alloc, io, super, data, decomp, &sel, &cache, &frag_table, inode, path, true },
),
.symlink, .ext_symlink => sel.async(
.void,
extractSymlink,
.{ alloc, io, inode, path, true },
),
else => sel.async(
.path,
extractNod,
.{ alloc, inode, path, true },
),
}
try loop.await(io);
}
fn dirOrder(_: void, a: PathReturn, b: PathReturn) std.math.Order {
return std.math.order(std.mem.count(u8, a.path, "/"), std.mem.count(u8, b.path, "/"));
}
fn finishLoop(alloc: std.mem.Allocator, io: Io, sel: *Io.Select(ReturnUnion), id_table: *Lookup.Table(u16), xattr_table: *XattrTable, start_num: u32, options: ExtractionOptions) !void {
var dirs: std.PriorityDequeue(PathReturn, void, dirOrder) = .empty;
defer dirs.deinit(alloc);
errdefer while (dirs.popMax()) |d|
if (d.hdr.num != start_num) alloc.free(d.path);
while (true) {
const value: ReturnUnion = try sel.await();
const path_ret = switch (value) {
.void => {
_ = sel.group.token.load(.unordered) orelse break;
continue;
},
.path => |p| try p,
};
if (options.ignore_permissions and (options.ignore_xattr or path_ret.xattr_idx == null)) {
if (path_ret.hdr.num != start_num)
alloc.free(path_ret.path);
continue;
}
if (path_ret.hdr.type == .dir or path_ret.hdr.type == .ext_dir) {
dirs.push(alloc, path_ret) catch |err| {
if (path_ret.hdr.num != start_num)
alloc.free(path_ret.path);
return err;
};
continue;
}
defer if (path_ret.hdr.num != start_num)
alloc.free(path_ret.path);
var file = try Io.Dir.cwd().openFile(io, path_ret.path, .{});
defer file.close(io);
if (!options.ignore_xattr and path_ret.xattr_idx != null) {
const xattr = try xattr_table.get(alloc, io, path_ret.xattr_idx.?);
defer xattr.deinit(alloc);
for (xattr.kvs) |kv| {
const res = std.os.linux.fsetxattr(file.handle, kv.key, kv.value.ptr, kv.value.len, 0);
if (res != 0)
return error.SetXattrError;
}
}
if (!options.ignore_permissions) {
try file.setTimestamps(io, .{
.modify_timestamp = .init(Io.Timestamp.fromNanoseconds(@as(i96, @intCast(path_ret.hdr.mod_time)) * std.time.ns_per_s)),
});
try file.setPermissions(io, @enumFromInt(path_ret.hdr.permissions));
try file.setOwner(io, try id_table.get(io, path_ret.hdr.uid_idx), try id_table.get(io, path_ret.hdr.gid_idx));
}
_ = sel.group.token.load(.unordered) orelse break;
}
while (dirs.popMax()) |path_ret| {
defer if (path_ret.hdr.num != start_num)
alloc.free(path_ret.path);
var file = try Io.Dir.cwd().openFile(io, path_ret.path, .{});
defer file.close(io);
if (!options.ignore_xattr and path_ret.xattr_idx != null) {
const xattr = try xattr_table.get(alloc, io, path_ret.xattr_idx.?);
defer xattr.deinit(alloc);
for (xattr.kvs) |kv| {
const res = std.os.linux.fsetxattr(file.handle, kv.key, kv.value.ptr, kv.value.len, 0);
if (res != 0)
return error.SetXattrError;
}
}
if (!options.ignore_permissions) {
try file.setTimestamps(io, .{
.modify_timestamp = .init(Io.Timestamp.fromNanoseconds(@as(i96, @intCast(path_ret.hdr.mod_time)) * std.time.ns_per_s)),
});
try file.setPermissions(io, @enumFromInt(path_ret.hdr.permissions));
try file.setOwner(io, try id_table.get(io, path_ret.hdr.uid_idx), try id_table.get(io, path_ret.hdr.gid_idx));
}
}
}
fn extractDir(
alloc: std.mem.Allocator,
io: Io,
super: Superblock,
data: []u8,
decomp: Decomp.Fn,
sel: *Io.Select(ReturnUnion),
cache: *Cache,
frag_table: *Lookup.Table(Lookup.FragEntry),
inode: Inode,
path: []const u8,
origin: bool,
) Error!PathReturn {
defer if (!origin) inode.deinit(alloc);
errdefer if (!origin) alloc.free(path);
var ret: PathReturn = .{
.hdr = inode.hdr,
.path = path,
};
try Io.Dir.cwd().createDirPath(io, path);
var dir: Directory = switch (inode.data) {
.dir => |d| blk: {
var meta: MetadataReader = .init(alloc, data, decomp, d.block_start + super.dir_start);
try meta.interface.discardAll(d.block_offset);
break :blk try Directory.init(alloc, &meta.interface, d.size);
},
.ext_dir => |d| blk: {
if (d.xattr_idx != 0xFFFFFFFF) ret.xattr_idx = d.xattr_idx;
var meta: MetadataReader = .init(alloc, data, decomp, d.block_start + super.dir_start);
try meta.interface.discardAll(d.block_offset);
break :blk try Directory.init(alloc, &meta.interface, d.size);
},
else => unreachable,
};
defer dir.deinit(alloc);
for (dir.entries) |entry| {
var new_inode: Inode = try .initEntry(alloc, data, decomp, super.inode_start, super.block_size, entry);
const new_path = std.mem.concat(alloc, u8, &.{ path, "/", entry.name }) catch |err| {
new_inode.deinit(alloc);
return err;
};
switch (entry.type) {
.dir => sel.async(.path, extractDir, .{ alloc, io, super, data, decomp, sel, cache, frag_table, new_inode, new_path, false }),
.file => sel.async(.path, extractFile, .{ alloc, io, data, decomp, super.block_size, cache, frag_table, new_inode, new_path, false }),
.symlink => sel.async(.void, extractSymlink, .{ alloc, io, new_inode, new_path, false }),
else => sel.async(.path, extractNod, .{ alloc, new_inode, new_path, false }),
}
}
return ret;
}
fn extractFile(
alloc: std.mem.Allocator,
io: Io,
data: []u8,
decomp: Decomp.Fn,
block_size: u32,
cache: *Cache,
frag_table: *Lookup.Table(Lookup.FragEntry),
inode: Inode,
path: []const u8,
origin: bool,
) Error!PathReturn {
defer if (!origin) inode.deinit(alloc);
errdefer if (!origin) alloc.free(path);
try io.checkCancel();
var ret: PathReturn = .{
.hdr = inode.hdr,
.path = path,
};
// var ext: DataExtractor = switch (inode.data) {
// .file => |f| blk: {
// var rdr: DataExtractor = .init(data, decomp, block_size, f.blocks, f.size, f.block_start);
// rdr.addCache(cache);
// if (f.frag_idx == 0xFFFFFFFF) break :blk rdr;
// const entry: Lookup.FragEntry = try frag_table.get(io, f.frag_idx);
// if (entry.size.uncompressed) {
// rdr.addFrag(data[entry.block_start..][0..entry.size.size], f.frag_offset);
// } else {
// rdr.addFrag(try cache.get(io, entry.block_start, entry.size.size), f.frag_offset);
// }
// break :blk rdr;
// },
// .ext_file => |f| blk: {
// if (f.xattr_idx != 0xFFFFFFFF) ret.xattr_idx = f.xattr_idx;
// var rdr: DataExtractor = .init(data, decomp, block_size, f.blocks, f.size, f.block_start);
// rdr.addCache(cache);
// if (f.frag_idx == 0xFFFFFFFF) break :blk rdr;
// const entry: Lookup.FragEntry = try frag_table.get(io, f.frag_idx);
// if (entry.size.uncompressed) {
// rdr.addFrag(data[entry.block_start..][0..entry.size.size], f.frag_offset);
// } else {
// rdr.addFrag(try cache.get(io, entry.block_start, entry.size.size), f.frag_offset);
// }
// break :blk rdr;
// },
// else => unreachable,
// };
// var atomic = try Io.Dir.cwd().createFileAtomic(io, path, .{});
// defer atomic.deinit(io);
// try ext.extractAsync(alloc, io, atomic.file);
// try atomic.link(io);
var rdr: DataReader = switch (inode.data) {
.file => |f| blk: {
var rdr: DataReader = .init(alloc, data, decomp, block_size, f.blocks, f.size, f.block_start);
rdr.addCache(io, cache);
if (f.frag_idx == 0xFFFFFFFF) break :blk rdr;
const entry: Lookup.FragEntry = try frag_table.get(io, f.frag_idx);
if (entry.size.uncompressed) {
rdr.addFrag(data[entry.block_start..][0..entry.size.size], f.frag_offset);
} else {
rdr.addFrag(try cache.get(io, entry.block_start, entry.size.size), f.frag_offset);
}
break :blk rdr;
},
.ext_file => |f| blk: {
if (f.xattr_idx != 0xFFFFFFFF) ret.xattr_idx = f.xattr_idx;
var rdr: DataReader = .init(alloc, data, decomp, block_size, f.blocks, f.size, f.block_start);
rdr.addCache(io, cache);
if (f.frag_idx == 0xFFFFFFFF) break :blk rdr;
const entry: Lookup.FragEntry = try frag_table.get(io, f.frag_idx);
if (entry.size.uncompressed) {
rdr.addFrag(data[entry.block_start..][0..entry.size.size], f.frag_offset);
} else {
rdr.addFrag(try cache.get(io, entry.block_start, entry.size.size), f.frag_offset);
}
break :blk rdr;
},
else => unreachable,
};
var atomic = try Io.Dir.cwd().createFileAtomic(io, path, .{});
defer atomic.deinit(io);
var writer = atomic.file.writer(io, &[0]u8{});
_ = try rdr.interface.streamRemaining(&writer.interface);
try writer.flush();
try atomic.link(io);
return ret;
}
fn extractSymlink(alloc: std.mem.Allocator, io: Io, inode: Inode, path: []const u8, origin: bool) Error!void {
defer if (!origin) {
inode.deinit(alloc);
alloc.free(path);
};
const target = switch (inode.data) {
.symlink => |s| s.target,
.ext_symlink => |s| s.target,
else => unreachable,
};
try Io.Dir.cwd().symLink(io, target, path, .{});
}
fn extractNod(alloc: std.mem.Allocator, inode: Inode, path: []const u8, origin: bool) Error!PathReturn {
errdefer if (!origin)
alloc.free(path);
var ret: PathReturn = .{
.hdr = inode.hdr,
.path = path,
};
var dev: u32 = 0;
var mode: u32 = undefined;
const DT = std.posix.DT;
switch (inode.data) {
.char_dev => |d| {
dev = d.device;
mode = DT.CHR;
},
.block_dev => |d| {
dev = d.device;
mode = DT.BLK;
},
.ext_char_dev => |d| {
if (d.xattr_idx != 0xFFFFFFFF) ret.xattr_idx = d.xattr_idx;
dev = d.device;
mode = DT.CHR;
},
.ext_block_dev => |d| {
if (d.xattr_idx != 0xFFFFFFFF) ret.xattr_idx = d.xattr_idx;
dev = d.device;
mode = DT.BLK;
},
.fifo => mode = DT.FIFO,
.socket => mode = DT.SOCK,
.ext_fifo => |i| {
if (i.xattr_idx != 0xFFFFFFFF) ret.xattr_idx = i.xattr_idx;
mode = DT.FIFO;
},
.ext_socket => |i| {
if (i.xattr_idx != 0xFFFFFFFF) ret.xattr_idx = i.xattr_idx;
mode = DT.SOCK;
},
else => unreachable,
}
const sentinel_path = try alloc.dupeSentinel(u8, path, 0);
defer alloc.free(sentinel_path);
const res = std.os.linux.mknod(sentinel_path, mode, dev);
if (res != 0)
return error.MknodError;
return ret;
}
// Types
const ReturnUnion = union(enum) {
path: Error!PathReturn,
void: Error!void,
};
const Error = error{MknodError} || Decomp.Error || Directory.Error || DataExtractor.Error || Io.Dir.CreateDirPathError ||
Io.Dir.SymLinkError || Io.File.Atomic.LinkError || Io.Reader.StreamRemainingError;
const PathReturn = struct {
hdr: Inode.Header,
path: []const u8,
xattr_idx: ?u32 = null,
};
+265
View File
@@ -0,0 +1,265 @@
const std = @import("std");
const Io = std.Io;
const DataReader = @import("data/reader.zig");
const Decomp = @import("decomp.zig");
const Directory = @import("directory.zig");
const ExtractionOptions = @import("options.zig");
const Inode = @import("inode.zig");
const Lookup = @import("lookup.zig");
const MetadaReader = @import("meta_rdr.zig");
const Superblock = @import("archive.zig").Superblock;
const Cache = @import("util/cache.zig");
const XattrTable = @import("xattr.zig");
pub fn extract(
alloc: std.mem.Allocator,
io: Io,
super: Superblock,
data: []u8,
decomp: Decomp.Fn,
inode: Inode,
ext_loc: []const u8,
options: ExtractionOptions,
) !void {
const path = std.mem.trim(u8, ext_loc, "/");
var cache: Cache = .init(alloc, data, decomp);
defer cache.deinit();
var frag_table: Lookup.Table(Lookup.FragEntry) = .init(alloc, data, decomp, super.frag_start, super.frag_count);
defer frag_table.deinit();
var id_table: Lookup.Table(u16) = .init(alloc, data, decomp, super.id_start, super.id_count);
defer id_table.deinit();
var xattr_table: XattrTable = .init(alloc, data, decomp, super.xattr_start);
defer xattr_table.deinit();
return switch (inode.hdr.type) {
.file, .ext_file => try extractFile(alloc, io, super, data, decomp, &cache, &frag_table, &id_table, &xattr_table, inode, path, options),
.dir, .ext_dir => try extractDir(alloc, io, super, data, decomp, &cache, &frag_table, &id_table, &xattr_table, inode, path, options),
.symlink, .ext_symlink => try extractSymlink(io, inode, path),
else => try extractNod(alloc, io, &id_table, &xattr_table, inode, path, options),
};
}
fn setMetadata(
alloc: std.mem.Allocator,
io: Io,
id_table: *Lookup.Table(u16),
xattr_table: *XattrTable,
inode: Inode,
path: []const u8,
options: ExtractionOptions,
xattr_idx: ?u32,
) !void {
if (options.ignore_permissions and (options.ignore_xattr or xattr_idx == null)) return;
var fil: Io.File = try Io.Dir.cwd().openFile(io, path, .{});
defer fil.close(io);
if (!options.ignore_xattr and xattr_idx != null) {
const xattr = try xattr_table.get(alloc, io, xattr_idx.?);
defer xattr.deinit(alloc);
for (xattr.kvs) |kv| {
const res = std.os.linux.fsetxattr(fil.handle, kv.key, kv.value.ptr, kv.value.len, 0);
if (res != 0)
return error.SetXattrError;
}
}
if (!options.ignore_permissions) {
try fil.setTimestamps(io, .{
.modify_timestamp = .init(Io.Timestamp.fromNanoseconds(@as(i96, @intCast(inode.hdr.mod_time)) * std.time.ns_per_s)),
});
try fil.setPermissions(io, @enumFromInt(inode.hdr.permissions));
try fil.setOwner(io, try id_table.get(io, inode.hdr.uid_idx), try id_table.get(io, inode.hdr.gid_idx));
}
}
fn extractDir(
alloc: std.mem.Allocator,
io: Io,
super: Superblock,
data: []u8,
decomp: Decomp.Fn,
cache: *Cache,
frag_table: *Lookup.Table(Lookup.FragEntry),
id_table: *Lookup.Table(u16),
xattr_table: *XattrTable,
inode: Inode,
path: []const u8,
options: ExtractionOptions,
) !void {
var xattr_idx: ?u32 = null;
try Io.Dir.cwd().createDirPath(io, path);
var dir: Directory = switch (inode.data) {
.dir => |d| blk: {
var meta: MetadaReader = .init(alloc, data, decomp, d.block_start + super.dir_start);
try meta.interface.discardAll(d.block_offset);
break :blk try Directory.init(alloc, &meta.interface, d.size);
},
.ext_dir => |d| blk: {
if (d.xattr_idx != 0xFFFFFFFF) xattr_idx = d.xattr_idx;
var meta: MetadaReader = .init(alloc, data, decomp, d.block_start + super.dir_start);
try meta.interface.discardAll(d.block_offset);
break :blk try Directory.init(alloc, &meta.interface, d.size);
},
else => unreachable,
};
defer dir.deinit(alloc);
for (dir.entries) |entry| {
var new_inode: Inode = try .initEntry(alloc, data, decomp, super.inode_start, super.block_size, entry);
defer new_inode.deinit(alloc);
const new_path = try std.mem.concat(alloc, u8, &.{ path, "/", entry.name });
defer alloc.free(new_path);
switch (entry.type) {
.file => try extractFile(alloc, io, super, data, decomp, cache, frag_table, id_table, xattr_table, new_inode, new_path, options),
.dir => try extractDir(alloc, io, super, data, decomp, cache, frag_table, id_table, xattr_table, new_inode, new_path, options),
.symlink => try extractSymlink(io, new_inode, new_path),
else => try extractNod(alloc, io, id_table, xattr_table, new_inode, new_path, options),
}
}
try setMetadata(alloc, io, id_table, xattr_table, inode, path, options, xattr_idx);
}
fn extractFile(
alloc: std.mem.Allocator,
io: Io,
super: Superblock,
data: []u8,
decomp: Decomp.Fn,
cache: *Cache,
frag_table: *Lookup.Table(Lookup.FragEntry),
id_table: *Lookup.Table(u16),
xattr_table: *XattrTable,
inode: Inode,
path: []const u8,
options: ExtractionOptions,
) !void {
var xattr_idx: ?u32 = null;
var rdr: DataReader = switch (inode.data) {
.file => |f| blk: {
var rdr: DataReader = .init(alloc, data, decomp, super.block_size, f.blocks, f.size, f.block_start);
rdr.addCache(io, cache);
if (f.frag_idx == 0xFFFFFFFF) break :blk rdr;
const entry: Lookup.FragEntry = try frag_table.get(io, f.frag_idx);
if (entry.size.uncompressed) {
rdr.addFrag(data[entry.block_start..][0..entry.size.size], f.frag_offset);
} else {
rdr.addFrag(try cache.get(io, entry.block_start, entry.size.size), f.frag_offset);
}
break :blk rdr;
},
.ext_file => |f| blk: {
if (f.xattr_idx != 0xFFFFFFFF) xattr_idx = f.xattr_idx;
var rdr: DataReader = .init(alloc, data, decomp, super.block_size, f.blocks, f.size, f.block_start);
rdr.addCache(io, cache);
if (f.frag_idx == 0xFFFFFFFF) break :blk rdr;
const entry: Lookup.FragEntry = try frag_table.get(io, f.frag_idx);
if (entry.size.uncompressed) {
rdr.addFrag(data[entry.block_start..][0..entry.size.size], f.frag_offset);
} else {
rdr.addFrag(try cache.get(io, entry.block_start, entry.size.size), f.frag_offset);
}
break :blk rdr;
},
else => unreachable,
};
var atomic = try Io.Dir.cwd().createFileAtomic(io, path, .{});
defer atomic.deinit(io);
var writer = atomic.file.writer(io, &[0]u8{});
_ = try rdr.interface.streamRemaining(&writer.interface);
try writer.flush();
try atomic.link(io);
try setMetadata(alloc, io, id_table, xattr_table, inode, path, options, xattr_idx);
}
fn extractSymlink(io: Io, inode: Inode, path: []const u8) !void {
return switch (inode.data) {
.symlink => |s| Io.Dir.cwd().symLink(io, s.target, path, .{}),
.ext_symlink => |s| Io.Dir.cwd().symLink(io, s.target, path, .{}),
else => unreachable,
};
}
fn extractNod(
alloc: std.mem.Allocator,
io: Io,
id_table: *Lookup.Table(u16),
xattr_table: *XattrTable,
inode: Inode,
path: []const u8,
options: ExtractionOptions,
) !void {
var dev: u32 = 0;
var mode: u32 = undefined;
var xattr_idx: ?u32 = null;
const DT = std.posix.DT;
switch (inode.data) {
.char_dev => |d| {
dev = d.device;
mode = DT.CHR;
},
.block_dev => |d| {
dev = d.device;
mode = DT.BLK;
},
.ext_char_dev => |d| {
if (d.xattr_idx != 0xFFFFFFFF) xattr_idx = d.xattr_idx;
dev = d.device;
mode = DT.CHR;
},
.ext_block_dev => |d| {
if (d.xattr_idx != 0xFFFFFFFF) xattr_idx = d.xattr_idx;
dev = d.device;
mode = DT.BLK;
},
.fifo => mode = DT.FIFO,
.socket => mode = DT.SOCK,
.ext_fifo => |i| {
if (i.xattr_idx != 0xFFFFFFFF) xattr_idx = i.xattr_idx;
mode = DT.FIFO;
},
.ext_socket => |i| {
if (i.xattr_idx != 0xFFFFFFFF) xattr_idx = i.xattr_idx;
mode = DT.SOCK;
},
else => unreachable,
}
const sentinel_path = try alloc.dupeSentinel(u8, path, 0);
defer alloc.free(sentinel_path);
const res = std.os.linux.mknod(sentinel_path, mode, dev);
if (res != 0)
return error.MknodError;
try setMetadata(alloc, io, id_table, xattr_table, inode, path, options, xattr_idx);
}
+24
View File
@@ -0,0 +1,24 @@
const std = @import("std");
const Io = std.Io;
const Superblock = @import("archive.zig").Superblock;
const Decomp = @import("decomp.zig");
const Inode = @import("inode.zig");
const ExtractionOptions = @import("options.zig");
const Single = @import("extract-single.zig");
const Multi = @import("extract-multi.zig");
pub fn extract(
alloc: std.mem.Allocator,
io: Io,
super: Superblock,
data: []u8,
decomp: Decomp.Fn,
inode: Inode,
ext_loc: []const u8,
options: ExtractionOptions,
) !void {
if (options.single_threaded)
return Single.extract(alloc, io, super, data, decomp, inode, ext_loc, options);
return Multi.extract(alloc, io, super, data, decomp, inode, ext_loc, options);
}
+119 -177
View File
@@ -1,202 +1,144 @@
//! A file/directory within the squashfs archive. //! A squashfs file/directory. Basically a wrapper around an Inode.
const std = @import("std"); const std = @import("std");
const File = std.fs.File; const Io = std.Io;
const WaitGroup = std.Thread.WaitGroup;
const Mutex = std.Thread.Mutex;
const Archive = @import("archive.zig"); const Directory = @import("directory.zig");
const DirEntry = @import("dir_entry.zig");
const ExtractionOptions = @import("options.zig");
const Inode = @import("inode.zig"); const Inode = @import("inode.zig");
const BlockSize = @import("inode_data/file.zig").BlockSize; const MetadataReader = @import("meta_rdr.zig");
const DataReader = @import("util/data.zig"); const Decomp = @import("decomp.zig");
const MetadataReader = @import("util/metadata.zig"); const Superblock = @import("archive.zig").Superblock;
const ExtractionOptions = @import("options.zig");
const Extract = @import("extract.zig");
const FileError = error{ const File = @This();
NotDirectory,
NotRegularFile,
NotSymlink,
NotDevice,
NotFound,
ExtractionPathExists,
};
const SfsFile = @This(); alloc: std.mem.Allocator,
archive: *Archive, super: Superblock,
data: []u8,
decomp: Decomp.Fn,
inode: Inode,
name: []const u8, name: []const u8,
inode: Inode,
/// Initialize a new File. /// The given allocator should have been used to create the name and inode.
/// name is copied to the File so can be safely freed afterwards. pub fn init(alloc: std.mem.Allocator, super: Superblock, data: []u8, decomp: Decomp.Fn, name: []const u8, inode: Inode) File {
pub fn init(archive: *Archive, inode: Inode, name: []const u8) !SfsFile {
const new_name = try archive.allocator().alloc(u8, name.len);
@memcpy(new_name, name);
return .{ return .{
.archive = archive, .alloc = alloc,
.super = super,
.data = data,
.decomp = decomp,
.name = name,
.inode = inode, .inode = inode,
.name = new_name,
}; };
} }
pub fn fromEntry(archive: *Archive, entry: DirEntry) !SfsFile { /// The name from the directory entry will be duplicated so it's safe to deinit the entry.
var rdr = try archive.fil.readerAt(entry.block_start + archive.super.inode_start, &[0]u8{}); pub fn initEntry(alloc: std.mem.Allocator, super: Superblock, data: []u8, decomp: Decomp.Fn, entry: Directory.Entry) !File {
var meta: MetadataReader = .init(archive.allocator(), &rdr.interface, archive.decomp); const inode: Inode = try .initEntry(alloc, data, decomp, super.inode_start, super.block_size, entry);
try meta.interface.discardAll(entry.block_offset); errdefer inode.deinit(alloc);
const inode: Inode = try .read(archive.allocator(), &meta.interface, archive.super.block_size);
errdefer inode.deinit(archive.allocator()); const new_name = try alloc.dupe(u8, entry.name);
return .init(archive, inode, entry.name);
return init(alloc, super, data, decomp, new_name, inode);
}
/// The given name is will be duplicated so it's safe to free it afterwards.
pub fn initRef(alloc: std.mem.Allocator, super: Superblock, data: []u8, decomp: Decomp.Fn, name: []const u8, ref: Inode.Ref) !File {
const inode: Inode = try .initRef(alloc, data, decomp, super.inode_start, super.block_size, ref);
errdefer inode.deinit(alloc);
const new_name = try alloc.dupe(u8, name);
return init(alloc, super, data, decomp, new_name, inode);
}
pub fn copy(self: File, alloc: std.mem.Allocator) !File {
var inode = self.inode;
switch (inode.data) {
.file => |*f| f.blocks = try alloc.dupe(Inode.DataBlock, self.inode.data.file.blocks),
.ext_file => |*f| f.blocks = try alloc.dupe(Inode.DataBlock, self.inode.data.file.blocks),
.symlink => |*s| s.target = try alloc.dupe(u8, self.inode.data.symlink.target),
.ext_symlink => |*s| s.target = try alloc.dupe(u8, self.inode.data.symlink.target),
else => {},
}
errdefer inode.deinit(alloc);
const new_name = try alloc.dupe(u8, self.name);
return init(alloc, self.super, self.data, self.decomp, new_name, inode);
}
pub fn deinit(self: File) void {
self.alloc.free(self.name);
self.inode.deinit(self.alloc);
} }
pub fn deinit(self: SfsFile) void { /// If the given File is a directory, open a sub-file at the given filepath.
var alloc = self.archive.allocator(); /// If the given filepath resolves to the calling File ("" or "."), a copy of the File is returned.
alloc.free(self.name); pub fn open(self: File, alloc: std.mem.Allocator, filepath: []const u8) !File {
self.inode.deinit(alloc); var size: u64 = 0;
} var meta: MetadataReader = switch (self.inode.data) {
.dir => |d| blk: {
size = d.size;
fn getEntries(self: SfsFile) ![]DirEntry { var meta: MetadataReader = .init(alloc, self.data, self.decomp, self.super.dir_start + d.block_start);
return self.inode.dirEntries(self.archive.allocator(), self.archive.*); try meta.interface.discardAll(d.block_offset);
} break :blk meta;
pub fn ownerUid(self: SfsFile) !u16 {
return self.archive.id(self.inode.hdr.uid_idx);
}
pub fn ownerGid(self: SfsFile) !u16 {
return self.archive.id(self.inode.hdr.gid_idx);
}
pub fn permissions(self: SfsFile) u16 {
return self.inode.hdr.permissions;
}
pub fn isRegular(self: SfsFile) bool {
return switch (self.inode.hdr.inode_type) {
.file, .ext_file => true,
else => false,
};
}
/// The returned DataReader will no longer work if the File's deinit function is called
/// or, more specifically, it's inode's deinit function is called.
pub fn dataReader(self: SfsFile) !DataReader {
return self.inode.dataReader(self.archive);
}
pub fn isDir(self: SfsFile) bool {
return switch (self.inode.hdr.inode_type) {
.dir, .ext_dir => true,
else => false,
};
}
pub fn iterate(self: SfsFile) !Iterator {
if (!self.isDir()) return FileError.NotDirectory;
return .{
.entries = try self.getEntries(),
.archive = self.archive,
};
}
/// 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, alloc: std.mem.Allocator, path: []const u8) !SfsFile {
if (!self.isDir()) return FileError.NotDirectory;
if (pathIsSelf(path)) return self;
// Recursively stip ending & leading path separators.
if (path[0] == '/') return self.open(path[1..]);
if (path[path.len - 1] == '/') return self.open(path[0 .. path.len - 1]);
const idx = std.mem.indexOf(u8, path, "/") orelse path.len;
const first_element = path[0..idx];
if (std.mem.eql(u8, first_element, ".")) return self.open(path[idx + 1 ..]);
const entries = try self.getEntries();
defer {
for (entries) |e| {
e.deinit(alloc);
}
alloc.free(entries);
}
var cur_slice = entries;
var split = cur_slice.len / 2;
while (cur_slice.len > 0) {
split = cur_slice.len / 2;
const comp = std.mem.order(u8, first_element, cur_slice[split].name);
switch (comp) {
.eq => {
var fil: SfsFile = try .fromEntry(self.archive, cur_slice[split]);
if (idx == path.len) {
return fil;
}
defer fil.deinit();
return fil.open(alloc, path[idx + 1 ..]);
}, },
.lt => cur_slice = cur_slice[0..split], .ext_dir => |d| blk: {
.gt => cur_slice = cur_slice[split + 1 ..], size = d.size;
}
}
return FileError.NotFound;
}
pub fn isSymlink(self: SfsFile) bool { var meta: MetadataReader = .init(alloc, self.data, self.decomp, self.super.dir_start + d.block_start);
return switch (self.inode.hdr.inode_type) { try meta.interface.discardAll(d.block_offset);
.symlink, .ext_symlink => true, break :blk meta;
else => false, },
else => return error.NotDirectory,
}; };
const path = std.mem.trim(u8, filepath, "/");
if (path.len == 0 or (path.len == 1 and path[0] == '.'))
return self.copy(alloc);
const first_element: []const u8 = std.mem.sliceTo(path, '/');
const file: File = blk: {
var directory: Directory = try .init(alloc, &meta.interface, size);
defer directory.deinit(alloc);
var entries = directory.entries;
var idx = entries.len / 2;
while (entries.len > 0) {
switch (std.mem.order(u8, first_element, entries[idx].name)) {
.eq => break,
.lt => entries = entries[0..idx],
.gt => entries = entries[idx..],
} }
pub fn symlinkPath(self: SfsFile) ![]const u8 { idx = entries.len / 2;
if (!self.isSymlink()) return FileError.NotSymlink; } else {
return switch (self.inode.data) { return error.NotFound;
.symlink => |s| s.target, }
.ext_symlink => |s| s.target,
else => unreachable, break :blk try initEntry(alloc, self.super, self.data, self.decomp, entries[idx]);
}; };
if (first_element.len == path.len)
return file;
defer file.deinit();
return file.open(alloc, path[first_element.len..]);
} }
/// Check if the File is a block or character device. pub fn extract(self: File, alloc: std.mem.Allocator, io: Io, location: []const u8, options: ExtractionOptions) !void {
pub fn isDevice(self: SfsFile) bool { return Extract.extract(
return switch (self.inode.hdr.inode_type) { alloc,
.block_dev, .char_dev, .ext_block_dev, .ext_char_dev => true, io,
else => false, self.super,
}; self.data,
self.decomp,
self.inode,
location,
options,
);
} }
/// If the File is a block or character device, get's it's device number.
pub fn devNum(self: SfsFile) !u32 {
if (!self.isDevice()) return FileError.NotDevice;
return switch (self.inode.data) {
.block_dev, .char_dev => |d| d.dev,
.ext_block_dev, .ext_char_dev => |d| d.dev,
else => unreachable,
};
}
/// 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, alloc: std.mem.Allocator, path: []const u8, options: ExtractionOptions) !void {
return self.inode.extractToThreaded(alloc, self.archive, path, options);
}
/// Utility function.
pub fn pathIsSelf(path: []const u8) bool {
if (path.len == 0) return true;
if (path.len == 1 and (path[0] == '/' or path[0] == '.')) return true;
if (path.len == 2 and (path[0] == '.' and path[1] == '/')) return true;
return false;
}
pub const Iterator = struct {
entries: []DirEntry,
archive: *Archive,
idx: u32 = 0,
pub fn next(self: *Iterator) !?SfsFile {
if (self.idx >= self.entries.len) return null;
defer self.idx += 1;
return try SfsFile.fromEntry(self.archive, self.entries[self.idx]);
}
pub fn deinit(self: Iterator) void {
var alloc = self.archive.allocator();
for (self.entries) |e| {
e.deinit(alloc);
}
alloc.free(self.entries);
}
};
+238 -481
View File
@@ -1,28 +1,76 @@
//! A file-system object. Represents a File or directory.
const std = @import("std"); const std = @import("std");
const Reader = std.Io.Reader; const Io = std.Io;
const WaitGroup = std.Thread.WaitGroup; const Reader = Io.Reader;
const Pool = std.Thread.Pool;
const Mutex = std.Thread.Mutex;
const Archive = @import("archive.zig"); const Decomp = @import("decomp.zig");
const DirEntry = @import("dir_entry.zig"); const Directory = @import("directory.zig");
const MetadataReader = @import("meta_rdr.zig");
const Superblock = @import("archive.zig").Superblock;
const Extract = @import("extract.zig");
const ExtractionOptions = @import("options.zig"); const ExtractionOptions = @import("options.zig");
const dir = @import("inode_data/dir.zig");
const file = @import("inode_data/file.zig");
const misc = @import("inode_data/misc.zig");
const DataReader = @import("util/data.zig");
const ThreadedDataReader = @import("util/data_threaded.zig");
const MetadataReader = @import("util/metadata.zig");
pub const Ref = packed struct { const Inode = @This();
hdr: Header,
data: Data,
pub fn init(alloc: std.mem.Allocator, block_size: u32, rdr: *Reader) !Inode {
var hdr: Header = undefined;
try rdr.readSliceEndian(Header, @ptrCast(&hdr), .little);
const data: Data = switch (hdr.type) {
.dir => .{ .dir = try .init(rdr) },
.ext_dir => .{ .ext_dir = try .init(rdr) },
.file => .{ .file = try .init(alloc, rdr, block_size) },
.ext_file => .{ .ext_file = try .init(alloc, rdr, block_size) },
.symlink => .{ .symlink = try .init(alloc, rdr) },
.ext_symlink => .{ .ext_symlink = try .init(alloc, rdr) },
.block_dev => .{ .block_dev = try .init(rdr) },
.ext_block_dev => .{ .ext_block_dev = try .init(rdr) },
.char_dev => .{ .char_dev = try .init(rdr) },
.ext_char_dev => .{ .ext_char_dev = try .init(rdr) },
.fifo => .{ .fifo = try .init(rdr) },
.ext_fifo => .{ .ext_fifo = try .init(rdr) },
.socket => .{ .socket = try .init(rdr) },
.ext_socket => .{ .ext_socket = try .init(rdr) },
};
return .{ .hdr = hdr, .data = data };
}
pub fn initRef(alloc: std.mem.Allocator, data: []u8, decomp: Decomp.Fn, inode_start: u64, block_size: u32, ref: Ref) !Inode {
var meta: MetadataReader = .init(alloc, data, decomp, inode_start + ref.block_start);
try meta.interface.discardAll(ref.block_offset);
return .init(alloc, block_size, &meta.interface);
}
pub fn initEntry(alloc: std.mem.Allocator, data: []u8, decomp: Decomp.Fn, inode_start: u64, block_size: u32, entry: Directory.Entry) !Inode {
var meta: MetadataReader = .init(alloc, data, decomp, inode_start + entry.block_start);
try meta.interface.discardAll(entry.block_offset);
return .init(alloc, block_size, &meta.interface);
}
pub fn deinit(self: Inode, alloc: std.mem.Allocator) void {
switch (self.data) {
.file => |f| alloc.free(f.blocks),
.ext_file => |f| alloc.free(f.blocks),
.symlink => |s| alloc.free(s.target),
.ext_symlink => |s| alloc.free(s.target),
else => {},
}
}
pub fn extract(self: Inode, alloc: std.mem.Allocator, io: Io, super: Superblock, data: []u8, decomp: Decomp.Fn, location: []const u8, options: ExtractionOptions) !void {
return Extract.extract(alloc, io, super, data, decomp, self, location, options);
}
// Types
pub const Ref = packed struct(u64) {
block_offset: u16, block_offset: u16,
block_start: u32, block_start: u32,
_: u16, _: u16,
}; };
pub const InodeType = enum(u16) { pub const Type = enum(u16) {
dir = 1, dir = 1,
file, file,
symlink, symlink,
@@ -39,25 +87,8 @@ pub const InodeType = enum(u16) {
ext_socket, ext_socket,
}; };
pub const InodeData = union(InodeType) { pub const Header = extern struct {
dir: dir.Dir, type: Type,
file: file.File,
symlink: misc.Symlink,
block_dev: misc.Dev,
char_dev: misc.Dev,
fifo: misc.IPC,
socket: misc.IPC,
ext_dir: dir.ExtDir,
ext_file: file.ExtFile,
ext_symlink: misc.ExtSymlink,
ext_block_dev: misc.ExtDev,
ext_char_dev: misc.ExtDev,
ext_fifo: misc.ExtIPC,
ext_socket: misc.ExtIPC,
};
pub const Header = packed struct {
inode_type: InodeType,
permissions: u16, permissions: u16,
uid_idx: u16, uid_idx: u16,
gid_idx: u16, gid_idx: u16,
@@ -65,475 +96,201 @@ pub const Header = packed struct {
num: u32, num: u32,
}; };
const Inode = @This(); pub const Data = union(Type) {
dir: Dir,
file: File,
symlink: Symlink,
block_dev: Dev,
char_dev: Dev,
fifo: IPC,
socket: IPC,
ext_dir: ExtDir,
ext_file: ExtFile,
ext_symlink: ExtSymlink,
ext_block_dev: ExtDev,
ext_char_dev: ExtDev,
ext_fifo: ExtIPC,
ext_socket: ExtIPC,
};
hdr: Header, pub const DataBlock = packed struct(u32) {
data: InodeData, size: u24,
uncompressed: bool,
_: u7,
};
pub const Dir = extern struct {
block_start: u32,
hard_links: u32,
size: u16,
block_offset: u16,
parent_num: u32,
fn init(rdr: *Reader) !Dir {
var new: Dir = undefined;
try rdr.readSliceEndian(Dir, @ptrCast(&new), .little);
return new;
}
};
pub const ExtDir = extern struct {
hard_links: u32,
size: u32,
block_start: u32,
parent_num: u32,
idx_count: u16,
block_offset: u16,
xattr_idx: u32,
fn init(rdr: *Reader) !ExtDir {
var new: ExtDir = undefined;
try rdr.readSliceEndian(ExtDir, @ptrCast(&new), .little);
return new;
}
};
pub const File = struct {
block_start: u32,
frag_idx: u32,
frag_offset: u32,
size: u32,
blocks: []DataBlock,
fn init(alloc: std.mem.Allocator, rdr: *Reader, block_size: u32) !File {
var data: [16]u8 = undefined;
try rdr.readSliceAll(&data);
const frag_idx = std.mem.readInt(u32, data[4..8], .little);
const size = std.mem.readInt(u32, data[12..], .little);
var blocks_num = size / block_size;
if (size % block_size != 0 and frag_idx == 0xFFFFFFFF)
blocks_num += 1;
const blocks = try alloc.alloc(DataBlock, blocks_num);
errdefer alloc.free(blocks);
try rdr.readSliceEndian(DataBlock, blocks, .little);
pub fn read(alloc: std.mem.Allocator, rdr: *Reader, block_size: u32) !Inode {
var hdr: Header = undefined;
try rdr.readSliceEndian(Header, @ptrCast(&hdr), .little);
return .{ return .{
.hdr = hdr, .block_start = std.mem.readInt(u32, data[0..4], .little),
.data = switch (hdr.inode_type) { .frag_idx = frag_idx,
.dir => .{ .dir = try .read(rdr) }, .frag_offset = std.mem.readInt(u32, data[8..12], .little),
.file => .{ .file = try .read(alloc, rdr, block_size) }, .size = size,
.symlink => .{ .symlink = try .read(alloc, rdr) }, .blocks = blocks,
.block_dev => .{ .block_dev = try .read(rdr) },
.char_dev => .{ .char_dev = try .read(rdr) },
.fifo => .{ .fifo = try .read(rdr) },
.socket => .{ .socket = try .read(rdr) },
.ext_dir => .{ .ext_dir = try .read(rdr) },
.ext_file => .{ .ext_file = try .read(alloc, rdr, block_size) },
.ext_symlink => .{ .ext_symlink = try .read(alloc, rdr) },
.ext_block_dev => .{ .ext_block_dev = try .read(rdr) },
.ext_char_dev => .{ .ext_char_dev = try .read(rdr) },
.ext_fifo => .{ .ext_fifo = try .read(rdr) },
.ext_socket => .{ .ext_socket = try .read(rdr) },
},
}; };
} }
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{}); pub const ExtFile = struct {
var meta: MetadataReader = .init(alloc, &rdr.interface, archive.decomp); block_start: u64,
try meta.interface.discardAll(entry.block_offset); size: u64,
return read(alloc, &meta.interface, archive.super.block_size); sparse: u64,
} hard_links: u32,
frag_idx: u32,
frag_offset: u32,
xattr_idx: u32,
blocks: []DataBlock,
pub fn deinit(self: Inode, alloc: std.mem.Allocator) void { fn init(alloc: std.mem.Allocator, rdr: *Reader, block_size: u32) !ExtFile {
switch (self.data) { var data: [40]u8 = undefined;
.file => |f| alloc.free(f.block_sizes), try rdr.readSliceAll(&data);
.ext_file => |f| alloc.free(f.block_sizes),
.symlink => |s| alloc.free(s.target),
.ext_symlink => |s| alloc.free(s.target),
else => {},
}
}
/// Get the data reader for a file inode. const frag_idx = std.mem.readInt(u32, data[28..32], .little);
pub fn dataReader(self: Inode, alloc: std.mem.Allocator, archive: *Archive) !DataReader { const size = std.mem.readInt(u64, data[8..16], .little);
return switch (self.hdr.inode_type) {
.file => readerFromData(alloc, archive, self.data.file), var blocks_num = size / block_size;
.ext_file => readerFromData(alloc, archive, self.data.ext_file), if (size % block_size != 0 and frag_idx == 0xFFFFFFFF)
else => error.NotRegularFile, blocks_num += 1;
const blocks = try alloc.alloc(DataBlock, blocks_num);
errdefer alloc.free(blocks);
try rdr.readSliceEndian(DataBlock, blocks, .little);
return .{
.block_start = std.mem.readInt(u64, data[0..8], .little),
.size = size,
.sparse = std.mem.readInt(u64, data[16..24], .little),
.hard_links = std.mem.readInt(u32, data[24..28], .little),
.frag_idx = frag_idx,
.frag_offset = std.mem.readInt(u32, data[32..36], .little),
.xattr_idx = std.mem.readInt(u32, data[36..], .little),
.blocks = blocks,
}; };
} }
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); pub const Symlink = struct {
if (data.frag_idx != 0xFFFFFFFF) hard_links: u32,
out.addFragment(try archive.frag_table.get(data.frag_idx), data.frag_block_offset); target: []const u8,
return out;
} fn init(alloc: std.mem.Allocator, rdr: *Reader) !Symlink {
/// Get a threaded data reader for a file inode. var data: [8]u8 = undefined;
pub fn threadedDataReader(self: Inode, alloc: std.mem.Allocator, archive: *Archive) !ThreadedDataReader { try rdr.readSliceAll(&data);
return switch (self.hdr.inode_type) {
.file => threadedReaderFromData(alloc, archive, self.data.file), const target_size = std.mem.readInt(u32, data[4..], .little);
.ext_file => threadedReaderFromData(alloc, archive, self.data.ext_file),
else => error.NotRegularFile, const target = try alloc.alloc(u8, target_size);
errdefer alloc.free(target);
try rdr.readSliceEndian(u8, target, .little);
return .{
.hard_links = std.mem.readInt(u32, data[0..4], .little),
.target = target,
}; };
} }
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); pub const ExtSymlink = struct {
if (data.frag_idx != 0xFFFFFFFF) hard_links: u32,
out.addFragment(try archive.frag_table.get(data.frag_idx), data.frag_block_offset); target: []const u8,
return out; xattr_idx: u32,
}
/// Get the directory entries for a directory inode. fn init(alloc: std.mem.Allocator, rdr: *Reader) !ExtSymlink {
pub fn dirEntries(self: Inode, alloc: std.mem.Allocator, archive: Archive) ![]DirEntry { const sym: Symlink = try .init(alloc, rdr);
return switch (self.hdr.inode_type) { errdefer alloc.free(sym.target);
.dir => entriesFromData(alloc, archive, self.data.dir),
.ext_dir => entriesFromData(alloc, archive, self.data.ext_dir), var xattr_idx: u32 = undefined;
else => error.NotDirectory, try rdr.readSliceEndian(u32, @ptrCast(&xattr_idx), .little);
return .{
.hard_links = sym.hard_links,
.target = sym.target,
.xattr_idx = xattr_idx,
}; };
} }
fn entriesFromData(alloc: std.mem.Allocator, archive: Archive, data: anytype) ![]DirEntry {
var rdr = try archive.fil.readerAt(archive.super.dir_start + data.block_start, &[0]u8{});
var meta: MetadataReader = .init(alloc, &rdr.interface, archive.decomp);
try meta.interface.discardAll(data.block_offset);
return DirEntry.readDir(alloc, &meta.interface, data.size);
}
/// Extract the inode to the given path. Single threaded.
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(alloc, archive, path[0 .. path.len - 1], options);
std.fs.cwd().makeDir(path) catch |err| {
if (err != std.fs.Dir.MakeError.PathAlreadyExists) return err;
}; };
const entries = try self.dirEntries(alloc, archive.*); pub const Dev = extern struct {
defer { hard_links: u32,
for (entries) |entry| entry.deinit(alloc); device: u32,
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); fn init(rdr: *Reader) !Dev {
defer inode.deinit(alloc); var new: Dev = undefined;
try inode.extractTo(alloc, archive, new_path, options); try rdr.readSliceEndian(Dev, @ptrCast(&new), .little);
} return new;
},
.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);
}
} }
}; };
pub const ExtDev = extern struct {
hard_links: u32,
device: u32,
xattr_idx: u32,
/// Extract the inode to the given path. Multi-threaded. fn init(rdr: *Reader) !ExtDev {
/// Functions identically to extractTo on all but regular files and directories. var new: ExtDev = undefined;
/// try rdr.readSliceEndian(ExtDev, @ptrCast(&new), .little);
/// If threads <= 1, then this just calls extractTo. return new;
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); pub const IPC = extern struct {
@memcpy(new_path[path.len + 1 ..], entry.name); hard_links: u32,
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. fn init(rdr: *Reader) !IPC {
fn extractThread( var new: IPC = undefined;
self: Inode, try rdr.readSliceEndian(IPC, @ptrCast(&new), .little);
alloc: std.mem.Allocator, return new;
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;
}; };
pub const ExtIPC = extern struct {
hard_links: u32,
xattr_idx: u32,
const entries = self.dirEntries(alloc, archive.*) catch |err| { fn init(rdr: *Reader) !ExtIPC {
if (options.verbose) var new: ExtIPC = undefined;
options.verbose_writer.?.print("Error getting directory entries for inode #{} (extracting to {s}): {}\n", .{ self.hdr.num, path, err }) catch {}; try rdr.readSliceEndian(ExtIPC, @ptrCast(&new), .little);
out_err.* = err; return new;
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;
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));
}
}
/// 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) {
// TODO
}
}
-32
View File
@@ -1,32 +0,0 @@
const Reader = @import("std").Io.Reader;
pub const Dir = packed struct {
block_start: u32,
hard_links: u32,
size: u16,
block_offset: u16,
parent_num: u32,
pub fn read(rdr: *Reader) !Dir {
var d: Dir = undefined;
try rdr.readSliceEndian(Dir, @ptrCast(&d), .little);
return d;
}
};
pub const ExtDir = packed struct {
hard_links: u32,
size: u32,
block_start: u32,
parent_num: u32,
idx_count: u16,
block_offset: u16,
xattr_id: u32,
// index: []DirIndex
pub fn read(rdr: *Reader) !ExtDir {
var d: ExtDir = undefined;
try rdr.readSliceEndian(Dir, @ptrCast(&d), .little);
return d;
}
};
-76
View File
@@ -1,76 +0,0 @@
const std = @import("std");
const Reader = std.Io.Reader;
pub const BlockSize = packed struct {
size: u24,
uncompressed: bool,
_: u7,
};
pub const File = struct {
block_start: u32, // bytes 0-3
frag_idx: u32, // bytes 4-7
frag_block_offset: u32, // bytes 8-11
size: u32, // bytes 12-15
block_sizes: []BlockSize,
pub fn read(alloc: std.mem.Allocator, rdr: *Reader, block_size: u32) !File {
var start: [16]u8 = undefined;
try rdr.readSliceAll(&start);
const frag_idx: u32 = std.mem.readInt(u32, start[4..8], .little);
const size: u32 = std.mem.readInt(u32, start[12..16], .little);
var num_blocks: u32 = size / block_size;
if (size % block_size != 0 and frag_idx == 0xFFFFFFFF) num_blocks += 1;
const sizes = try alloc.alloc(BlockSize, num_blocks);
errdefer alloc.free(sizes);
try rdr.readSliceEndian(BlockSize, sizes, .little);
return .{
.block_start = std.mem.readInt(u32, start[0..4], .little),
.frag_idx = frag_idx,
.frag_block_offset = std.mem.readInt(u32, start[8..12], .little),
.size = size,
.block_sizes = sizes,
};
}
pub fn deinit(self: File, alloc: std.mem.Allocator) void {
alloc.free(self.block_sizes);
}
};
pub const ExtFile = struct {
block_start: u64, // bytes 0-7
size: u64, // bytes 8-15
sparse: u64, // bytes 16-23
hard_links: u32, // bytes 24-27
frag_idx: u32, // bytes 28-31
frag_block_offset: u32, // bytes 32-35
xattr_idx: u32, // bytes 36-39
block_sizes: []BlockSize,
pub fn read(alloc: std.mem.Allocator, rdr: *Reader, block_size: u32) !ExtFile {
var start: [40]u8 = undefined;
try rdr.readSliceAll(&start);
const frag_idx: u32 = std.mem.readInt(u32, start[28..32], .little);
const size: u64 = std.mem.readInt(u64, start[8..16], .little);
var num_blocks: u32 = @truncate(size / block_size);
if (size % block_size != 0 and frag_idx == 0xFFFFFFFF) num_blocks += 1;
const sizes = try alloc.alloc(BlockSize, num_blocks);
errdefer alloc.free(sizes);
try rdr.readSliceEndian(BlockSize, sizes, .little);
return .{
.block_start = std.mem.readInt(u64, start[0..8], .little),
.size = size,
.sparse = std.mem.readInt(u64, start[16..24], .little),
.hard_links = std.mem.readInt(u32, start[24..28], .little),
.frag_idx = frag_idx,
.frag_block_offset = std.mem.readInt(u32, start[32..36], .little),
.xattr_idx = std.mem.readInt(u32, start[36..40], .little),
.block_sizes = sizes,
};
}
pub fn deinit(self: ExtFile, alloc: std.mem.Allocator) void {
alloc.free(self.block_sizes);
}
};
-98
View File
@@ -1,98 +0,0 @@
const std = @import("std");
const Reader = std.Io.Reader;
pub const Symlink = struct {
hard_links: u32,
target: []const u8,
pub fn read(alloc: std.mem.Allocator, rdr: *Reader) !Symlink {
var start: [8]u8 = undefined;
try rdr.readSliceAll(&start);
const target_size = std.mem.readInt(u32, start[4..8], .little);
const target = try alloc.alloc(u8, target_size + 1);
errdefer alloc.free(target);
try rdr.readSliceEndian(u8, target, .little);
return .{
.hard_links = std.mem.readInt(u32, start[0..4], .little),
.target = target,
};
}
pub fn deinit(self: Symlink, alloc: std.mem.Allocator) void {
alloc.free(self.target);
}
};
pub const ExtSymlink = struct {
hard_links: u32,
target: []const u8,
xattr_idx: u32,
pub fn read(alloc: std.mem.Allocator, rdr: *Reader) !ExtSymlink {
var start: [8]u8 = undefined;
try rdr.readSliceAll(&start);
const target_size = std.mem.readInt(u32, start[4..8], .little);
const target = try alloc.alloc(u8, target_size + 1);
errdefer alloc.free(target);
try rdr.readSliceEndian(u8, target, .little);
var xattr_idx: u32 = undefined;
try rdr.readSliceEndian(u32, @ptrCast(&xattr_idx), .little);
return .{
.hard_links = std.mem.readInt(u32, start[0..4], .little),
.target = target,
.xattr_idx = xattr_idx,
};
}
pub fn deinit(self: ExtSymlink, alloc: std.mem.Allocator) void {
alloc.free(self.target);
}
};
/// A block or character device.
pub const Dev = packed struct {
hard_links: u32,
dev: u32,
pub fn read(rdr: *Reader) !Dev {
var d: Dev = undefined;
try rdr.readSliceEndian(Dev, @ptrCast(&d), .little);
return d;
}
};
/// An extended block or character device.
pub const ExtDev = packed struct {
hard_links: u32,
dev: u32,
xattr_idx: u32,
pub fn read(rdr: *Reader) !ExtDev {
var d: ExtDev = undefined;
try rdr.readSliceEndian(ExtDev, @ptrCast(&d), .little);
return d;
}
};
/// A socket or FIFO file.
pub const IPC = packed struct {
hard_links: u32,
pub fn read(rdr: *Reader) !IPC {
var d: IPC = undefined;
try rdr.readSliceEndian(IPC, @ptrCast(&d), .little);
return d;
}
};
/// An extended socket or FIFO file.
pub const ExtIPC = packed struct {
hard_links: u32,
xattr_idx: u32,
pub fn read(rdr: *Reader) !ExtIPC {
var d: ExtIPC = undefined;
try rdr.readSliceEndian(ExtIPC, @ptrCast(&d), .little);
return d;
}
};
+83
View File
@@ -0,0 +1,83 @@
const std = @import("std");
const Io = std.Io;
const DataBlock = @import("inode.zig").DataBlock;
const Decomp = @import("decomp.zig");
const MetadataReader = @import("meta_rdr.zig");
const ProtectedMap = @import("util/protected_map.zig").ProtectedMap;
pub fn Table(comptime T: anytype) type {
return struct {
const VALUES_PER_BLOCK = 8192 / @sizeOf(T);
const Self = @This();
data: []u8,
decomp: Decomp.Fn,
table_start: u64,
table_num: u32,
table: ProtectedMap(u32, []T, getBlock),
pub fn init(alloc: std.mem.Allocator, data: []u8, decomp: Decomp.Fn, table_start: u64, table_num: u32) Self {
return .{
.data = data,
.decomp = decomp,
.table_start = table_start,
.table_num = table_num,
.table = .init(alloc),
};
}
pub fn deinit(self: *Self) void {
var iter = self.table.map.valueIterator();
while (iter.next()) |val| {
if (val.err != null) continue;
self.table.alloc.free(val.value);
}
self.table.deinit();
}
pub fn get(self: *Self, io: Io, idx: u32) !T {
const block = idx / VALUES_PER_BLOCK;
const block_idx = idx % VALUES_PER_BLOCK;
const values = try self.table.getOrPut(io, block, .{
self.table.alloc,
self.data,
self.decomp,
self.table_start,
self.table_num,
block,
});
return values.*[block_idx];
}
fn getBlock(alloc: std.mem.Allocator, data: []u8, decomp: Decomp.Fn, table_start: u64, table_num: u32, block_idx: u32) ![]T {
const offset: u64 = std.mem.readInt(u64, data[table_start + (block_idx * 8) ..][0..8], .little);
const block = try alloc.alloc(T, if (block_idx == (table_num - 1) / VALUES_PER_BLOCK)
table_num % VALUES_PER_BLOCK
else
VALUES_PER_BLOCK);
var meta: MetadataReader = .init(alloc, data, decomp, offset);
try meta.interface.readSliceEndian(T, block, .little);
return block;
}
};
}
// Types
pub const FragEntry = extern struct {
block_start: u64,
size: DataBlock,
_: u32,
};
+114
View File
@@ -0,0 +1,114 @@
const std = @import("std");
const Io = std.Io;
const Reader = Io.Reader;
const Writer = Io.Writer;
const Limit = Io.Limit;
const Decomp = @import("decomp.zig");
const MetadataReader = @This();
alloc: std.mem.Allocator,
data: []u8,
decomp: Decomp.Fn,
cur_offset: u64,
block: [8192]u8 = undefined,
interface: Reader = .{
.buffer = &[0]u8{},
.end = 0,
.seek = 0,
.vtable = &.{
.stream = stream,
.discard = discard,
.readVec = readVec,
},
},
pub fn init(alloc: std.mem.Allocator, data: []u8, decomp: Decomp.Fn, offset: u64) MetadataReader {
return .{
.alloc = alloc,
.data = data,
.decomp = decomp,
.cur_offset = offset,
};
}
fn advance(self: *MetadataReader) Reader.Error!void {
self.interface.seek = 0;
errdefer self.interface.end = 0;
const hdr: Header = @bitCast(std.mem.readInt(u16, self.data[self.cur_offset..][0..2], .little));
if (hdr.size == 0 or hdr.size > 8192) return Reader.Error.ReadFailed;
defer self.cur_offset += hdr.size + 2;
const block = self.data[self.cur_offset + 2 ..][0..hdr.size];
if (hdr.uncompressed) {
self.interface.end = hdr.size;
self.interface.buffer = block;
return;
}
const decomp_size = self.decomp(self.alloc, block, &self.block) catch return Reader.Error.ReadFailed;
self.interface.buffer = self.block[0..decomp_size];
self.interface.end = decomp_size;
}
fn stream(r: *Reader, w: *Writer, limit: Limit) Reader.StreamError!usize {
if (r.seek >= r.end) {
const self: *MetadataReader = @fieldParentPtr("interface", r);
try self.advance();
}
if (limit == .nothing) return 0;
const to_write = @min(@intFromEnum(limit), r.end - r.seek);
const wrote = try w.write(r.buffer[r.seek..][0..to_write]);
r.seek += wrote;
return wrote;
}
fn discard(r: *Reader, limit: Limit) Reader.Error!usize {
if (r.seek >= r.end) {
const self: *MetadataReader = @fieldParentPtr("interface", r);
try self.advance();
}
if (limit == .nothing) return 0;
const to_discard = @min(@intFromEnum(limit), r.end - r.seek);
r.seek += to_discard;
return to_discard;
}
fn readVec(r: *Reader, vec: [][]u8) Reader.Error!usize {
if (r.seek >= r.end) {
const self: *MetadataReader = @fieldParentPtr("interface", r);
try self.advance();
}
var wrote: usize = 0;
for (vec) |v| {
const to_cpy = @min(v.len, r.end - r.seek);
@memcpy(v[0..to_cpy], r.buffer[r.seek..][0..to_cpy]);
wrote += to_cpy;
r.seek += to_cpy;
if (r.seek >= r.end) break;
}
return wrote;
}
// Types
const Header = packed struct(u16) {
size: u15,
uncompressed: bool,
};
+5 -8
View File
@@ -5,8 +5,8 @@ const Writer = std.Io.Writer;
const ExtractionOptions = @This(); const ExtractionOptions = @This();
/// The number of threads used for extraction. 0 implies single threaded. /// Whether to use single threaded extraction. For finer control, create an std.Io instance.
threads: usize = 1, single_threaded: bool = false,
/// Don't set the file's owner & permissions after extraction /// Don't set the file's owner & permissions after extraction
ignore_permissions: bool = false, ignore_permissions: bool = false,
/// Don't set xattr values. Currently xattrs are never set anyway. /// Don't set xattr values. Currently xattrs are never set anyway.
@@ -18,12 +18,9 @@ verbose: bool = false,
/// Where to print verbose log. /// Where to print verbose log.
verbose_writer: ?*Writer = null, verbose_writer: ?*Writer = null,
pub const SingleThreadedDefault: ExtractionOptions = .{}; pub const default: ExtractionOptions = .{};
pub fn Default() !ExtractionOptions { pub const single_threaded_default: ExtractionOptions = .{ .single_threaded = true };
return .{
.threads = try std.Thread.getCpuCount(),
};
}
pub fn VerboseDefault(wrt: *Writer) !ExtractionOptions { pub fn VerboseDefault(wrt: *Writer) !ExtractionOptions {
return .{ return .{
.verbose = true, .verbose = true,
+5
View File
@@ -1,2 +1,7 @@
pub const Archive = @import("archive.zig"); pub const Archive = @import("archive.zig");
pub const File = @import("file.zig");
pub const ExtractionOptions = @import("options.zig"); pub const ExtractionOptions = @import("options.zig");
test {
@import("std").testing.refAllDecls(@import("test.zig"));
}
-63
View File
@@ -1,63 +0,0 @@
const std = @import("std");
const math = std.math;
const CompressionType = @import("decomp.zig").CompressionType;
const InodeRef = @import("inode.zig").Ref;
const SQUASHFS_MAGIC: u32 = std.mem.readInt(u32, "hsqs", .little);
const SuperblockError = error{
InvalidMagic,
InvalidBlockLog,
InvalidVersion,
InvalidCheck,
};
/// A squashfs Superblock
pub const Superblock = packed struct {
magic: u32,
inode_count: u32,
mod_time: u32,
block_size: u32,
frag_count: u32,
compression: CompressionType,
block_log: u16,
flags: packed struct {
inode_uncompressed: bool,
data_uncompressed: bool,
check: bool,
frag_uncompressed: bool,
fragment_never: bool,
fragment_always: bool,
duplicates: bool,
exportable: bool,
xattr_uncompressed: bool,
xattr_never: bool,
compression_options: bool,
ids_uncompressed: bool,
_: u4,
},
id_count: u16,
ver_maj: u16,
ver_min: u16,
root_ref: InodeRef,
size: u64,
id_start: u64,
xattr_start: u64,
inode_start: u64,
dir_start: u64,
frag_start: u64,
export_start: u64,
/// Validate the Superblock. If an error is returned, it's likely the archive is corrupted or not a squashfs archive.
pub fn validate(self: Superblock) !void {
if (self.magic != SQUASHFS_MAGIC)
return SuperblockError.InvalidMagic;
if (self.flags.check)
return SuperblockError.InvalidCheck;
if (self.ver_maj != 4 or self.ver_min != 0)
return SuperblockError.InvalidVersion;
if (math.log2(self.block_size) != self.block_log)
return SuperblockError.InvalidBlockLog;
}
};
-77
View File
@@ -1,77 +0,0 @@
const std = @import("std");
const Mutex = std.Thread.Mutex;
const DecompFn = @import("decomp.zig").DecompFn;
const MetadataReader = @import("util/metadata.zig");
const OffsetFile = @import("util/offset_file.zig");
const TableError = error{
InvalidIndex,
};
/// A two-layer metadata table.
pub fn Table(T: anytype) type {
return struct {
const Self = @This();
const VALS_PER_BLOCK = 8192 / @sizeOf(T);
alloc: std.mem.Allocator,
fil: OffsetFile,
decomp: DecompFn,
tab_start: u64,
tab: std.AutoHashMap(u32, []T),
values: u32,
mut: Mutex = .{},
pub fn init(alloc: std.mem.Allocator, fil: OffsetFile, decomp: DecompFn, tab_start: u64, values: u32) !Self {
return .{
.alloc = alloc,
.fil = fil,
.decomp = decomp,
.tab_start = tab_start,
.tab = .init(alloc),
.values = values,
};
}
pub fn deinit(self: *Self) void {
var iter = self.tab.valueIterator();
while (iter.next()) |s| {
self.alloc.free(s.*);
}
self.tab.deinit();
}
pub fn get(self: *Self, idx: u32) !T {
if (idx >= self.values) return TableError.InvalidIndex;
const block_num = idx / VALS_PER_BLOCK;
const idx_offset = idx - (block_num * VALS_PER_BLOCK);
if (self.tab.contains(block_num)) {
const block = self.tab.get(block_num).?;
return block[idx_offset];
}
self.mut.lock();
defer self.mut.unlock();
// Double check in case of race condition..
if (self.tab.contains(block_num)) {
const block = self.tab.get(block_num).?;
return block[idx_offset];
}
const is_last = (self.values - 1) / VALS_PER_BLOCK == block_num;
const slice_size = if (is_last) self.values - (block_num * VALS_PER_BLOCK) else VALS_PER_BLOCK;
const slice = try self.alloc.alloc(T, slice_size);
var rdr = try self.fil.readerAt(self.tab_start + (8 * block_num), &[0]u8{});
var offset: u64 = 0;
try rdr.interface.readSliceEndian(u64, @ptrCast(&offset), .little);
rdr = try self.fil.readerAt(offset, &[0]u8{});
var meta: MetadataReader = .init(self.alloc, &rdr.interface, self.decomp);
try meta.interface.readSliceEndian(T, @ptrCast(slice), .little);
try self.tab.put(block_num, slice);
return slice[idx_offset];
}
};
}
+98 -29
View File
@@ -1,49 +1,118 @@
const std = @import("std"); const std = @import("std");
const stuff = @import("builtin"); const Io = std.Io;
const testing = std.testing;
const Archive = @import("archive.zig"); const Archive = @import("archive.zig");
const Superblock = @import("super.zig").Superblock;
const TestArchive = "testing/LinuxPATest.sfs"; const TestArchive = "testing/LinuxPATest.sfs";
test "Basics" { test "Basics" {
var fil = try std.fs.cwd().openFile(TestArchive, .{}); const io = testing.io;
defer fil.close(); const alloc = testing.allocator;
var sfs: Archive = try .init(std.testing.allocator, fil);
defer sfs.deinit(); var archive_file = try Io.Dir.cwd().openFile(io, TestArchive, .{});
if (sfs.super != LinuxPATestCorrectSuperblock) { defer archive_file.close(io);
std.debug.print("Superblock wrong\nShould be: {}\n\nis: {}\n", .{ LinuxPATestCorrectSuperblock, sfs.super });
return error.BadSuperblock; var archive: Archive = try .init(io, archive_file, 0);
} defer archive.deinit(io);
try testing.expectEqualDeep(archive.super, LinuxPATestCorrectSuperblock);
var root = try archive.root(alloc);
defer root.deinit();
} }
const TestFile = "Start.exe"; const TestFile = "Start.exe";
const TestFileExtractLocation = "testing/Start.exe"; const TestFileExtractLocation = "testing/Start.exe";
test "ExtractSingleFile" { test "ExtractSingleFileMT" {
std.fs.cwd().deleteFile(TestFileExtractLocation) catch {}; const io = testing.io;
var fil = try std.fs.cwd().openFile(TestArchive, .{}); const alloc = testing.allocator;
defer fil.close();
var sfs: Archive = try .init(std.testing.allocator, fil); Io.Dir.cwd().deleteFile(io, TestFileExtractLocation) catch {};
defer sfs.deinit();
var test_fil = try sfs.open(TestFile); var archive_file = try Io.Dir.cwd().openFile(io, TestArchive, .{});
defer test_fil.deinit(); defer archive_file.close(io);
try test_fil.extract(TestFileExtractLocation, .Default);
//TODO: validate extracted file. var archive: Archive = try .init(io, archive_file, 0);
defer archive.deinit(io);
var start_exe = try archive.open(alloc, TestFile);
defer start_exe.deinit();
try start_exe.extract(alloc, io, TestFileExtractLocation, .default);
} }
const TestFullExtractLocation = "testing/TestExtract"; test "ExtractSingleFileST" {
const io = testing.io;
const alloc = testing.allocator;
test "ExtractCompleteArchive" { Io.Dir.cwd().deleteFile(io, TestFileExtractLocation) catch {};
std.fs.cwd().deleteTree(TestFullExtractLocation) catch {};
var fil = try std.fs.cwd().openFile(TestArchive, .{}); var archive_file = try Io.Dir.cwd().openFile(io, TestArchive, .{});
defer fil.close(); defer archive_file.close(io);
var sfs: Archive = try .init(std.testing.allocator, fil);
defer sfs.deinit(); var archive: Archive = try .init(io, archive_file, 0);
try sfs.extract(TestFullExtractLocation, .Default); defer archive.deinit(io);
var start_exe = try archive.open(alloc, TestFile);
defer start_exe.deinit();
try start_exe.extract(alloc, io, TestFileExtractLocation, .single_threaded_default);
} }
const LinuxPATestCorrectSuperblock: Superblock = .{ const TestFullExtractLocationSTOption = "testing/TestExtractSTOption";
test "ExtractCompleteArchiveSingleThreadedOption" {
const io = testing.io;
const alloc = testing.allocator;
Io.Dir.cwd().deleteTree(io, TestFullExtractLocationSTOption) catch {};
var archive_file = try Io.Dir.cwd().openFile(io, TestArchive, .{});
defer archive_file.close(io);
var archive: Archive = try .init(io, archive_file, 0);
defer archive.deinit(io);
try archive.extract(alloc, io, TestFullExtractLocationSTOption, .single_threaded_default);
}
const TestFullExtractLocationMT = "testing/TestExtractMT";
test "ExtractCompleteArchiveMultiThreaded" {
const io = testing.io;
const alloc = testing.allocator;
Io.Dir.cwd().deleteTree(io, TestFullExtractLocationMT) catch {};
var archive_file = try Io.Dir.cwd().openFile(io, TestArchive, .{});
defer archive_file.close(io);
var archive: Archive = try .init(io, archive_file, 0);
defer archive.deinit(io);
try archive.extract(alloc, io, TestFullExtractLocationMT, .default);
}
const TestFullExtractLocationSTIo = "testing/TestExtractSTIo";
// test "ExtractCompleteArchiveSingleThreadedIo" {
// const io = Io.Threaded.global_single_threaded.io();
// const alloc = testing.allocator;
// Io.Dir.cwd().deleteFile(io, TestFullExtractLocationSTIo) catch {};
// var archive_file = try Io.Dir.cwd().openFile(io, TestArchive, .{});
// defer archive_file.close(io);
// var archive: Archive = try .init(io, archive_file, 0);
// defer archive.deinit(io);
// try archive.extract(alloc, io, TestFullExtractLocationSTIo, .default);
// }
const LinuxPATestCorrectSuperblock: Archive.Superblock = .{
.magic = std.mem.readInt(u32, "hsqs", .little), .magic = std.mem.readInt(u32, "hsqs", .little),
.inode_count = 2974, .inode_count = 2974,
.mod_time = 1632696724, .mod_time = 1632696724,
+52
View File
@@ -0,0 +1,52 @@
const std = @import("std");
const Io = std.Io;
const Decomp = @import("../decomp.zig");
const CacheMap = @import("protected_map.zig").ProtectedMap(u64, CachedBlock, decompressBlock);
const Cache = @This();
alloc: std.mem.Allocator,
data: []u8,
decomp: Decomp.Fn,
cache: CacheMap,
pub fn init(alloc: std.mem.Allocator, data: []u8, decomp: Decomp.Fn) Cache {
return .{
.alloc = alloc,
.data = data,
.decomp = decomp,
.cache = .init(alloc),
};
}
pub fn deinit(self: *Cache) void {
self.cache.deinit();
}
pub fn get(self: *Cache, io: Io, offset: u64, compressed_size: u32) Error![]u8 {
const res: *CachedBlock = try self.cache.getOrPut(io, offset, .{ self.alloc, self.data, self.decomp, offset, compressed_size });
return res.block[0..res.size];
}
fn decompressBlock(alloc: std.mem.Allocator, data: []u8, decomp: Decomp.Fn, offset: u64, compressed_size: u32) !CachedBlock {
const block = data[offset..][0..compressed_size];
var new_cache: CachedBlock = undefined;
new_cache.size = @truncate(try decomp(alloc, block, &new_cache.block));
return new_cache;
}
// Types
pub const Error = CacheMap.Error;
const CachedBlock = struct {
block: [1024 * 1024]u8,
size: u32,
};
-174
View File
@@ -1,174 +0,0 @@
//! A reader for a regular file.
const std = @import("std");
const Reader = std.Io.Reader;
const Writer = std.Io.Writer;
const Limit = std.Io.Limit;
const Archive = @import("../archive.zig");
const FragEntry = Archive.FragEntry;
const DecompFn = @import("../decomp.zig").DecompFn;
const BlockSize = @import("../inode_data/file.zig").BlockSize;
const OffsetFile = @import("offset_file.zig");
const DataReader = @This();
alloc: std.mem.Allocator,
fil: OffsetFile,
decomp: DecompFn,
block_size: u32,
blocks: []BlockSize,
frag: ?FragEntry = null, // TODO: do something better?
frag_offset: u32 = 0,
size: u64,
interface: Reader,
cur_offset: u64,
block_idx: u32 = 0,
pub fn init(alloc: std.mem.Allocator, archive: Archive, blocks: []BlockSize, start: u64, size: u64) DataReader {
return .{
.alloc = alloc,
.fil = archive.fil,
.decomp = archive.decomp,
.block_size = archive.super.block_size,
.blocks = blocks,
.size = size,
.cur_offset = start,
.interface = .{
.end = 0,
.seek = 0,
.buffer = &[0]u8{},
.vtable = &.{
.stream = stream,
.discard = discard,
.readVec = readVec,
},
},
};
}
pub fn deinit(self: *DataReader) void {
self.alloc.free(self.interface.buffer);
self.interface.end = 0;
self.interface.seek = 0;
}
pub fn addFragment(self: *DataReader, entry: FragEntry, frag_offset: u32) void {
self.frag = entry;
self.frag_offset = frag_offset;
}
fn numBlocks(self: DataReader) usize {
var res = self.blocks.len;
if (self.frag != null) res += 1;
return res;
}
fn advance(self: *DataReader) !void {
if (self.block_idx > self.blocks.len or (self.block_idx == self.blocks.len and self.frag == null)) {
if (self.interface.buffer.len > 0) {
self.alloc.free(self.interface.buffer);
self.interface.buffer = &[0]u8{};
self.interface.end = 0;
self.interface.seek = 0;
}
return Reader.Error.EndOfStream;
}
defer self.block_idx += 1;
const cur_block_size = if (self.block_idx == self.numBlocks() - 1) self.size % self.block_size else self.block_size;
try self.resizeBuffer(cur_block_size);
self.interface.seek = 0;
self.interface.end = cur_block_size;
if (self.block_idx == self.blocks.len) { // fragment
var rdr = try self.fil.readerAt(self.frag.?.start, &[0]u8{});
if (self.frag.?.size.uncompressed) {
try rdr.interface.discardAll(self.frag_offset);
try rdr.interface.readSliceAll(self.interface.buffer);
return;
}
const tmp_buf = try self.alloc.alloc(u8, self.frag.?.size.size);
defer self.alloc.free(tmp_buf);
try rdr.interface.readSliceAll(tmp_buf);
const needed_block = try self.alloc.alloc(u8, self.block_size);
defer self.alloc.free(needed_block);
_ = try self.decomp(self.alloc, tmp_buf, needed_block);
@memcpy(self.interface.buffer, needed_block[self.frag_offset .. self.frag_offset + cur_block_size]);
return;
}
const block = self.blocks[self.block_idx];
if (block.size == 0) {
@memset(self.interface.buffer, 0);
return;
}
var rdr = try self.fil.readerAt(self.cur_offset, &[0]u8{});
self.cur_offset += block.size;
if (block.uncompressed) {
try rdr.interface.readSliceAll(self.interface.buffer);
return;
}
const tmp_buf = try self.alloc.alloc(u8, block.size);
defer self.alloc.free(tmp_buf);
try rdr.interface.readSliceAll(tmp_buf);
_ = try self.decomp(self.alloc, tmp_buf, self.interface.buffer);
}
/// Does not guarentee that data currently in the buffer is retained.
fn resizeBuffer(self: *DataReader, size: usize) !void {
if (self.interface.buffer.len == size) return;
if (!self.alloc.resize(self.interface.buffer, size)) {
self.alloc.free(self.interface.buffer);
self.interface.buffer = self.alloc.alloc(u8, size) catch |err| {
self.interface.buffer = &[0]u8{};
return err;
};
} else {
self.interface.buffer.len = size;
}
}
fn stream(rdr: *Reader, wrt: *Writer, limit: Limit) Reader.StreamError!usize {
var self: *DataReader = @alignCast(@fieldParentPtr("interface", rdr));
if (rdr.seek >= rdr.end) self.advance() catch |err| {
if (err == error.EndOfStream) return error.EndOfStream;
std.log.err("Error advancing data reader: {}\n", .{err});
return Reader.Error.ReadFailed;
};
if (limit == .nothing) return 0;
const to_read = @min(rdr.end - rdr.seek, @intFromEnum(limit));
const res = try wrt.write(rdr.buffer[rdr.seek .. rdr.seek + to_read]);
rdr.seek += res;
return res;
}
fn discard(rdr: *Reader, limit: Limit) Reader.Error!usize {
var self: *DataReader = @alignCast(@fieldParentPtr("interface", rdr));
if (rdr.seek >= rdr.end) self.advance() catch |err| {
if (err == error.EndOfStream) return error.EndOfStream;
std.log.err("Error advancing data reader: {}\n", .{err});
return Reader.Error.ReadFailed;
};
if (limit == .nothing) return 0;
const to_adv = @min(rdr.end - rdr.seek, @intFromEnum(limit));
rdr.seek += to_adv;
return to_adv;
}
fn readVec(rdr: *Reader, vec: [][]u8) Reader.Error!usize {
var self: *DataReader = @alignCast(@fieldParentPtr("interface", rdr));
if (rdr.seek >= rdr.end) self.advance() catch |err| {
if (err == error.EndOfStream) return error.EndOfStream;
std.log.err("Error advancing data reader: {}\n", .{err});
return Reader.Error.ReadFailed;
};
var cur_red: usize = 0;
for (vec) |s| {
const to_copy: usize = @min(rdr.end - rdr.seek, s.len);
@memcpy(s[0..to_copy], rdr.buffer[rdr.seek .. rdr.seek + to_copy]);
rdr.seek += to_copy;
cur_red += to_copy;
if (rdr.end == rdr.seek) break;
}
return cur_red;
}
-183
View File
@@ -1,183 +0,0 @@
//! Similiar to DataReader, but set-up for threaded writing to files.
const std = @import("std");
const Reader = std.Io.Reader;
const Writer = std.Io.Writer;
const Limit = std.Io.Limit;
const WaitGroup = std.Thread.WaitGroup;
const Pool = std.Thread.Pool;
const Archive = @import("../archive.zig");
const FragEntry = Archive.FragEntry;
const DecompFn = @import("../decomp.zig").DecompFn;
const BlockSize = @import("../inode_data/file.zig").BlockSize;
const OffsetFile = @import("offset_file.zig");
const ThreadedDataReader = @This();
alloc: std.mem.Allocator,
fil: OffsetFile,
decomp: DecompFn,
block_size: u32,
blocks: []BlockSize,
frag: ?FragEntry = null, // TODO: do something better?
frag_offset: u32 = 0,
size: u64,
start_offset: u64,
pub fn init(alloc: std.mem.Allocator, archive: Archive, blocks: []BlockSize, start: u64, size: u64) ThreadedDataReader {
return .{
.alloc = alloc,
.fil = archive.fil,
.decomp = archive.decomp,
.block_size = archive.super.block_size,
.blocks = blocks,
.size = size,
.start_offset = start,
};
}
pub fn addFragment(self: *ThreadedDataReader, entry: FragEntry, frag_offset: u32) void {
self.frag = entry;
self.frag_offset = frag_offset;
}
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.
/// If multiple errors occur, thread spawning errors will have, then the last decompression error that occurs;
///
/// 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_read_offset: u64 = self.start_offset;
for (0..self.blocks.len) |i| {
const cur_block_size = if (i == self.numBlocks() - 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 });
cur_write_offset += cur_block_size;
cur_read_offset += self.blocks[i].size;
}
if (self.frag != null) {
try pool.spawn(workThreadFragment, .{ self, file, cur_write_offset, &wg, &out_err });
}
pool.waitAndWork(&wg);
if (out_err != null) return out_err.?;
}
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 {
defer wg.finish();
var wrt = fil.writer(&[0]u8{});
wrt.seekTo(write_offset) catch |err| {
out_err.* = err;
return;
};
defer wrt.interface.flush() catch |err| {
out_err.* = err;
};
if (block.size == 0) {
wrt.interface.splatByteAll(0, cur_block_size) catch |err| {
out_err.* = err;
return;
};
return;
}
var rdr = self.fil.readerAt(read_offset, &[0]u8{}) catch |err| {
out_err.* = err;
return;
};
if (block.uncompressed) {
rdr.interface.streamExact(&wrt.interface, block.size) catch |err| {
out_err.* = err;
return;
};
return;
}
// TODO: shared buffers
const read_buf = self.alloc.alloc(u8, block.size) catch |err| {
out_err.* = err;
return;
};
defer self.alloc.free(read_buf);
rdr.interface.readSliceAll(read_buf) catch |err| {
out_err.* = err;
return;
};
// TODO: shared buffers
const res_buf = self.alloc.alloc(u8, cur_block_size) catch |err| {
out_err.* = err;
return;
};
defer self.alloc.free(res_buf);
_ = self.decomp(self.alloc, read_buf, res_buf) catch |err| {
out_err.* = err;
return;
};
wrt.interface.writeAll(res_buf) catch |err| {
out_err.* = err;
return;
};
}
fn workThreadFragment(self: ThreadedDataReader, fil: std.fs.File, write_offset: u64, wg: *WaitGroup, out_err: *?anyerror) void {
defer wg.finish();
var wrt = fil.writer(&[0]u8{});
wrt.seekTo(write_offset) catch |err| {
out_err.* = err;
return;
};
defer wrt.interface.flush() catch |err| {
out_err.* = err;
};
var rdr = self.fil.readerAt(self.frag.?.start, &[0]u8{}) catch |err| {
out_err.* = err;
return;
};
if (self.frag.?.size.uncompressed) {
rdr.interface.discardAll(self.frag_offset) catch |err| {
out_err.* = err;
return;
};
rdr.interface.streamExact(&wrt.interface, self.size % self.block_size) catch |err| {
out_err.* = err;
return;
};
return;
}
const tmp_buf = self.alloc.alloc(u8, self.frag.?.size.size) catch |err| {
out_err.* = err;
return;
};
defer self.alloc.free(tmp_buf);
rdr.interface.readSliceAll(tmp_buf) catch |err| {
out_err.* = err;
return;
};
const needed_block = self.alloc.alloc(u8, self.block_size) catch |err| {
out_err.* = err;
return;
};
defer self.alloc.free(needed_block);
_ = self.decomp(self.alloc, tmp_buf, needed_block) catch |err| {
out_err.* = err;
return;
};
wrt.interface.writeAll(needed_block[self.frag_offset .. self.frag_offset + (self.size % self.block_size)]) catch |err| {
out_err.* = err;
return;
};
}
-97
View File
@@ -1,97 +0,0 @@
const std = @import("std");
const Reader = std.Io.Reader;
const Writer = std.Io.Writer;
const Limit = std.Io.Limit;
const StreamError = std.Io.Reader.StreamError;
const DecompFn = @import("../decomp.zig").DecompFn;
const BlockHeader = packed struct {
size: u15,
uncompressed: bool,
};
const This = @This();
alloc: std.mem.Allocator,
rdr: *Reader,
decomp: DecompFn,
buf: [8192]u8 = undefined,
interface: Reader,
err: ?anyerror = null,
pub fn init(alloc: std.mem.Allocator, rdr: *Reader, decomp: DecompFn) This {
return .{
.alloc = alloc,
.rdr = rdr,
.decomp = decomp,
.interface = .{
.buffer = &[0]u8{},
.end = 0,
.seek = 0,
.vtable = &.{
.stream = stream,
.discard = discard,
.readVec = readVec,
},
},
};
}
fn advance(self: *This) !void {
self.interface.seek = 0;
var hdr: BlockHeader = undefined;
try self.rdr.readSliceEndian(BlockHeader, @ptrCast(&hdr), .little);
if (hdr.uncompressed) {
try self.rdr.readSliceEndian(u8, self.buf[0..hdr.size], .little);
self.interface.end = hdr.size;
self.interface.buffer = self.buf[0..hdr.size];
return;
}
var tmp_buf: [8192]u8 = undefined;
try self.rdr.readSliceAll(tmp_buf[0..hdr.size]);
self.interface.end = try self.decomp(self.alloc, tmp_buf[0..hdr.size], &self.buf);
self.interface.buffer = self.buf[0..self.interface.end];
}
fn stream(rdr: *Reader, wrt: *Writer, limit: Limit) StreamError!usize {
const self: *This = @fieldParentPtr("interface", rdr);
if (rdr.end == rdr.seek) self.advance() catch |err| {
self.err = err;
return StreamError.ReadFailed;
};
if (@intFromEnum(limit) == 0) return 0;
const to_write = @min(rdr.end - rdr.seek, @intFromEnum(limit));
const wrote = try wrt.write(self.buf[rdr.seek .. rdr.seek + to_write]);
self.interface.seek += wrote;
return wrote;
}
fn discard(rdr: *Reader, limit: Limit) error{ EndOfStream, ReadFailed }!usize {
const self: *This = @fieldParentPtr("interface", rdr);
if (rdr.end == rdr.seek) self.advance() catch |err| {
self.err = err;
return StreamError.ReadFailed;
};
if (@intFromEnum(limit) == 0) return 0;
const to_skip = @min(rdr.end - rdr.seek, @intFromEnum(limit));
rdr.seek += to_skip;
return to_skip;
}
fn readVec(rdr: *Reader, vec: [][]u8) error{ EndOfStream, ReadFailed }!usize {
const self: *This = @fieldParentPtr("interface", rdr);
if (rdr.end == rdr.seek) self.advance() catch |err| {
self.err = err;
return StreamError.ReadFailed;
};
var cur_red: usize = 0;
for (vec) |s| {
const to_copy: usize = @min(rdr.end - rdr.seek, s.len);
@memcpy(s[0..to_copy], self.buf[rdr.seek .. rdr.seek + to_copy]);
rdr.seek += to_copy;
cur_red += to_copy;
if (rdr.end == rdr.seek) break;
}
return cur_red;
}
-20
View File
@@ -1,20 +0,0 @@
//! A File where it's meaningful (to us) content starts at a given offset.
const std = @import("std");
const File = std.fs.File;
const Reader = std.fs.File.Reader;
const OffsetFile = @This();
fil: File,
offset: u64,
pub fn init(fil: File, init_offset: u64) OffsetFile {
return .{ .fil = fil, .offset = init_offset };
}
pub fn readerAt(self: OffsetFile, offset: u64, buffer: []u8) !Reader {
var rdr = self.fil.reader(buffer);
try rdr.seekTo(self.offset + offset);
return rdr;
}
+91
View File
@@ -0,0 +1,91 @@
const std = @import("std");
const Io = std.Io;
pub fn ProtectedMap(comptime K: anytype, comptime T: anytype, comptime create_fn: anytype) type {
const fn_info = @typeInfo(@TypeOf(create_fn));
std.debug.assert(std.meta.activeTag(fn_info) == .@"fn");
const ret_info = fn_info.@"fn".return_type;
std.debug.assert(ret_info != null);
std.debug.assert(ret_info == T or
(std.meta.activeTag(@typeInfo(ret_info.?)) == .error_union and @typeInfo(ret_info.?).error_union.payload == T));
return struct {
const Map = @This();
alloc: std.mem.Allocator,
map: std.AutoHashMap(K, ProtectedValue),
mut: Io.RwLock = .init,
pub fn init(alloc: std.mem.Allocator) Map {
return .{
.alloc = alloc,
.map = .init(alloc),
};
}
pub fn deinit(self: *Map) void {
self.map.deinit();
}
pub fn getOrPut(self: *Map, io: Io, key: K, create_fn_args: std.meta.ArgsTuple(@TypeOf(create_fn))) Error!*T {
{
try self.mut.lockShared(io);
defer self.mut.unlockShared(io);
const value = self.map.getPtr(key);
if (value != null) {
if (!value.?.filled.isSet())
try value.?.filled.wait(io);
if (value.?.err != null) return value.?.err.?;
return &value.?.value;
}
}
try self.mut.lock(io);
const res = self.map.getOrPut(key) catch |err| {
self.mut.unlock(io);
return err;
};
if (res.found_existing) {
self.mut.unlock(io);
return self.getOrPut(io, key, create_fn_args);
}
res.value_ptr.* = .{};
defer res.value_ptr.filled.set(io);
self.mut.unlock(io);
self.mut.lockSharedUncancelable(io);
defer self.mut.unlockShared(io);
res.value_ptr.value = if (@TypeOf(CreateError) == void)
@call(.auto, create_fn, create_fn_args)
else
@call(.auto, create_fn, create_fn_args) catch |err| {
res.value_ptr.err = err;
return err;
};
return &res.value_ptr.value;
}
// Map Types
pub const Error = error{ Canceled, OutOfMemory } ||
if (@TypeOf(CreateError) == void) error{} else CreateError;
const CreateError: type = switch (@typeInfo(ret_info.?)) {
.error_union => |e| e.error_set,
else => void,
};
const ProtectedValue = struct {
value: T = undefined,
err: ?CreateError = null,
filled: Io.Event = .unset,
};
};
}
+134
View File
@@ -0,0 +1,134 @@
const std = @import("std");
const Io = std.Io;
const Decomp = @import("decomp.zig");
const Lookup = @import("lookup.zig");
const MetadataReader = @import("meta_rdr.zig");
const Ref = @import("inode.zig").Ref;
const XattrTable = @This();
table_start: u64,
table: Lookup.Table(Entry),
pub fn init(alloc: std.mem.Allocator, data: []u8, decomp: Decomp.Fn, table_start: u64) XattrTable {
const start = std.mem.readInt(u64, data[table_start..][0..8], .little);
const table_num = std.mem.readInt(u32, data[table_start + 8 ..][0..4], .little);
return .{
.table_start = start,
.table = .init(alloc, data, decomp, table_start + 16, table_num),
};
}
pub fn deinit(self: *XattrTable) void {
self.table.deinit();
}
pub fn get(self: *XattrTable, alloc: std.mem.Allocator, io: Io, idx: u32) !Xattr {
const entry: Entry = try self.table.get(io, idx);
var out: std.ArrayList(KeyValue) = try .initCapacity(alloc, entry.count);
errdefer {
for (out.items) |kv|
kv.deinit(alloc);
out.deinit(alloc);
}
var meta: MetadataReader = .init(alloc, self.table.data, self.table.decomp, self.table_start + entry.ref.block_start);
try meta.interface.discardAll(entry.ref.block_offset);
for (0..entry.count) |_| {
var key_entry: KeyEntry = undefined;
try meta.interface.readSliceEndian(KeyEntry, @ptrCast(&key_entry), .little);
const prefix = switch (key_entry.type.type) {
.user => "user.",
.trusted => "trusted.",
.security => "security.",
};
const key = try alloc.allocSentinel(u8, key_entry.name_size + prefix.len, 0);
errdefer alloc.free(key);
@memcpy(key[0..prefix.len], prefix);
try meta.interface.readSliceEndian(u8, key[prefix.len..][0..key_entry.name_size], .little);
const value = if (key_entry.type.out_of_line) blk: {
try meta.interface.discardAll(4);
var ref: Ref = undefined;
try meta.interface.readSliceEndian(Ref, @ptrCast(&ref), .little);
var value_meta: MetadataReader = .init(alloc, self.table.data, self.table.decomp, self.table_start + ref.block_start);
try value_meta.interface.discardAll(ref.block_offset);
break :blk try readValue(alloc, &value_meta.interface);
} else try readValue(alloc, &meta.interface);
const ptr = out.addOneAssumeCapacity();
ptr.* = .{
.key = key,
.value = value,
};
}
return .{ .kvs = try out.toOwnedSlice(alloc) };
}
fn readValue(alloc: std.mem.Allocator, rdr: *Io.Reader) ![]u8 {
var size: u32 = undefined;
try rdr.readSliceEndian(u32, @ptrCast(&size), .little);
const value = try alloc.alloc(u8, size);
errdefer alloc.free(value);
try rdr.readSliceEndian(u8, value, .little);
return value;
}
// Types
const Xattr = struct {
kvs: []KeyValue,
pub fn deinit(self: Xattr, alloc: std.mem.Allocator) void {
for (self.kvs) |kv|
kv.deinit(alloc);
alloc.free(self.kvs);
}
};
const KeyValue = struct {
key: [:0]const u8,
value: []const u8,
pub fn deinit(self: KeyValue, alloc: std.mem.Allocator) void {
alloc.free(self.key);
alloc.free(self.value);
}
};
const KeyEntry = extern struct {
type: packed struct(u16) {
type: enum(u8) {
user,
trusted,
security,
},
out_of_line: bool,
_: u7,
},
name_size: u16,
};
const Entry = extern struct {
ref: Ref,
count: u32,
size: u32,
};
View File