46 Commits

Author SHA1 Message Date
Caleb J. Gardner b28508a9c2 Work on mutli-threaded extraction 2026-07-25 03:16:06 -05:00
Caleb J. Gardner d3adb35401 Fixed some bugs with mutli-threaded extraction 2026-07-24 06:55:01 -05:00
Caleb Gardner 33f85f959e Some minor work with mutli-threaded extraction 2026-07-18 06:25:35 -05:00
Caleb Gardner 28f7d1fe28 Finished (probably not actually) mutli-threaded extraction 2026-07-11 00:47:39 -05:00
Caleb Gardner 7058f737c2 Re-writing much of multi-threaded extraction 2026-07-10 06:10:45 -05:00
Caleb J. Gardner 58b465bca2 Some work on re-factoring multi-threaded extraction 2026-07-03 18:08:22 -05:00
Caleb J. Gardner c238274cc7 Trying to fix race conditions... 2026-06-26 20:40:23 -05:00
Caleb J. Gardner 2de9a6a664 Finished re-writing multi-threaded extraction 2026-06-22 07:04:41 -05:00
Caleb Gardner 86c9829a23 Re-working multi-threaded extraction 2026-06-20 06:34:33 -05:00
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
Caleb J. Gardner 760e11df0b Fixed github workflow 2026-02-28 22:38:47 -06:00
Caleb J. Gardner 5f629df47c alloc-ified many functions.
Updated README
2026-02-28 22:33:57 -06:00
Caleb J. Gardner b0160e005b Some fixes 2026-02-17 05:54:32 -06:00
40 changed files with 2646 additions and 2134 deletions
+8 -8
View File
@@ -13,19 +13,19 @@ jobs:
uses: actions/checkout@v6
- uses: mlugg/setup-zig@v2
- 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
run: zig build -Drelease=true -Dversion=${{ github.ref_name }}
- name: Move normal build out
run: mv zig-out/bin/unsquashfs ./
run: zig build --release=fast -Duse_zig_decomp=true -Dversion=${{ github.ref_name }}
- name: Move zig build out
run: mv zig-out/bin/unsquashfs ./unsquashfs-x86_64-zig-libs
- name: Rebuild with C libraries
run: zig build -Drelease=true -Duse_c_libs=true -Dversion="${{ github.ref_name }}"
run: zig build --release=fast -Dversion="${{ github.ref_name }}"
- name: Move C build out
run: mv zig-out/bin/unsquashfs ./unsquashfs-c-libs
run: mv zig-out/bin/unsquashfs ./unsquashfs-x86_64-c-libs
- name: Release
uses: softprops/action-gh-release@v2
with:
prerelease: true
files: |
unsquashfs
unsquashfs-c-libs
unsquashfs-x86_64-zig-libs
unsquashfs-x86_64-c-libs
+1
View File
@@ -2,3 +2,4 @@ testing/
.zig-cache/
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",
],
},
]
+29 -7
View File
@@ -6,19 +6,23 @@ A library and application to decompress or view squashfs archives.
## Current State
Overall works, but currently is missing some features (see below). Extraction is a bit slow compared to the normal `unsquashfs` (from my _very_ basic testing it's about ~3x slower). Only properly work on Linux, any other OSes probably won't work fully and are untested.
Overall works, but currently is missing some features ([see below](#capabilities)) and has significantly slow performance compared to `unsquashfs` ([see below](#performance)).
## Build options
> `-Duse_c_libs`
> `-Duse_c_libs=true`
Instead of using Zig's standard library for decompression, use the system's C libraries. Has the benefit of being much faster and enabling LZO and LZ4 decompression.
> `-Dallow_lzo`
> `-Dallow_lzo=true`
Enable compiling with LZO decompression support. The LZO library currently has some issues with Zig when imported so it's easier to just disable it by default. Only has an effect when using `-Duse_c_libs=true`.
> `-Dversion`
> `-Ddebug=true`
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`
Sets the version of `unsquashfs` shown when `--version` is passed.
@@ -26,11 +30,29 @@ Sets the version of `unsquashfs` shown when `--version` is passed.
Most features are present except for the following:
* mod_time is not set on extraction
* xattrs are not applied on extraction
* When using Zig decompression libraries then lzo and lz4 compression types are unavailable. I don't _currently_ plan on spending the time to find and validate a library since neither is popular.
* When using C decompression libraries, lzo is not supported by default due to [some issues](#build-considerations). If it's needed it's trivial to fix, but it's easiest to just leave it disabled.
## Building considerations
## Performance
This is some basic observation's I've made about this library's performance when compared to `unsquashfs`. Unless otherwise stated, most observations were made when extracting my test archive (which is fairly small and uses zstd compression) and with `--release=fast`.
Currently, my only performance checks are checking execution time, nothing deeper.
* 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.
Example Times:
* *unsquashfs, multi-threaded*: .12s
* *unsquashfs, single-threaded*: .13s
* *C-libs, single-threaded*: .45s
* *C-libs, multi-threaded*: .20s
* *Zig-libs, single-threaded*: 5.78s
* *Zig-libs, multi-threaded*: 1.08s
## Build considerations
Compilation without `use_c_libs` works completely fine, but Zig has issues with some symbols from the lzo library that needs to be manually fixed. In particular you need to fix the definitions for `lzo_bytep` and `lzo_voidp` to be `*u8` and `?*anyopaque` respectively. Due to this, you have to manually enable LZO decompression using `-Dallow_lzo=true` when building.
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"
+104 -58
View File
@@ -1,79 +1,125 @@
const std = @import("std");
pub fn build(b: *std.Build) !void {
const use_c_libs_option = b.option(bool, "use_c_libs", "Use C versions of decompression libraries instead of the Zig standard library ones");
const allow_lzo = b.option(bool, "allow_lzo", "Compile with lzo support");
const 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 optimize = b.standardOptimizeOption(.{});
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseFast });
const mod = b.addModule("zig_squashfs", .{
.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,
.link_libc = if (use_c_libs_option == true) true else false,
.target = target,
.root_source_file = b.path("c.h"),
});
mod.addOptions("config", zig_squashfs_options);
if (use_c_libs_option == true) {
mod.linkSystemLibrary("zlib", .{});
mod.linkSystemLibrary("lzma", .{});
if (allow_lzo == true)
mod.linkSystemLibrary("minilzo", .{});
mod.linkSystemLibrary("lz4", .{});
mod.linkSystemLibrary("zstd", .{});
if (allow_lzo) c.defineCMacro("ALLOW_LZO", null);
if (!use_zig_decomp and dynamic) {
c.linkSystemLibrary("z", .{});
c.linkSystemLibrary("lzma", .{});
c.linkSystemLibrary("lz4", .{});
c.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 = if (use_c_libs_option == true) true else false,
.imports = &.{
.{ .name = "zig_squashfs", .module = mod },
},
});
exe_mod.addOptions("config", unsquashfs_options);
const exe = b.addExecutable(.{
.name = "unsquashfs",
.root_module = exe_mod,
});
const lib = b.addLibrary(.{
.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(exe);
const run_step = b.step("run", "Run the app");
const run_cmd = b.addRunArtifact(exe);
run_step.dependOn(&run_cmd.step);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const mod_tests = b.addTest(.{
.root_module = mod,
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() },
},
}),
});
const run_mod_tests = b.addRunArtifact(mod_tests);
b.installArtifact(exe);
const lib_tests = b.addTest(.{
.root_module = lib.root_module,
.use_llvm = debug,
});
const run_lib_tests = b.addRunArtifact(lib_tests);
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
.use_llvm = debug,
});
const run_exe_tests = b.addRunArtifact(exe_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);
// zls build check steps
const lib_check = b.addLibrary(.{
.name = "squashfs",
.root_module = lib.root_module,
.use_llvm = debug,
});
const exe_check = b.addExecutable(.{
.name = "unsquashfs",
.root_module = exe.root_module,
.use_llvm = debug,
});
const check = b.step("check", "Check if unsquashfs compiles");
check.dependOn(&lib_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,
.version = "0.0.1",
.version = "0.0.6",
.fingerprint = 0x37ba29474b87f145, // Changing this has security and trust implications.
.minimum_zig_version = "0.15.2",
// This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`.
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
//.example = .{
// // When updating this field to a new URL, be sure to delete the corresponding
// // `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
// // which will prevent zig from using it.
// .url = "https://example.com/foo.tar.gz",
//
// // This is computed from the file contents of the directory of files that is
// // obtained after fetching `url` and applying the inclusion rules given by
// // `paths`.
// //
// // This field is the source of truth; packages do not come from a `url`; they
// // come from a `hash`. `url` is just one of many possible mirrors for how to
// // obtain a package matching this `hash`.
// //
// // Uses the [multihash](https://multiformats.io/multihash/) format.
// .hash = "...",
//
// // When this is provided, the package is found in a directory relative to the
// // build root. In this case the package's hash is irrelevant and therefore not
// // computed. This field and `url` are mutually exclusive.
// .path = "foo",
//
// // When this is set to `true`, a package is declared to be lazily
// // fetched. This makes the dependency only get fetched if it is
// // actually used.
// .lazy = false,
//},
.zlib_ng = .{
.url = "git+https://github.com/CalebQ42/zig-zlib-ng#915db2dbd4a540d8a933336d67520b786f168f01",
.hash = "zlib_ng-2.3.3-pre1-2HYS4MlFAADc-eSZv4_xjkxZRCdGUVOYZKdtqhybstgk",
},
.zstd = .{
.url = "git+https://github.com/allyourcodebase/zstd.git?ref=1.5.7-1#e1a501be57f42c541e8a5597e4b59a074dfd09a3",
.hash = "zstd-1.5.7-1-KEItkAMwAAD6OKY3m0OOmXG7aL-aLUfrDqbP5J5oYapU",
},
.lz4 = .{
.url = "git+https://github.com/allyourcodebase/lz4.git?ref=1.10.0-6#41f52ab227caf9d48cf88c89a4d2946caa12b102",
.hash = "lz4-1.10.0-6-ewyzw-4NAAAWDpY4xpiqr4LQhZQAC0x_rGnW2iPh6jk2",
},
.xz = .{
.url = "git+https://github.com/akunaakwei/zig-xz.git#e2d389262c8291907e3e4c6fb119819141c16c0f",
.hash = "xz-5.8.2-6v47_JYeAABSL-jonprpL5-E_YaaGc4B5xrbe93WsJ3G",
},
.minilzo = .{
.url = "git+https://github.com/CalebQ42/zig-minilzo.git#f72a0e20a30fad38cd10104f393c8f3440478697",
.hash = "minilzo-2.10.0-Ij7BO7YLAABd6oK2YdimXwvFTOC8wD1sNm0PHGoRNEwR",
},
},
.paths = .{
"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
+114 -151
View File
@@ -1,168 +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 File = std.fs.File;
const builtin = @import("builtin");
const Io = std.Io;
const MemoryMap = Io.File.MemoryMap;
const Decomp = @import("decomp.zig");
const ExtractionOptions = @import("options.zig");
const Inode = @import("inode.zig");
const InodeRef = Inode.Ref;
const BlockSize = @import("inode_data/file.zig").BlockSize;
const SfsFile = @import("file.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 File = @import("file.zig");
const ExtractionOptions = @import("options.zig");
const Extract = @import("extract.zig");
const Archive = @This();
// 4 Gigs
const DEFAULT_MEM_SIZE = 4 * 1024 * 1024 * 1024;
parent_alloc: std.mem.Allocator,
alloc: std.heap.ThreadSafeAllocator,
// alloc: std.heap.FixedBufferAllocator,
// fixed_buf: []u8,
thread_count: usize,
fil: OffsetFile,
super: Superblock,
setup: bool = false,
map: MemoryMap,
decomp: Decomp.Fn,
decomp: Decomp.DecompFn,
pub fn init(io: Io, file: Io.File, offset: u64) !Archive {
var rdr = file.reader(io, &[0]u8{});
try rdr.seekTo(offset);
frag_table: Table(FragEntry) = undefined,
id_table: Table(u16) = undefined,
export_table: Table(InodeRef) = undefined,
/// Default settings using std.Thread.getCpuCount() threads and the minimum of 4gb or half of system memory for memory usage.
pub fn init(alloc: std.mem.Allocator, fil: File) !Archive {
return initAdvanced(
alloc,
fil,
0,
try std.Thread.getCpuCount(),
);
}
/// Create the Archive dictating the amount of threads used for extraction.
/// If you're planning on only interacting with a small number of files, it should be fine to use few (or one) threads.
pub fn initAdvanced(alloc: std.mem.Allocator, fil: File, offset: u64, threads: usize) !Archive {
var super: Superblock = undefined;
const red = try fil.pread(@ptrCast(&super), offset);
std.debug.assert(red == @sizeOf(Superblock));
try super.validate();
// const fixed_buf = try alloc.alloc(u8, mem);
return .{
.parent_alloc = alloc,
.alloc = .{ .child_allocator = alloc },
// .fixed_buf = fixed_buf,
.thread_count = if (threads > 0) threads else try std.Thread.getCpuCount(),
.fil = .init(fil, offset),
.decomp = switch (super.compression) {
.gzip => Decomp.gzipDecompress,
.lzma => Decomp.lzmaDecompress,
.xz => Decomp.xzDecompress,
.zstd => Decomp.zstdDecompress,
.lz4 => if (config.use_c_libs) Decomp.cLz4 else return error.Lz4Unsupported,
.lzo => if (config.use_c_libs and config.allow_lzo) Decomp.lzoDecompress else return error.LzoUnsupported,
},
try rdr.interface.readSliceEndian(Superblock, @ptrCast(&super), .little);
try super.validate();
return .{
.super = super,
.map = try file.createMemoryMap(io, .{
.offset = offset,
.len = super.size,
.protection = .{ .read = true },
}),
.decomp = try Decomp.getFn(super.compression),
};
}
pub fn deinit(self: *Archive) void {
// self.parent_alloc.free(self.fixed_buf);
if (self.setup) {
self.frag_table.deinit();
self.export_table.deinit();
self.id_table.deinit();
pub fn deinit(self: *Archive, io: Io) void {
self.map.destroy(io);
}
pub fn root(self: Archive, alloc: std.mem.Allocator) !File {
return .initRef(alloc, self.super, self.map.memory, self.decomp, "", self.super.root_ref);
}
pub fn open(self: Archive, alloc: std.mem.Allocator, filepath: []const u8) !File {
const root_file = try self.root(alloc);
const path = std.mem.trim(u8, filepath, "/");
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 extract(self: Archive, alloc: std.mem.Allocator, io: Io, location: []const u8, options: ExtractionOptions) !void {
const root_inode: Inode = try .initRef(
alloc,
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);
}
// Superblock
const SQUASHFS_MAGIC: u32 = std.mem.readInt(u32, "hsqs", .little);
const SuperblockError = error{
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;
}
}
pub fn allocator(self: *Archive) std.mem.Allocator {
return self.alloc.allocator();
}
fn setupValues(self: *Archive) !void {
const alloc = self.allocator();
self.frag_table = try .init(alloc, self.fil, self.decomp, self.super.frag_start, self.super.frag_count);
self.id_table = try .init(alloc, self.fil, self.decomp, self.super.id_start, self.super.id_count);
self.export_table = try .init(alloc, self.fil, self.decomp, self.super.export_start, self.super.inode_count);
self.setup = true;
}
pub fn id(self: *Archive, idx: u32) !u16 {
if (!self.setup) try self.setupValues();
return self.id_table.get(idx);
}
pub fn frag(self: *Archive, idx: u32) !FragEntry {
if (!self.setup) try self.setupValues();
return self.frag_table.get(idx);
}
pub fn inode(self: *Archive, num: u32) !Inode {
if (!self.setup) try self.setupValues();
const ref = try self.export_table.get(num - 1);
var rdr = try self.fil.readerAt(ref.block_start + self.super.inode_start, &[0]u8{});
var meta: MetadataReader = .init(self.allocator(), &rdr.interface, &self.decomp);
try meta.interface.discardAll(ref.block_offset);
return try .read(self.allocator(), &meta.interface, self.super.block_size);
}
pub fn root(self: *Archive) !SfsFile {
if (!self.setup) try self.setupValues();
var rdr = try self.fil.readerAt(self.super.root_ref.block_start + self.super.inode_start, &[0]u8{});
var meta: MetadataReader = .init(self.allocator(), &rdr.interface, self.decomp);
try meta.interface.discardAll(self.super.root_ref.block_offset);
const in: Inode = try .read(self.allocator(), &meta.interface, self.super.block_size);
return .init(self, in, "");
}
pub fn open(self: *Archive, path: []const u8) !SfsFile {
if (!self.setup) try self.setupValues();
var root_fil = try self.root();
defer if (!SfsFile.pathIsSelf(path)) root_fil.deinit();
return root_fil.open(path);
}
pub fn extract(self: *Archive, path: []const u8, options: ExtractionOptions) !void {
if (!self.setup) try self.setupValues();
var alloc = self.allocator();
var ext_path: []u8 = undefined;
if (std.fs.cwd().statFile(path)) |stat| {
if (stat.kind == .directory) {
ext_path = @constCast(path);
} else return error.ExtractionPathExists;
} else |err| {
if (err == error.FileNotFound) {
ext_path = @constCast(path);
} else {
std.log.err("Error stat-ing extraction path {s}: {}\n", .{ path, err });
return err;
}
}
defer if (ext_path.len > path.len) alloc.free(ext_path);
var rdr = try self.fil.readerAt(self.super.root_ref.block_start + self.super.inode_start, &[0]u8{});
var meta: MetadataReader = .init(self.allocator(), &rdr.interface, self.decomp);
try meta.interface.discardAll(self.super.root_ref.block_offset);
const in: Inode = try .read(self.allocator(), &meta.interface, self.super.block_size);
try in.extractToThreaded(self, ext_path, options, self.thread_count);
}
};
+101 -18
View File
@@ -1,9 +1,10 @@
const std = @import("std");
const Writer = std.Io.Writer;
const Io = std.Io;
const Writer = Io.Writer;
const builtin = @import("builtin");
const config = @import("config");
const squashfs = @import("zig_squashfs");
const build = @import("build");
const squashfs = @import("squashfs");
//TODO: Add more options
const help_mgs =
@@ -12,12 +13,18 @@ const help_mgs =
\\
\\Options:
\\ -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.
\\ -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.
\\ -v Verbose
\\
\\ --force Force extraction. If the destination already exists, it will be deleted.
\\
\\ --help Display this messages
\\ --version Display the version
\\
@@ -26,31 +33,82 @@ const help_mgs =
const errors = error{InvalidArguments};
var archive: []const u8 = "";
var extLoc: []const u8 = "squashfs-root";
var ext_loc: []const u8 = "squashfs-root";
var offset: u64 = 0;
var threads: u32 = 0;
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 {
const alloc = std.heap.smp_allocator;
var stdout = std.fs.File.stdout();
var out = stdout.writer(&[0]u8{});
var limited: ?Io.Threaded = null;
pub fn main(init: std.process.Init) !void {
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 {};
try handleArgs(alloc, &out.interface);
try handleArgs(alloc, init.minimal.args, &out.interface);
if (archive.len == 0) {
try out.interface.print("You must provide a squashfs archive\n", .{});
try out.interface.print(help_mgs, .{});
return;
}
var fil: std.fs.File = try std.fs.cwd().openFile(archive, .{}); //TODO: Handle error gracefully.
defer fil.close();
var arc: squashfs.Archive = try .initAdvanced(alloc, fil, offset, threads); //TODO: Update when memory size matters. //TODO: Handle error gracefully.
defer arc.deinit();
try arc.extract(extLoc, if (verbose) .VerboseDefault(&out.interface) else .Default); //TODO: Handle error gracefully.
var fil = try Io.Dir.cwd().openFile(io, archive, .{}); //TODO: Handle error gracefully.
defer fil.close(io);
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 = .{
.single_threaded = (threads == 1),
.verbose = verbose,
.verbose_writer = if (verbose) &out.interface else null,
.ignore_xattr = ignore_xattrs,
.ignore_permissions = ignore_permissions,
};
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();
}
if (force)
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, out: *Writer) !void {
var args = try std.process.argsWithAllocator(alloc);
fn handleArgs(alloc: std.mem.Allocator, main_args: std.process.Args, out: *Writer) !void {
var args = main_args.iterate();
defer args.deinit();
_ = args.next(); // args[0] is the application launch command.
while (args.next()) |arg| {
@@ -65,13 +123,29 @@ fn handleArgs(alloc: std.mem.Allocator, out: *Writer) !void {
return errors.InvalidArguments;
};
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")) {
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;
}
extLoc = nxt.?;
ext_loc = std.mem.trim(u8, nxt.?, "/");
continue;
} else if (std.mem.eql(u8, arg, "-p")) {
const nxt = args.next();
@@ -87,9 +161,18 @@ fn handleArgs(alloc: std.mem.Allocator, out: *Writer) !void {
} else if (std.mem.eql(u8, arg, "-v")) {
verbose = true;
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")) {
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 });
std.process.exit(0);
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;
}
+116
View File
@@ -0,0 +1,116 @@
const std = @import("std");
const Io = std.Io;
const Decomp = @import("../decomp.zig");
const FileFinish = @import("../extract-multi.zig").FileFinish;
const Multi = @import("../extract-multi.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, select: *Io.Select(Multi.SelectUnion), finish: *FileFinish) !void {
if (self.size == 0) return;
var read_offset: u64 = self.start;
for (0.., self.blocks) |i, block| {
try select.concurrent(.reg, blockThread, .{ self, alloc, io, finish.file.file, read_offset, @truncate(i), finish });
read_offset += block.size;
}
if (self.frag_data != null)
try select.concurrent(.reg, fragThread, .{ self, io, finish.file.file, finish });
}
fn blockThread(
self: Extractor,
alloc: std.mem.Allocator,
io: Io,
file: Io.File,
read_offset: u64,
block_idx: u32,
finish: *FileFinish,
) Multi.Error!void {
const size = if (self.frag_data == null and block_idx == self.blocks.len - 1)
self.size % self.block_size
else
self.block_size;
const block = self.blocks[block_idx];
var wrt = file.writer(io, &[0]u8{});
try wrt.seekTo(block_idx * self.block_size);
if (block.size == 0) {
try wrt.interface.splatByteAll(0, size);
try finish.finish(io);
return;
}
const data = self.data[read_offset..][0..block.size];
if (block.uncompressed) {
try wrt.interface.writeAll(data);
try finish.finish(io);
return;
}
if (self.cache != null) {
const decomp_block = try self.cache.?.get(io, read_offset, block.size);
try wrt.interface.writeAll(decomp_block);
} else {
const tmp = try alloc.alloc(u8, size);
defer alloc.free(tmp);
_ = try self.decomp(alloc, data, tmp);
try wrt.interface.writeAll(tmp);
}
try finish.finish(io);
}
fn fragThread(
self: Extractor,
io: Io,
file: Io.File,
finish: *FileFinish,
) Multi.Error!void {
const size = self.size % self.block_size;
var wrt = file.writer(io, &[0]u8{});
try wrt.seekTo(self.blocks.len * self.block_size);
try wrt.interface.writeAll(self.frag_data.?[self.frag_offset..][0..size]);
try finish.finish(io);
}
+180
View File
@@ -0,0 +1,180 @@
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_alloc: bool = false,
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 deinit(self: *Reader) void {
if (self.block_alloc)
self.alloc.free(self.interface.buffer);
}
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_alloc) self.alloc.free(self.interface.buffer);
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.block_alloc = true;
self.interface.buffer = self.alloc.alloc(u8, size) catch return error.ReadFailed;
errdefer {
self.alloc.free(self.interface.buffer);
self.interface.buffer = &[0]u8{};
}
_ = self.decomp(self.alloc, self.data[self.offset..][0..block.size], self.interface.buffer) catch return error.ReadFailed;
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 Reader = std.Io.Reader;
const builtin = @import("builtin");
const build = @import("build_options");
const config = if (builtin.is_test) .{
.use_c_libs = builtin.link_libc == true,
.allow_lzo = false, // Change once LZO compilation is fixed
} else @import("config");
const c = @import("c_decomp.zig");
const zig = @import("zig_decomp.zig");
const c = @cImport({
@cInclude("zlib.h");
@cInclude("lzma.h");
@cInclude("lz4.h");
@cInclude("zstd.h");
@cInclude("zstd_errors.h");
if (config.allow_lzo)
@cInclude("lzo/minilzo.h");
});
pub fn getFn(e: Enum) !Fn {
return switch (e) {
.gzip => if (build.use_zig_decomp) zig.zlib else c.zlib,
.lzma => if (build.use_zig_decomp) zig.lzma else c.lzma,
.lzo => if (build.use_zig_decomp or !build.allow_lzo) error.LzoUnsupported else c.lzo,
.xz => if (build.use_zig_decomp) zig.xz else c.lzma,
.lz4 => if (build.use_zig_decomp) error.Lz4Unsupported else c.lz4,
.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,
lzma,
lzo,
@@ -29,232 +28,7 @@ pub const CompressionType = enum(u16) {
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 gzipDecompress = if (config.use_c_libs) cGzip else zigGzip;
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,
pub const Error = error{
OutOfMemory,
DecompressionFailed,
};
-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,
};
+503
View File
@@ -0,0 +1,503 @@
const std = @import("std");
const Io = std.Io;
const Atomic = std.atomic.Value;
const DataExtractor = @import("data/extractor.zig");
const Decomp = @import("decomp.zig");
const Directory = @import("directory.zig");
const ExtractionOption = @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, filepath: []const u8, options: ExtractionOption) !void {
const path = std.mem.trim(u8, filepath, "/");
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();
var cache: Cache = .init(alloc, data, decomp);
defer cache.deinit();
var err: ?Error = null;
var group: Io.Group = .init;
group.async(
io,
groupExtract,
.{ alloc, io, data, decomp, super, &group, &frag_table, &id_table, &xattr_table, &cache, &err, inode, path, options, null },
);
}
fn groupExtract(
alloc: std.mem.Allocator,
io: Io,
data: []u8,
decomp: Decomp.Fn,
super: Superblock,
group: *Io.Group,
frag_table: *Lookup.Table(Lookup.FragEntry),
id_table: *Lookup.Table(u16),
xattr_table: *XattrTable,
cache: *Cache,
err: *?Error,
inode: Inode,
path: []const u8,
options: ExtractionOption,
parent: ?*Parent,
) error{Canceled}!void {
if (err != null) {
if (parent != null) {
alloc.free(path);
inode.deinit(alloc);
parent.?.finish(alloc, io);
}
return;
}
switch (inode.hdr.type) {
.dir, .ext_dir => group.async(io, extractDir, .{ alloc, io, data, decomp, super, group, frag_table, id_table, xattr_table, cache, inode, path, options, parent }),
.file, .ext_file => group.async(io, extractReg, .{ alloc, io, data, decomp, super.block_size, group, frag_table, cache, inode, path, options, parent }),
.symlink, .ext_symlink => group.async(io, extractSymlink, .{ alloc, io, inode, path, parent }),
else => group.async(io, extractNod, .{ alloc, io, id_table, xattr_table, inode, path, options, parent }),
}
}
fn extractDir(
alloc: std.mem.Allocator,
io: Io,
data: []u8,
decomp: Decomp.Fn,
super: Superblock,
group: *Io.Group,
frag_table: *Lookup.Table(Lookup.FragEntry),
id_table: *Lookup.Table(u16),
xattr_table: *XattrTable,
cache: *Cache,
err: *?Error,
inode: Inode,
path: []const u8,
options: ExtractionOption,
parent: ?*Parent,
) Error!void {
var xattr_idx: u32 = 0xFFFFFFFF;
const dir: Directory = switch (inode.data) {
.dir => |d| blk: {
var meta: MetadataReader = .init(alloc, data, decomp, super.dir_start + d.block_start);
try meta.interface.discardAll(d.block_offset);
break :blk try .init(alloc, &meta.interface, d.size);
},
.ext_dir => |d| blk: {
xattr_idx = d.xattr_idx;
var meta: MetadataReader = .init(alloc, data, decomp, super.dir_start + d.block_start);
try meta.interface.discardAll(d.block_offset);
break :blk try .init(alloc, &meta.interface, d.size);
},
else => unreachable,
};
defer dir.deinit(alloc);
try Io.Dir.cwd().createDirPath(io, path);
if (dir.entries.len == 0) {
try setMetadataPath(alloc, io, inode.hdr, id_table, xattr_table, path, xattr_idx, options);
if (parent != null) {
try parent.?.finish(io);
}
return;
}
const cur_dir = try Parent.create(alloc, id_table, xattr_table, err, inode.hdr, path, options, parent, xattr_idx, dir.entries);
for (dir.entries) |entry| {
const 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 |e| {
new_inode.deinit(alloc);
return e;
};
group.async(
io,
groupExtract,
.{ alloc, io, data, decomp, super, group, frag_table, id_table, xattr_table, cache, err, new_inode, new_path, options, cur_dir },
);
}
}
fn extractReg(
alloc: std.mem.Allocator,
io: Io,
data: []u8,
decomp: Decomp.Fn,
block_size: u32,
group: *Io.Group,
frag_table: *Lookup.Table(Lookup.FragEntry),
id_table: *Lookup.Table(u16),
xattr_table: *XattrTable,
cache: *Cache,
err: *?Error,
inode: Inode,
path: []const u8,
options: ExtractionOption,
parent: ?*Parent,
) Error!void {
var blocks: usize = 0;
var xattr_idx: u32 = 0xFFFFFFFF;
var ext: DataExtractor = switch (inode.data) {
.file => |f| blk: {
var ext: DataExtractor = .init(data, decomp, block_size, f.blocks, f.block_start, f.size);
blocks = f.blocks.len;
if (f.frag_idx != 0xFFFFFFFF) {
blocks += 1;
const frag: Lookup.FragEntry = try frag_table.get(io, f.frag_idx);
if (frag.size.uncompressed) {
ext.addFrag(data[frag.block_start..][0..frag.size.size], f.frag_offset);
} else {
const frag_block = try cache.get(io, frag.block_start, frag.size.size);
ext.addFrag(frag_block, f.frag_offset);
}
}
break :blk ext;
},
.ext_file => |f| blk: {
xattr_idx = f.xattr_idx;
var ext: DataExtractor = .init(data, decomp, block_size, f.blocks, f.block_start, f.size);
blocks = f.blocks.len;
if (f.frag_idx != 0xFFFFFFFF) {
blocks += 1;
const frag: Lookup.FragEntry = try frag_table.get(io, f.frag_idx);
if (frag.size.uncompressed) {
ext.addFrag(data[frag.block_start..][0..frag.size.size], f.frag_offset);
} else {
const frag_block = try cache.get(io, frag.block_start, frag.size.size);
ext.addFrag(frag_block, f.frag_offset);
}
}
break :blk ext;
},
else => unreachable,
};
const fin = try FileFinish.create(alloc, io, id_table, xattr_table, err, inode.hdr, path, options, parent, xattr_idx, blocks);
try ext.extractAsync(alloc, io, group, fin);
}
fn extractSymlink(alloc: std.mem.Allocator, io: Io, inode: Inode, path: []const u8, parent: ?*Parent) Error!void {
defer if (parent != null) {
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, .{});
if (parent != null)
parent.?.finish(io);
}
fn extractNod(
alloc: std.mem.Allocator,
io: Io,
id_table: *Lookup.Table(u16),
xattr_table: *XattrTable,
inode: Inode,
path: []const u8,
parent: ?*Parent,
options: ExtractionOption,
) Error!void {
var xattr_idx: u32 = 0xFFFFFFFF;
var device: u32 = 0;
var mode: u32 = undefined;
const DT = std.os.linux.DT;
switch (inode.data) {
.block_dev => |d| {
mode = DT.BLK;
device = d.device;
},
.ext_block_dev => |d| {
mode = DT.BLK;
device = d.device;
xattr_idx = d.xattr_idx;
},
.char_dev => |d| {
mode = DT.CHR;
device = d.device;
},
.ext_char_dev => |d| {
mode = DT.CHR;
device = d.device;
xattr_idx = d.xattr_idx;
},
.fifo => {
mode = DT.FIFO;
},
.ext_fifo => |f| {
mode = DT.FIFO;
xattr_idx = f.xattr_idx;
},
.socket => {
mode = DT.SOCK;
},
.ext_socket => |s| {
mode = DT.SOCK;
xattr_idx = s.xattr_idx;
},
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, device);
if (res != 0)
return Error.Mknod;
try setMetadataPath(alloc, io, inode.hdr, id_table, xattr_table, path, xattr_idx, options);
if (parent != null)
try parent.?.finish(io);
}
fn setMetadataPath(
alloc: std.mem.Allocator,
io: Io,
hdr: Inode.Header,
id_table: *Lookup.Table(u16),
xattr_table: *XattrTable,
path: []const u8,
xattr_idx: u32,
options: ExtractionOption,
) Error!void {
if (options.ignore_permissions and (options.ignore_xattr or xattr_idx == 0xFFFFFFFF)) return;
var file = try Io.Dir.cwd().openFile(io, path, .{});
defer file.close(io);
return setMetadata(alloc, io, hdr, id_table, xattr_table, file, xattr_idx, options);
}
fn setMetadata(
alloc: std.mem.Allocator,
io: Io,
hdr: Inode.Header,
id_table: *Lookup.Table(u16),
xattr_table: *XattrTable,
file: Io.File,
xattr_idx: u32,
options: ExtractionOption,
) Error!void {
if (options.ignore_permissions and (options.ignore_xattr or xattr_idx == 0xFFFFFFFF)) return;
if (!options.ignore_xattr and xattr_idx != 0xFFFFFFFF) {
var xattr = try xattr_table.get(alloc, io, 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.SetXattr;
}
}
if (options.ignore_permissions) return;
try file.setTimestamps(io, .{
.modify_timestamp = .init(
Io.Timestamp.fromNanoseconds(@as(i96, @intCast(hdr.mod_time)) * std.time.ns_per_s),
),
});
try file.setPermissions(io, @enumFromInt(hdr.permissions));
try file.setOwner(io, try id_table.get(io, hdr.uid_idx), try id_table.get(io, hdr.gid_idx));
}
// Types
pub const Error = error{ Mknod, SetXattr } || std.mem.Allocator.Error || Io.Cancelable || Io.Reader.Error || Io.Dir.CreateDirPathError || Cache.Error ||
Io.File.SetPermissionsError || Io.Dir.SymLinkError || Io.File.SeekError || Io.Writer.Error || Io.File.Atomic.LinkError || Io.ConcurrentError;
const FileFinish = struct {
id_table: *Lookup.Table(u16),
xattr_table: *XattrTable,
err: *?Error,
inode: Inode,
options: ExtractionOption,
parent: ?*Parent,
xattr_idx: u32,
file: Io.File.Atomic,
atomic: Atomic(usize),
has_err: bool = false,
fn create(
alloc: std.mem.Allocator,
io: Io,
id_table: *Lookup.Table(u16),
xattr_table: *XattrTable,
err: *?Error,
hdr: Inode.Header,
path: []const u8,
options: ExtractionOption,
parent: ?*Parent,
xattr_idx: u32,
blocks: usize,
) Error!*FileFinish {
const out = try alloc.create(FileFinish);
out.* = .{
.id_table = id_table,
.xattr_table = xattr_table,
.err = err,
.hdr = hdr,
.path = path,
.options = options,
.parent = parent,
.xattr_idx = xattr_idx,
.file = Io.Dir.cwd().createFileAtomic(io, path, .{}),
.atomic = .init(blocks),
};
return out;
}
fn finish(self: *FileFinish, alloc: std.mem.Allocator, io: Io) void {
if (self.atomic.fetchSub(1, .release) > 0) return;
defer {
if (self.parent != null) {
alloc.free(self.path);
if (self.has_err)
self.parent.?.errFinish(alloc, io)
else
self.parent.?.finish(alloc, io);
}
alloc.destroy(self);
}
if (self.has_err) return;
setMetadata(alloc, io, self.hdr, self.id_table, self.xattr_table, self.file, self.xattr_idx, self.options) catch |err| {
self.has_err = true;
self.err.* = err;
return;
};
self.file.link(io) catch |err| {
self.has_err = true;
self.err.* = err;
return;
};
self.file.deinit(io);
}
fn errFinish(self: *FileFinish, alloc: std.mem.Allocator, io: Io) void {
self.has_err = true;
if (self.atomic.fetchSub(1, .release) > 0) return;
if (self.parent != null) {
alloc.free(self.path);
self.parent.?.errFinish(alloc, io);
}
self.file.deinit(io);
alloc.destroy(self);
}
};
const Parent = struct {
id_table: *Lookup.Table(u16),
xattr_table: *XattrTable,
err: *?Error,
hdr: Inode.Header,
path: []const u8,
options: ExtractionOption,
parent: ?*Parent,
xattr_idx: u32,
atomic: Atomic(usize),
has_err: bool = false,
fn create(
alloc: std.mem.Allocator,
id_table: *Lookup.Table(u16),
xattr_table: *XattrTable,
err: *?Error,
hdr: Inode.Header,
path: []const u8,
options: ExtractionOption,
parent: ?*Parent,
xattr_idx: u32,
num: usize,
) Error!*Parent {
const out = try alloc.create(Parent);
out.* = .{
.id_table = id_table,
.xattr_table = xattr_table,
.err = err,
.hdr = hdr,
.path = path,
.options = options,
.parent = parent,
.xattr_idx = xattr_idx,
.atomic = .init(num),
};
return out;
}
fn finish(self: *Parent, alloc: std.mem.Allocator, io: Io) void {
if (self.atomic.fetchSub(1, .release) > 0) return;
defer {
if (self.parent != null) {
alloc.free(self.path);
if (self.has_err)
self.parent.?.errFinish(alloc, io)
else
self.parent.?.finish(alloc, io);
}
alloc.destroy(self);
}
if (self.has_err) return;
setMetadataPath(alloc, io, self.hdr, self.id_table, self.xattr_table, self.path, self.xattr_idx, self.options) catch |err| {
self.has_err = true;
self.err.* = err;
};
}
fn errFinish(self: *Parent, alloc: std.mem.Allocator, io: Io) void {
self.has_err = true;
if (self.atomic.fetchSub(1, .release) > 0) return;
if (self.parent != null) {
alloc.free(self.path);
self.parent.?.errFinish(alloc, io);
}
alloc.destroy(self);
}
};
+267
View File
@@ -0,0 +1,267 @@
const std = @import("std");
const Io = std.Io;
const Archive = @import("archive.zig");
const Superblock = Archive.Superblock;
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 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,
};
defer rdr.deinit();
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 Decomp = @import("decomp.zig");
const ExtractionOptions = @import("options.zig");
const Inode = @import("inode.zig");
const Multi = @import("extract-multi.zig");
const Single = @import("extract-single.zig");
const Superblock = @import("archive.zig").Superblock;
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);
}
+113 -199
View File
@@ -1,230 +1,144 @@
//! A file/directory within the squashfs archive.
//! A squashfs file/directory. Basically a wrapper around an Inode.
const std = @import("std");
const File = std.fs.File;
const WaitGroup = std.Thread.WaitGroup;
const Mutex = std.Thread.Mutex;
const Io = std.Io;
const Archive = @import("archive.zig");
const DirEntry = @import("dir_entry.zig");
const ExtractionOptions = @import("options.zig");
const Directory = @import("directory.zig");
const Inode = @import("inode.zig");
const BlockSize = @import("inode_data/file.zig").BlockSize;
const DataReader = @import("util/data.zig");
const MetadataReader = @import("util/metadata.zig");
const MetadataReader = @import("meta_rdr.zig");
const Decomp = @import("decomp.zig");
const Superblock = @import("archive.zig").Superblock;
const ExtractionOptions = @import("options.zig");
const Extract = @import("extract.zig");
const FileError = error{
NotDirectory,
NotRegularFile,
NotSymlink,
NotDevice,
NotFound,
ExtractionPathExists,
};
const File = @This();
const SfsFile = @This();
alloc: std.mem.Allocator,
archive: *Archive,
super: Superblock,
data: []u8,
decomp: Decomp.Fn,
inode: Inode,
name: []const u8,
inode: Inode,
/// Initialize a new File.
/// name is copied to the File so can be safely freed afterwards.
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);
/// The given allocator should have been used to create the name and inode.
pub fn init(alloc: std.mem.Allocator, super: Superblock, data: []u8, decomp: Decomp.Fn, name: []const u8, inode: Inode) File {
return .{
.archive = archive,
.alloc = alloc,
.super = super,
.data = data,
.decomp = decomp,
.name = name,
.inode = inode,
.name = new_name,
};
}
pub fn fromEntry(archive: *Archive, entry: DirEntry) !SfsFile {
var rdr = try archive.fil.readerAt(entry.block_start + archive.super.inode_start, &[0]u8{});
var meta: MetadataReader = .init(archive.allocator(), &rdr.interface, archive.decomp);
try meta.interface.discardAll(entry.block_offset);
const inode: Inode = try .read(archive.allocator(), &meta.interface, archive.super.block_size);
errdefer inode.deinit(archive.allocator());
return .init(archive, inode, entry.name);
}
/// The name from the directory entry will be duplicated so it's safe to deinit the entry.
pub fn initEntry(alloc: std.mem.Allocator, super: Superblock, data: []u8, decomp: Decomp.Fn, entry: Directory.Entry) !File {
const inode: Inode = try .initEntry(alloc, data, decomp, super.inode_start, super.block_size, entry);
errdefer inode.deinit(alloc);
pub fn deinit(self: SfsFile) void {
var alloc = self.archive.allocator();
alloc.free(self.name);
self.inode.deinit(alloc);
}
const new_name = try alloc.dupe(u8, entry.name);
fn getEntries(self: SfsFile) ![]DirEntry {
return self.inode.dirEntries(self.archive.allocator(), self.archive.*);
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);
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;
}
const new_name = try alloc.dupe(u8, name);
pub fn isRegular(self: SfsFile) bool {
return switch (self.inode.hdr.inode_type) {
.file, .ext_file => true,
else => false,
};
return init(alloc, super, data, decomp, new_name, inode);
}
/// 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, 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 {
var alloc = self.archive.allocator();
for (entries) |e| {
e.deinit(alloc);
}
alloc.free(entries);
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 => {},
}
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(path[idx + 1 ..]);
},
.lt => cur_slice = cur_slice[0..split],
.gt => cur_slice = cur_slice[split + 1 ..],
}
}
return FileError.NotFound;
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 isSymlink(self: SfsFile) bool {
return switch (self.inode.hdr.inode_type) {
.symlink, .ext_symlink => true,
else => false,
};
}
pub fn symlinkPath(self: SfsFile) ![]const u8 {
if (!self.isSymlink()) return FileError.NotSymlink;
return switch (self.inode.data) {
.symlink => |s| s.target,
.ext_symlink => |s| s.target,
else => unreachable,
};
}
/// If the given File is a directory, open a sub-file at the given filepath.
/// If the given filepath resolves to the calling File ("" or "."), a copy of the File is returned.
pub fn open(self: File, alloc: std.mem.Allocator, filepath: []const u8) !File {
var size: u64 = 0;
var meta: MetadataReader = switch (self.inode.data) {
.dir => |d| blk: {
size = d.size;
/// Check if the File is a block or character device.
pub fn isDevice(self: SfsFile) bool {
return switch (self.inode.hdr.inode_type) {
.block_dev, .char_dev, .ext_block_dev, .ext_char_dev => true,
else => false,
};
}
/// 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,
};
}
var meta: MetadataReader = .init(alloc, self.data, self.decomp, self.super.dir_start + d.block_start);
try meta.interface.discardAll(d.block_offset);
break :blk meta;
},
.ext_dir => |d| blk: {
size = d.size;
/// Extract the given File to the path. If File is a regular file, the path must be a directory or not exist.
/// If the gievn path is a folder, the File's contents will be extracted within.
pub fn extract(self: *SfsFile, path: []const u8, options: ExtractionOptions) !void {
var alloc = self.archive.allocator();
var ext_path: []u8 = undefined;
if (std.fs.cwd().statFile(path)) |stat| {
if (stat.kind == .directory) {
if (!self.isDir()) {
const has_end_sep = path[path.len - 1] == '/';
const alloc_size = if (has_end_sep)
path.len + self.name.len
else
path.len + self.name.len + 1;
ext_path = try alloc.alloc(u8, alloc_size);
@memcpy(ext_path[0..path.len], path);
@memcpy(ext_path[ext_path.len - self.name.len ..], self.name);
if (!has_end_sep) ext_path[path.len] = '/';
} else {
ext_path = @constCast(path);
var meta: MetadataReader = .init(alloc, self.data, self.decomp, self.super.dir_start + d.block_start);
try meta.interface.discardAll(d.block_offset);
break :blk meta;
},
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..],
}
} else return FileError.ExtractionPathExists;
} else |err| {
if (err == error.FileNotFound) {
ext_path = @constCast(path);
idx = entries.len / 2;
} else {
std.log.err("Error stat-ing extraction path {s}: {}\n", .{ path, err });
return err;
return error.NotFound;
}
}
defer if (ext_path.len > path.len) alloc.free(ext_path);
return self.inode.extractToThreaded(self.archive, path, options, self.archive.thread_count);
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..]);
}
/// 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 fn extract(self: File, alloc: std.mem.Allocator, io: Io, location: []const u8, options: ExtractionOptions) !void {
return Extract.extract(
alloc,
io,
self.super,
self.data,
self.decomp,
self.inode,
location,
options,
);
}
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);
}
};
+247 -489
View File
@@ -1,28 +1,76 @@
//! A file-system object. Represents a File or directory.
const std = @import("std");
const Reader = std.Io.Reader;
const WaitGroup = std.Thread.WaitGroup;
const Pool = std.Thread.Pool;
const Mutex = std.Thread.Mutex;
const Io = std.Io;
const Reader = Io.Reader;
const Archive = @import("archive.zig");
const DirEntry = @import("dir_entry.zig");
const Decomp = @import("decomp.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 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_start: u32,
_: u16,
};
pub const InodeType = enum(u16) {
pub const Type = enum(u16) {
dir = 1,
file,
symlink,
@@ -39,25 +87,8 @@ pub const InodeType = enum(u16) {
ext_socket,
};
pub const InodeData = union(InodeType) {
dir: dir.Dir,
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,
pub const Header = extern struct {
type: Type,
permissions: u16,
uid_idx: u16,
gid_idx: u16,
@@ -65,474 +96,201 @@ pub const Header = packed struct {
num: u32,
};
const Inode = @This();
hdr: Header,
data: InodeData,
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 .{
.hdr = hdr,
.data = switch (hdr.inode_type) {
.dir => .{ .dir = try .read(rdr) },
.file => .{ .file = try .read(alloc, rdr, block_size) },
.symlink => .{ .symlink = try .read(alloc, rdr) },
.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{});
var meta: MetadataReader = .init(alloc, &rdr.interface, archive.decomp);
try meta.interface.discardAll(entry.block_offset);
return read(alloc, &meta.interface, archive.super.block_size);
}
pub fn deinit(self: Inode, alloc: std.mem.Allocator) void {
switch (self.data) {
.file => |f| alloc.free(f.block_sizes),
.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.
pub fn dataReader(self: Inode, alloc: std.mem.Allocator, archive: *Archive) !DataReader {
return switch (self.hdr.inode_type) {
.file => readerFromData(alloc, archive, self.data.file),
.ext_file => readerFromData(alloc, archive, self.data.ext_file),
else => error.NotRegularFile,
};
}
fn readerFromData(alloc: std.mem.Allocator, archive: *Archive, data: anytype) !DataReader {
var out: DataReader = .init(alloc, archive.*, data.block_sizes, data.block_start, data.size);
if (data.frag_idx != 0xFFFFFFFF)
out.addFragment(try archive.frag(data.frag_idx), data.frag_block_offset);
return out;
}
/// Get a threaded data reader for a file inode.
pub fn threadedDataReader(self: Inode, alloc: std.mem.Allocator, archive: *Archive) !ThreadedDataReader {
return switch (self.hdr.inode_type) {
.file => threadedReaderFromData(alloc, archive, self.data.file),
.ext_file => threadedReaderFromData(alloc, archive, self.data.ext_file),
else => error.NotRegularFile,
};
}
fn threadedReaderFromData(alloc: std.mem.Allocator, archive: *Archive, data: anytype) !ThreadedDataReader {
var out: ThreadedDataReader = .init(alloc, archive.*, data.block_sizes, data.block_start, data.size);
if (data.frag_idx != 0xFFFFFFFF)
out.addFragment(try archive.frag(data.frag_idx), data.frag_block_offset);
return out;
}
/// Get the directory entries for a directory inode.
pub fn dirEntries(self: Inode, alloc: std.mem.Allocator, archive: Archive) ![]DirEntry {
return switch (self.hdr.inode_type) {
.dir => entriesFromData(alloc, archive, self.data.dir),
.ext_dir => entriesFromData(alloc, archive, self.data.ext_dir),
else => error.NotDirectory,
};
}
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, 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.extractTo(archive, path[0 .. path.len - 1], options);
std.fs.cwd().makeDir(path) catch |err| {
if (err != std.fs.Dir.MakeError.PathAlreadyExists) return err;
};
var alloc = archive.allocator();
const entries = try self.dirEntries(alloc, archive.*);
defer {
for (entries) |entry| entry.deinit(alloc);
alloc.free(entries);
}
for (entries) |entry| {
var new_path = try alloc.alloc(u8, path.len + 1 + entry.name.len);
@memcpy(new_path[0..path.len], path);
@memcpy(new_path[path.len + 1 ..], entry.name);
new_path[path.len] = '/';
defer alloc.free(new_path);
var inode: Inode = try readFromEntry(alloc, archive, entry);
defer inode.deinit(alloc);
try inode.extractTo(archive, new_path, options);
}
},
.file, .ext_file => try self.extractRegFile(archive.allocator(), 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(hdr.uid_idx),
.gid = try archive.id(hdr.gid_idx),
.perm = hdr.permissions,
.mod_time = hdr.mod_time,
.ignore_permissions = options.ignore_permissions,
.ignore_xattr = options.ignore_xattr,
.wg = .{ .state = .init(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 = 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 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,
};
/// Extract the inode to the given path. Multi-threaded.
/// Functions identically to extractTo on all but regular files and directories.
///
/// If threads <= 1, then this just calls extractTo.
pub fn extractToThreaded(self: Inode, archive: *Archive, path: []const u8, options: ExtractionOptions, threads: usize) !void {
if (threads <= 1) return self.extractTo(archive, path, options);
switch (self.hdr.inode_type) {
.dir, .ext_dir => {
// Removing any trailing separators since that's the easiest path forward.
if (path[path.len - 1] == '/') return self.extractToThreaded(archive, path[0 .. path.len - 1], options, threads);
pub const DataBlock = packed struct(u32) {
size: u24,
uncompressed: bool,
_: u7,
};
// 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();
pub const Dir = extern struct {
block_start: u32,
hard_links: u32,
size: u16,
block_offset: u16,
parent_num: u32,
// Arena Allocator
var arena_alloc: std.heap.ArenaAllocator = .init(archive.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 = alloc, .n_jobs = 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(self.hdr.uid_idx), try archive.id(self.hdr.gid_idx));
}
},
.file, .ext_file => {
const alloc = archive.allocator();
var pool: Pool = undefined;
try pool.init(.{ .allocator = alloc, .n_jobs = threads });
defer pool.deinit();
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(self.hdr.uid_idx), try archive.id(self.hdr.gid_idx));
}
},
.symlink, .ext_symlink => try self.extractSymlink(path),
else => try self.extractDevice(archive, path, options),
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 extractThreadEntry(
entry: DirEntry,
alloc: std.mem.Allocator,
archive: *Archive,
path: []const u8,
options: ExtractionOptions,
wg: *WaitGroup,
pool: *Pool,
out_err: *?anyerror,
parent: ?*Parent,
) void {
var new_path = alloc.alloc(u8, path.len + entry.name.len + 1) catch |err| {
wg.finish();
out_err.* = err;
return;
};
@memcpy(new_path[0..path.len], path);
@memcpy(new_path[path.len + 1 ..], entry.name);
new_path[path.len] = '/';
var inode = readFromEntry(alloc, archive, entry) catch |err| {
out_err.* = err;
wg.finish();
return;
};
inode.extractThread(alloc, archive, new_path, options, wg, pool, out_err, parent);
}
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,
/// Extract threadedly the inode to the path.
fn extractThread(
self: Inode,
alloc: std.mem.Allocator,
archive: *Archive,
path: []const u8,
options: ExtractionOptions,
wg: *WaitGroup,
pool: *Pool,
out_err: *?anyerror,
parent: ?*Parent,
) void {
if (options.verbose)
options.verbose_writer.?.print("Extracting inode #{} to {s}\n", .{ self.hdr.num, path }) catch {};
defer {
if (parent != null) parent.?.finish() catch |err| {
if (options.verbose)
options.verbose_writer.?.print("Error setting folder permission to {s}: {}\n", .{ path, err }) catch {};
out_err.* = err;
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);
return .{
.block_start = std.mem.readInt(u32, data[0..4], .little),
.frag_idx = frag_idx,
.frag_offset = std.mem.readInt(u32, data[8..12], .little),
.size = size,
.blocks = blocks,
};
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 ExtFile = struct {
block_start: u64,
size: u64,
sparse: u64,
hard_links: u32,
frag_idx: u32,
frag_offset: u32,
xattr_idx: u32,
blocks: []DataBlock,
const entries = self.dirEntries(alloc, archive.*) catch |err| {
if (options.verbose)
options.verbose_writer.?.print("Error getting directory entries for inode #{} (extracting to {s}): {}\n", .{ self.hdr.num, path, err }) catch {};
out_err.* = err;
return;
};
const p = Parent.create(alloc, self.hdr, archive, path, options, entries.len) catch |err| {
out_err.* = err;
return;
};
wg.startMany(entries.len);
// defer files.deinit(alloc); We don't need to do this due to ArenaAllocator
for (entries) |entry| {
if (entry.inode_type == .dir) {
extractThreadEntry(entry, alloc, archive, path, options, wg, pool, out_err, p);
continue;
}
pool.spawn(
extractThreadEntry,
.{
entry,
alloc,
archive,
path,
options,
wg,
pool,
out_err,
p,
},
) catch |err| {
wg.finish();
if (options.verbose)
options.verbose_writer.?.print("Error starting extraction thread: {}\n", .{err}) catch {};
out_err.* = err;
continue;
};
}
},
.file, .ext_file => {
self.extractRegFileThreaded(alloc, archive, path, options, pool) catch |err| {
if (options.verbose)
options.verbose_writer.?.print("Error extracting file inode #{} to {s}: {}\n", .{ self.hdr.num, path, err }) catch {};
out_err.* = err;
};
},
.symlink, .ext_symlink => {
self.extractSymlink(path) catch |err| {
if (options.verbose)
options.verbose_writer.?.print("Error extracting symlink inode #{} to {s}: {}\n", .{ self.hdr.num, path, err }) catch {};
out_err.* = err;
};
},
else => {
self.extractDevice(archive, path, options) catch |err| {
if (options.verbose)
options.verbose_writer.?.print("Error extracting device/IPC inode #{} to {s}: {}\n", .{ self.hdr.num, path, err }) catch {};
out_err.* = err;
};
},
}
}
/// Creates and writes the inode file contents to the given path.
/// Optionally set owner & permissions.
///
/// Assumes the inode is a file or ext_file type.
fn extractRegFile(self: Inode, alloc: std.mem.Allocator, archive: *Archive, path: []const u8, options: ExtractionOptions) !void {
var fil = try std.fs.cwd().createFile(path, .{ .exclusive = true });
defer fil.close();
var wrt = fil.writer(&[0]u8{});
var dat_rdr = try self.dataReader(alloc, archive);
defer dat_rdr.deinit();
_ = try dat_rdr.interface.streamRemaining(&wrt.interface);
try wrt.interface.flush();
fn init(alloc: std.mem.Allocator, rdr: *Reader, block_size: u32) !ExtFile {
var data: [40]u8 = undefined;
try rdr.readSliceAll(&data);
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(self.hdr.uid_idx), try archive.id(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 frag_idx = std.mem.readInt(u32, data[28..32], .little);
const size = std.mem.readInt(u64, data[8..16], .little);
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(self.hdr.uid_idx), try archive.id(self.hdr.gid_idx));
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);
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,
};
}
}
/// 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,
};
pub const Symlink = struct {
hard_links: u32,
target: []const u8,
fn init(alloc: std.mem.Allocator, rdr: *Reader) !Symlink {
var data: [8]u8 = undefined;
try rdr.readSliceAll(&data);
const target_size = std.mem.readInt(u32, data[4..], .little);
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,
};
}
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;
},
};
pub const ExtSymlink = struct {
hard_links: u32,
target: []const u8,
xattr_idx: u32,
fn init(alloc: std.mem.Allocator, rdr: *Reader) !ExtSymlink {
const sym: Symlink = try .init(alloc, rdr);
errdefer alloc.free(sym.target);
var xattr_idx: u32 = undefined;
try rdr.readSliceEndian(u32, @ptrCast(&xattr_idx), .little);
return .{
.hard_links = sym.hard_links,
.target = sym.target,
.xattr_idx = xattr_idx,
};
}
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(self.hdr.uid_idx), try archive.id(self.hdr.gid_idx));
};
pub const Dev = extern struct {
hard_links: u32,
device: u32,
fn init(rdr: *Reader) !Dev {
var new: Dev = undefined;
try rdr.readSliceEndian(Dev, @ptrCast(&new), .little);
return new;
}
if (!options.ignore_xattr) {
// TODO
};
pub const ExtDev = extern struct {
hard_links: u32,
device: u32,
xattr_idx: u32,
fn init(rdr: *Reader) !ExtDev {
var new: ExtDev = undefined;
try rdr.readSliceEndian(ExtDev, @ptrCast(&new), .little);
return new;
}
}
};
pub const IPC = extern struct {
hard_links: u32,
fn init(rdr: *Reader) !IPC {
var new: IPC = undefined;
try rdr.readSliceEndian(IPC, @ptrCast(&new), .little);
return new;
}
};
pub const ExtIPC = extern struct {
hard_links: u32,
xattr_idx: u32,
fn init(rdr: *Reader) !ExtIPC {
var new: ExtIPC = undefined;
try rdr.readSliceEndian(ExtIPC, @ptrCast(&new), .little);
return new;
}
};
-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,
};
+7 -2
View File
@@ -5,6 +5,8 @@ const Writer = std.Io.Writer;
const ExtractionOptions = @This();
/// Whether to use single threaded extraction. For finer control, create an std.Io instance.
single_threaded: bool = false,
/// Don't set the file's owner & permissions after extraction
ignore_permissions: bool = false,
/// Don't set xattr values. Currently xattrs are never set anyway.
@@ -16,10 +18,13 @@ verbose: bool = false,
/// Where to print verbose log.
verbose_writer: ?*Writer = null,
pub const Default: ExtractionOptions = .{};
pub fn VerboseDefault(wrt: *Writer) ExtractionOptions {
pub const default: ExtractionOptions = .{};
pub const single_threaded_default: ExtractionOptions = .{ .single_threaded = true };
pub fn VerboseDefault(wrt: *Writer) !ExtractionOptions {
return .{
.verbose = true,
.verbose_writer = wrt,
.threads = try std.Thread.getCpuCount(),
};
}
+6
View File
@@ -1 +1,7 @@
pub const Archive = @import("archive.zig");
pub const File = @import("file.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 stuff = @import("builtin");
const Io = std.Io;
const testing = std.testing;
const Archive = @import("archive.zig");
const Superblock = @import("super.zig").Superblock;
const TestArchive = "testing/LinuxPATest.sfs";
test "Basics" {
var fil = try std.fs.cwd().openFile(TestArchive, .{});
defer fil.close();
var sfs: Archive = try .init(std.testing.allocator, fil);
defer sfs.deinit();
if (sfs.super != LinuxPATestCorrectSuperblock) {
std.debug.print("Superblock wrong\nShould be: {}\n\nis: {}\n", .{ LinuxPATestCorrectSuperblock, sfs.super });
return error.BadSuperblock;
}
const io = testing.io;
const alloc = testing.allocator;
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 testing.expectEqualDeep(archive.super, LinuxPATestCorrectSuperblock);
var root = try archive.root(alloc);
defer root.deinit();
}
const TestFile = "Start.exe";
const TestFileExtractLocation = "testing/Start.exe";
test "ExtractSingleFile" {
std.fs.cwd().deleteFile(TestFileExtractLocation) catch {};
var fil = try std.fs.cwd().openFile(TestArchive, .{});
defer fil.close();
var sfs: Archive = try .init(std.testing.allocator, fil);
defer sfs.deinit();
var test_fil = try sfs.open(TestFile);
defer test_fil.deinit();
try test_fil.extract(TestFileExtractLocation, .Default);
//TODO: validate extracted file.
test "ExtractSingleFileMT" {
const io = testing.io;
const alloc = testing.allocator;
Io.Dir.cwd().deleteFile(io, TestFileExtractLocation) 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);
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" {
std.fs.cwd().deleteTree(TestFullExtractLocation) catch {};
var fil = try std.fs.cwd().openFile(TestArchive, .{});
defer fil.close();
var sfs: Archive = try .init(std.testing.allocator, fil);
defer sfs.deinit();
try sfs.extract(TestFullExtractLocation, .Default);
Io.Dir.cwd().deleteFile(io, TestFileExtractLocation) 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);
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),
.inode_count = 2974,
.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;
}
+85
View File
@@ -0,0 +1,85 @@
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);
const value = self.map.getPtr(key);
self.mut.unlockShared(io);
if (value != null) {
if (!value.?.filled.isSet())
try value.?.filled.wait(io);
if (value.?.err != null) return value.?.err.?;
return &value.?.value;
}
}
var value: *ProtectedValue = blk: {
try self.mut.lock(io);
defer self.mut.unlock(io);
const res = try self.map.getOrPut(key);
if (res.found_existing)
return self.getOrPut(io, key, create_fn_args);
res.value_ptr.* = .{};
break :blk res.value_ptr;
};
defer value.filled.set(io);
value.value = if (@TypeOf(CreateError) == void)
@call(.auto, create_fn, create_fn_args)
else
@call(.auto, create_fn, create_fn_args) catch |err| {
value.err = err;
return err;
};
return &value.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