Compare commits
99 Commits
zig-0.14
...
3239bf0e01
| Author | SHA1 | Date | |
|---|---|---|---|
| 3239bf0e01 | |||
| 0df14b8adc | |||
| 8186c3fe9a | |||
| 2b49395ab2 | |||
| 84a9cf17b9 | |||
| d1d453ac29 | |||
| 69ce562b6c | |||
| 10e9b66ac6 | |||
| 3c57a2d1e4 | |||
| 700993b0e3 | |||
| 78d1ee2937 | |||
| 2b0625e178 | |||
| ad05e5dff1 | |||
| 688ca53206 | |||
| 93a55aa5c7 | |||
| d76b164e45 | |||
| 5521b2ce6a | |||
| cbd2697c19 | |||
| a3f7b86e67 | |||
| ab606bdfa5 | |||
| 274d088490 | |||
| b67d02074d | |||
| 4b2b7021c7 | |||
| a1b9828578 | |||
| 8e4661c4c6 | |||
| 54aaf30ea5 | |||
| df22cf6529 | |||
| 6b5c830234 | |||
| 116234cf9c | |||
| 4601e8f323 | |||
| 50cae8b63d | |||
| 8b8c9a772f | |||
| f563648b20 | |||
| 8d4f3d72f8 | |||
| 0b6129f1ae | |||
| 7308faad36 | |||
| c9499251f8 | |||
| d470ca98e3 | |||
| a606f5e11a | |||
| a4e23a840d | |||
| 3a10572953 | |||
| edfe919c1b | |||
| 4515610082 | |||
| beca6a2ae6 | |||
| 760e11df0b | |||
| 5f629df47c | |||
| b0160e005b | |||
| b7b99325da | |||
| b22e4d003d | |||
| a41c37fef4 | |||
| 2d079d77f7 | |||
| 48f4235875 | |||
| 567dea8a0b | |||
| f78e5c7386 | |||
| 81e975c0d9 | |||
| fd274a8072 | |||
| 50ae79637e | |||
| ee41dc7278 | |||
| c34acebf51 | |||
| bdbda29d39 | |||
| ca4e867ddc | |||
| b066835066 | |||
| 2363bd7d10 | |||
| e619005b77 | |||
| 28b44891b3 | |||
| 4829c802a3 | |||
| 570db9632a | |||
| 0076294675 | |||
| fd9e3d595b | |||
| b8189490eb | |||
| 6adc1d5c0c | |||
| 5ec12b5786 | |||
| b892adacd7 | |||
| 2760ad6ccb | |||
| 61311433b9 | |||
| 053d64a954 | |||
| 0e0222cd02 | |||
| 9c0dfbadc2 | |||
| db2fb4b9f2 | |||
| 067eaa87c2 | |||
| b64a3ec44a | |||
| 704215e1a9 | |||
| bcfd983f8d | |||
| 75502da1d0 | |||
| a316ba569f | |||
| a0f3f45885 | |||
| f771ef7623 | |||
| 0d2576f5ee | |||
| a76803aad1 | |||
| 1ff1e91d5e | |||
| 2bcbc16613 | |||
| 3c98cf2cdb | |||
| 2c392cf250 | |||
| 5d4e7b1435 | |||
| 7aed59b5b1 | |||
| f3fb8a128f | |||
| 23bb19644b | |||
| ed14f13d9a | |||
| 428f938c3a |
@@ -0,0 +1,31 @@
|
||||
name: Release Build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- uses: mlugg/setup-zig@v2
|
||||
- name: Install deps
|
||||
run: sudo apt update && sudo apt install -y liblzma-dev liblzo2-dev
|
||||
- name: Build normal version
|
||||
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 --release=fast -Dversion="${{ github.ref_name }}"
|
||||
- name: Move C build out
|
||||
run: mv zig-out/bin/unsquashfs ./unsquashfs-x86_64-c-libs
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
prerelease: true
|
||||
files: |
|
||||
unsquashfs-x86_64-zig-libs
|
||||
unsquashfs-x86_64-c-libs
|
||||
@@ -1,2 +1,5 @@
|
||||
testing/
|
||||
|
||||
.zig-cache/
|
||||
zig-out/
|
||||
zig-pkg/
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Project-local debug tasks
|
||||
//
|
||||
// For more documentation on how to configure debug tasks,
|
||||
// see: https://zed.dev/docs/debugger
|
||||
[
|
||||
{
|
||||
"label": "Build & Run",
|
||||
|
||||
"adapter": "CodeLLDB",
|
||||
"request": "launch",
|
||||
|
||||
"build": {
|
||||
"command": "zig",
|
||||
"args": ["build", "-Ddebug=true"],
|
||||
},
|
||||
|
||||
"program": "zig-out/bin/unsquashfs",
|
||||
"args": [
|
||||
"--force",
|
||||
"-d",
|
||||
"testing/TestExtractUnsquashfs",
|
||||
"testing/LinuxPATest.sfs",
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Caleb Gardner
|
||||
Copyright (c) 2026 Caleb Gardner
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# zig-squashfs
|
||||
|
||||
This is my experiments to learn Zig. Might amount to something. Might not.
|
||||
|
||||
A library and application to decompress or view squashfs archives.
|
||||
|
||||
## Current State
|
||||
|
||||
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=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=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`.
|
||||
|
||||
> `-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.
|
||||
|
||||
## Capabilities
|
||||
|
||||
Most features are present except for the following:
|
||||
|
||||
* 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.
|
||||
|
||||
## 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.
|
||||
|
||||
```zig
|
||||
pub const lzo_bytep = @compileError("unable to translate C expr: unexpected token ''");
|
||||
// /usr/include/lzo/lzoconf.h:148:9
|
||||
pub const lzo_charp = @compileError("unable to translate C expr: unexpected token ''");
|
||||
// /usr/include/lzo/lzoconf.h:149:9
|
||||
pub const lzo_voidp = @compileError("unable to translate C expr: unexpected token ''");
|
||||
```
|
||||
|
||||
to
|
||||
|
||||
```zig
|
||||
pub const lzo_bytep = *u8;
|
||||
// /usr/include/lzo/lzoconf.h:148:9
|
||||
pub const lzo_charp = @compileError("unable to translate C expr: unexpected token ''");
|
||||
// /usr/include/lzo/lzoconf.h:149:9
|
||||
pub const lzo_voidp = ?*anyopaque;
|
||||
```
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#! /usr/bin/env bash
|
||||
|
||||
ARCHIVE="testing/LinuxPATest.sfs"
|
||||
|
||||
REF_EXT_LOC="testing/LinuxPAReference"
|
||||
PROG_EXT_LOC="testing/LinuxPABinTest"
|
||||
|
||||
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"
|
||||
@@ -0,0 +1,96 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) !void {
|
||||
// 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;
|
||||
var debug = b.option(bool, "debug", "Enable options to make debugging easier.");
|
||||
const version_string_option = b.option([]const u8, "version", "Version of the library/binary");
|
||||
|
||||
// const zig_squashfs_options = b.addOptions();
|
||||
// zig_squashfs_options.addOption(bool, "use_zig_decomp", use_zig_decomp);
|
||||
// zig_squashfs_options.addOption(bool, "allow_lzo", allow_lzo);
|
||||
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
if (optimize == .Debug) debug = true;
|
||||
|
||||
const lib = b.addLibrary(.{
|
||||
.name = "squashfs",
|
||||
.root_module = b.createModule(.{
|
||||
.optimize = if (debug == true) .Debug else optimize,
|
||||
.target = target,
|
||||
.valgrind = debug,
|
||||
.root_source_file = b.path("src/root.zig"),
|
||||
.link_libc = true,
|
||||
}),
|
||||
.use_llvm = debug,
|
||||
});
|
||||
|
||||
const zstd = b.dependency("zstd", .{ .optimize = optimize, .target = target });
|
||||
lib.root_module.linkLibrary(zstd.artifact("zstd"));
|
||||
|
||||
const c = b.addTranslateC(.{
|
||||
.optimize = optimize,
|
||||
.target = target,
|
||||
.root_source_file = b.path("src/c.h"),
|
||||
});
|
||||
lib.root_module.addImport("c", c.createModule());
|
||||
|
||||
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),
|
||||
);
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "unsquashfs",
|
||||
.root_module = b.createModule(.{
|
||||
.optimize = if (debug == true) .Debug else optimize,
|
||||
.target = target,
|
||||
.valgrind = debug,
|
||||
.root_source_file = b.path("src/bin/unsquashfs.zig"),
|
||||
.imports = &.{
|
||||
.{ .name = "zig_squashfs", .module = lib.root_module },
|
||||
},
|
||||
}),
|
||||
.use_llvm = debug,
|
||||
});
|
||||
exe.root_module.addOptions("config", unsquashfs_options);
|
||||
|
||||
b.installArtifact(lib);
|
||||
b.installArtifact(exe);
|
||||
|
||||
const mod_tests = b.addTest(.{
|
||||
.root_module = b.createModule(.{
|
||||
.optimize = .Debug,
|
||||
.target = target,
|
||||
.root_source_file = b.path("src/test.zig"),
|
||||
.imports = &.{
|
||||
.{ .name = "c", .module = c.createModule() },
|
||||
},
|
||||
.valgrind = true,
|
||||
}),
|
||||
.use_llvm = true,
|
||||
});
|
||||
mod_tests.root_module.linkLibrary(zstd.artifact("zstd"));
|
||||
|
||||
const run_mod_tests = b.addRunArtifact(mod_tests);
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&run_mod_tests.step);
|
||||
|
||||
// zls build check steps
|
||||
const lib_check = b.addLibrary(.{
|
||||
.name = "squashfs",
|
||||
.root_module = exe.root_module,
|
||||
});
|
||||
const exe_check = b.addExecutable(.{
|
||||
.name = "unsquashfs",
|
||||
.root_module = lib.root_module,
|
||||
});
|
||||
const check = b.step("check", "Check if unsquashfs compiles");
|
||||
check.dependOn(&lib_check.step);
|
||||
check.dependOn(&exe_check.step);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
.{
|
||||
.name = .squashfs,
|
||||
.version = "0.0.6",
|
||||
.fingerprint = 0x37ba29474b87f145, // Changing this has security and trust implications.
|
||||
.minimum_zig_version = "0.15.2",
|
||||
.dependencies = .{
|
||||
.zlib_ng = .{
|
||||
.url = "git+https://github.com/CalebQ42/zig-zlib-ng#5f2f02dfb28acca2517dacbbd09e9b987f57b133",
|
||||
.hash = "zlib_ng-2.3.3-pre1-2HYS4ClFAABW8KlHMyBHtlNKE3V7kCS8wqfxawG7xeaa",
|
||||
},
|
||||
.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",
|
||||
},
|
||||
.minilzo = .{
|
||||
.url = "git+https://github.com/CalebQ42/zig-minilzo.git#7cbae997b91a44d74b7cd6c073584dc9562a6c90",
|
||||
.hash = "minilzo-2.10.0-Ij7BO8wLAADeWI4Pe4jp8XTDsDaquZR14oZ7_9yKKDWP",
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
},
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
|
||||
const Decomp = @import("decomp.zig");
|
||||
const ExtractionOptions = @import("options.zig");
|
||||
const File = @import("file.zig");
|
||||
const Inode = @import("inode.zig");
|
||||
const LookupTable = @import("lookup_table.zig");
|
||||
const Decompressor = @import("util/decompressor.zig");
|
||||
const MetadataReader = @import("util/metadata.zig");
|
||||
const Utils = @import("util/misc.zig");
|
||||
const OffsetFile = @import("util/offset_file.zig");
|
||||
|
||||
const Archive = @This();
|
||||
|
||||
file: OffsetFile,
|
||||
super: Superblock,
|
||||
|
||||
stateless_decomp: *const Decompressor,
|
||||
|
||||
pub fn init(io: Io, file: std.Io.File, offset: u64) !Archive {
|
||||
var rdr = file.reader(io, &[0]u8{});
|
||||
try rdr.seekTo(offset);
|
||||
var super: Superblock = undefined;
|
||||
try rdr.interface.readSliceEndian(Superblock, @ptrCast(&super), .little);
|
||||
|
||||
return .{
|
||||
.file = try .init(io, file, super.size, offset),
|
||||
.super = super,
|
||||
|
||||
.stateless_decomp = try Decomp.StatelessDecomp(super.compression),
|
||||
};
|
||||
}
|
||||
|
||||
/// The root folder of the Archive. Used to open other Files.
|
||||
pub fn root(self: Archive, alloc: std.mem.Allocator, io: Io) !File {
|
||||
const root_inode = try Utils.inodeFromRef(
|
||||
alloc,
|
||||
io,
|
||||
self.file,
|
||||
self.stateless_decomp,
|
||||
self.super.inode_start,
|
||||
self.super.block_size,
|
||||
self.super.root_ref,
|
||||
);
|
||||
return .init(alloc, self, root_inode, "");
|
||||
}
|
||||
/// Opens a File within the archive.
|
||||
pub fn open(self: Archive, alloc: std.mem.Allocator, io: Io, filepath: []const u8) !File {
|
||||
const root_file = try self.root(alloc, io);
|
||||
const path = std.mem.trim(u8, filepath, "/");
|
||||
if (Utils.pathIsSelf(path))
|
||||
return root_file;
|
||||
defer root_file.deinit();
|
||||
return root_file.open(alloc, io, filepath);
|
||||
}
|
||||
|
||||
/// Returns the inode with the given inode number.
|
||||
/// Requires that the archive is exportable (has an export lookup table).
|
||||
pub fn inode(self: Archive, alloc: std.mem.Allocator, io: Io, num: u32) !Inode {
|
||||
if (!self.super.flags.exportable)
|
||||
return error.NotExportable;
|
||||
const ref = try LookupTable.lookupValue(
|
||||
Inode.Ref,
|
||||
alloc,
|
||||
io,
|
||||
&self.stateless_decomp,
|
||||
self.file,
|
||||
self.super.export_start,
|
||||
num + 1,
|
||||
);
|
||||
return Utils.inodeFromRef(
|
||||
alloc,
|
||||
io,
|
||||
self.file,
|
||||
&self.stateless_decomp,
|
||||
self.super.inode_start,
|
||||
self.super.block_size,
|
||||
ref,
|
||||
);
|
||||
}
|
||||
/// Returns a value at the given index from the Archive's id (uid/gid) table.
|
||||
pub fn idTable(self: Archive, alloc: std.mem.Allocator, io: Io, idx: u32) !u16 {
|
||||
return LookupTable.lookupValue(
|
||||
u16,
|
||||
alloc,
|
||||
io,
|
||||
&self.stateless_decomp,
|
||||
self.file,
|
||||
self.super.id_start,
|
||||
idx,
|
||||
);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
};
|
||||
|
||||
// Extraction
|
||||
|
||||
/// Extract the entire archive contents to the given directory.
|
||||
pub fn extract(self: Archive, alloc: std.mem.Allocator, io: Io, extract_dir: []const u8, options: ExtractionOptions) !void {
|
||||
const root_inode = try Utils.inodeFromRef(
|
||||
alloc,
|
||||
self.file,
|
||||
self.stateless_decomp,
|
||||
self.super.inode_start,
|
||||
self.super.block_size,
|
||||
self.super.root_ref,
|
||||
);
|
||||
return root_inode.extract(alloc, io, self.file, self.super, extract_dir, options);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
const Writer = Io.Writer;
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const config = @import("config");
|
||||
const squashfs = @import("zig_squashfs");
|
||||
|
||||
//TODO: Add more options
|
||||
const help_mgs =
|
||||
\\
|
||||
\\Usage: unsquashfs [options] <archive>
|
||||
\\
|
||||
\\Options:
|
||||
\\ -d <location> Extract to the given location instead of "squashfs-root"
|
||||
\\
|
||||
\\ -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 not 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
|
||||
\\
|
||||
;
|
||||
|
||||
const errors = error{InvalidArguments};
|
||||
|
||||
var archive: []const u8 = "";
|
||||
var extLoc: []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;
|
||||
|
||||
pub fn main(init: std.process.Init) !void {
|
||||
const alloc = init.gpa;
|
||||
const io = init.io;
|
||||
|
||||
var stdout = std.Io.File.stdout();
|
||||
defer stdout.close(io);
|
||||
var out = stdout.writer(io, &[0]u8{});
|
||||
defer out.interface.flush() catch {};
|
||||
|
||||
try handleArgs(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 = 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: Handle error gracefully.
|
||||
const options: squashfs.ExtractionOptions = .{
|
||||
.verbose = verbose,
|
||||
.verbose_writer = if (verbose) &out.interface else null,
|
||||
.ignore_xattr = ignore_xattrs,
|
||||
.ignore_permissions = ignore_permissions,
|
||||
};
|
||||
|
||||
if (force)
|
||||
try Io.Dir.cwd().deleteTree(io, extLoc);
|
||||
try arc.extract(alloc, io, extLoc, options); //TODO: Handle error gracefully.
|
||||
}
|
||||
|
||||
fn handleArgs(args: std.process.Args, out: *Writer) !void {
|
||||
var arg_iter = args.iterate();
|
||||
defer arg_iter.deinit();
|
||||
_ = arg_iter.next(); // args[0] is the application launch command.
|
||||
while (arg_iter.next()) |arg| {
|
||||
if (std.mem.eql(u8, arg, "-o")) {
|
||||
const nxt = arg_iter.next();
|
||||
if (nxt == null or nxt.?.len == 0) {
|
||||
try out.print("-o must be followed by a number\n", .{});
|
||||
return errors.InvalidArguments;
|
||||
}
|
||||
offset = std.fmt.parseInt(u64, nxt.?, 10) catch {
|
||||
try out.print("-o must be followed by a number\n", .{});
|
||||
return errors.InvalidArguments;
|
||||
};
|
||||
continue;
|
||||
} else if (std.mem.eql(u8, arg, "-d")) {
|
||||
const nxt = arg_iter.next();
|
||||
if (nxt == null or nxt.?.len == 0) {
|
||||
try out.print("-d must be followed by a location\n", .{});
|
||||
return errors.InvalidArguments;
|
||||
}
|
||||
extLoc = nxt.?;
|
||||
continue;
|
||||
} else if (std.mem.eql(u8, arg, "-p")) {
|
||||
const nxt = arg_iter.next();
|
||||
if (nxt == null or nxt.?.len == 0) {
|
||||
try out.print("-p must be followed by a number\n", .{});
|
||||
return errors.InvalidArguments;
|
||||
}
|
||||
threads = std.fmt.parseInt(u32, nxt.?, 10) catch {
|
||||
try out.print("-p must be followed by a number\n", .{});
|
||||
return errors.InvalidArguments;
|
||||
};
|
||||
continue;
|
||||
} 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 out.print("\nBuilt using Zig {s} in {} mode\n", .{ builtin.zig_version_string, builtin.mode });
|
||||
std.process.exit(0);
|
||||
return;
|
||||
} else if (std.mem.eql(u8, arg, "--help")) {
|
||||
try out.print(help_mgs, .{});
|
||||
std.process.exit(0);
|
||||
return;
|
||||
}
|
||||
if (archive.len > 0) {
|
||||
try out.print("you can only provide one file at a time\n", .{});
|
||||
try out.print(help_mgs, .{});
|
||||
return errors.InvalidArguments;
|
||||
}
|
||||
archive = arg;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Decompressor = @import("util/decompressor.zig");
|
||||
|
||||
pub const Enum = enum(u16) {
|
||||
gzip = 1,
|
||||
lzma,
|
||||
lzo,
|
||||
xz,
|
||||
lz4,
|
||||
zstd,
|
||||
};
|
||||
|
||||
pub fn StatelessDecomp(val: Enum) !*const Decompressor {
|
||||
return switch (val) {
|
||||
.gzip => &@import("decomp/zlib.zig").stateless_decompressor,
|
||||
.lzma => &@import("decomp/lzma.zig").stateless_decompressor,
|
||||
.lzo => error.LzoUnsupported,
|
||||
.xz => &@import("decomp/xz.zig").stateless_decompressor,
|
||||
.lz4 => error.Lz4Unsupported,
|
||||
.zstd => &@import("decomp/zstd.zig").stateless_decompressor,
|
||||
};
|
||||
}
|
||||
|
||||
pub const Decomp = union(enum) {
|
||||
gzip: @import("decomp/zlib.zig"),
|
||||
lzma: @import("decomp/lzma.zig"),
|
||||
lzo: void,
|
||||
xz: @import("decomp/xz.zig"),
|
||||
lz4: void,
|
||||
zstd: @import("decomp/zstd.zig"),
|
||||
|
||||
pub fn deinit(self: *Decomp) void {
|
||||
switch (self.*) {
|
||||
.gzip => self.gzip.deinit(),
|
||||
.lzma => self.lzma.deinit(),
|
||||
.xz => self.xz.deinit(),
|
||||
.zstd => self.zstd.deinit(),
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decompressor(self: *Decomp) *Decompressor {
|
||||
return switch (self.*) {
|
||||
.gzip => &self.gzip.interface,
|
||||
.lzma => &self.lzma.interface,
|
||||
.xz => &self.xz.interface,
|
||||
.zstd => &self.zstd.interface,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
const std = @import("std");
|
||||
const Reader = std.Io.Reader;
|
||||
const lzma = std.compress.lzma;
|
||||
const Node = std.SinglyLinkedList.Node;
|
||||
|
||||
const Decompressor = @import("../util/decompressor.zig");
|
||||
const Error = Decompressor.Error;
|
||||
|
||||
const Self = @This();
|
||||
|
||||
const Buffer = struct {
|
||||
node: Node,
|
||||
buf: []u8,
|
||||
};
|
||||
|
||||
interface: Decompressor = .{ .decomp_fn = decomp },
|
||||
|
||||
alloc: std.mem.Allocator,
|
||||
|
||||
block_size: u32,
|
||||
buffers: std.ArrayList(Buffer),
|
||||
buffer_queue: std.SinglyLinkedList = .{},
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator, block_size: u32) !Self {
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
|
||||
.block_size = block_size,
|
||||
.buffers = try .initCapacity(alloc, 5),
|
||||
};
|
||||
}
|
||||
pub fn deinit(self: *Self) void {
|
||||
for (self.buffers.items) |buf|
|
||||
self.alloc.free(buf.buf);
|
||||
self.buffers.deinit(self.alloc);
|
||||
}
|
||||
|
||||
fn decomp(d: ?*const Decompressor, alloc: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
|
||||
if (d == null) {
|
||||
var buf = try alloc.alloc(u8, in.len * 2);
|
||||
defer alloc.free(buf);
|
||||
return lzmaDecomp(alloc, &buf, in, out) catch |err| return switch (err) {
|
||||
error.OutOfMemory => Error.OutOfMemory,
|
||||
else => Error.ReadFailed,
|
||||
};
|
||||
}
|
||||
var self: *Self = @fieldParentPtr("interface", @constCast(d.?));
|
||||
const buf_node = self.buffer_queue.popFirst();
|
||||
var buf: *Buffer = undefined;
|
||||
if (buf_node == null) {
|
||||
const new_buf = try self.buffers.addOne(self.alloc);
|
||||
new_buf.* = .{ .node = .{}, .buf = try self.alloc.alloc(u8, self.block_size) };
|
||||
buf = new_buf;
|
||||
} else {
|
||||
buf = @fieldParentPtr("node", buf_node.?);
|
||||
}
|
||||
defer self.buffer_queue.prepend(&buf.node);
|
||||
return lzmaDecomp(self.alloc, &buf.buf, in, out) catch |err| {
|
||||
// self.err = err;
|
||||
return switch (err) {
|
||||
error.OutOfMemory => Error.OutOfMemory,
|
||||
else => Error.ReadFailed,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
inline fn lzmaDecomp(alloc: std.mem.Allocator, buffer: *[]u8, in: []u8, out: []u8) !usize {
|
||||
var rdr: Reader = .fixed(in);
|
||||
var d = try lzma.Decompress.initOptions(&rdr, alloc, buffer.*, .{ .allow_incomplete = true }, 3 * 1024 * 1024);
|
||||
defer {
|
||||
buffer.* = d.takeBuffer();
|
||||
d.deinit();
|
||||
}
|
||||
|
||||
return d.reader.readSliceShort(out);
|
||||
}
|
||||
|
||||
// Stateless
|
||||
|
||||
pub const stateless_decompressor: Decompressor = .{ .decomp_fn = statelessDecomp };
|
||||
|
||||
fn statelessDecomp(_: ?*const Decompressor, alloc: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
|
||||
var buf = try alloc.alloc(u8, in.len);
|
||||
defer alloc.free(buf);
|
||||
return lzmaDecomp(alloc, &buf, in, out) catch return Error.ReadFailed;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
const std = @import("std");
|
||||
const Reader = std.Io.Reader;
|
||||
const xz = std.compress.xz;
|
||||
const Node = std.SinglyLinkedList.Node;
|
||||
|
||||
const Decompressor = @import("../util/decompressor.zig");
|
||||
const Error = Decompressor.Error;
|
||||
|
||||
const Self = @This();
|
||||
|
||||
const Buffer = struct {
|
||||
node: Node,
|
||||
buf: []u8,
|
||||
};
|
||||
|
||||
interface: Decompressor = .{ .decomp_fn = decomp },
|
||||
|
||||
alloc: std.mem.Allocator,
|
||||
|
||||
block_size: u32,
|
||||
buffers: std.ArrayList(Buffer),
|
||||
buffer_queue: std.SinglyLinkedList = .{},
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator, block_size: u32) !Self {
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
|
||||
.block_size = block_size,
|
||||
.buffers = try .initCapacity(alloc, 5),
|
||||
};
|
||||
}
|
||||
pub fn deinit(self: *Self) void {
|
||||
for (self.buffers.items) |buf|
|
||||
self.alloc.free(buf.buf);
|
||||
self.buffers.deinit(self.alloc);
|
||||
}
|
||||
|
||||
fn decomp(d: ?*const Decompressor, alloc: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
|
||||
if (d == null) {
|
||||
var buf = try alloc.alloc(u8, in.len * 2);
|
||||
defer alloc.free(buf);
|
||||
return xzDecomp(alloc, &buf, in, out) catch return Error.ReadFailed;
|
||||
}
|
||||
var self: *Self = @fieldParentPtr("interface", @constCast(d.?));
|
||||
const buf_node = self.buffer_queue.popFirst();
|
||||
var buf: *Buffer = undefined;
|
||||
if (buf_node == null) {
|
||||
const new_buf = try self.buffers.addOne(self.alloc);
|
||||
new_buf.* = .{ .node = .{}, .buf = try self.alloc.alloc(u8, self.block_size) };
|
||||
buf = new_buf;
|
||||
} else {
|
||||
buf = @fieldParentPtr("node", buf_node.?);
|
||||
}
|
||||
defer self.buffer_queue.prepend(&buf.node);
|
||||
return xzDecomp(self.alloc, &buf.buf, in, out) catch {
|
||||
// self.err = err;
|
||||
return Error.ReadFailed;
|
||||
};
|
||||
}
|
||||
|
||||
inline fn xzDecomp(alloc: std.mem.Allocator, buffer: *[]u8, in: []u8, out: []u8) !usize {
|
||||
var rdr: Reader = .fixed(in);
|
||||
var d = try xz.Decompress.init(&rdr, alloc, buffer.*);
|
||||
defer {
|
||||
buffer.* = d.takeBuffer();
|
||||
d.deinit();
|
||||
}
|
||||
|
||||
return d.reader.readSliceShort(out);
|
||||
}
|
||||
|
||||
// Stateless
|
||||
|
||||
pub const stateless_decompressor: Decompressor = .{ .decomp_fn = statelessDecomp };
|
||||
|
||||
fn statelessDecomp(_: ?*const Decompressor, alloc: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
|
||||
var buf = try alloc.alloc(u8, in.len);
|
||||
defer alloc.free(buf);
|
||||
return xzDecomp(alloc, &buf, in, out) catch return Error.ReadFailed;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
const std = @import("std");
|
||||
const Reader = std.Io.Reader;
|
||||
const flate = std.compress.flate;
|
||||
const Node = std.SinglyLinkedList.Node;
|
||||
|
||||
const Decompressor = @import("../util/decompressor.zig");
|
||||
const Error = Decompressor.Error;
|
||||
|
||||
const Self = @This();
|
||||
|
||||
const Buffer = struct {
|
||||
node: Node,
|
||||
buf: []u8,
|
||||
};
|
||||
|
||||
interface: Decompressor = .{ .decomp_fn = decomp },
|
||||
|
||||
alloc: std.mem.Allocator,
|
||||
|
||||
block_size: u32,
|
||||
buffers: std.ArrayList(Buffer),
|
||||
buffer_queue: std.SinglyLinkedList = .{},
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator, block_size: u32) !Self {
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
|
||||
.block_size = block_size,
|
||||
.buffers = try .initCapacity(alloc, 5),
|
||||
};
|
||||
}
|
||||
pub fn deinit(self: *Self) void {
|
||||
for (self.buffers.items) |buf|
|
||||
self.alloc.free(buf.buf);
|
||||
self.buffers.deinit(self.alloc);
|
||||
}
|
||||
|
||||
fn decomp(d: ?*const Decompressor, alloc: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
|
||||
if (d == null) {
|
||||
const buf = try alloc.alloc(u8, in.len * 2);
|
||||
defer alloc.free(buf);
|
||||
return zlibDecomp(buf, in, out);
|
||||
}
|
||||
var self: *Self = @fieldParentPtr("interface", @constCast(d.?));
|
||||
const buf_node = self.buffer_queue.popFirst();
|
||||
var buf: *Buffer = undefined;
|
||||
if (buf_node == null) {
|
||||
const new_buf = try self.buffers.addOne(self.alloc);
|
||||
new_buf.* = .{ .node = .{}, .buf = try self.alloc.alloc(u8, self.block_size) };
|
||||
buf = new_buf;
|
||||
} else {
|
||||
buf = @fieldParentPtr("node", buf_node.?);
|
||||
}
|
||||
defer self.buffer_queue.prepend(&buf.node);
|
||||
return zlibDecomp(buf.buf, in, out);
|
||||
}
|
||||
|
||||
inline fn zlibDecomp(buffer: []u8, in: []u8, out: []u8) !usize {
|
||||
var rdr: Reader = .fixed(in);
|
||||
var d = flate.Decompress.init(&rdr, .zlib, buffer);
|
||||
|
||||
return d.reader.readSliceShort(out);
|
||||
}
|
||||
|
||||
// Stateless
|
||||
|
||||
pub const stateless_decompressor: Decompressor = .{ .decomp_fn = statelessDecomp };
|
||||
|
||||
fn statelessDecomp(_: ?*const Decompressor, alloc: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
|
||||
const buf = try alloc.alloc(u8, in.len * 2);
|
||||
defer alloc.free(buf);
|
||||
return zlibDecomp(buf, in, out);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
const Reader = std.Io.Reader;
|
||||
const zstd = std.compress.zstd;
|
||||
const Node = std.SinglyLinkedList.Node;
|
||||
|
||||
const c = @import("c");
|
||||
|
||||
const Decompressor = @import("../util/decompressor.zig");
|
||||
const Error = Decompressor.Error;
|
||||
|
||||
const Queue = std.Io.Queue([]u8);
|
||||
|
||||
const Self = @This();
|
||||
|
||||
interface: Decompressor = .{ .decomp_fn = decomp },
|
||||
|
||||
alloc: std.mem.Allocator,
|
||||
io: Io,
|
||||
|
||||
block_size: u32,
|
||||
buf: [][]u8,
|
||||
buf_queue: Queue,
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator, io: Io, block_size: u32) !Self {
|
||||
const buf = try alloc.alloc([]u8, 20); // TODO: Choose a better number instead of a random one.
|
||||
var queue: Queue = .init(buf);
|
||||
for (0..20) |_|
|
||||
try queue.putOne(io, try alloc.alloc(u8, block_size + zstd.block_size_max));
|
||||
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.io = io,
|
||||
|
||||
.block_size = block_size,
|
||||
.buf = buf,
|
||||
.buf_queue = queue,
|
||||
};
|
||||
}
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.buf_queue.close(self.io);
|
||||
for (self.buf) |buf|
|
||||
self.alloc.free(buf);
|
||||
self.alloc.free(self.buf);
|
||||
}
|
||||
|
||||
fn decomp(d: ?*const Decompressor, alloc: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
|
||||
if (d == null) {
|
||||
const buf = try alloc.alloc(u8, in.len * 2);
|
||||
defer alloc.free(buf);
|
||||
return zstdDecomp(buf, in, out);
|
||||
}
|
||||
var self: *Self = @fieldParentPtr("interface", @constCast(d.?));
|
||||
|
||||
const buf = self.buf_queue.getOne(self.io) catch return Error.ReadFailed;
|
||||
defer self.buf_queue.putOne(self.io, buf) catch {};
|
||||
|
||||
return zstdDecomp(buf, in, out);
|
||||
}
|
||||
|
||||
inline fn zstdDecomp(buffer: []u8, in: []u8, out: []u8) !usize {
|
||||
var rdr: Reader = .fixed(in);
|
||||
var d = zstd.Decompress.init(&rdr, buffer, .{ .window_len = @truncate(out.len) });
|
||||
|
||||
return d.reader.readSliceShort(out);
|
||||
}
|
||||
|
||||
// Stateless
|
||||
|
||||
pub const stateless_decompressor: Decompressor = .{ .decomp_fn = statelessDecomp };
|
||||
|
||||
fn statelessDecomp(_: ?*const Decompressor, alloc: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
|
||||
_ = alloc;
|
||||
const res = c.ZSTD_decompress(out.ptr, out.len, in.ptr, in.len);
|
||||
if (c.ZSTD_isError(res) == 1)
|
||||
return Error.ReadFailed;
|
||||
return res;
|
||||
// const buf = try alloc.alloc(u8, out.len + zstd.block_size_max);
|
||||
// defer alloc.free(buf);
|
||||
// return zstdDecomp(buf, in, out);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
const std = @import("std");
|
||||
const Reader = std.Io.Reader;
|
||||
|
||||
const Inode = @import("inode.zig");
|
||||
|
||||
pub const Error = error{OutOfMemory} || Reader.Error;
|
||||
|
||||
const DirEntry = @This();
|
||||
|
||||
block_start: u32,
|
||||
block_offset: u16,
|
||||
type: Inode.Type,
|
||||
name: []const u8,
|
||||
|
||||
pub fn deinit(self: DirEntry, alloc: std.mem.Allocator) void {
|
||||
alloc.free(self.name);
|
||||
}
|
||||
|
||||
pub fn readDirectory(alloc: std.mem.Allocator, rdr: *Reader, size: u32) Error![]DirEntry {
|
||||
var hdr: Header = undefined;
|
||||
var raw: RawEntry = undefined;
|
||||
var out: std.ArrayList(DirEntry) = try .initCapacity(alloc, 30);
|
||||
errdefer {
|
||||
for (out.items) |ent|
|
||||
alloc.free(ent.name);
|
||||
out.deinit(alloc);
|
||||
}
|
||||
|
||||
var tot_red: u32 = 3;
|
||||
while (tot_red < size) {
|
||||
try rdr.readSliceEndian(Header, @ptrCast(&hdr), .little);
|
||||
try out.ensureUnusedCapacity(alloc, hdr.count + 1);
|
||||
|
||||
tot_red += @sizeOf(Header);
|
||||
|
||||
for (0..hdr.count + 1) |_| {
|
||||
try rdr.readSliceEndian(RawEntry, @ptrCast(&raw), .little);
|
||||
|
||||
const new_name = try alloc.alloc(u8, raw.name_size + 1);
|
||||
try rdr.readSliceEndian(u8, new_name, .little);
|
||||
|
||||
const new = out.addOneAssumeCapacity();
|
||||
new.* = .{
|
||||
.block_start = hdr.block_start,
|
||||
.block_offset = raw.block_offset,
|
||||
.type = raw.type,
|
||||
.name = new_name,
|
||||
};
|
||||
|
||||
tot_red += @sizeOf(RawEntry) + raw.name_size + 1;
|
||||
}
|
||||
}
|
||||
return out.toOwnedSlice(alloc);
|
||||
}
|
||||
|
||||
// Types
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
//! An easier to use wrapper around an inode.
|
||||
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
|
||||
const Archive = @import("archive.zig");
|
||||
const DirEntry = @import("directory.zig");
|
||||
const ExtractionOptions = @import("options.zig");
|
||||
const Inode = @import("inode.zig");
|
||||
const DataExtractor = @import("util/data_extractor.zig");
|
||||
const Decompressor = @import("util/decompressor.zig");
|
||||
const MetadataReader = @import("util/metadata.zig");
|
||||
const SharedCache = @import("util/shared_cache.zig");
|
||||
|
||||
const File = @This();
|
||||
|
||||
alloc: std.mem.Allocator,
|
||||
|
||||
archive: Archive,
|
||||
|
||||
inode: Inode,
|
||||
name: []const u8,
|
||||
|
||||
/// Creates a new File from an inode. Takes ownership of the Inode and creates a copy of the given name.
|
||||
/// Requires the given allocator was used to create the Inode.
|
||||
pub fn init(alloc: std.mem.Allocator, archive: Archive, in: Inode, name: []const u8) !File {
|
||||
const new_name = try alloc.alloc(u8, name.len);
|
||||
@memcpy(new_name, name);
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
|
||||
.archive = archive,
|
||||
|
||||
.inode = in,
|
||||
.name = new_name,
|
||||
};
|
||||
}
|
||||
pub fn fromDirEntry(alloc: std.mem.Allocator, archive: Archive, ent: DirEntry) !File {
|
||||
var rdr = archive.file.readerAt(archive.super.inode_start + ent.block_start);
|
||||
var meta: MetadataReader = .init(alloc, &rdr.interface, archive.stateless_decomp);
|
||||
try meta.interface.discardAll(ent.block_offset);
|
||||
|
||||
var in: Inode = try .read(alloc, &meta.interface, archive.super.block_size);
|
||||
errdefer in.deinit(alloc);
|
||||
return .init(alloc, archive, in, ent.name);
|
||||
}
|
||||
pub fn deinit(self: File) void {
|
||||
self.alloc.free(self.name);
|
||||
self.inode.deinit(self.alloc);
|
||||
}
|
||||
|
||||
pub fn open(self: File, alloc: std.mem.Allocator, io: Io, filepath: []const u8) !File {
|
||||
const entries = try self.inode.readDirectory(
|
||||
alloc,
|
||||
io,
|
||||
self.archive.file,
|
||||
self.archive.stateless_decomp,
|
||||
self.archive.super.dir_start,
|
||||
);
|
||||
defer {
|
||||
for (entries) |ent|
|
||||
alloc.free(ent.name);
|
||||
alloc.free(entries);
|
||||
}
|
||||
const path = std.mem.trim(u8, filepath, "/");
|
||||
const first_element: []const u8 = std.mem.sliceTo(path, '/');
|
||||
|
||||
var search_slice = entries;
|
||||
var idx: usize = undefined;
|
||||
while (search_slice.len > 0) {
|
||||
idx = search_slice.len / 2;
|
||||
const middle = search_slice[idx];
|
||||
switch (std.mem.order(u8, first_element, middle.name)) {
|
||||
.eq => break,
|
||||
.lt => search_slice = search_slice[0..idx],
|
||||
.gt => search_slice = search_slice[idx + 1 ..],
|
||||
}
|
||||
} else return Error.FileNotFound;
|
||||
|
||||
const first_elem_file = try fromDirEntry(alloc, io, self.archive, search_slice[idx]);
|
||||
if (first_element.len == path.len)
|
||||
return first_elem_file;
|
||||
defer first_elem_file.deinit();
|
||||
return first_elem_file.open(alloc, io, path[first_element.len + 1 ..]);
|
||||
}
|
||||
|
||||
pub fn extract(self: File, alloc: std.mem.Allocator, io: Io, filepath: []const u8, options: ExtractionOptions) !void {
|
||||
return self.inode.extract(alloc, io, self.archive.file, self.archive.super, filepath, options);
|
||||
}
|
||||
|
||||
// Types
|
||||
|
||||
pub const Error = error{
|
||||
FileNotFound,
|
||||
} || Inode.Error;
|
||||
@@ -0,0 +1,85 @@
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
|
||||
const BlockSize = @import("inode_data/file.zig").BlockSize;
|
||||
const LookupTable = @import("lookup_table.zig");
|
||||
const Decompressor = @import("util/decompressor.zig");
|
||||
const MetadataReader = @import("util/metadata.zig");
|
||||
const OffsetFile = @import("util/offset_file.zig");
|
||||
|
||||
const FragManager = @This();
|
||||
|
||||
pub const FragEntry = extern struct {
|
||||
start: u64,
|
||||
size: BlockSize,
|
||||
_: u32,
|
||||
};
|
||||
|
||||
alloc: std.mem.Allocator,
|
||||
fil: OffsetFile,
|
||||
decomp: *const Decompressor,
|
||||
block_size: u32,
|
||||
|
||||
entries: []FragEntry,
|
||||
|
||||
frag_cache: std.array_hash_map.Auto(u32, []u8),
|
||||
cache_mut: std.Io.RwLock = .init,
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator, fil: OffsetFile, decomp: *const Decompressor, frag_start: u64, frag_num: u32, block_size: u32) !FragManager {
|
||||
const first_offset: u64 = std.mem.readInt(u64, @ptrCast(fil.map.memory[frag_start .. frag_start + 8]), .little);
|
||||
|
||||
var rdr = fil.readerAt(first_offset);
|
||||
var meta: MetadataReader = .init(alloc, &rdr, decomp);
|
||||
|
||||
const entries = try alloc.alloc(FragEntry, frag_num);
|
||||
errdefer alloc.free(entries);
|
||||
|
||||
try meta.interface.readSliceEndian(FragEntry, entries, .little);
|
||||
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.fil = fil,
|
||||
.decomp = decomp,
|
||||
.block_size = block_size,
|
||||
|
||||
.entries = entries,
|
||||
|
||||
.frag_cache = .empty,
|
||||
};
|
||||
}
|
||||
pub fn deinit(self: *FragManager, io: Io) void {
|
||||
self.cache_mut.lockUncancelable(io);
|
||||
self.alloc.free(self.entries);
|
||||
for (self.frag_cache.values()) |v|
|
||||
self.alloc.free(v);
|
||||
self.frag_cache.deinit(self.alloc);
|
||||
}
|
||||
|
||||
pub fn get(self: *FragManager, io: Io, idx: u32) ![]u8 {
|
||||
{
|
||||
try self.cache_mut.lockShared(io);
|
||||
defer self.cache_mut.unlockShared(io);
|
||||
if (self.frag_cache.contains(idx))
|
||||
return self.frag_cache.get(idx).?;
|
||||
}
|
||||
|
||||
try self.cache_mut.lock(io);
|
||||
defer self.cache_mut.unlock(io);
|
||||
|
||||
if (self.frag_cache.contains(idx))
|
||||
return self.frag_cache.get(idx).?;
|
||||
|
||||
const entry = self.entries[idx];
|
||||
|
||||
const out = try self.alloc.alloc(u8, if (entry.size.uncompressed) entry.size.size else self.block_size);
|
||||
|
||||
if (entry.size.uncompressed) {
|
||||
@memcpy(out, self.fil.map.memory[entry.start .. entry.start + entry.size.size]);
|
||||
} else {
|
||||
@branchHint(.likely);
|
||||
_ = try self.decomp.Decompress(self.alloc, self.fil.map.memory[entry.start .. entry.start + entry.size.size], out);
|
||||
}
|
||||
|
||||
try self.frag_cache.put(self.alloc, idx, out);
|
||||
return out;
|
||||
}
|
||||
+424
@@ -0,0 +1,424 @@
|
||||
//! A file-system object. Represents a File or directory.
|
||||
|
||||
const std = @import("std");
|
||||
const Reader = std.Io.Reader;
|
||||
const Io = std.Io;
|
||||
|
||||
const Archive = @import("archive.zig");
|
||||
const Decomp = @import("decomp.zig").Decomp;
|
||||
const DirEntry = @import("directory.zig");
|
||||
const ExtractionOptions = @import("options.zig");
|
||||
const FragEntry = @import("frag.zig").FragEntry;
|
||||
const FragManager = @import("frag.zig");
|
||||
const dir = @import("inode_data/dir.zig");
|
||||
const file = @import("inode_data/file.zig");
|
||||
const misc = @import("inode_data/misc.zig");
|
||||
const LookupTable = @import("lookup_table.zig");
|
||||
const CachedTable = LookupTable.CachedTable;
|
||||
const DataExtractor = @import("util/data_extractor.zig");
|
||||
const DataReader = @import("util/data_reader.zig");
|
||||
const Decompressor = @import("util/decompressor.zig");
|
||||
const MetadataReader = @import("util/metadata.zig");
|
||||
const OffsetFile = @import("util/offset_file.zig");
|
||||
const SharedCache = @import("util/shared_cache.zig");
|
||||
const XattrTable = @import("xattr_table.zig");
|
||||
|
||||
const Inode = @This();
|
||||
|
||||
hdr: Header,
|
||||
data: Data,
|
||||
|
||||
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 deinit(self: Inode, alloc: std.mem.Allocator) void {
|
||||
switch (self.data) {
|
||||
.file => |d| d.deinit(alloc),
|
||||
.symlink => |d| d.deinit(alloc),
|
||||
.ext_file => |d| d.deinit(alloc),
|
||||
.ext_symlink => |d| d.deinit(alloc),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
// Utility Functions
|
||||
|
||||
/// Read the directory entries
|
||||
pub fn readDirectory(self: Inode, alloc: std.mem.Allocator, fil: OffsetFile, decomp: *const Decompressor, dir_offset: u64) ![]DirEntry {
|
||||
return switch (self.data) {
|
||||
.dir => |d| readDirFromData(alloc, fil, decomp, dir_offset, d),
|
||||
.ext_dir => |d| readDirFromData(alloc, fil, decomp, dir_offset, d),
|
||||
else => Error.NotDirectory,
|
||||
};
|
||||
}
|
||||
fn readDirFromData(alloc: std.mem.Allocator, fil: OffsetFile, decomp: *const Decompressor, dir_offset: u64, d: anytype) ![]DirEntry {
|
||||
var rdr = fil.readerAt(dir_offset + d.block_start);
|
||||
var meta: MetadataReader = .init(alloc, &rdr, decomp);
|
||||
try meta.interface.discardAll(d.block_offset);
|
||||
|
||||
return DirEntry.readDirectory(alloc, &meta.interface, d.size);
|
||||
}
|
||||
/// Get a reader for a regular file's data.
|
||||
pub fn dataReader(self: Inode, alloc: std.mem.Allocator, io: Io, fil: OffsetFile, cache: *SharedCache, decomp: *const Decompressor, block_size: u32) !DataReader {
|
||||
return switch (self.data) {
|
||||
.file => |f| getReaderFromData(alloc, io, fil, cache, decomp, block_size, f),
|
||||
.ext_file => |f| getReaderFromData(alloc, io, fil, cache, decomp, block_size, f),
|
||||
else => Error.NotRegularFile,
|
||||
};
|
||||
}
|
||||
fn getReaderFromData(alloc: std.mem.Allocator, io: Io, fil: OffsetFile, cache: *SharedCache, decomp: *const Decompressor, block_size: u32, d: anytype) !DataReader {
|
||||
const ext: DataReader = .init(alloc, io, fil, cache, decomp, block_size, d.size, d.block_start, d.blocks);
|
||||
if (d.frag_block_offset == 0xFFFFFFFF) {
|
||||
// TODO:
|
||||
return error.TODO;
|
||||
}
|
||||
return ext;
|
||||
}
|
||||
/// Get an extractor for a regular file's data.
|
||||
pub fn dataExtractor(self: Inode, fil: OffsetFile, cache: *SharedCache, decomp: *const Decompressor, block_size: u32) !DataExtractor {
|
||||
return switch (self.data) {
|
||||
.file => |f| getExtractorFromData(fil, cache, decomp, block_size, f),
|
||||
.ext_file => |f| getExtractorFromData(fil, cache, decomp, block_size, f),
|
||||
else => Error.NotRegularFile,
|
||||
};
|
||||
}
|
||||
fn getExtractorFromData(fil: OffsetFile, cache: *SharedCache, decomp: *const Decompressor, block_size: u32, d: anytype) !DataExtractor {
|
||||
const ext: DataExtractor = .init(fil, cache, decomp, block_size, d.size, d.block_start, d.blocks);
|
||||
if (d.frag_block_offset == 0xFFFFFFFF) {
|
||||
// TODO:
|
||||
return error.TODO;
|
||||
}
|
||||
return ext;
|
||||
}
|
||||
/// Get a symlink's target path
|
||||
pub fn symlinkTarget(self: Inode) ![]const u8 {
|
||||
return switch (self.data) {
|
||||
.symlink => |s| s.target,
|
||||
.ext_symlink => |s| s.target,
|
||||
else => Error.NotSymlink,
|
||||
};
|
||||
}
|
||||
/// Get inode's gid
|
||||
pub fn gid(self: Inode, alloc: std.mem.Allocator, io: Io, fil: OffsetFile, decomp: *const Decompressor, id_table_start: u64) !u16 {
|
||||
return LookupTable.lookupValue(u16, alloc, io, decomp, fil, id_table_start, self.hdr.gid_idx);
|
||||
}
|
||||
/// Get inode's uid
|
||||
pub fn uid(self: Inode, alloc: std.mem.Allocator, io: Io, fil: OffsetFile, decomp: *const Decompressor, id_table_start: u64) !u16 {
|
||||
return LookupTable.lookupValue(u16, alloc, io, decomp, fil, id_table_start, self.hdr.uid_idx);
|
||||
}
|
||||
/// Get the inode's xattr values as an index into the Archive's xattr table.
|
||||
/// Returns error.NoXattr if the inode doesn't have extended attributes.
|
||||
pub fn xattrIndex(self: Inode) !u32 {
|
||||
const idx = switch (self.data) {
|
||||
.ext_dir => |e| e.xattr_idx,
|
||||
.ext_file => |e| e.xattr_idx,
|
||||
.ext_symlink => |e| e.xattr_idx,
|
||||
.ext_block_dev => |e| e.xattr_idx,
|
||||
.ext_char_dev => |e| e.xattr_idx,
|
||||
.ext_fifo => |e| e.xattr_idx,
|
||||
.ext_socket => |e| e.xattr_idx,
|
||||
else => return error.NoXattr,
|
||||
};
|
||||
if (idx == 0xFFFFFFFF) return error.NoXattr;
|
||||
return idx;
|
||||
}
|
||||
// Get an inode's xattr values. If the inode does not have xattr values (including if the inode is not an extended type), an empty slice is returned.
|
||||
pub fn xattrValues(self: Inode, alloc: std.mem.Allocator, io: Io, fil: OffsetFile, decomp: *const Decompressor, xattr_table_start: u64) ![]XattrTable.XattrOwned {
|
||||
const idx = self.xattrIndex() catch &[0]XattrTable.XattrOwned{};
|
||||
return XattrTable.statelessLookup(alloc, io, decomp, fil, xattr_table_start, idx);
|
||||
}
|
||||
|
||||
// Types
|
||||
|
||||
pub const Error = error{
|
||||
NotDirectory,
|
||||
NotRegularFile,
|
||||
NotSymlink,
|
||||
NotExtended,
|
||||
};
|
||||
|
||||
pub const Ref = packed struct(u64) {
|
||||
block_offset: u16,
|
||||
block_start: u32,
|
||||
_: u16 = 0,
|
||||
};
|
||||
|
||||
pub const Type = enum(u16) {
|
||||
dir = 1,
|
||||
file,
|
||||
symlink,
|
||||
block_dev,
|
||||
char_dev,
|
||||
fifo,
|
||||
socket,
|
||||
ext_dir,
|
||||
ext_file,
|
||||
ext_symlink,
|
||||
ext_block_dev,
|
||||
ext_char_dev,
|
||||
ext_fifo,
|
||||
ext_socket,
|
||||
};
|
||||
|
||||
pub const Data = union(Type) {
|
||||
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 = extern struct {
|
||||
inode_type: Type,
|
||||
permissions: u16,
|
||||
uid_idx: u16,
|
||||
gid_idx: u16,
|
||||
mod_time: u32,
|
||||
num: u32,
|
||||
};
|
||||
|
||||
// Extract
|
||||
|
||||
const ExtractError = error{ MknodFailed, CannotSetXattr } || DataExtractor.Error || DirEntry.Error ||
|
||||
Decompressor.Error || Io.File.Atomic.InitError || Io.File.Atomic.LinkError || Io.Dir.SymLinkError;
|
||||
const PathRet = struct {
|
||||
path: []const u8,
|
||||
inode: Inode,
|
||||
xattr_idx: ?u32 = null,
|
||||
|
||||
fn setMetadata(path_ret: PathRet, alloc: std.mem.Allocator, io: Io, id_table: *CachedTable(u16), xattr_table: ?*XattrTable, options: ExtractionOptions) !void {
|
||||
var fil = Io.Dir.cwd().openFile(io, path_ret.path, .{}) catch |err| {
|
||||
std.debug.print("{s}: {}\n", .{ path_ret.path, err });
|
||||
return err;
|
||||
};
|
||||
defer fil.close(io);
|
||||
|
||||
if (!options.ignore_permissions) {
|
||||
try fil.setPermissions(io, @enumFromInt(path_ret.inode.hdr.permissions));
|
||||
try fil.setOwner(io, try id_table.get(io, path_ret.inode.hdr.uid_idx), try id_table.get(io, path_ret.inode.hdr.gid_idx));
|
||||
}
|
||||
if (xattr_table != null) {
|
||||
const idx = path_ret.inode.xattrIndex() catch return;
|
||||
|
||||
const xattrs = try xattr_table.?.get(alloc, io, idx);
|
||||
defer {
|
||||
for (xattrs) |x|
|
||||
x.deinit(alloc);
|
||||
alloc.free(xattrs);
|
||||
}
|
||||
|
||||
const sentinel_path = try std.mem.concatWithSentinel(alloc, u8, &[_][]const u8{path_ret.path}, 0);
|
||||
defer alloc.free(sentinel_path);
|
||||
for (xattrs) |x| {
|
||||
const xattr_ret = std.os.linux.fsetxattr(fil.handle, x.key, x.value.ptr, x.value.len, 0);
|
||||
if (xattr_ret != 0)
|
||||
return ExtractError.CannotSetXattr;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
fn DirCompare(_: void, a: PathRet, b: PathRet) std.math.Order {
|
||||
return std.math.order(std.mem.count(u8, a.path, "/"), std.mem.count(u8, b.path, "/"));
|
||||
}
|
||||
const ExtractReturnUnion = union(enum) {
|
||||
path_ret: ExtractError!PathRet,
|
||||
};
|
||||
const Tables = struct {
|
||||
id: LookupTable.CachedTable(u16),
|
||||
frag: LookupTable.CachedTable(FragEntry),
|
||||
xattr: XattrTable,
|
||||
};
|
||||
|
||||
/// Extracts the given inode to the given path. If the inode not a directory, the given path must not exist.
|
||||
/// If the inode is a directory the path must not exist or be a directory.
|
||||
pub fn extract(
|
||||
self: Inode,
|
||||
alloc: std.mem.Allocator,
|
||||
io: Io,
|
||||
fil: OffsetFile,
|
||||
super: Archive.Superblock,
|
||||
filepath: []const u8,
|
||||
options: ExtractionOptions,
|
||||
) !void {
|
||||
const path = std.mem.trimEnd(u8, filepath, "/");
|
||||
|
||||
const decomp = try @import("decomp.zig").StatelessDecomp(super.compression);
|
||||
|
||||
var frag_mgr: FragManager = try .init(alloc, fil, decomp, super.frag_start, super.frag_count, super.block_size);
|
||||
defer frag_mgr.deinit(io);
|
||||
|
||||
var arena: std.heap.ArenaAllocator = .init(alloc);
|
||||
defer arena.deinit();
|
||||
|
||||
var sel_buf: [10]ExtractReturnUnion = undefined;
|
||||
var sel: Io.Select(ExtractReturnUnion) = .init(io, &sel_buf);
|
||||
defer sel.cancelDiscard();
|
||||
|
||||
sel.async(.path_ret, extractReal, .{ self, alloc, io, fil, super, decomp, &arena, &sel, &frag_mgr, path });
|
||||
|
||||
var id_table: CachedTable(u16) = .init(alloc, fil, decomp, super.id_start, super.id_count);
|
||||
defer id_table.deinit(io);
|
||||
|
||||
var xattr_table: ?XattrTable = if (super.flags.xattr_never or options.ignore_xattr or !@hasField(std.os, "linux"))
|
||||
null
|
||||
else
|
||||
try .init(alloc, fil, decomp, super.xattr_start);
|
||||
defer if (xattr_table != null) xattr_table.?.deinit(io);
|
||||
|
||||
var dir_queue: std.PriorityDequeue(PathRet, void, DirCompare) = .empty;
|
||||
defer dir_queue.deinit(alloc);
|
||||
|
||||
while (true) {
|
||||
if (sel.group.token.load(.unordered) == null) break;
|
||||
|
||||
const ret = try sel.await();
|
||||
const path_ret = try ret.path_ret;
|
||||
|
||||
if (options.ignore_permissions and xattr_table == null) continue;
|
||||
|
||||
if (path_ret.inode.hdr.inode_type == .dir or path_ret.inode.hdr.inode_type == .ext_dir) {
|
||||
try dir_queue.push(alloc, path_ret);
|
||||
continue;
|
||||
}
|
||||
|
||||
try path_ret.setMetadata(alloc, io, &id_table, if (xattr_table == null) null else &xattr_table.?, options);
|
||||
}
|
||||
|
||||
var iter = dir_queue.iterator();
|
||||
while (iter.next()) |path_ret|
|
||||
try path_ret.setMetadata(alloc, io, &id_table, if (xattr_table == null) null else &xattr_table.?, options);
|
||||
}
|
||||
pub fn extractReal(
|
||||
self: Inode,
|
||||
alloc: std.mem.Allocator,
|
||||
io: Io,
|
||||
fil: OffsetFile,
|
||||
super: Archive.Superblock,
|
||||
decomp: *const Decompressor,
|
||||
inode_arena: *std.heap.ArenaAllocator,
|
||||
sel: *Io.Select(ExtractReturnUnion),
|
||||
frag_mgr: *FragManager,
|
||||
path: []const u8,
|
||||
) ExtractError!PathRet {
|
||||
switch (self.hdr.inode_type) {
|
||||
.dir, .ext_dir => {
|
||||
try Io.Dir.cwd().createDir(io, path, @enumFromInt(0o777));
|
||||
|
||||
const entries = self.readDirectory(alloc, fil, decomp, super.dir_start) catch |err| switch (err) {
|
||||
Error.NotDirectory, Error.NotExtended, Error.NotRegularFile, Error.NotSymlink => unreachable,
|
||||
else => |e| return e,
|
||||
};
|
||||
defer {
|
||||
for (entries) |e|
|
||||
e.deinit(alloc);
|
||||
alloc.free(entries);
|
||||
}
|
||||
|
||||
const inode_alloc = inode_arena.allocator();
|
||||
|
||||
for (entries) |e| {
|
||||
const new_path = try std.mem.concat(inode_alloc, u8, &[_][]const u8{ path, "/", e.name });
|
||||
|
||||
var rdr = fil.readerAt(super.inode_start + e.block_start);
|
||||
var meta: MetadataReader = .init(alloc, &rdr, decomp);
|
||||
try meta.interface.discardAll(e.block_offset);
|
||||
|
||||
const new_inode = try read(inode_alloc, &meta.interface, super.block_size);
|
||||
|
||||
sel.async(.path_ret, extractReal, .{ new_inode, alloc, io, fil, super, decomp, inode_arena, sel, frag_mgr, new_path });
|
||||
}
|
||||
},
|
||||
.file, .ext_file => {
|
||||
var atomic = try Io.Dir.cwd().createFileAtomic(io, path, .{ .make_path = true });
|
||||
defer atomic.deinit(io);
|
||||
|
||||
var ext: DataExtractor = switch (self.data) {
|
||||
.file => |f| blk: {
|
||||
var ext: DataExtractor = .init(fil, decomp, super.block_size, f.size, f.block_start, f.block_sizes);
|
||||
if (f.frag_idx != 0xFFFFFFFF)
|
||||
ext.addFrag(f.frag_block_offset, try frag_mgr.get(io, f.frag_idx));
|
||||
break :blk ext;
|
||||
},
|
||||
.ext_file => |f| blk: {
|
||||
var ext: DataExtractor = .init(fil, decomp, super.block_size, f.size, f.block_start, f.block_sizes);
|
||||
if (f.frag_idx != 0xFFFFFFFF)
|
||||
ext.addFrag(f.frag_block_offset, try frag_mgr.get(io, f.frag_idx));
|
||||
break :blk ext;
|
||||
},
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
try ext.extractAsync(alloc, io, atomic.file);
|
||||
|
||||
try atomic.link(io);
|
||||
},
|
||||
.symlink, .ext_symlink => try Io.Dir.cwd().symLink(io, self.symlinkTarget() catch unreachable, path, .{}),
|
||||
else => {
|
||||
var mode: u32 = undefined;
|
||||
var dev: u32 = 0;
|
||||
|
||||
const DT = std.posix.DT;
|
||||
|
||||
switch (self.data) {
|
||||
.char_dev => |d| {
|
||||
dev = d.dev;
|
||||
mode = DT.CHR;
|
||||
},
|
||||
.ext_char_dev => |d| {
|
||||
dev = d.dev;
|
||||
mode = DT.CHR;
|
||||
},
|
||||
.block_dev => |d| {
|
||||
dev = d.dev;
|
||||
mode = DT.BLK;
|
||||
},
|
||||
.ext_block_dev => |d| {
|
||||
dev = d.dev;
|
||||
mode = DT.BLK;
|
||||
},
|
||||
.fifo, .ext_fifo => mode = DT.FIFO,
|
||||
.socket, .ext_socket => mode = DT.SOCK,
|
||||
else => unreachable,
|
||||
}
|
||||
|
||||
const sentinel_path = try std.mem.concatWithSentinel(alloc, u8, &[_][]const u8{path}, 0);
|
||||
const res = std.os.linux.mknod(sentinel_path, mode, dev);
|
||||
alloc.free(sentinel_path);
|
||||
if (res != 0)
|
||||
return ExtractError.MknodFailed;
|
||||
},
|
||||
}
|
||||
return .{
|
||||
.path = path,
|
||||
.inode = self,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
const Reader = @import("std").Io.Reader;
|
||||
|
||||
pub const Dir = extern 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 = extern struct {
|
||||
hard_links: u32,
|
||||
size: u32,
|
||||
block_start: u32,
|
||||
parent_num: u32,
|
||||
idx_count: u16,
|
||||
block_offset: u16,
|
||||
xattr_idx: u32,
|
||||
// index: []DirIndex
|
||||
|
||||
pub fn read(rdr: *Reader) !ExtDir {
|
||||
var d: ExtDir = undefined;
|
||||
try rdr.readSliceEndian(ExtDir, @ptrCast(&d), .little);
|
||||
return d;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
const std = @import("std");
|
||||
const Reader = std.Io.Reader;
|
||||
|
||||
pub const BlockSize = packed struct(u32) {
|
||||
size: u24,
|
||||
uncompressed: bool,
|
||||
_: u7,
|
||||
};
|
||||
|
||||
const FileRawRead = extern struct {
|
||||
block_start: u32,
|
||||
frag_idx: u32,
|
||||
frag_block_offset: u32,
|
||||
size: u32,
|
||||
};
|
||||
|
||||
pub const File = struct {
|
||||
block_start: u32,
|
||||
frag_idx: u32,
|
||||
frag_block_offset: u32,
|
||||
size: u32,
|
||||
block_sizes: []BlockSize,
|
||||
|
||||
pub fn read(alloc: std.mem.Allocator, rdr: *Reader, block_size: u32) !File {
|
||||
var raw: FileRawRead = undefined;
|
||||
try rdr.readSliceEndian(FileRawRead, @ptrCast(&raw), .little);
|
||||
|
||||
var num_blocks: u32 = raw.size / block_size;
|
||||
if (raw.size % block_size != 0 and raw.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 = raw.block_start,
|
||||
.frag_idx = raw.frag_idx,
|
||||
.frag_block_offset = raw.frag_block_offset,
|
||||
.size = raw.size,
|
||||
.block_sizes = sizes,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: File, alloc: std.mem.Allocator) void {
|
||||
alloc.free(self.block_sizes);
|
||||
}
|
||||
};
|
||||
|
||||
const ExtFileRawRead = extern struct {
|
||||
block_start: u64,
|
||||
size: u64,
|
||||
sparse: u64,
|
||||
hard_links: u32,
|
||||
frag_idx: u32,
|
||||
frag_block_offset: u32,
|
||||
xattr_idx: u32,
|
||||
};
|
||||
|
||||
pub const ExtFile = struct {
|
||||
block_start: u64,
|
||||
size: u64,
|
||||
sparse: u64,
|
||||
hard_links: u32,
|
||||
frag_idx: u32,
|
||||
frag_block_offset: u32,
|
||||
xattr_idx: u32,
|
||||
block_sizes: []BlockSize,
|
||||
|
||||
pub fn read(alloc: std.mem.Allocator, rdr: *Reader, block_size: u32) !ExtFile {
|
||||
var raw: ExtFileRawRead = undefined;
|
||||
try rdr.readSliceEndian(ExtFileRawRead, @ptrCast(&raw), .little);
|
||||
|
||||
var num_blocks: u32 = @truncate(raw.size / block_size);
|
||||
if (raw.size % block_size != 0 and raw.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 = raw.block_start,
|
||||
.size = raw.size,
|
||||
.sparse = raw.sparse,
|
||||
.hard_links = raw.hard_links,
|
||||
.frag_idx = raw.frag_idx,
|
||||
.frag_block_offset = raw.frag_block_offset,
|
||||
.xattr_idx = raw.xattr_idx,
|
||||
.block_sizes = sizes,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: ExtFile, alloc: std.mem.Allocator) void {
|
||||
alloc.free(self.block_sizes);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
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 = extern 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 = extern 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 = extern 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 = extern 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;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
|
||||
const Decompressor = @import("util/decompressor.zig");
|
||||
const MetadataReader = @import("util/metadata.zig");
|
||||
const OffsetFile = @import("util/offset_file.zig");
|
||||
|
||||
pub fn lookupValue(comptime T: anytype, alloc: std.mem.Allocator, decomp: *const Decompressor, file: OffsetFile, table_start: u64, idx: u32) !T {
|
||||
const T_PER_BLOCK: u16 = 8192 / @sizeOf(T);
|
||||
|
||||
const block = idx / T_PER_BLOCK;
|
||||
const block_offset = idx % T_PER_BLOCK;
|
||||
|
||||
const offset_pos = table_start + (8 * block);
|
||||
const offset: u64 = std.mem.readInt(u64, @ptrCast(file.map.memory[offset_pos .. offset_pos + 8]), .little);
|
||||
|
||||
var rdr = file.readerAt(offset);
|
||||
var meta: MetadataReader = .init(alloc, &rdr, decomp);
|
||||
|
||||
try meta.interface.discardAll(@sizeOf(T) * block_offset);
|
||||
var out: T = undefined;
|
||||
try meta.interface.readSliceEndian(T, @ptrCast(&out), .little);
|
||||
return out;
|
||||
}
|
||||
|
||||
pub const Error = Io.Cancelable || Io.File.Reader.SeekError || Io.Reader.ReadAllocError;
|
||||
|
||||
pub fn CachedTable(comptime T: anytype) type {
|
||||
return struct {
|
||||
const T_PER_BLOCK: u16 = 8192 / @sizeOf(T);
|
||||
|
||||
const Table = @This();
|
||||
|
||||
alloc: std.mem.Allocator,
|
||||
fil: OffsetFile,
|
||||
decomp: *const Decompressor,
|
||||
|
||||
table_start: u64,
|
||||
total_num: u32,
|
||||
|
||||
table: std.AutoHashMap(u32, []T),
|
||||
|
||||
mut: Io.RwLock = .init,
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator, fil: OffsetFile, decomp: *const Decompressor, offset: u64, total_num: u32) Table {
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.fil = fil,
|
||||
.decomp = decomp,
|
||||
|
||||
.table_start = offset,
|
||||
.total_num = total_num,
|
||||
|
||||
.table = .init(alloc),
|
||||
};
|
||||
}
|
||||
pub fn deinit(self: *Table, io: Io) void {
|
||||
self.mut.lockUncancelable(io);
|
||||
var iter = self.table.valueIterator();
|
||||
while (iter.next()) |val|
|
||||
self.alloc.free(val.*);
|
||||
self.table.deinit();
|
||||
}
|
||||
|
||||
pub fn fill(self: *Table, io: Io) Error!void {
|
||||
try self.mut.lock(io);
|
||||
defer self.mut.unlock(io);
|
||||
|
||||
var num_blocks = self.total_num / T_PER_BLOCK;
|
||||
if (self.total_num % T_PER_BLOCK > 0)
|
||||
num_blocks += 1;
|
||||
|
||||
for (0..num_blocks) |block| {
|
||||
const offset_pos = self.table_start + (8 * block);
|
||||
const offset: u64 = std.mem.readInt(u64, @ptrCast(self.fil.map.memory[offset_pos .. offset_pos + 8]), .little);
|
||||
|
||||
const len: u16 = if (self.total_num % T_PER_BLOCK != 0 and block == (self.total_num - 1) / T_PER_BLOCK)
|
||||
@truncate(self.total_num % T_PER_BLOCK)
|
||||
else
|
||||
T_PER_BLOCK;
|
||||
|
||||
var rdr = self.fil.readerAt(offset);
|
||||
var meta: MetadataReader = .init(self.alloc, &rdr.interface, self.decomp);
|
||||
|
||||
const slice = try meta.interface.readSliceEndianAlloc(self.alloc, T, len, .little);
|
||||
try self.table.put(@truncate(block), slice);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(self: *Table, io: Io, idx: u32) Error!T {
|
||||
const block = idx / T_PER_BLOCK;
|
||||
const block_offset = idx % T_PER_BLOCK;
|
||||
|
||||
{
|
||||
try self.mut.lockShared(io);
|
||||
defer self.mut.unlockShared(io);
|
||||
|
||||
if (self.table.contains(block))
|
||||
return self.table.get(block).?[block_offset];
|
||||
}
|
||||
|
||||
try self.mut.lock(io);
|
||||
defer self.mut.unlock(io);
|
||||
|
||||
if (self.table.contains(block))
|
||||
return self.table.get(block).?[block_offset];
|
||||
|
||||
const offset_pos = self.table_start + (8 * block);
|
||||
const offset: u64 = std.mem.readInt(u64, @ptrCast(self.fil.map.memory[offset_pos .. offset_pos + 8]), .little);
|
||||
|
||||
const len: u16 = if (self.total_num % T_PER_BLOCK != 0 and block == (self.total_num - 1) / T_PER_BLOCK)
|
||||
@truncate(self.total_num % T_PER_BLOCK)
|
||||
else
|
||||
T_PER_BLOCK;
|
||||
|
||||
var rdr = self.fil.readerAt(offset);
|
||||
var meta: MetadataReader = .init(self.alloc, &rdr, self.decomp);
|
||||
|
||||
const slice = try meta.interface.readSliceEndianAlloc(self.alloc, T, len, .little);
|
||||
try self.table.put(@truncate(block), slice);
|
||||
|
||||
return slice[block_offset];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//! Options for file/directory extraction.
|
||||
|
||||
const std = @import("std");
|
||||
const Writer = std.Io.Writer;
|
||||
|
||||
const ExtractionOptions = @This();
|
||||
|
||||
// /// The number of threads used for extraction. 0 implies single threaded.
|
||||
// threads: usize = 1, // As of Zig 0.16 this should no longer be necessary, instead this should be set by the io instance used.
|
||||
/// 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.
|
||||
ignore_xattr: bool = false,
|
||||
/// Replace symlinks with their target.
|
||||
dereference_symlinks: bool = false,
|
||||
/// Verbose logging. If true, verbose_writer must be set
|
||||
verbose: bool = false,
|
||||
/// Where to print verbose log.
|
||||
verbose_writer: ?*Writer = null,
|
||||
|
||||
pub const SingleThreadedDefault: ExtractionOptions = .{};
|
||||
pub fn Default() !ExtractionOptions {
|
||||
return .{
|
||||
.threads = try std.Thread.getCpuCount(),
|
||||
};
|
||||
}
|
||||
pub fn VerboseDefault(wrt: *Writer) !ExtractionOptions {
|
||||
return .{
|
||||
.verbose = true,
|
||||
.verbose_writer = wrt,
|
||||
.threads = try std.Thread.getCpuCount(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub const Archive = @import("archive.zig");
|
||||
pub const ExtractionOptions = @import("options.zig");
|
||||
@@ -0,0 +1,89 @@
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
const io = std.testing.io;
|
||||
const alloc = std.testing.allocator;
|
||||
const stuff = @import("builtin");
|
||||
|
||||
const Archive = @import("archive.zig");
|
||||
const Superblock = Archive.Superblock;
|
||||
|
||||
const TestArchive = "testing/LinuxPATest.sfs";
|
||||
|
||||
test "Basics" {
|
||||
std.debug.print("Starting test: Basics...\n", .{});
|
||||
|
||||
var fil = try Io.Dir.cwd().openFile(io, TestArchive, .{});
|
||||
defer fil.close(io);
|
||||
var sfs: Archive = try .init(io, fil, 0);
|
||||
try std.testing.expectEqualDeep(sfs.super, LinuxPATestCorrectSuperblock);
|
||||
const root_file = try sfs.root(alloc, io);
|
||||
defer root_file.deinit();
|
||||
}
|
||||
|
||||
const TestFile = "Start.exe";
|
||||
const TestFileExtractLocation = "testing/Start.exe";
|
||||
|
||||
test "ExtractSingleFile" {
|
||||
std.debug.print("Starting test: ExtractSingleFile...\n", .{});
|
||||
|
||||
Io.Dir.cwd().deleteFile(io, TestFileExtractLocation) catch {};
|
||||
var fil = try Io.Dir.cwd().openFile(io, TestArchive, .{});
|
||||
defer fil.close(io);
|
||||
var sfs: Archive = try .init(io, fil, 0);
|
||||
var test_fil = try sfs.open(alloc, io, TestFile);
|
||||
defer test_fil.deinit();
|
||||
try test_fil.extract(alloc, io, TestFileExtractLocation, try .Default());
|
||||
//TODO: validate extracted file.
|
||||
}
|
||||
|
||||
const TestFullExtractLocation = "testing/TestExtract";
|
||||
|
||||
test "ExtractCompleteArchive" {
|
||||
std.debug.print("Starting test: ExtractCompleteArchive...\n", .{});
|
||||
|
||||
Io.Dir.cwd().deleteTree(io, TestFullExtractLocation) catch {};
|
||||
var fil = try Io.Dir.cwd().openFile(io, TestArchive, .{});
|
||||
defer fil.close(io);
|
||||
var sfs: Archive = try .init(io, fil, 0);
|
||||
try sfs.extract(alloc, io, TestFullExtractLocation, try .Default());
|
||||
}
|
||||
|
||||
const LinuxPATestCorrectSuperblock: Superblock = .{
|
||||
.magic = std.mem.readInt(u32, "hsqs", .little),
|
||||
.inode_count = 2974,
|
||||
.mod_time = 1632696724,
|
||||
.block_size = 131072,
|
||||
.frag_count = 264,
|
||||
.compression = .zstd,
|
||||
.block_log = 17,
|
||||
.flags = .{
|
||||
.inode_uncompressed = false,
|
||||
.data_uncompressed = false,
|
||||
.check = false,
|
||||
.frag_uncompressed = false,
|
||||
.fragment_never = false,
|
||||
.fragment_always = false,
|
||||
.duplicates = true,
|
||||
.exportable = true,
|
||||
.xattr_uncompressed = false,
|
||||
.xattr_never = false,
|
||||
.compression_options = false,
|
||||
.ids_uncompressed = false,
|
||||
._ = 0,
|
||||
},
|
||||
.id_count = 1,
|
||||
.ver_maj = 4,
|
||||
.ver_min = 0,
|
||||
.root_ref = .{
|
||||
.block_offset = 1363,
|
||||
.block_start = 29237,
|
||||
._ = 0,
|
||||
},
|
||||
.size = 106841744,
|
||||
.id_start = 106841632,
|
||||
.xattr_start = 106841720,
|
||||
.inode_start = 106778274,
|
||||
.dir_start = 106807998,
|
||||
.frag_start = 106837747,
|
||||
.export_start = 106841602,
|
||||
};
|
||||
@@ -0,0 +1,145 @@
|
||||
//! The DataExtractor is meant to extract a regular file's data to a given file asyncronously.
|
||||
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
|
||||
const FragEntry = @import("../frag.zig").FragEntry;
|
||||
const BlockSize = @import("../inode_data/file.zig").BlockSize;
|
||||
const Decompressor = @import("decompressor.zig");
|
||||
const OffsetFile = @import("offset_file.zig");
|
||||
|
||||
// const SharedCache = @import("shared_cache.zig");
|
||||
|
||||
pub const Error = Decompressor.Error || Io.File.MemoryMap.CreateError || Io.File.WritePositionalError;
|
||||
|
||||
const DataExtractor = @This();
|
||||
|
||||
fil: OffsetFile,
|
||||
decomp: *const Decompressor,
|
||||
block_size: u32,
|
||||
|
||||
file_size: u64,
|
||||
start: u64,
|
||||
blocks: []BlockSize,
|
||||
|
||||
frag_offset: u32 = 0,
|
||||
frag_block: ?[]u8 = null,
|
||||
|
||||
err: ?Error = null,
|
||||
|
||||
pub fn init(fil: OffsetFile, decomp: *const Decompressor, block_size: u32, file_size: u64, data_start: u64, blocks: []BlockSize) DataExtractor {
|
||||
return .{
|
||||
.fil = fil,
|
||||
.decomp = decomp,
|
||||
.block_size = block_size,
|
||||
|
||||
.file_size = file_size,
|
||||
.start = data_start,
|
||||
.blocks = blocks,
|
||||
};
|
||||
}
|
||||
pub fn addFrag(self: *DataExtractor, frag_offset: u32, block: []u8) void {
|
||||
self.frag_offset = frag_offset;
|
||||
self.frag_block = block;
|
||||
}
|
||||
|
||||
fn numBlocks(self: DataExtractor) usize {
|
||||
var num = self.blocks.len;
|
||||
if (self.frag_block != null) num += 1;
|
||||
return num;
|
||||
}
|
||||
|
||||
/// Starts extracting the data using the given group to spawn async tasks.
|
||||
pub fn extractAsync(self: DataExtractor, alloc: std.mem.Allocator, io: Io, fil: Io.File) Error!void {
|
||||
var group: Io.Group = .init;
|
||||
defer group.cancel(io);
|
||||
var err: ?Error = null;
|
||||
|
||||
var read_offset: u64 = self.start;
|
||||
for (0..self.blocks.len) |idx| {
|
||||
group.async(io, blockThread, .{ self, alloc, io, fil, read_offset, idx, &err });
|
||||
read_offset += self.blocks[idx].size;
|
||||
}
|
||||
if (self.frag_block != null)
|
||||
group.async(io, fragThread, .{ self, io, fil, &err });
|
||||
|
||||
group.await(io) catch |cancel| return err orelse cancel;
|
||||
}
|
||||
|
||||
fn blockThread(self: DataExtractor, alloc: std.mem.Allocator, io: Io, fil: Io.File, read_offset: u64, idx: usize, ret_err: *?Error) Io.Cancelable!void {
|
||||
const block = self.blocks[idx];
|
||||
|
||||
const cur_block_size = if (idx == self.numBlocks() - 1)
|
||||
self.file_size % self.block_size
|
||||
else
|
||||
self.block_size;
|
||||
|
||||
const write_offset = self.block_size * idx;
|
||||
|
||||
var wrt = fil.writer(io, &[0]u8{});
|
||||
wrt.seekTo(write_offset) catch |err| {
|
||||
ret_err.* = err;
|
||||
if (err == error.Canceled) io.recancel();
|
||||
return Io.Cancelable.Canceled;
|
||||
};
|
||||
|
||||
if (block.size == 0) {
|
||||
wrt.interface.splatByteAll(0, cur_block_size) catch |err| {
|
||||
ret_err.* = err;
|
||||
if (err == error.Canceled) io.recancel();
|
||||
return Io.Cancelable.Canceled;
|
||||
};
|
||||
} else {
|
||||
if (block.uncompressed) {
|
||||
wrt.interface.writeAll(self.fil.map.memory[read_offset..][0..cur_block_size]) catch |err| {
|
||||
ret_err.* = err;
|
||||
if (err == error.Canceled) io.recancel();
|
||||
return Io.Cancelable.Canceled;
|
||||
};
|
||||
} else {
|
||||
@branchHint(.likely);
|
||||
|
||||
var tmp: [1024 * 1024]u8 = undefined;
|
||||
|
||||
_ = self.decomp.Decompress(alloc, self.fil.map.memory[read_offset..][0..block.size], tmp[0..cur_block_size]) catch |err| {
|
||||
ret_err.* = err;
|
||||
return Io.Cancelable.Canceled;
|
||||
};
|
||||
|
||||
wrt.interface.writeAll(tmp[0..cur_block_size]) catch |err| {
|
||||
ret_err.* = err;
|
||||
if (err == error.Canceled) io.recancel();
|
||||
return Io.Cancelable.Canceled;
|
||||
};
|
||||
}
|
||||
}
|
||||
wrt.flush() catch |err| {
|
||||
ret_err.* = err;
|
||||
if (err == error.Canceled) io.recancel();
|
||||
return Io.Cancelable.Canceled;
|
||||
};
|
||||
}
|
||||
fn fragThread(self: DataExtractor, io: Io, fil: Io.File, ret_err: *?Error) Io.Cancelable!void {
|
||||
const cur_block_size = self.file_size % self.block_size;
|
||||
|
||||
const write_offset = self.blocks.len * self.block_size;
|
||||
|
||||
var wrt = fil.writer(io, &[0]u8{});
|
||||
wrt.seekTo(write_offset) catch |err| {
|
||||
ret_err.* = err;
|
||||
if (err == error.Canceled) io.recancel();
|
||||
return Io.Cancelable.Canceled;
|
||||
};
|
||||
|
||||
wrt.interface.writeAll(self.frag_block.?[self.frag_offset..][0..cur_block_size]) catch |err| {
|
||||
ret_err.* = err;
|
||||
if (err == error.Canceled) io.recancel();
|
||||
return Io.Cancelable.Canceled;
|
||||
};
|
||||
|
||||
wrt.flush() catch |err| {
|
||||
ret_err.* = err;
|
||||
if (err == error.Canceled) io.recancel();
|
||||
return Io.Cancelable.Canceled;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
//! DataReader reads a regular file's data linearly from start to finish using Io.Reader interface.
|
||||
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
const Reader = Io.Reader;
|
||||
const Writer = Io.Writer;
|
||||
const Limit = Io.Limit;
|
||||
|
||||
const BlockSize = @import("../inode_data/file.zig").BlockSize;
|
||||
const Decompressor = @import("decompressor.zig");
|
||||
const OffsetFile = @import("offset_file.zig");
|
||||
|
||||
// const SharedCache = @import("shared_cache.zig");
|
||||
|
||||
const DataReader = @This();
|
||||
|
||||
alloc: std.mem.Allocator,
|
||||
|
||||
fil: OffsetFile,
|
||||
io: Io,
|
||||
decomp: *const Decompressor,
|
||||
block_size: u32,
|
||||
|
||||
file_size: u64,
|
||||
cur_offset: u64,
|
||||
blocks: []BlockSize,
|
||||
|
||||
frag_offset: u32 = 0,
|
||||
frag_block: ?[]u8 = null,
|
||||
|
||||
block_idx: usize = 0,
|
||||
sparse_block: bool = false,
|
||||
|
||||
interface: Io.Reader,
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator, io: Io, fil: OffsetFile, decomp: *const Decompressor, block_size: u32, file_size: u64, data_start: u64, blocks: []BlockSize) !DataReader {
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
|
||||
.fil = fil,
|
||||
.io = io,
|
||||
.decomp = decomp,
|
||||
.block_size = block_size,
|
||||
|
||||
.file_size = file_size,
|
||||
.cur_offset = data_start,
|
||||
.blocks = blocks,
|
||||
|
||||
.interface = .{
|
||||
.buffer = try alloc.alloc(u8, block_size),
|
||||
.seek = 0,
|
||||
.end = 0,
|
||||
.vtable = &.{
|
||||
.stream = stream,
|
||||
.discard = discard,
|
||||
.readVec = readVec,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
pub fn deinit(self: *DataReader) void {
|
||||
self.alloc.free(self.interface.buffer);
|
||||
}
|
||||
pub fn addFrag(self: *DataReader, frag_offset: u32, block: []u8) void {
|
||||
self.frag_offset = frag_offset;
|
||||
self.frag_block = block;
|
||||
}
|
||||
|
||||
fn numBlocks(self: DataReader) usize {
|
||||
var num = self.blocks.len;
|
||||
if (self.frag_block != null) num += 1;
|
||||
return num;
|
||||
}
|
||||
fn advanceBuffer(self: *DataReader) !void {
|
||||
if (self.block_idx >= self.numBlocks())
|
||||
return Reader.Error.EndOfStream;
|
||||
|
||||
errdefer self.interface.end = 0;
|
||||
defer self.block_idx += 1;
|
||||
|
||||
self.interface.end = if (self.block_idx == self.numBlocks() - 1)
|
||||
self.file_size % self.block_size
|
||||
else
|
||||
self.block_size;
|
||||
|
||||
// Fragment
|
||||
if (self.block_idx == self.blocks.len) {
|
||||
@memcpy(self.interface.buffer[0..self.interface.end], self.frag_block.?[self.frag_offset .. self.frag_offset + self.interface.end]);
|
||||
self.interface.seek = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal Block
|
||||
const block = self.blocks[self.block_idx];
|
||||
if (block.size == 0) {
|
||||
self.interface.seek = 0;
|
||||
self.sparse_block = true;
|
||||
return;
|
||||
} else {
|
||||
self.sparse_block = false;
|
||||
}
|
||||
if (block.uncompressed) {
|
||||
@memcpy(self.interface.buffer[0..self.interface.end], self.fil.map.memory[self.cur_offset .. self.cur_offset + self.interface.end]);
|
||||
self.cur_offset += self.interface.end;
|
||||
} else {
|
||||
@branchHint(.likely);
|
||||
|
||||
_ = try self.decomp.Decompress(self.alloc, self.fil.map.memory[self.cur_offset .. self.cur_offset + block.size], self.interface.buffer[0..self.interface.end]);
|
||||
self.cur_offset += block.size;
|
||||
}
|
||||
self.interface.seek = 0;
|
||||
}
|
||||
|
||||
fn stream(rdr: *Reader, wrt: *Writer, limit: Limit) Reader.StreamError!usize {
|
||||
var data: *DataReader = @fieldParentPtr("interface", rdr);
|
||||
if (rdr.seek == rdr.end)
|
||||
data.advanceBuffer() catch |err| return switch (err) {
|
||||
error.ReadFailed => error.ReadFailed,
|
||||
error.EndOfStream => error.EndOfStream,
|
||||
else => error.ReadFailed,
|
||||
};
|
||||
|
||||
switch (limit) {
|
||||
.nothing => return 0,
|
||||
.unlimited => {
|
||||
const wrote = if (data.sparse_block)
|
||||
try wrt.splatByte(0, rdr.end - rdr.seek)
|
||||
else
|
||||
try wrt.write(rdr.buffer[rdr.seek..rdr.end]);
|
||||
rdr.seek += wrote;
|
||||
return wrote;
|
||||
},
|
||||
else => {
|
||||
const to_read = @min(rdr.end - rdr.seek, @intFromEnum(limit));
|
||||
const wrote = if (data.sparse_block)
|
||||
try wrt.splatByte(0, to_read)
|
||||
else
|
||||
try wrt.write(rdr.buffer[rdr.seek .. rdr.seek + to_read]);
|
||||
rdr.seek += wrote;
|
||||
return wrote;
|
||||
},
|
||||
}
|
||||
}
|
||||
fn discard(rdr: *Reader, limit: Limit) Reader.Error!usize {
|
||||
var data: *DataReader = @fieldParentPtr("interface", rdr);
|
||||
if (rdr.seek == rdr.end)
|
||||
data.advanceBuffer() catch |err| return switch (err) {
|
||||
error.ReadFailed => error.ReadFailed,
|
||||
error.EndOfStream => error.EndOfStream,
|
||||
else => error.ReadFailed,
|
||||
};
|
||||
|
||||
switch (limit) {
|
||||
.nothing => return 0,
|
||||
.unlimited => {
|
||||
const adv = rdr.end - rdr.seek;
|
||||
rdr.seek = rdr.end;
|
||||
return adv;
|
||||
},
|
||||
else => {
|
||||
const adv = @min(rdr.end - rdr.seek, @intFromEnum(limit));
|
||||
rdr.seek += adv;
|
||||
return adv;
|
||||
},
|
||||
}
|
||||
}
|
||||
fn readVec(rdr: *Reader, vec: [][]u8) Reader.Error!usize {
|
||||
var data: *DataReader = @fieldParentPtr("interface", rdr);
|
||||
if (rdr.seek == rdr.end)
|
||||
data.advanceBuffer() catch |err| return switch (err) {
|
||||
error.ReadFailed => error.ReadFailed,
|
||||
error.EndOfStream => error.EndOfStream,
|
||||
else => error.ReadFailed,
|
||||
};
|
||||
|
||||
var wrote: usize = 0;
|
||||
for (vec) |buf| {
|
||||
if (rdr.seek == rdr.end) break;
|
||||
|
||||
const to_copy = @min(rdr.end - rdr.seek, buf.len);
|
||||
if (data.sparse_block)
|
||||
@memset(buf[0..to_copy], 0)
|
||||
else
|
||||
@memcpy(buf[0..to_copy], rdr.buffer[rdr.seek .. rdr.seek + to_copy]);
|
||||
rdr.seek += to_copy;
|
||||
wrote += to_copy;
|
||||
}
|
||||
return wrote;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//! A decompression interface
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const Decompressor = @This();
|
||||
|
||||
pub const Error = std.Io.Reader.StreamError || std.mem.Allocator.Error;
|
||||
|
||||
/// The actual decompression function.
|
||||
/// If the given decompressor is null, then the decompression should be done "stateless" without lasting allocations.
|
||||
decomp_fn: *const fn (?*const Decompressor, std.mem.Allocator, in: []u8, out: []u8) Error!usize,
|
||||
|
||||
pub fn Decompress(self: *const Decompressor, alloc: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
|
||||
return self.decomp_fn(self, alloc, in, out);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
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 Decompressor = @import("decompressor.zig");
|
||||
|
||||
const BlockHeader = packed struct(u16) {
|
||||
size: u15,
|
||||
uncompressed: bool,
|
||||
};
|
||||
|
||||
const This = @This();
|
||||
|
||||
alloc: std.mem.Allocator,
|
||||
rdr: *Reader,
|
||||
decomp: *const Decompressor,
|
||||
|
||||
cur_block_start: u32 = 0,
|
||||
next_start_start: u32 = 0,
|
||||
buf: [8192]u8 = undefined,
|
||||
|
||||
err: ?anyerror = null,
|
||||
interface: Reader = .{
|
||||
.buffer = &[0]u8{},
|
||||
.end = 0,
|
||||
.seek = 0,
|
||||
.vtable = &.{
|
||||
.stream = stream,
|
||||
.discard = discard,
|
||||
.readVec = readVec,
|
||||
// TODO: Potentially add rebase so that we can guarentee that self.block_start & interface.seek is correct.
|
||||
},
|
||||
},
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator, rdr: *Reader, decomp: *const Decompressor) This {
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.rdr = rdr,
|
||||
.decomp = decomp,
|
||||
};
|
||||
}
|
||||
|
||||
fn advance(self: *This) !void {
|
||||
self.interface.seek = 0;
|
||||
var hdr: BlockHeader = undefined;
|
||||
try self.rdr.readSliceEndian(BlockHeader, @ptrCast(&hdr), .little);
|
||||
self.cur_block_start = self.next_start_start;
|
||||
self.next_start_start += hdr.size;
|
||||
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;
|
||||
} else {
|
||||
@branchHint(.likely);
|
||||
var tmp_buf: [8192]u8 = undefined;
|
||||
try self.rdr.readSliceAll(tmp_buf[0..hdr.size]);
|
||||
self.interface.end = try self.decomp.Decompress(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) Reader.Error!usize {
|
||||
const self: *This = @fieldParentPtr("interface", rdr);
|
||||
if (rdr.end == rdr.seek) self.advance() catch |err| {
|
||||
self.err = err;
|
||||
return error.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) Reader.Error!usize {
|
||||
const self: *This = @fieldParentPtr("interface", rdr);
|
||||
if (rdr.end == rdr.seek) self.advance() catch |err| {
|
||||
self.err = err;
|
||||
return 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], 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;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//! Miscellaneous utility functions.
|
||||
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
|
||||
const Inode = @import("../inode.zig");
|
||||
const Decompressor = @import("decompressor.zig");
|
||||
const MetadataReader = @import("metadata.zig");
|
||||
const OffsetFile = @import("offset_file.zig");
|
||||
|
||||
/// check is the path is referencing itself ("" or ".").
|
||||
/// separators must be trimmed before calling this function for it to work properly.
|
||||
pub fn pathIsSelf(path: []const u8) bool {
|
||||
if (path.len == 0) return true;
|
||||
if (path.len > 1) return false;
|
||||
return path[0] == '.';
|
||||
}
|
||||
/// Creates an Inode from an Inode.Ref.
|
||||
pub fn inodeFromRef(alloc: std.mem.Allocator, file: OffsetFile, decomp: *const Decompressor, inode_start: u64, block_size: u32, ref: Inode.Ref) !Inode {
|
||||
var rdr = file.readerAt(inode_start + ref.block_start);
|
||||
var meta: MetadataReader = .init(alloc, &rdr, decomp);
|
||||
try meta.interface.discardAll(ref.block_offset);
|
||||
|
||||
return .read(alloc, &meta.interface, block_size);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//! A File where it's meaningful (to us) content starts at a given offset.
|
||||
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
const File = Io.File;
|
||||
const Reader = Io.Reader;
|
||||
|
||||
const OffsetFile = @This();
|
||||
|
||||
map: Io.File.MemoryMap,
|
||||
|
||||
pub fn init(io: Io, fil: File, archive_size: u64, init_offset: u64) !OffsetFile {
|
||||
return .{
|
||||
.map = try fil.createMemoryMap(io, .{
|
||||
.protection = .{ .read = true, .write = false, .execute = false },
|
||||
.len = archive_size,
|
||||
.offset = init_offset,
|
||||
}),
|
||||
};
|
||||
}
|
||||
pub fn deinit(self: @This(), io: Io) void {
|
||||
self.map.destroy(io);
|
||||
}
|
||||
|
||||
pub fn readerAt(self: OffsetFile, offset: u64) Reader {
|
||||
return .fixed(self.map.memory[offset..]);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
const Node = std.SinglyLinkedList.Node;
|
||||
|
||||
const SharedCache = @This();
|
||||
|
||||
pub const CACHE_SIZE = 1024 * 1024;
|
||||
|
||||
pub const BufferNode = struct {
|
||||
node: Node,
|
||||
cache: [CACHE_SIZE]u8,
|
||||
};
|
||||
|
||||
alloc: std.mem.Allocator,
|
||||
|
||||
caches: std.ArrayList(BufferNode),
|
||||
cache_queue: std.SinglyLinkedList,
|
||||
queue_mut: Io.Mutex,
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator, init_cache_size: u32) !SharedCache {
|
||||
const caches: std.ArrayList(BufferNode) = try .initCapacity(alloc, init_cache_size);
|
||||
var queue: std.SinglyLinkedList = .{};
|
||||
for (caches.items) |item|
|
||||
queue.prepend(&item.node);
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
|
||||
.caches = caches,
|
||||
.cache_queue = queue,
|
||||
};
|
||||
}
|
||||
pub fn deinit(self: *SharedCache) void {
|
||||
self.caches.deinit(self.alloc);
|
||||
}
|
||||
|
||||
pub fn getCache(self: *SharedCache, io: Io) !*BufferNode {
|
||||
self.queue_mut.lock(io);
|
||||
const nxt = self.cache_queue.popFirst();
|
||||
self.queue_mut.unlock(io);
|
||||
if (nxt == null) {
|
||||
const new = try self.caches.addOne(self.alloc);
|
||||
new.* = .{
|
||||
.node = .{},
|
||||
.cache = undefined,
|
||||
};
|
||||
return new;
|
||||
}
|
||||
return @fieldParentPtr("node", nxt.?);
|
||||
}
|
||||
pub fn returnCache(self: *SharedCache, buf: *BufferNode) void {
|
||||
self.cache_queue.prepend(buf);
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
|
||||
const InodeRef = @import("inode.zig").Ref;
|
||||
const LookupTable = @import("lookup_table.zig");
|
||||
const Decompressor = @import("util/decompressor.zig");
|
||||
const MetadataReader = @import("util/metadata.zig");
|
||||
const OffsetFile = @import("util/offset_file.zig");
|
||||
|
||||
const XattrCachedTable = @This();
|
||||
|
||||
alloc: std.mem.Allocator,
|
||||
|
||||
fil: OffsetFile,
|
||||
decomp: *const Decompressor,
|
||||
|
||||
kv_start: u64,
|
||||
|
||||
table: LookupTable.CachedTable(TableValue),
|
||||
value_cache: std.AutoHashMap(InodeRef, []const u8),
|
||||
value_mut: Io.RwLock = .init,
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator, fil: OffsetFile, decomp: *const Decompressor, xattr_start: u64) !XattrCachedTable {
|
||||
const start: u64 = std.mem.readInt(u64, @ptrCast(fil.map.memory[xattr_start .. xattr_start + 8]), .little);
|
||||
const num: u64 = std.mem.readInt(u64, @ptrCast(fil.map.memory[xattr_start + 8 .. xattr_start + 16]), .little);
|
||||
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
|
||||
.fil = fil,
|
||||
.decomp = decomp,
|
||||
|
||||
.kv_start = start,
|
||||
|
||||
.table = .init(alloc, fil, decomp, xattr_start + 16, num),
|
||||
.value_cache = .init(alloc),
|
||||
};
|
||||
}
|
||||
pub fn deinit(self: *XattrCachedTable, io: Io) void {
|
||||
self.value_mut.lockUncancelable(io);
|
||||
self.table.deinit(io);
|
||||
self.value_cache.deinit();
|
||||
}
|
||||
|
||||
pub fn get(self: *XattrCachedTable, alloc: std.mem.Allocator, io: Io, idx: u32) ![]XattrSemiOwned {
|
||||
const lookup = try self.table.get(io, idx);
|
||||
|
||||
var rdr = self.fil.readerAt(self.kv_start + lookup.ref.block_start);
|
||||
var meta: MetadataReader = .init(alloc, &rdr, self.decomp);
|
||||
try meta.interface.discardAll(lookup.ref.block_offset);
|
||||
|
||||
const out = try alloc.alloc(XattrSemiOwned, lookup.count);
|
||||
errdefer alloc.free(out);
|
||||
|
||||
for (0..lookup.count) |i| {
|
||||
var key_entry: KeyEntry = undefined;
|
||||
try meta.interface.readSliceEndian(KeyEntry, @ptrCast(&key_entry), .little);
|
||||
|
||||
const key: [:0]u8 = switch (key_entry.type.namespace) {
|
||||
.user => blk: {
|
||||
const tmp = try alloc.alloc(u8, key_entry.name_size + 1 + 5);
|
||||
errdefer alloc.free(tmp);
|
||||
try meta.interface.readSliceEndian(u8, tmp[5 .. tmp.len - 1], .little);
|
||||
@memcpy(tmp[0..5], "user.");
|
||||
tmp[tmp.len - 1] = 0;
|
||||
break :blk @ptrCast(tmp);
|
||||
},
|
||||
.trusted => blk: {
|
||||
const tmp = try alloc.alloc(u8, key_entry.name_size + 1 + 8);
|
||||
errdefer alloc.free(tmp);
|
||||
try meta.interface.readSliceEndian(u8, tmp[8 .. tmp.len - 1], .little);
|
||||
@memcpy(tmp[0..8], "trusted.");
|
||||
tmp[tmp.len - 1] = 0;
|
||||
break :blk @ptrCast(tmp);
|
||||
},
|
||||
.security => blk: {
|
||||
const tmp = try alloc.alloc(u8, key_entry.name_size + 1 + 9);
|
||||
errdefer alloc.free(tmp);
|
||||
try meta.interface.readSliceEndian(u8, tmp[9 .. tmp.len - 1], .little);
|
||||
@memcpy(tmp[0..9], "security.");
|
||||
tmp[tmp.len - 1] = 0;
|
||||
break :blk @ptrCast(tmp);
|
||||
},
|
||||
};
|
||||
errdefer alloc.free(key);
|
||||
|
||||
if (key_entry.type.out_of_line) {
|
||||
var value: ValueOutOfLineEntry = undefined;
|
||||
try meta.interface.readSliceEndian(ValueOutOfLineEntry, @ptrCast(&value), .little);
|
||||
|
||||
out[i] = .{
|
||||
.key = key,
|
||||
.value = try self.valueAt(io, value.ref),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
const val_ref: InodeRef = .{ .block_start = meta.cur_block_start, .block_offset = @truncate(meta.interface.seek) };
|
||||
|
||||
{
|
||||
try self.value_mut.lockShared(io);
|
||||
defer self.value_mut.unlockShared(io);
|
||||
if (self.value_cache.contains(val_ref)) {
|
||||
out[i] = .{
|
||||
.key = key,
|
||||
.value = try self.valueAt(io, val_ref),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
}
|
||||
try self.value_mut.lock(io);
|
||||
defer self.value_mut.unlock(io);
|
||||
|
||||
if (self.value_cache.contains(val_ref)) {
|
||||
out[i] = .{
|
||||
.key = key,
|
||||
.value = try self.valueAt(io, val_ref),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
var val_size: u32 = undefined;
|
||||
try meta.interface.readSliceEndian(u32, @ptrCast(&val_size), .little);
|
||||
|
||||
const val = try self.alloc.alloc(u8, val_size);
|
||||
errdefer alloc.free(val);
|
||||
try meta.interface.readSliceEndian(u8, val, .little);
|
||||
|
||||
try self.value_cache.put(val_ref, val);
|
||||
out[i] = .{
|
||||
.key = key,
|
||||
.value = val,
|
||||
};
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
fn valueAt(self: *XattrCachedTable, io: Io, ref: InodeRef) ![]const u8 {
|
||||
try self.value_mut.lock(io);
|
||||
defer self.value_mut.unlock(io);
|
||||
|
||||
if (self.value_cache.contains(ref)) return self.value_cache.get(ref).?;
|
||||
|
||||
var rdr = self.fil.readerAt(self.kv_start + ref.block_start);
|
||||
var meta: MetadataReader = .init(self.alloc, &rdr, self.decomp);
|
||||
try meta.interface.discardAll(ref.block_offset);
|
||||
|
||||
var val_size: u32 = undefined;
|
||||
try meta.interface.readSliceEndian(u32, @ptrCast(&val_size), .little);
|
||||
|
||||
const val = try self.alloc.alloc(u8, val_size);
|
||||
errdefer self.alloc.free(val);
|
||||
try meta.interface.readSliceEndian(u8, val, .little);
|
||||
|
||||
try self.value_cache.put(ref, val);
|
||||
return val;
|
||||
}
|
||||
|
||||
// Types
|
||||
|
||||
/// An Xattr return value where the reciever only owns the key value.
|
||||
pub const XattrSemiOwned = struct {
|
||||
key: [:0]const u8,
|
||||
value: []const u8,
|
||||
|
||||
pub fn deinit(self: XattrSemiOwned, alloc: std.mem.Allocator) void {
|
||||
alloc.free(self.key);
|
||||
}
|
||||
};
|
||||
/// An Xattr return value where the reciever owns both the key & value.
|
||||
pub const XattrOwned = struct {
|
||||
key: [:0]const u8,
|
||||
value: []const u8,
|
||||
|
||||
pub fn deinit(self: XattrSemiOwned, alloc: std.mem.Allocator) void {
|
||||
alloc.free(self.key);
|
||||
alloc.free(self.value);
|
||||
}
|
||||
};
|
||||
|
||||
const TableValue = extern struct {
|
||||
ref: InodeRef,
|
||||
count: u32,
|
||||
size: u32,
|
||||
};
|
||||
|
||||
const KeyEntry = extern struct {
|
||||
type: XattrPrefix,
|
||||
name_size: u16,
|
||||
};
|
||||
const ValueOutOfLineEntry = extern struct {
|
||||
_: u32,
|
||||
ref: InodeRef,
|
||||
};
|
||||
|
||||
const XattrPrefix = packed struct(u16) {
|
||||
namespace: enum(u8) {
|
||||
user,
|
||||
trusted,
|
||||
security,
|
||||
|
||||
fn prefixSize(self: @This()) u16 {
|
||||
return switch (self) {
|
||||
.user => 5,
|
||||
.trusted => 8,
|
||||
.security => 9,
|
||||
};
|
||||
}
|
||||
},
|
||||
out_of_line: bool,
|
||||
_: u7,
|
||||
};
|
||||
|
||||
// Stateless
|
||||
|
||||
pub fn statelessLookup(alloc: std.mem.Allocator, io: Io, decomp: *const Decompressor, fil: OffsetFile, table_start: u64, idx: u16) ![]XattrOwned {
|
||||
const kv_start: u64 = std.mem.readInt(u64, @ptrCast(fil.map.memory[table_start .. table_start + 8]), .little);
|
||||
|
||||
const lookup = try LookupTable.lookupValue(TableValue, alloc, io, decomp, fil, table_start + 16, idx);
|
||||
|
||||
var rdr = fil.readerAt(kv_start + lookup.ref.block_start);
|
||||
var meta: MetadataReader = .init(alloc, &rdr.interface, decomp);
|
||||
try meta.interface.discardAll(lookup.ref.block_offset);
|
||||
|
||||
const out = try alloc.alloc(XattrOwned, lookup.count);
|
||||
errdefer alloc.free(out);
|
||||
|
||||
for (0..lookup.count) |i| {
|
||||
const key_entry: KeyEntry = undefined;
|
||||
try meta.interface.readSliceEndian(KeyEntry, @ptrCast(&key_entry), .little);
|
||||
|
||||
const key = switch (key_entry.type.namespace) {
|
||||
.user => blk: {
|
||||
const tmp = try alloc.alloc(u8, key_entry.name_size + 1 + 5);
|
||||
errdefer alloc.free(tmp);
|
||||
try meta.interface.readSliceEndian(u8, tmp[5 .. tmp.len - 1], .little);
|
||||
@memset(tmp[0..5], "user.");
|
||||
break :blk tmp;
|
||||
},
|
||||
.trusted => blk: {
|
||||
const tmp = try alloc.alloc(u8, key_entry.name_size + 1 + 8);
|
||||
errdefer alloc.free(tmp);
|
||||
try meta.interface.readSliceEndian(u8, tmp[8 .. tmp.len - 1], .little);
|
||||
@memset(tmp[0..8], "trusted.");
|
||||
break :blk tmp;
|
||||
},
|
||||
.security => blk: {
|
||||
const tmp = try alloc.alloc(u8, key_entry.name_size + 1 + 9);
|
||||
errdefer alloc.free(tmp);
|
||||
try meta.interface.readSliceEndian(u8, tmp[9 .. tmp.len - 1], .little);
|
||||
@memset(tmp[0..9], "security.");
|
||||
break :blk tmp;
|
||||
},
|
||||
};
|
||||
key[key.len - 1] = 0;
|
||||
errdefer alloc.free(key);
|
||||
|
||||
if (key_entry.type.out_of_line) {
|
||||
const value: ValueOutOfLineEntry = undefined;
|
||||
try meta.interface.readSliceEndian(ValueOutOfLineEntry, @ptrCast(&value), .little);
|
||||
|
||||
var ool_rdr = fil.readerAt(kv_start + value.ref.block_start);
|
||||
var ool_meta: MetadataReader = .init(alloc, &ool_rdr.interface, decomp);
|
||||
try ool_meta.interface.discardAll(value.ref.block_offset);
|
||||
|
||||
var val_size: u32 = undefined;
|
||||
try ool_meta.interface.readSliceEndian(val_size, @ptrCast(&val_size), .little);
|
||||
|
||||
const val = try alloc.alloc(u8, val_size);
|
||||
errdefer alloc.free(val);
|
||||
try ool_meta.interface.readSliceEndian(u8, val, .little);
|
||||
|
||||
out[i] = .{
|
||||
.key = key,
|
||||
.value = val,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
var val_size: u32 = undefined;
|
||||
try meta.interface.readSliceEndian(val_size, @ptrCast(&val_size), .little);
|
||||
|
||||
const val = try alloc.alloc(u8, val_size);
|
||||
errdefer alloc.free(val);
|
||||
try meta.interface.readSliceEndian(u8, val, .little);
|
||||
|
||||
out[i] = .{
|
||||
.key = key,
|
||||
.value = val,
|
||||
};
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user