Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c2a3eb420f | |||
| eb1b940854 | |||
| ba2f069a4a | |||
| 7c4089f826 | |||
| 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-cache/
|
||||||
zig-out/
|
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", "-Duse_c_libs=true", "-Ddebug=true"],
|
||||||
|
},
|
||||||
|
|
||||||
|
"program": "zig-out/bin/unsquashfs",
|
||||||
|
"args": [
|
||||||
|
"--force",
|
||||||
|
"-d",
|
||||||
|
"testing/TestExtractUnsquashfs",
|
||||||
|
"testing/LinuxPATest.sfs",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Folder-specific settings
|
||||||
|
//
|
||||||
|
// For a full list of overridable settings, and general information on folder-specific settings,
|
||||||
|
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
|
||||||
|
{
|
||||||
|
"lsp": {
|
||||||
|
"zls": {
|
||||||
|
"initialization_options": {
|
||||||
|
"usePlaceholders": false,
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"build_on_save": true,
|
||||||
|
"use_placeholders": false,
|
||||||
|
"build_on_save_args": ["-fincremental", "-Dallow_lzo=true"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
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;
|
||||||
|
```
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Build = std.Build;
|
||||||
|
const Step = Build.Step;
|
||||||
|
|
||||||
|
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.") orelse false;
|
||||||
|
const dynamic = b.option(bool, "dynamic", "Use dynamic linking for C libraries (if used).") orelse false;
|
||||||
|
var version_string = b.option([]const u8, "version", "Version of the library/binary") orelse "0.0.0-testing";
|
||||||
|
|
||||||
|
const target = b.standardTargetOptions(.{});
|
||||||
|
var optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
version_string = std.mem.trimStart(u8, version_string, "v");
|
||||||
|
const version = try std.SemanticVersion.parse(version_string);
|
||||||
|
const unsquashfs_options = b.addOptions();
|
||||||
|
unsquashfs_options.addOption(
|
||||||
|
std.SemanticVersion,
|
||||||
|
"version",
|
||||||
|
version,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (debug) optimize = .Debug;
|
||||||
|
if (optimize == .Debug) debug = true;
|
||||||
|
|
||||||
|
const c_import = b.addTranslateC(.{
|
||||||
|
.root_source_file = b.path("src/c.h"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
if (allow_lzo) c_import.defineCMacro("ALLOW_LZO", null);
|
||||||
|
if (dynamic) {
|
||||||
|
c_import.linkSystemLibrary("zlib-ng", .{});
|
||||||
|
c_import.linkSystemLibrary("lzma", .{});
|
||||||
|
if (allow_lzo)
|
||||||
|
c_import.linkSystemLibrary("minilzo", .{});
|
||||||
|
c_import.linkSystemLibrary("lz4", .{});
|
||||||
|
c_import.linkSystemLibrary("zstd", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
var lib = b.addLibrary(.{
|
||||||
|
.name = "squashfs",
|
||||||
|
.root_module = b.addModule("squashfs", .{
|
||||||
|
.root_source_file = b.path("src/root.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.valgrind = debug,
|
||||||
|
.error_tracing = debug,
|
||||||
|
.strip = !debug,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "config", .module = zig_squashfs_options.createModule() },
|
||||||
|
.{ .name = "c", .module = c_import.createModule() },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
.use_llvm = debug,
|
||||||
|
.version = version,
|
||||||
|
});
|
||||||
|
|
||||||
|
const deps = try getDependencies(b, target, optimize, allow_lzo, dynamic);
|
||||||
|
|
||||||
|
for (deps) |d|
|
||||||
|
lib.root_module.linkLibrary(d);
|
||||||
|
|
||||||
|
const exe = b.addExecutable(.{
|
||||||
|
.name = "unsquashfs",
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/bin/unsquashfs.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "config", .module = unsquashfs_options.createModule() },
|
||||||
|
.{ .name = "squashfs", .module = lib.root_module },
|
||||||
|
},
|
||||||
|
.valgrind = debug,
|
||||||
|
.error_tracing = debug,
|
||||||
|
.strip = !debug,
|
||||||
|
}),
|
||||||
|
.use_llvm = debug,
|
||||||
|
.version = version,
|
||||||
|
});
|
||||||
|
|
||||||
|
b.installArtifact(lib);
|
||||||
|
b.installArtifact(exe);
|
||||||
|
|
||||||
|
const mod_tests = b.addTest(.{
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/root.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.valgrind = true,
|
||||||
|
.error_tracing = true,
|
||||||
|
.strip = false,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "config", .module = zig_squashfs_options.createModule() },
|
||||||
|
.{ .name = "c", .module = c_import.createModule() },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
.use_llvm = debug, // Helps with lldb degugging
|
||||||
|
});
|
||||||
|
|
||||||
|
for (deps) |d|
|
||||||
|
mod_tests.root_module.linkLibrary(d);
|
||||||
|
|
||||||
|
if (dynamic) {
|
||||||
|
mod_tests.root_module.linkSystemLibrary("zlib-ng", .{});
|
||||||
|
mod_tests.root_module.linkSystemLibrary("lzma", .{});
|
||||||
|
mod_tests.root_module.linkSystemLibrary("minilzo", .{});
|
||||||
|
mod_tests.root_module.linkSystemLibrary("lz4", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = lib.root_module,
|
||||||
|
});
|
||||||
|
const exe_check = b.addExecutable(.{
|
||||||
|
.name = "unsquashfs",
|
||||||
|
.root_module = exe.root_module,
|
||||||
|
});
|
||||||
|
const check = b.step("check", "Check if unsquashfs compiles");
|
||||||
|
check.dependOn(&lib_check.step);
|
||||||
|
check.dependOn(&exe_check.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getDependencies(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, allow_lzo: bool, dynamic: bool) ![]*Step.Compile {
|
||||||
|
if (dynamic) return &.{};
|
||||||
|
|
||||||
|
var list: std.ArrayList(*Step.Compile) = .empty;
|
||||||
|
errdefer list.clearAndFree(b.allocator);
|
||||||
|
|
||||||
|
var zlib_ng = b.dependency("zlib_ng", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
try list.append(b.allocator, zlib_ng.artifact("zng"));
|
||||||
|
|
||||||
|
var xz = b.dependency("xz", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
try list.append(b.allocator, xz.artifact("lzma"));
|
||||||
|
|
||||||
|
if (allow_lzo) {
|
||||||
|
var minilzo = b.dependency("minilzo", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
try list.append(b.allocator, minilzo.artifact("minilzo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var lz4 = b.dependency("lz4", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
try list.append(b.allocator, lz4.artifact("lz4"));
|
||||||
|
|
||||||
|
var zstd = b.dependency("zstd", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
try list.append(b.allocator, zstd.artifact("zstd"));
|
||||||
|
|
||||||
|
return list.toOwnedSlice(b.allocator);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
.{
|
||||||
|
.name = .squashfs,
|
||||||
|
.version = "0.0.6",
|
||||||
|
.fingerprint = 0x37ba29474b87f145, // Changing this has security and trust implications.
|
||||||
|
.minimum_zig_version = "0.16.1",
|
||||||
|
.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",
|
||||||
|
},
|
||||||
|
.xz = .{
|
||||||
|
.url = "git+https://github.com/akunaakwei/zig-xz.git#e2d389262c8291907e3e4c6fb119819141c16c0f",
|
||||||
|
.hash = "xz-5.8.2-6v47_JYeAABSL-jonprpL5-E_YaaGc4B5xrbe93WsJ3G",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.paths = .{
|
||||||
|
"build.zig",
|
||||||
|
"build.zig.zon",
|
||||||
|
"src",
|
||||||
|
|
||||||
|
"LICENSE",
|
||||||
|
"README.md",
|
||||||
|
},
|
||||||
|
}
|
||||||
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
zig test \
|
||||||
|
-lc \
|
||||||
|
-lz \
|
||||||
|
-llzma \
|
||||||
|
-lminilzo \
|
||||||
|
-llz4 \
|
||||||
|
-lzstd \
|
||||||
|
src/test.zig
|
||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Io = std.Io;
|
||||||
|
const MemoryMap = Io.File.MemoryMap;
|
||||||
|
|
||||||
|
const c = @import("c");
|
||||||
|
const config = @import("config");
|
||||||
|
|
||||||
|
const ExtractionOptions = @import("options.zig");
|
||||||
|
const File = @import("file.zig");
|
||||||
|
const Inode = @import("inode.zig");
|
||||||
|
const Superblock = @import("super.zig").Superblock;
|
||||||
|
const DecompCache = @import("util/decomp_cache.zig");
|
||||||
|
const CompressionType = @import("util/decompress.zig").CompressionType;
|
||||||
|
|
||||||
|
const Archive = @This();
|
||||||
|
|
||||||
|
const CACHE_MIN = 16 * 1024 * 1024;
|
||||||
|
const CACHE_MAX = 1 * 1024 * 1024 * 1024;
|
||||||
|
|
||||||
|
cache: DecompCache,
|
||||||
|
|
||||||
|
super: Superblock,
|
||||||
|
|
||||||
|
/// Open a squashfs archive from an Io.File.
|
||||||
|
pub fn init(alloc: std.mem.Allocator, io: Io, fil: Io.File) !Archive {
|
||||||
|
return initAdvanced(alloc, io, fil, 0, 0);
|
||||||
|
}
|
||||||
|
/// If max_cache_size is zero, a size is selected based on system ram, up to 1GB with a minimum of 16MB.
|
||||||
|
pub fn initAdvanced(alloc: std.mem.Allocator, io: Io, file: Io.File, offset: u64, max_cache_size: 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);
|
||||||
|
try super.validate();
|
||||||
|
|
||||||
|
if (!config.use_zig_decomp and config.allow_lzo)
|
||||||
|
_ = c.lzo_init();
|
||||||
|
|
||||||
|
const cache_size = blk: {
|
||||||
|
if (max_cache_size > CACHE_MIN) break :blk CACHE_MIN;
|
||||||
|
const sys_mem = std.process.totalSystemMemory() catch break :blk CACHE_MIN;
|
||||||
|
var min = @min(CACHE_MAX, sys_mem / 4);
|
||||||
|
if (min < CACHE_MIN and sys_mem > CACHE_MIN)
|
||||||
|
min = CACHE_MIN;
|
||||||
|
break :blk min;
|
||||||
|
};
|
||||||
|
return .{
|
||||||
|
.cache = try .init(
|
||||||
|
alloc,
|
||||||
|
try file.createMemoryMap(
|
||||||
|
io,
|
||||||
|
.{
|
||||||
|
.offset = offset,
|
||||||
|
.len = super.size,
|
||||||
|
.protection = .{ .read = true },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
super.compression,
|
||||||
|
cache_size,
|
||||||
|
),
|
||||||
|
|
||||||
|
.super = super,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
pub fn deinit(self: *Archive, io: Io) void {
|
||||||
|
self.cache.deinit(io);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn root(self: *Archive, alloc: std.mem.Allocator, io: Io) !File {
|
||||||
|
return .fromRef(alloc, io, self, "", self.super.root_ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open(self: *Archive, alloc: std.mem.Allocator, io: Io, filepath: []const u8) !File {
|
||||||
|
const path = std.mem.trim(u8, filepath, "/");
|
||||||
|
|
||||||
|
var root_file = try self.root(alloc, io);
|
||||||
|
|
||||||
|
if (path.len == 0 or std.mem.eql(u8, path, ".")) return root_file;
|
||||||
|
defer root_file.deinit();
|
||||||
|
|
||||||
|
return root_file.open(alloc, io, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract(self: *Archive, alloc: std.mem.Allocator, io: Io, ext_dir: []const u8, options: ExtractionOptions) !void {
|
||||||
|
const root_inode: Inode = try .fromRef(alloc, io, &self.cache, self.super.inode_start, self.super.block_size, self.super.root_ref);
|
||||||
|
return root_inode.extract(
|
||||||
|
alloc,
|
||||||
|
io,
|
||||||
|
&self.cache,
|
||||||
|
self.super.dir_start,
|
||||||
|
self.super.inode_start,
|
||||||
|
self.super.frag_start,
|
||||||
|
self.super.block_size,
|
||||||
|
self.super.id_start,
|
||||||
|
self.super.xattr_start,
|
||||||
|
ext_dir,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Io = std.Io;
|
||||||
|
const Writer = std.Io.Writer;
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
const config = @import("config");
|
||||||
|
const squashfs = @import("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 no present or zero, the system's logical cores count is used.
|
||||||
|
\\ -v Verbose
|
||||||
|
\\
|
||||||
|
\\ --force Force extraction. If the destination already exists, it will be deleted.
|
||||||
|
\\
|
||||||
|
\\ --help Display this messages
|
||||||
|
\\ --version Display the version
|
||||||
|
\\
|
||||||
|
;
|
||||||
|
|
||||||
|
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 = Io.File.stdout();
|
||||||
|
var out = stdout.writer(io, &[0]u8{});
|
||||||
|
defer out.interface.flush() catch {};
|
||||||
|
|
||||||
|
try handleArgs(&out.interface, init.minimal.args);
|
||||||
|
if (archive.len == 0) {
|
||||||
|
try out.interface.print("You must provide a squashfs archive\n", .{});
|
||||||
|
try out.interface.print(help_mgs, .{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fil: std.Io.File = try Io.Dir.cwd().openFile(io, archive, .{}); //TODO: Handle error gracefully.
|
||||||
|
defer fil.close(io);
|
||||||
|
var arc: squashfs.Archive = try .initAdvanced(alloc, io, fil, offset, 0); //TODO: Update when memory size matters. //TODO: Handle error gracefully.
|
||||||
|
defer arc.deinit(io);
|
||||||
|
const options: squashfs.ExtractionOptions = .{
|
||||||
|
.single_threaded = threads == 1,
|
||||||
|
.verbose = verbose,
|
||||||
|
.verbose_writer = if (verbose) &out.interface else null,
|
||||||
|
.ignore_xattr = ignore_xattrs,
|
||||||
|
.ignore_permissions = ignore_permissions,
|
||||||
|
};
|
||||||
|
if (force)
|
||||||
|
try Io.Dir.cwd().deleteTree(io, extLoc);
|
||||||
|
try arc.extract(alloc, io, extLoc, options); //TODO: Handle error gracefully.
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handleArgs(out: *Writer, args: std.process.Args) !void {
|
||||||
|
var arg_iter = args.iterate();
|
||||||
|
_ = 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
std.testing.refAllDecls(squashfs.Archive);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#ifdef ALLOW_LZO
|
||||||
|
#include <lzo/minilzo.h>
|
||||||
|
#endif
|
||||||
|
#include <zlib-ng.h>
|
||||||
|
#include <zstd.h>
|
||||||
|
#include <lz4.h>
|
||||||
|
#include <lzma.h>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Reader = std.Io.Reader;
|
||||||
|
|
||||||
|
const Inode = @import("inode.zig");
|
||||||
|
|
||||||
|
const Header = extern struct {
|
||||||
|
count: u32,
|
||||||
|
block_start: u32,
|
||||||
|
num: u32,
|
||||||
|
};
|
||||||
|
const Entry = extern struct {
|
||||||
|
block_offset: u16,
|
||||||
|
num_offset: i16,
|
||||||
|
inode_type: Inode.Type,
|
||||||
|
name_size: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Error = error{OutOfMemory} || std.Io.Reader.Error;
|
||||||
|
|
||||||
|
const DirEntry = @This();
|
||||||
|
|
||||||
|
inode_type: Inode.Type,
|
||||||
|
name: []const u8,
|
||||||
|
|
||||||
|
block_start: u32,
|
||||||
|
block_offset: u32,
|
||||||
|
num: u32,
|
||||||
|
|
||||||
|
pub fn deinit(self: DirEntry, alloc: std.mem.Allocator) void {
|
||||||
|
alloc.free(self.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readEntries(alloc: std.mem.Allocator, rdr: *Reader, size: u32) Error![]DirEntry {
|
||||||
|
var out: std.ArrayList(DirEntry) = try .initCapacity(alloc, 50);
|
||||||
|
errdefer out.deinit(alloc);
|
||||||
|
|
||||||
|
var tot_read: u32 = 3;
|
||||||
|
while (tot_read < size) {
|
||||||
|
var hdr: Header = undefined;
|
||||||
|
try rdr.readSliceEndian(Header, @ptrCast(&hdr), .little);
|
||||||
|
tot_read += @sizeOf(Header);
|
||||||
|
|
||||||
|
try out.ensureUnusedCapacity(alloc, hdr.count + 1);
|
||||||
|
|
||||||
|
for (0..hdr.count + 1) |_| {
|
||||||
|
var ent: Entry = undefined;
|
||||||
|
try rdr.readSliceEndian(Entry, @ptrCast(&ent), .little);
|
||||||
|
tot_read += @sizeOf(Entry) + ent.name_size + 1;
|
||||||
|
|
||||||
|
const name = try alloc.alloc(u8, ent.name_size + 1);
|
||||||
|
errdefer alloc.free(name);
|
||||||
|
|
||||||
|
try rdr.readSliceEndian(u8, name, .little);
|
||||||
|
|
||||||
|
out.appendAssumeCapacity(.{
|
||||||
|
.inode_type = ent.inode_type,
|
||||||
|
.name = name,
|
||||||
|
|
||||||
|
.block_offset = ent.block_offset,
|
||||||
|
.block_start = hdr.block_start,
|
||||||
|
.num = @intCast(@as(i64, @intCast(hdr.num)) + ent.num_offset),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.toOwnedSlice(alloc);
|
||||||
|
}
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
//! A wrapper around an Inode to make common activities easier.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Io = std.Io;
|
||||||
|
|
||||||
|
const Archive = @import("archive.zig");
|
||||||
|
const DirEntry = @import("dir_entry.zig");
|
||||||
|
const ExtractionOptions = @import("options.zig");
|
||||||
|
const Inode = @import("inode.zig");
|
||||||
|
const LookupTable = @import("lookup_table.zig");
|
||||||
|
const MetadataReader = @import("util/metadata.zig");
|
||||||
|
|
||||||
|
pub const Error = error{
|
||||||
|
NotFound,
|
||||||
|
};
|
||||||
|
|
||||||
|
const File = @This();
|
||||||
|
|
||||||
|
alloc: std.mem.Allocator,
|
||||||
|
archive: *Archive,
|
||||||
|
|
||||||
|
name: []const u8,
|
||||||
|
inode: Inode,
|
||||||
|
|
||||||
|
pub fn fromEntry(alloc: std.mem.Allocator, io: Io, archive: *Archive, entry: DirEntry) !File {
|
||||||
|
var meta: MetadataReader = .init(io, &archive.cache, archive.super.inode_start + entry.block_start);
|
||||||
|
defer meta.deinit();
|
||||||
|
try meta.interface.discardAll(entry.block_offset);
|
||||||
|
|
||||||
|
const new_name = try alloc.alloc(u8, entry.name.len);
|
||||||
|
errdefer alloc.free(new_name);
|
||||||
|
@memcpy(new_name, entry.name);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.alloc = alloc,
|
||||||
|
.archive = archive,
|
||||||
|
|
||||||
|
.name = new_name,
|
||||||
|
.inode = try .fromReader(alloc, &meta.interface, archive.super.block_size),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/// Create a File from an Inode.Ref. name should be created using the alloc given.
|
||||||
|
pub fn fromRef(alloc: std.mem.Allocator, io: Io, archive: *Archive, name: []const u8, ref: Inode.Ref) !File {
|
||||||
|
return .{
|
||||||
|
.alloc = alloc,
|
||||||
|
.archive = archive,
|
||||||
|
|
||||||
|
.name = name,
|
||||||
|
.inode = try .fromRef(
|
||||||
|
alloc,
|
||||||
|
io,
|
||||||
|
&archive.cache,
|
||||||
|
archive.super.inode_start,
|
||||||
|
archive.super.block_size,
|
||||||
|
ref,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
pub fn copy(alloc: std.mem.Allocator, from: File) !File {
|
||||||
|
const new_name = try alloc.alloc(u8, from.name.len);
|
||||||
|
errdefer alloc.free(new_name);
|
||||||
|
@memcpy(new_name, from.name);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.alloc = alloc,
|
||||||
|
.archive = from.archive,
|
||||||
|
|
||||||
|
.inode = try .copy(alloc, from.inode),
|
||||||
|
.name = new_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 path = std.mem.trim(u8, filepath, "/");
|
||||||
|
|
||||||
|
if (path.len == 0 or std.mem.eql(u8, path, ".")) return .copy(alloc, self);
|
||||||
|
|
||||||
|
const first_element = std.mem.sliceTo(path, '/');
|
||||||
|
|
||||||
|
const entries = try self.inode.readDirectory(alloc, io, &self.archive.cache, self.archive.super.dir_start);
|
||||||
|
defer {
|
||||||
|
for (entries) |entry|
|
||||||
|
entry.deinit(alloc);
|
||||||
|
alloc.free(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Potentially I could use linear searching on small dir tables...
|
||||||
|
var search_slice = entries;
|
||||||
|
var idx = search_slice.len / 2;
|
||||||
|
while (search_slice.len > 0) {
|
||||||
|
const order = std.mem.order(u8, first_element, search_slice[idx].name);
|
||||||
|
switch (order) {
|
||||||
|
.eq => break,
|
||||||
|
.gt => search_slice = search_slice[idx..],
|
||||||
|
.lt => search_slice = search_slice[0..idx],
|
||||||
|
}
|
||||||
|
idx = search_slice.len / 2;
|
||||||
|
}
|
||||||
|
if (search_slice.len == 0) return Error.NotFound;
|
||||||
|
|
||||||
|
var fil: File = try .fromEntry(alloc, io, self.archive, search_slice[idx]);
|
||||||
|
if (path.len == first_element.len) return fil;
|
||||||
|
defer fil.deinit();
|
||||||
|
|
||||||
|
return fil.open(alloc, io, filepath[first_element.len..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract(self: File, alloc: std.mem.Allocator, io: Io, path: []const u8, options: ExtractionOptions) !void {
|
||||||
|
return self.inode.extract(
|
||||||
|
alloc,
|
||||||
|
io,
|
||||||
|
&self.archive.cache,
|
||||||
|
self.archive.super.dir_start,
|
||||||
|
self.archive.super.inode_start,
|
||||||
|
self.archive.super.frag_start,
|
||||||
|
self.archive.super.block_size,
|
||||||
|
self.archive.super.id_start,
|
||||||
|
self.archive.super.xattr_start,
|
||||||
|
path,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const BlockSize = @import("inode_data/file.zig").BlockSize;
|
||||||
|
|
||||||
|
pub const FragEntry = extern struct {
|
||||||
|
start: u64,
|
||||||
|
size: BlockSize,
|
||||||
|
_: u32,
|
||||||
|
};
|
||||||
+434
@@ -0,0 +1,434 @@
|
|||||||
|
//! A file-system object. Represents a File or directory.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Io = std.Io;
|
||||||
|
const Reader = Io.Reader;
|
||||||
|
|
||||||
|
const DirEntry = @import("dir_entry.zig");
|
||||||
|
const ExtractionOptions = @import("options.zig");
|
||||||
|
const FragEntry = @import("frag.zig").FragEntry;
|
||||||
|
const DirTypes = @import("inode_data/dir.zig");
|
||||||
|
const FileTypes = @import("inode_data/file.zig");
|
||||||
|
const MiscTypes = @import("inode_data/misc.zig");
|
||||||
|
const LookupTable = @import("lookup_table.zig");
|
||||||
|
const DataExtract = @import("util/data_extract.zig");
|
||||||
|
const DecompCache = @import("util/decomp_cache.zig");
|
||||||
|
const MetadataReader = @import("util/metadata.zig");
|
||||||
|
|
||||||
|
pub const Ref = packed struct(u64) {
|
||||||
|
block_offset: u16,
|
||||||
|
block_start: u32,
|
||||||
|
_: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
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: DirTypes.Dir,
|
||||||
|
file: FileTypes.File,
|
||||||
|
symlink: MiscTypes.Symlink,
|
||||||
|
block_dev: MiscTypes.Dev,
|
||||||
|
char_dev: MiscTypes.Dev,
|
||||||
|
fifo: MiscTypes.IPC,
|
||||||
|
socket: MiscTypes.IPC,
|
||||||
|
ext_dir: DirTypes.ExtDir,
|
||||||
|
ext_file: FileTypes.ExtFile,
|
||||||
|
ext_symlink: MiscTypes.ExtSymlink,
|
||||||
|
ext_block_dev: MiscTypes.ExtDev,
|
||||||
|
ext_char_dev: MiscTypes.ExtDev,
|
||||||
|
ext_fifo: MiscTypes.ExtIPC,
|
||||||
|
ext_socket: MiscTypes.ExtIPC,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Header = packed struct {
|
||||||
|
inode_type: Type,
|
||||||
|
permissions: u16,
|
||||||
|
uid_idx: u16,
|
||||||
|
gid_idx: u16,
|
||||||
|
mod_time: u32,
|
||||||
|
num: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Error = error{
|
||||||
|
NotDirectory,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Inode = @This();
|
||||||
|
|
||||||
|
hdr: Header,
|
||||||
|
data: Data,
|
||||||
|
|
||||||
|
pub fn fromRef(alloc: std.mem.Allocator, io: Io, cache: *DecompCache, inode_start: u64, block_size: u32, ref: Ref) !Inode {
|
||||||
|
var meta: MetadataReader = .init(io, cache, ref.block_start + inode_start);
|
||||||
|
defer meta.deinit();
|
||||||
|
try meta.interface.discardAll(ref.block_offset);
|
||||||
|
return fromReader(alloc, &meta.interface, block_size);
|
||||||
|
}
|
||||||
|
pub fn fromReader(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 copy(alloc: std.mem.Allocator, from: Inode) !Inode {
|
||||||
|
var new = from;
|
||||||
|
switch (from.data) {
|
||||||
|
.file => |f| {
|
||||||
|
new.data.file.block_sizes = try alloc.alloc(FileTypes.BlockSize, f.block_sizes.len);
|
||||||
|
@memcpy(new.data.file.block_sizes, f.block_sizes);
|
||||||
|
},
|
||||||
|
.ext_file => |f| {
|
||||||
|
new.data.ext_file.block_sizes = try alloc.alloc(FileTypes.BlockSize, f.block_sizes.len);
|
||||||
|
@memcpy(new.data.ext_file.block_sizes, f.block_sizes);
|
||||||
|
},
|
||||||
|
.symlink => |s| {
|
||||||
|
const new_target = try alloc.alloc(u8, s.target.len);
|
||||||
|
@memcpy(new_target, s.target);
|
||||||
|
new.data.symlink.target = new_target;
|
||||||
|
},
|
||||||
|
.ext_symlink => |s| {
|
||||||
|
const new_target = try alloc.alloc(u8, s.target.len);
|
||||||
|
@memcpy(new_target, s.target);
|
||||||
|
new.data.ext_symlink.target = new_target;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
return new;
|
||||||
|
}
|
||||||
|
pub fn deinit(self: Inode, alloc: std.mem.Allocator) void {
|
||||||
|
switch (self.data) {
|
||||||
|
.file => |f| alloc.free(f.block_sizes),
|
||||||
|
.ext_file => |f| alloc.free(f.block_sizes),
|
||||||
|
.symlink => |s| alloc.free(s.target),
|
||||||
|
.ext_symlink => |s| alloc.free(s.target),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readDirectory(self: Inode, alloc: std.mem.Allocator, io: Io, cache: *DecompCache, dir_start: u64) ![]DirEntry {
|
||||||
|
return switch (self.data) {
|
||||||
|
.dir => |d| readDirectoryFromData(alloc, io, cache, dir_start, d),
|
||||||
|
.ext_dir => |d| readDirectoryFromData(alloc, io, cache, dir_start, d),
|
||||||
|
else => Error.NotDirectory,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
fn readDirectoryFromData(alloc: std.mem.Allocator, io: Io, cache: *DecompCache, dir_start: u64, d: anytype) ![]DirEntry {
|
||||||
|
var meta: MetadataReader = .init(io, cache, dir_start + d.block_start);
|
||||||
|
defer meta.deinit();
|
||||||
|
try meta.interface.discardAll(d.block_offset);
|
||||||
|
|
||||||
|
return DirEntry.readEntries(alloc, &meta.interface, d.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extraction
|
||||||
|
|
||||||
|
pub fn extract(
|
||||||
|
self: Inode,
|
||||||
|
alloc: std.mem.Allocator,
|
||||||
|
io: Io,
|
||||||
|
cache: *DecompCache,
|
||||||
|
dir_start: u64,
|
||||||
|
inode_start: u64,
|
||||||
|
frag_start: u64,
|
||||||
|
block_size: u32,
|
||||||
|
id_start: u64,
|
||||||
|
xattr_start: u64,
|
||||||
|
ext_loc: []const u8,
|
||||||
|
options: ExtractionOptions,
|
||||||
|
) !void {
|
||||||
|
const path = std.mem.trimEnd(u8, ext_loc, "/");
|
||||||
|
|
||||||
|
var sel_val: std.atomic.Value(usize) = .init(1);
|
||||||
|
|
||||||
|
var sel_buf: [5]ExtractUnion = undefined;
|
||||||
|
var sel: Io.Select(ExtractUnion) = .init(io, &sel_buf);
|
||||||
|
defer sel.cancelDiscard();
|
||||||
|
|
||||||
|
var meta_loop = io.async(metadataLoop, .{ alloc, io, cache, id_start, xattr_start, &sel, &sel_val, options });
|
||||||
|
defer _ = meta_loop.cancel(io) catch {};
|
||||||
|
|
||||||
|
sel.async(.ret, extractReal, .{ self, alloc, io, cache, dir_start, inode_start, frag_start, block_size, &sel, &sel_val, path, true });
|
||||||
|
|
||||||
|
try meta_loop.await(io);
|
||||||
|
}
|
||||||
|
fn extractReal(
|
||||||
|
self: Inode,
|
||||||
|
alloc: std.mem.Allocator,
|
||||||
|
io: Io,
|
||||||
|
cache: *DecompCache,
|
||||||
|
dir_start: u64,
|
||||||
|
inode_start: u64,
|
||||||
|
frag_start: u64,
|
||||||
|
block_size: u32,
|
||||||
|
sel: *Io.Select(ExtractUnion),
|
||||||
|
sel_val: *std.atomic.Value(usize),
|
||||||
|
path: []const u8,
|
||||||
|
origin: bool,
|
||||||
|
) ExtractionError!ExtractReturn {
|
||||||
|
errdefer if (!origin) {
|
||||||
|
self.deinit(alloc);
|
||||||
|
alloc.free(path);
|
||||||
|
};
|
||||||
|
switch (self.hdr.inode_type) {
|
||||||
|
.dir, .ext_dir => {
|
||||||
|
try Io.Dir.cwd().createDir(io, path, @enumFromInt(0o777));
|
||||||
|
|
||||||
|
const entries = self.readDirectory(alloc, io, cache, dir_start) catch |err| switch (err) {
|
||||||
|
error.NotDirectory => unreachable,
|
||||||
|
else => |e| return e,
|
||||||
|
};
|
||||||
|
defer {
|
||||||
|
for (entries) |entry|
|
||||||
|
entry.deinit(alloc);
|
||||||
|
alloc.free(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries.len != 0) {
|
||||||
|
_ = sel_val.fetchAdd(entries.len, .acq_rel);
|
||||||
|
|
||||||
|
for (entries) |entry| {
|
||||||
|
var meta: MetadataReader = .init(io, cache, inode_start + entry.block_start);
|
||||||
|
defer meta.deinit();
|
||||||
|
try meta.interface.discardAll(entry.block_offset);
|
||||||
|
|
||||||
|
var new_inode: Inode = try .fromReader(alloc, &meta.interface, block_size);
|
||||||
|
errdefer new_inode.deinit(alloc);
|
||||||
|
|
||||||
|
const new_path = try std.mem.concat(alloc, u8, &.{ path, "/", entry.name });
|
||||||
|
errdefer alloc.free(new_path);
|
||||||
|
sel.async(
|
||||||
|
.ret,
|
||||||
|
extractReal,
|
||||||
|
.{ new_inode, alloc, io, cache, dir_start, inode_start, frag_start, block_size, sel, sel_val, new_path, false },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.file, .ext_file => {
|
||||||
|
std.debug.print("{s} {}\n", .{ path, self.data });
|
||||||
|
// var atomic = try Io.Dir.cwd().createFileAtomic(io, path, .{});
|
||||||
|
// defer atomic.deinit(io);
|
||||||
|
|
||||||
|
// var data: DataExtract = undefined;
|
||||||
|
// var frag_offset: ?u64 = null;
|
||||||
|
// switch (self.data) {
|
||||||
|
// .file => |f| {
|
||||||
|
// data = .init(cache.decomp, cache.map, block_size, f.block_start, f.size, f.block_sizes);
|
||||||
|
// if (f.frag_idx != 0xFFFFFFFF) {
|
||||||
|
// const entry: FragEntry = try LookupTable.lookup(FragEntry, io, cache, frag_start, f.frag_idx);
|
||||||
|
// if (entry.size.uncompressed) {
|
||||||
|
// data.addFrag(cache.map.memory[entry.start..][0..entry.size.size], f.frag_offset);
|
||||||
|
// } else {
|
||||||
|
// frag_offset = entry.start;
|
||||||
|
// const block = try cache.checkoutBlock(io, entry.start, entry.size.size, block_size);
|
||||||
|
// data.addFrag(block, f.frag_offset);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// .ext_file => |f| {
|
||||||
|
// data = .init(cache.decomp, cache.map, block_size, f.block_start, f.size, f.block_sizes);
|
||||||
|
// if (f.frag_idx != 0xFFFFFFFF) {
|
||||||
|
// const entry: FragEntry = try LookupTable.lookup(FragEntry, io, cache, frag_start, f.frag_idx);
|
||||||
|
// if (entry.size.uncompressed) {
|
||||||
|
// data.addFrag(cache.map.memory[entry.start..][0..entry.size.size], f.frag_offset);
|
||||||
|
// } else {
|
||||||
|
// frag_offset = entry.start;
|
||||||
|
// const block = try cache.checkoutBlock(io, entry.start, entry.size.size, block_size);
|
||||||
|
// data.addFrag(block, f.frag_offset);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// else => unreachable,
|
||||||
|
// }
|
||||||
|
// defer if (frag_offset != null) cache.checkinBlock(io, frag_offset.?) catch {};
|
||||||
|
// try data.asyncExtract(alloc, io, atomic.file);
|
||||||
|
|
||||||
|
// try atomic.link(io);
|
||||||
|
},
|
||||||
|
.symlink, .ext_symlink => {
|
||||||
|
const target = switch (self.data) {
|
||||||
|
.symlink => |s| s.target,
|
||||||
|
.ext_symlink => |s| s.target,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
try Io.Dir.cwd().symLink(io, target, path, .{});
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
var dev: u32 = 0;
|
||||||
|
var mode: u32 = undefined;
|
||||||
|
|
||||||
|
const DT = std.os.linux.DT;
|
||||||
|
|
||||||
|
switch (self.data) {
|
||||||
|
.block_dev => |d| {
|
||||||
|
mode = DT.BLK;
|
||||||
|
dev = d.dev;
|
||||||
|
},
|
||||||
|
.ext_block_dev => |d| {
|
||||||
|
mode = DT.BLK;
|
||||||
|
dev = d.dev;
|
||||||
|
},
|
||||||
|
.char_dev => |d| {
|
||||||
|
mode = DT.CHR;
|
||||||
|
dev = d.dev;
|
||||||
|
},
|
||||||
|
.ext_char_dev => |d| {
|
||||||
|
mode = DT.CHR;
|
||||||
|
dev = d.dev;
|
||||||
|
},
|
||||||
|
.fifo, .ext_fifo => mode = DT.FIFO,
|
||||||
|
.socket, .ext_socket => mode = DT.SOCK,
|
||||||
|
else => unreachable,
|
||||||
|
}
|
||||||
|
|
||||||
|
const sentinel_path = try std.mem.concatWithSentinel(alloc, u8, &.{path}, 0);
|
||||||
|
defer alloc.free(sentinel_path);
|
||||||
|
const res = std.os.linux.mknod(sentinel_path, mode, dev);
|
||||||
|
if (res != 0)
|
||||||
|
return ExtractionError.Mknod;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.path = path,
|
||||||
|
.inode = self,
|
||||||
|
.origin = origin,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExtractUnion = union { ret: ExtractionError!ExtractReturn };
|
||||||
|
pub const ExtractionError = error{ SetXattr, Mknod, Canceled } || DirEntry.Error || Io.Dir.CreateFileAtomicError || DataExtract.Error || Io.File.Atomic.LinkError ||
|
||||||
|
Io.Dir.SymLinkError;
|
||||||
|
|
||||||
|
const ExtractReturn = struct {
|
||||||
|
path: []const u8,
|
||||||
|
inode: Inode,
|
||||||
|
origin: bool,
|
||||||
|
|
||||||
|
fn deinit(self: ExtractReturn, alloc: std.mem.Allocator) void {
|
||||||
|
if (self.origin) return;
|
||||||
|
alloc.free(self.path);
|
||||||
|
self.inode.deinit(alloc);
|
||||||
|
}
|
||||||
|
fn setMetadata(self: ExtractReturn, alloc: std.mem.Allocator, io: Io, cache: *DecompCache, id_start: u64, xattr_start: u64, options: ExtractionOptions) !void {
|
||||||
|
if (options.ignore_permissions and options.ignore_xattr) return;
|
||||||
|
|
||||||
|
var fil = try Io.Dir.cwd().openFile(io, self.path, .{});
|
||||||
|
defer fil.close(io);
|
||||||
|
|
||||||
|
if (!options.ignore_permissions) {
|
||||||
|
try fil.setTimestamps(io, .{ .modify_timestamp = .{
|
||||||
|
.new = .{ .nanoseconds = @as(i96, @intCast(self.inode.hdr.mod_time)) * std.time.ns_per_s },
|
||||||
|
} });
|
||||||
|
try fil.setPermissions(io, @enumFromInt(self.inode.hdr.permissions));
|
||||||
|
try fil.setOwner(
|
||||||
|
io,
|
||||||
|
try LookupTable.lookup(u16, io, cache, id_start, self.inode.hdr.uid_idx),
|
||||||
|
try LookupTable.lookup(u16, io, cache, id_start, self.inode.hdr.gid_idx),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (options.ignore_xattr) return;
|
||||||
|
const xattr_idx: u32 = switch (self.inode.data) {
|
||||||
|
.ext_dir => |d| d.xattr_idx,
|
||||||
|
.ext_file => |f| f.xattr_idx,
|
||||||
|
.ext_symlink => |s| s.xattr_idx,
|
||||||
|
.ext_block_dev, .ext_char_dev => |d| d.xattr_idx,
|
||||||
|
.ext_fifo, .ext_socket => |i| i.xattr_idx,
|
||||||
|
else => return,
|
||||||
|
};
|
||||||
|
if (xattr_idx == 0xFFFFFFFF) return;
|
||||||
|
const xattrs = try LookupTable.xattrLookup(alloc, io, cache, xattr_start, xattr_idx);
|
||||||
|
defer {
|
||||||
|
for (xattrs) |kv|
|
||||||
|
kv.deinit(alloc);
|
||||||
|
alloc.free(xattrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (xattrs) |kv| {
|
||||||
|
const res = std.os.linux.fsetxattr(fil.handle, kv.key.ptr, kv.value.ptr, kv.value.len, 0);
|
||||||
|
if (res != 0)
|
||||||
|
return ExtractionError.SetXattr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn metadataLoop(
|
||||||
|
alloc: std.mem.Allocator,
|
||||||
|
io: Io,
|
||||||
|
cache: *DecompCache,
|
||||||
|
id_start: u64,
|
||||||
|
xattr_start: u64,
|
||||||
|
sel: *Io.Select(ExtractUnion),
|
||||||
|
sel_val: *std.atomic.Value(usize),
|
||||||
|
options: ExtractionOptions,
|
||||||
|
) !void {
|
||||||
|
errdefer {
|
||||||
|
while (sel.group.token.load(.unordered) != null) {
|
||||||
|
const ret = sel.queue.getOne(io) catch break;
|
||||||
|
|
||||||
|
const res = ret.ret catch continue;
|
||||||
|
res.deinit(alloc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var dir_queue: std.PriorityDequeue(ExtractReturn, void, dirReturnQueueOrder) = .empty;
|
||||||
|
defer {
|
||||||
|
while (dir_queue.popMax()) |ret|
|
||||||
|
ret.deinit(alloc);
|
||||||
|
dir_queue.deinit(alloc);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (sel_val.load(.unordered) > 0) {
|
||||||
|
defer _ = sel_val.fetchSub(1, .acq_rel);
|
||||||
|
const ret = try sel.queue.getOne(io);
|
||||||
|
|
||||||
|
const res = try ret.ret;
|
||||||
|
|
||||||
|
if (res.inode.hdr.inode_type == .dir or res.inode.hdr.inode_type == .ext_dir) {
|
||||||
|
try dir_queue.push(alloc, res);
|
||||||
|
} else {
|
||||||
|
defer res.deinit(alloc);
|
||||||
|
try res.setMetadata(alloc, io, cache, id_start, xattr_start, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (dir_queue.popMax()) |res| {
|
||||||
|
defer res.deinit(alloc);
|
||||||
|
try res.setMetadata(alloc, io, cache, id_start, xattr_start, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dirReturnQueueOrder(_: void, a: ExtractReturn, b: ExtractReturn) std.math.Order {
|
||||||
|
return std.math.order(std.mem.count(u8, a.path, "/"), std.mem.count(u8, b.path, "/"));
|
||||||
|
}
|
||||||
@@ -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,91 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Reader = std.Io.Reader;
|
||||||
|
|
||||||
|
pub const BlockSize = packed struct(u32) {
|
||||||
|
size: u24,
|
||||||
|
uncompressed: bool,
|
||||||
|
_: u7,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const File = struct {
|
||||||
|
block_start: u32, // bytes 0-3
|
||||||
|
frag_idx: u32, // bytes 4-7
|
||||||
|
frag_offset: u32, // bytes 8-11
|
||||||
|
size: u32, // bytes 12-15
|
||||||
|
block_sizes: []BlockSize,
|
||||||
|
|
||||||
|
pub fn read(alloc: std.mem.Allocator, rdr: *Reader, block_size: u32) !File {
|
||||||
|
const raw_values = extern struct {
|
||||||
|
block_start: u32, // bytes 0-3
|
||||||
|
frag_idx: u32, // bytes 4-7
|
||||||
|
frag_offset: u32, // bytes 8-11
|
||||||
|
size: u32, // bytes 12-15
|
||||||
|
};
|
||||||
|
var values: raw_values = undefined;
|
||||||
|
try rdr.readSliceEndian(@TypeOf(values), @ptrCast(&values), .little);
|
||||||
|
|
||||||
|
var num_blocks: u32 = values.size / block_size;
|
||||||
|
if (values.size % block_size != 0 and values.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 = values.block_start,
|
||||||
|
.frag_idx = values.frag_idx,
|
||||||
|
.frag_offset = values.frag_offset,
|
||||||
|
.size = values.size,
|
||||||
|
.block_sizes = sizes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: File, alloc: std.mem.Allocator) void {
|
||||||
|
alloc.free(self.block_sizes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ExtFile = struct {
|
||||||
|
block_start: u64, // bytes 0-7
|
||||||
|
size: u64, // bytes 8-15
|
||||||
|
sparse: u64, // bytes 16-23
|
||||||
|
hard_links: u32, // bytes 24-27
|
||||||
|
frag_idx: u32, // bytes 28-31
|
||||||
|
frag_offset: u32, // bytes 32-35
|
||||||
|
xattr_idx: u32, // bytes 36-39
|
||||||
|
block_sizes: []BlockSize,
|
||||||
|
|
||||||
|
pub fn read(alloc: std.mem.Allocator, rdr: *Reader, block_size: u32) !ExtFile {
|
||||||
|
const raw_values = extern struct {
|
||||||
|
block_start: u64, // bytes 0-7
|
||||||
|
size: u64, // bytes 8-15
|
||||||
|
sparse: u64, // bytes 16-23
|
||||||
|
hard_links: u32, // bytes 24-27
|
||||||
|
frag_idx: u32, // bytes 28-31
|
||||||
|
frag_offset: u32, // bytes 32-35
|
||||||
|
xattr_idx: u32, // bytes 36-39
|
||||||
|
};
|
||||||
|
var values: raw_values = undefined;
|
||||||
|
try rdr.readSliceEndian(@TypeOf(values), @ptrCast(&values), .little);
|
||||||
|
|
||||||
|
var num_blocks: u32 = @truncate(values.size / block_size);
|
||||||
|
if (values.size % block_size != 0 and values.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 = values.block_start,
|
||||||
|
.size = values.size,
|
||||||
|
.sparse = values.sparse,
|
||||||
|
.hard_links = values.hard_links,
|
||||||
|
.frag_idx = values.frag_idx,
|
||||||
|
.frag_offset = values.frag_offset,
|
||||||
|
.xattr_idx = values.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,124 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Io = std.Io;
|
||||||
|
|
||||||
|
const Inode = @import("inode.zig");
|
||||||
|
const DecompCache = @import("util/decomp_cache.zig");
|
||||||
|
const MetadataReader = @import("util/metadata.zig");
|
||||||
|
|
||||||
|
pub fn lookup(comptime T: anytype, io: Io, cache: *DecompCache, table_start: u64, idx: u32) !T {
|
||||||
|
const PER_BLOCK = 8192 / @sizeOf(T);
|
||||||
|
|
||||||
|
const block_idx = idx / PER_BLOCK;
|
||||||
|
const block_offset = idx % PER_BLOCK;
|
||||||
|
|
||||||
|
if (table_start + (block_idx * 8) > cache.map.memory.len) return error.ReadFailed;
|
||||||
|
const offset: u64 = std.mem.readInt(u64, cache.map.memory[table_start + (block_idx * 8) ..][0..8], .little);
|
||||||
|
|
||||||
|
var meta: MetadataReader = .init(io, cache, offset);
|
||||||
|
defer meta.deinit();
|
||||||
|
try meta.interface.discardAll(block_offset * @sizeOf(T));
|
||||||
|
|
||||||
|
var new: T = undefined;
|
||||||
|
try meta.interface.readSliceEndian(T, @ptrCast(&new), .little);
|
||||||
|
return new;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const XattrKV = struct {
|
||||||
|
key: [:0]u8,
|
||||||
|
value: []u8,
|
||||||
|
|
||||||
|
pub fn deinit(self: XattrKV, alloc: std.mem.Allocator) void {
|
||||||
|
alloc.free(self.key);
|
||||||
|
alloc.free(self.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const LookupValue = extern struct {
|
||||||
|
ref: Inode.Ref,
|
||||||
|
count: u32,
|
||||||
|
size: u32,
|
||||||
|
};
|
||||||
|
const KeyEntry = extern struct {
|
||||||
|
prefix: packed struct(u16) {
|
||||||
|
prefix: enum(u8) {
|
||||||
|
user,
|
||||||
|
trusted,
|
||||||
|
security,
|
||||||
|
},
|
||||||
|
out_of_line: bool,
|
||||||
|
_: u7,
|
||||||
|
},
|
||||||
|
name_size: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn xattrLookup(alloc: std.mem.Allocator, io: Io, cache: *DecompCache, xattr_start: u64, idx: u32) ![]XattrKV {
|
||||||
|
const table_start = std.mem.readInt(u64, cache.map.memory[xattr_start..][0..8], .little);
|
||||||
|
|
||||||
|
const val: LookupValue = try lookup(
|
||||||
|
LookupValue,
|
||||||
|
io,
|
||||||
|
cache,
|
||||||
|
xattr_start + 16,
|
||||||
|
idx,
|
||||||
|
);
|
||||||
|
|
||||||
|
const out = try alloc.alloc(XattrKV, val.count);
|
||||||
|
errdefer alloc.free(out);
|
||||||
|
|
||||||
|
var meta: MetadataReader = .init(io, cache, table_start + val.ref.block_start);
|
||||||
|
defer meta.deinit();
|
||||||
|
try meta.interface.discardAll(val.ref.block_offset);
|
||||||
|
|
||||||
|
for (out) |*kv| {
|
||||||
|
var key_entry: KeyEntry = undefined;
|
||||||
|
try meta.interface.readSliceEndian(KeyEntry, @ptrCast(&key_entry), .little);
|
||||||
|
|
||||||
|
const prefix_len: u16 = switch (key_entry.prefix.prefix) {
|
||||||
|
.user => 5,
|
||||||
|
.trusted => 8,
|
||||||
|
.security => 9,
|
||||||
|
};
|
||||||
|
var key_len = key_entry.name_size;
|
||||||
|
key_len += prefix_len;
|
||||||
|
|
||||||
|
kv.key = try alloc.allocSentinel(u8, key_len, 0);
|
||||||
|
errdefer alloc.free(kv.key);
|
||||||
|
|
||||||
|
try meta.interface.readSliceEndian(u8, kv.key[prefix_len..], .little);
|
||||||
|
switch (key_entry.prefix.prefix) {
|
||||||
|
.user => @memcpy(kv.key[0..prefix_len], "user."),
|
||||||
|
.trusted => @memcpy(kv.key[0..prefix_len], "trusted."),
|
||||||
|
.security => @memcpy(kv.key[0..prefix_len], "security."),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key_entry.prefix.out_of_line) {
|
||||||
|
try meta.interface.discardAll(8);
|
||||||
|
|
||||||
|
var ool_ref: Inode.Ref = undefined;
|
||||||
|
try meta.interface.readSliceEndian(Inode.Ref, @ptrCast(&ool_ref), .little);
|
||||||
|
|
||||||
|
var ool_meta: MetadataReader = .init(io, cache, table_start + ool_ref.block_start);
|
||||||
|
defer ool_meta.deinit();
|
||||||
|
try ool_meta.interface.discardAll(ool_ref.block_offset);
|
||||||
|
|
||||||
|
kv.value = try readValue(alloc, &ool_meta.interface);
|
||||||
|
errdefer alloc.free(kv.value);
|
||||||
|
} else {
|
||||||
|
kv.value = try readValue(alloc, &meta.interface);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readValue(alloc: std.mem.Allocator, rdr: *Io.Reader) ![]u8 {
|
||||||
|
var val_size: u32 = undefined;
|
||||||
|
try rdr.readSliceEndian(u32, @ptrCast(&val_size), .little);
|
||||||
|
|
||||||
|
const val = try alloc.alloc(u8, val_size);
|
||||||
|
errdefer alloc.free(val);
|
||||||
|
|
||||||
|
try rdr.readSliceEndian(u8, val, .little);
|
||||||
|
|
||||||
|
return val;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
//! Options for file/directory extraction.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Writer = std.Io.Writer;
|
||||||
|
|
||||||
|
const ExtractionOptions = @This();
|
||||||
|
|
||||||
|
/// Force single-threaded extraction. Io.Threaded.global_single_threaded also works.
|
||||||
|
single_threaded: bool = false,
|
||||||
|
/// Don't set the file's owner, permissions, & modify time after extraction.
|
||||||
|
ignore_permissions: bool = false,
|
||||||
|
/// Don't set xattr values.
|
||||||
|
ignore_xattr: bool = false,
|
||||||
|
/// Replace symlinks with their target. Currently doesn't do anything.
|
||||||
|
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 defaultSingleThreaded: ExtractionOptions = .{ .single_threaded = true };
|
||||||
|
pub const default: ExtractionOptions = .{};
|
||||||
|
|
||||||
|
pub fn VerboseDefault(wrt: *Writer) !ExtractionOptions {
|
||||||
|
return .{
|
||||||
|
.verbose = true,
|
||||||
|
.verbose_writer = wrt,
|
||||||
|
.threads = try std.Thread.getCpuCount(),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pub const Archive = @import("archive.zig");
|
||||||
|
pub const ExtractionOptions = @import("options.zig");
|
||||||
|
const Test = @import("test.zig");
|
||||||
|
|
||||||
|
test {
|
||||||
|
@import("std").testing.refAllDecls(Test);
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const math = std.math;
|
||||||
|
|
||||||
|
const InodeRef = @import("inode.zig").Ref;
|
||||||
|
const CompressionType = @import("util/decompress.zig").CompressionType;
|
||||||
|
|
||||||
|
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: CompressionType,
|
||||||
|
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: InodeRef,
|
||||||
|
size: u64,
|
||||||
|
id_start: u64,
|
||||||
|
xattr_start: u64,
|
||||||
|
inode_start: u64,
|
||||||
|
dir_start: u64,
|
||||||
|
frag_start: u64,
|
||||||
|
export_start: u64,
|
||||||
|
|
||||||
|
/// Validate the Superblock. If an error is returned, it's likely the archive is corrupted or not a squashfs archive.
|
||||||
|
pub fn validate(self: Superblock) !void {
|
||||||
|
if (self.magic != SQUASHFS_MAGIC)
|
||||||
|
return SuperblockError.InvalidMagic;
|
||||||
|
if (self.flags.check)
|
||||||
|
return SuperblockError.InvalidCheck;
|
||||||
|
if (self.ver_maj != 4 or self.ver_min != 0)
|
||||||
|
return SuperblockError.InvalidVersion;
|
||||||
|
if (math.log2(self.block_size) != self.block_log)
|
||||||
|
return SuperblockError.InvalidBlockLog;
|
||||||
|
}
|
||||||
|
};
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Io = std.Io;
|
||||||
|
const stuff = @import("builtin");
|
||||||
|
|
||||||
|
const Archive = @import("archive.zig");
|
||||||
|
const Superblock = @import("super.zig").Superblock;
|
||||||
|
|
||||||
|
const TestArchive = "testing/LinuxPATest.sfs";
|
||||||
|
|
||||||
|
test "Basics" {
|
||||||
|
const io = std.testing.io;
|
||||||
|
const alloc = std.testing.allocator;
|
||||||
|
|
||||||
|
var fil = try Io.Dir.cwd().openFile(io, TestArchive, .{});
|
||||||
|
defer fil.close(io);
|
||||||
|
var sfs: Archive = try .init(alloc, io, fil);
|
||||||
|
defer sfs.deinit(io);
|
||||||
|
try std.testing.expectEqualDeep(sfs.super, LinuxPATestCorrectSuperblock);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TestFile = "Start.exe";
|
||||||
|
const TestFileExtractLocation = "testing/Start.exe";
|
||||||
|
|
||||||
|
test "ExtractSingleFile" {
|
||||||
|
const io = std.testing.io;
|
||||||
|
const alloc = std.testing.allocator;
|
||||||
|
|
||||||
|
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(alloc, io, fil);
|
||||||
|
defer sfs.deinit(io);
|
||||||
|
var test_fil = try sfs.open(alloc, io, TestFile);
|
||||||
|
defer test_fil.deinit();
|
||||||
|
try test_fil.extract(alloc, io, TestFileExtractLocation, .default);
|
||||||
|
//TODO: validate extracted file.
|
||||||
|
}
|
||||||
|
|
||||||
|
const TestDir = "Documents";
|
||||||
|
const TestDirExtractLocation = "testing/Documents";
|
||||||
|
|
||||||
|
test "ExtractSmallDir" {
|
||||||
|
const io = std.testing.io;
|
||||||
|
const alloc = std.testing.allocator;
|
||||||
|
|
||||||
|
Io.Dir.cwd().deleteTree(io, TestDirExtractLocation) catch {};
|
||||||
|
var fil = try Io.Dir.cwd().openFile(io, TestArchive, .{});
|
||||||
|
defer fil.close(io);
|
||||||
|
var sfs: Archive = try .init(alloc, io, fil);
|
||||||
|
defer sfs.deinit(io);
|
||||||
|
var test_fil = try sfs.open(alloc, io, TestDir);
|
||||||
|
defer test_fil.deinit();
|
||||||
|
try test_fil.extract(alloc, io, TestDirExtractLocation, .default);
|
||||||
|
//TODO: validate extracted file.
|
||||||
|
}
|
||||||
|
|
||||||
|
const TestFullExtractLocation = "testing/TestExtract";
|
||||||
|
|
||||||
|
test "ExtractCompleteArchive" {
|
||||||
|
const io = std.testing.io;
|
||||||
|
const alloc = std.testing.allocator;
|
||||||
|
|
||||||
|
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(alloc, io, fil);
|
||||||
|
defer sfs.deinit(io);
|
||||||
|
try sfs.extract(alloc, io, TestFullExtractLocation, .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,61 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const c = @import("c");
|
||||||
|
|
||||||
|
const Error = @import("decompress.zig").Error;
|
||||||
|
|
||||||
|
pub fn zlibDecompress(_: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
|
||||||
|
var strem: c.zng_stream = .{
|
||||||
|
.next_in = in.ptr,
|
||||||
|
.avail_in = @truncate(in.len),
|
||||||
|
.next_out = out.ptr,
|
||||||
|
.avail_out = @truncate(out.len),
|
||||||
|
};
|
||||||
|
var res = c.zng_inflateInit(&strem);
|
||||||
|
if (res != c.Z_OK) return Error.ReadFailed;
|
||||||
|
defer _ = c.zng_inflateEnd(&strem);
|
||||||
|
|
||||||
|
res = c.zng_inflate(&strem, c.Z_FULL_FLUSH);
|
||||||
|
if (res != c.Z_OK) return Error.ReadFailed;
|
||||||
|
|
||||||
|
return strem.total_out;
|
||||||
|
}
|
||||||
|
pub fn lzmaDecompress(_: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
|
||||||
|
var strem: c.lzma_stream = .{
|
||||||
|
.next_in = in.ptr,
|
||||||
|
.avail_in = in.len,
|
||||||
|
.next_out = out.ptr,
|
||||||
|
.avail_out = out.len,
|
||||||
|
};
|
||||||
|
var res = c.lzma_auto_decoder(&strem, out.len * 2, 0);
|
||||||
|
if (res != c.LZMA_OK) return Error.ReadFailed;
|
||||||
|
defer c.lzma_end(&strem);
|
||||||
|
|
||||||
|
while (res == c.LZMA_OK)
|
||||||
|
res = c.lzma_code(&strem, c.LZMA_RUN);
|
||||||
|
if (res != c.LZMA_FINISH) return Error.ReadFailed;
|
||||||
|
|
||||||
|
return strem.total_out;
|
||||||
|
}
|
||||||
|
pub fn lzoDecompress(_: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
|
||||||
|
var out_len = out.len;
|
||||||
|
const res = c.lzo1x_decompress(in.ptr, in.len, out.ptr, &out_len, null);
|
||||||
|
if (res != c.LZO_E_OK) return Error.ReadFailed;
|
||||||
|
return out_len;
|
||||||
|
}
|
||||||
|
pub fn lz4Decompress(_: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
|
||||||
|
const res = c.LZ4_decompress_safe(
|
||||||
|
in.ptr,
|
||||||
|
out.ptr,
|
||||||
|
@bitCast(@as(u32, @truncate(in.len))),
|
||||||
|
@bitCast(@as(u32, @truncate(out.len))),
|
||||||
|
);
|
||||||
|
if (res < 0) return Error.ReadFailed;
|
||||||
|
return @abs(res);
|
||||||
|
}
|
||||||
|
pub fn zstdDecompress(_: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
|
||||||
|
const res = c.ZSTD_decompress(out.ptr, out.len, in.ptr, in.len);
|
||||||
|
if (c.ZSTD_isError(res) != 0)
|
||||||
|
return Error.ReadFailed;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Io = std.Io;
|
||||||
|
|
||||||
|
const BlockSize = @import("../inode_data/file.zig").BlockSize;
|
||||||
|
const Decompress = @import("decompress.zig");
|
||||||
|
|
||||||
|
const DataExtract = @This();
|
||||||
|
|
||||||
|
decomp: Decompress.Fn,
|
||||||
|
map: Io.File.MemoryMap,
|
||||||
|
|
||||||
|
block_size: u32,
|
||||||
|
block_start: u64,
|
||||||
|
size: u64,
|
||||||
|
blocks: []BlockSize,
|
||||||
|
|
||||||
|
frag_data: ?[]u8 = null,
|
||||||
|
frag_offset: u32 = undefined,
|
||||||
|
|
||||||
|
pub fn init(decomp: Decompress.Fn, map: Io.File.MemoryMap, block_size: u32, block_start: u64, size: u64, blocks: []BlockSize) DataExtract {
|
||||||
|
return .{
|
||||||
|
.decomp = decomp,
|
||||||
|
.map = map,
|
||||||
|
|
||||||
|
.block_size = block_size,
|
||||||
|
.block_start = block_start,
|
||||||
|
.size = size,
|
||||||
|
.blocks = blocks,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
pub fn addFrag(self: *DataExtract, frag_block: []u8, frag_offset: u32) void {
|
||||||
|
self.frag_data = frag_block;
|
||||||
|
self.frag_offset = frag_offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Error = error{} || Io.File.MemoryMap.CreateError || Io.File.WritePositionalError || Decompress.Error || Io.File.MemoryMap.SetLengthError;
|
||||||
|
|
||||||
|
pub fn asyncExtract(self: DataExtract, alloc: std.mem.Allocator, io: Io, fil: Io.File) Error!void {
|
||||||
|
if (self.size == 0) return;
|
||||||
|
|
||||||
|
try fil.writePositionalAll(io, &.{0}, self.size - 1);
|
||||||
|
var map = try fil.createMemoryMap(io, .{ .len = self.size, .protection = .{ .write = true }, .undefined_contents = true });
|
||||||
|
defer map.destroy(io);
|
||||||
|
|
||||||
|
var group: Io.Group = .init;
|
||||||
|
defer group.cancel(io);
|
||||||
|
|
||||||
|
var ret_err: ?Error = null;
|
||||||
|
|
||||||
|
var offset: u64 = self.block_start;
|
||||||
|
for (0..self.blocks.len) |i| {
|
||||||
|
group.async(io, blockThread, .{ self, alloc, map, offset, i, &ret_err });
|
||||||
|
offset += self.blocks[i].size;
|
||||||
|
}
|
||||||
|
if (self.frag_data != null)
|
||||||
|
group.async(io, fragThread, .{ self, map });
|
||||||
|
|
||||||
|
try group.await(io);
|
||||||
|
if (ret_err != null) return ret_err.?;
|
||||||
|
return map.write(io);
|
||||||
|
}
|
||||||
|
fn blockThread(self: DataExtract, alloc: std.mem.Allocator, map: Io.File.MemoryMap, read_offset: u64, idx: usize, ret_err: *?Error) error{Canceled}!void {
|
||||||
|
const block = self.blocks[idx];
|
||||||
|
const write_offset = idx * self.block_size;
|
||||||
|
|
||||||
|
const size = if (self.frag_data == null and idx == self.blocks.len - 1)
|
||||||
|
self.size % self.block_size
|
||||||
|
else
|
||||||
|
self.block_size;
|
||||||
|
|
||||||
|
if (block.size == 0) {
|
||||||
|
@memset(map.memory[write_offset..][0..size], 0);
|
||||||
|
return;
|
||||||
|
} else if (block.uncompressed) {
|
||||||
|
@memcpy(map.memory[write_offset..][0..size], self.map.memory[read_offset..][0..block.size]);
|
||||||
|
}
|
||||||
|
_ = self.decomp(alloc, self.map.memory[read_offset..][0..block.size], map.memory[write_offset..][0..size]) catch |err| {
|
||||||
|
ret_err.* = err;
|
||||||
|
return error.Canceled;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
fn fragThread(self: DataExtract, map: Io.File.MemoryMap) error{Canceled}!void {
|
||||||
|
const size = self.size % self.block_size;
|
||||||
|
@memcpy(map.memory[self.blocks.len * self.block_size ..][0..size], self.frag_data.?[self.frag_offset..][0..size]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Io = std.Io;
|
||||||
|
const ArrayHashMap = std.array_hash_map.Auto;
|
||||||
|
const Atomic = std.atomic.Value;
|
||||||
|
|
||||||
|
const Decompress = @import("decompress.zig");
|
||||||
|
const Fn = Decompress.Fn;
|
||||||
|
const DecompressType = Decompress.CompressionType;
|
||||||
|
|
||||||
|
const DecompCache = @This();
|
||||||
|
|
||||||
|
const Cache = struct {
|
||||||
|
cache: []u8,
|
||||||
|
usage: Atomic(u32),
|
||||||
|
};
|
||||||
|
|
||||||
|
arena: std.heap.ArenaAllocator,
|
||||||
|
decomp: Fn,
|
||||||
|
|
||||||
|
map: Io.File.MemoryMap,
|
||||||
|
|
||||||
|
cache: ArrayHashMap(u64, Cache),
|
||||||
|
mut: Io.RwLock = .init,
|
||||||
|
cond: Io.Condition = .init,
|
||||||
|
|
||||||
|
max_size: u64,
|
||||||
|
cur_size: u64 = 0,
|
||||||
|
|
||||||
|
pub fn init(alloc: std.mem.Allocator, map: Io.File.MemoryMap, decomp_type: DecompressType, max_size: u64) !DecompCache {
|
||||||
|
return .{
|
||||||
|
.arena = .init(alloc),
|
||||||
|
.decomp = try Decompress.getDecompressFn(decomp_type),
|
||||||
|
|
||||||
|
.map = map,
|
||||||
|
|
||||||
|
.cache = .empty,
|
||||||
|
|
||||||
|
.max_size = max_size,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
pub fn deinit(self: *DecompCache, io: Io) void {
|
||||||
|
self.mut.lockUncancelable(io);
|
||||||
|
self.cache.deinit(self.arena.child_allocator);
|
||||||
|
self.arena.deinit();
|
||||||
|
self.map.destroy(io);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn makeRoom(self: *DecompCache, io: Io, size: u32) !void {
|
||||||
|
if (size + self.cur_size < self.max_size) return;
|
||||||
|
var iter = self.cache.iterator();
|
||||||
|
while (iter.next()) |ent| {
|
||||||
|
const val = ent.value_ptr;
|
||||||
|
if (val.usage.load(.unordered) == 0) {
|
||||||
|
self.cur_size -= val.cache.len;
|
||||||
|
_ = self.cache.orderedRemove(ent.key_ptr.*);
|
||||||
|
}
|
||||||
|
if (size + self.cur_size < self.max_size) return;
|
||||||
|
}
|
||||||
|
try self.cond.wait(io, &self.mut.mutex);
|
||||||
|
return self.makeRoom(io, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn checkinBlock(self: *DecompCache, io: Io, offset: u64) !void {
|
||||||
|
self.mut.lockSharedUncancelable(io);
|
||||||
|
defer self.mut.unlockShared(io);
|
||||||
|
|
||||||
|
const get = self.cache.getPtr(offset);
|
||||||
|
if (get == null) return error.NotACachedBlock;
|
||||||
|
const res = get.?.usage.fetchSub(1, .acq_rel);
|
||||||
|
if (res == 0) self.cond.broadcast(io);
|
||||||
|
}
|
||||||
|
pub fn checkoutBlock(self: *DecompCache, io: Io, offset: u64, data_size: u32, max_result_size: u32) ![]u8 {
|
||||||
|
{
|
||||||
|
try self.mut.lockShared(io);
|
||||||
|
defer self.mut.unlockShared(io);
|
||||||
|
|
||||||
|
const get = self.cache.getPtr(offset);
|
||||||
|
if (get != null) {
|
||||||
|
_ = get.?.usage.fetchAdd(1, .acq_rel);
|
||||||
|
return get.?.cache;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try self.mut.lock(io);
|
||||||
|
defer self.mut.unlock(io);
|
||||||
|
|
||||||
|
try self.makeRoom(io, max_result_size);
|
||||||
|
|
||||||
|
var alloc = self.arena.allocator();
|
||||||
|
const buf_alloc = self.arena.child_allocator;
|
||||||
|
|
||||||
|
var out = try alloc.alloc(u8, max_result_size);
|
||||||
|
errdefer alloc.free(out);
|
||||||
|
|
||||||
|
const out_size = try self.decomp(buf_alloc, self.map.memory[offset..][0..data_size], out);
|
||||||
|
if (out_size != max_result_size) {
|
||||||
|
if (alloc.resize(out, out_size)) {
|
||||||
|
out.len = out_size;
|
||||||
|
} else {
|
||||||
|
const new_out = try alloc.alloc(u8, out_size);
|
||||||
|
@memcpy(new_out, out[0..out_size]);
|
||||||
|
alloc.free(out);
|
||||||
|
out = new_out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.cache.put(buf_alloc, offset, .{
|
||||||
|
.cache = out,
|
||||||
|
.usage = .init(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Io = std.Io;
|
||||||
|
|
||||||
|
const config = @import("config");
|
||||||
|
|
||||||
|
const c_decomp = @import("c_decomp.zig");
|
||||||
|
const zig_decomp = @import("zig_decomp.zig");
|
||||||
|
|
||||||
|
pub const Error = Io.Reader.Error || std.mem.Allocator.Error;
|
||||||
|
|
||||||
|
pub const Fn = *const fn (alloc: std.mem.Allocator, in: []u8, out: []u8) Error!usize;
|
||||||
|
|
||||||
|
pub const CompressionType = enum(u16) {
|
||||||
|
gzip = 1,
|
||||||
|
lzma,
|
||||||
|
lzo,
|
||||||
|
xz,
|
||||||
|
lz4,
|
||||||
|
zstd,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn getDecompressFn(t: CompressionType) !Fn {
|
||||||
|
return if (config.use_zig_decomp) switch (t) {
|
||||||
|
.lzo => error.LzoUnsupported,
|
||||||
|
.lz4 => error.Lz4Unsupported,
|
||||||
|
.gzip => zig_decomp.zlibDecompress,
|
||||||
|
.lzma => zig_decomp.lzmaDecompress,
|
||||||
|
.xz => zig_decomp.xzDecompress,
|
||||||
|
.zstd => zig_decomp.zstdDecompress,
|
||||||
|
} else switch (t) {
|
||||||
|
.gzip => c_decomp.zlibDecompress,
|
||||||
|
.lzma => c_decomp.lzmaDecompress,
|
||||||
|
.lzo => if (config.allow_lzo) c_decomp.lzoDecompress else error.LzoUnsupported,
|
||||||
|
.xz => c_decomp.lzmaDecompress,
|
||||||
|
.lz4 => c_decomp.lz4Decompress,
|
||||||
|
.zstd => c_decomp.zstdDecompress,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
//! A cache for decompressed blocks. Used for Metadata & fragments.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Io = std.Io;
|
||||||
|
const Reader = Io.Reader;
|
||||||
|
const Writer = Io.Writer;
|
||||||
|
const Limit = Io.Limit;
|
||||||
|
|
||||||
|
const DecompCache = @import("decomp_cache.zig");
|
||||||
|
|
||||||
|
const MetadataReader = @This();
|
||||||
|
|
||||||
|
const BlockHeader = packed struct(u16) {
|
||||||
|
size: u15,
|
||||||
|
uncompressed: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
io: Io,
|
||||||
|
|
||||||
|
cur_offset: u64 = 0,
|
||||||
|
next_offset: u64,
|
||||||
|
|
||||||
|
cache: *DecompCache,
|
||||||
|
|
||||||
|
buf_uncompress: bool = false,
|
||||||
|
|
||||||
|
interface: Reader = .{
|
||||||
|
.buffer = &[0]u8{},
|
||||||
|
.end = 0,
|
||||||
|
.seek = 0,
|
||||||
|
.vtable = &.{
|
||||||
|
.stream = stream,
|
||||||
|
.discard = discard,
|
||||||
|
.readVec = readVec,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
pub fn init(io: Io, cache: *DecompCache, offset: u64) MetadataReader {
|
||||||
|
return .{
|
||||||
|
.io = io,
|
||||||
|
|
||||||
|
.next_offset = offset,
|
||||||
|
|
||||||
|
.cache = cache,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
pub fn deinit(self: *MetadataReader) void {
|
||||||
|
if (self.cur_offset != 0 and !self.buf_uncompress)
|
||||||
|
self.cache.checkinBlock(self.io, self.cur_offset) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance(self: *MetadataReader) !void {
|
||||||
|
if (self.interface.buffer.len > 0 and !self.buf_uncompress)
|
||||||
|
self.cache.checkinBlock(self.io, self.cur_offset) catch |err| {
|
||||||
|
std.debug.print("UH OH! {}\n", .{err});
|
||||||
|
return error.ReadFailed;
|
||||||
|
};
|
||||||
|
const hdr: BlockHeader = @bitCast(std.mem.readInt(u16, self.cache.map.memory[self.next_offset..][0..2], .little));
|
||||||
|
self.cur_offset = self.next_offset + 2;
|
||||||
|
self.next_offset = self.cur_offset + hdr.size;
|
||||||
|
|
||||||
|
self.buf_uncompress = hdr.uncompressed;
|
||||||
|
if (hdr.uncompressed) {
|
||||||
|
self.interface.buffer = self.cache.map.memory[self.cur_offset..][0..hdr.size];
|
||||||
|
self.interface.end = hdr.size;
|
||||||
|
self.interface.seek = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.interface.buffer = try self.cache.checkoutBlock(self.io, self.cur_offset, hdr.size, 8192);
|
||||||
|
self.interface.end = self.interface.buffer.len;
|
||||||
|
self.interface.seek = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stream(r: *Reader, w: *Writer, limit: Limit) Reader.StreamError!usize {
|
||||||
|
if (r.seek == r.end) {
|
||||||
|
var self: *MetadataReader = @fieldParentPtr("interface", r);
|
||||||
|
self.advance() catch return Reader.Error.ReadFailed;
|
||||||
|
}
|
||||||
|
if (limit == .nothing) return 0;
|
||||||
|
const to_write = @min(r.end - r.seek, @intFromEnum(limit));
|
||||||
|
const wrote = try w.write(r.buffer[r.seek..][0..to_write]);
|
||||||
|
r.seek += wrote;
|
||||||
|
return wrote;
|
||||||
|
}
|
||||||
|
fn discard(r: *Reader, limit: Limit) Reader.Error!usize {
|
||||||
|
if (r.seek == r.end) {
|
||||||
|
var self: *MetadataReader = @fieldParentPtr("interface", r);
|
||||||
|
self.advance() catch return Reader.Error.ReadFailed;
|
||||||
|
}
|
||||||
|
if (limit == .nothing) return 0;
|
||||||
|
const to_skip = @min(r.end - r.seek, @intFromEnum(limit));
|
||||||
|
r.seek += to_skip;
|
||||||
|
return to_skip;
|
||||||
|
}
|
||||||
|
fn readVec(r: *Reader, vec: [][]u8) Reader.Error!usize {
|
||||||
|
if (r.seek == r.end) {
|
||||||
|
var self: *MetadataReader = @fieldParentPtr("interface", r);
|
||||||
|
self.advance() catch return Reader.Error.ReadFailed;
|
||||||
|
}
|
||||||
|
if (vec.len == 0) return 0;
|
||||||
|
var total_copied: usize = 0;
|
||||||
|
for (vec) |v| {
|
||||||
|
const to_cpy = @min(r.end - r.seek, v.len);
|
||||||
|
@memcpy(v[0..to_cpy], r.buffer[r.seek..][0..to_cpy]);
|
||||||
|
r.seek += to_cpy;
|
||||||
|
total_copied += to_cpy;
|
||||||
|
if (r.seek == r.end) break;
|
||||||
|
}
|
||||||
|
return total_copied;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Reader = std.Io.Reader;
|
||||||
|
const flate = std.compress.flate;
|
||||||
|
const zstd = std.compress.zstd;
|
||||||
|
const xz = std.compress.xz;
|
||||||
|
const lzma = std.compress.lzma;
|
||||||
|
|
||||||
|
const Error = @import("decompress.zig").Error;
|
||||||
|
|
||||||
|
pub fn zlibDecompress(_: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
|
||||||
|
var buf: [flate.max_window_len]u8 = undefined;
|
||||||
|
|
||||||
|
var rdr: Reader = .fixed(in);
|
||||||
|
var decomp: flate.Decompress = .init(&rdr, .zlib, &buf);
|
||||||
|
|
||||||
|
return decomp.reader.readSliceShort(out);
|
||||||
|
}
|
||||||
|
pub fn zstdDecompress(alloc: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
|
||||||
|
const buf = try alloc.alloc(u8, in.len + zstd.block_size_max);
|
||||||
|
defer alloc.free(buf);
|
||||||
|
|
||||||
|
var rdr: Reader = .fixed(in);
|
||||||
|
var decomp: zstd.Decompress = .init(&rdr, buf, .{ .window_len = in.len });
|
||||||
|
|
||||||
|
return decomp.reader.readSliceShort(out);
|
||||||
|
}
|
||||||
|
pub fn lzmaDecompress(alloc: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
|
||||||
|
var rdr: Reader = .fixed(in);
|
||||||
|
var decomp: lzma.Decompress = .initOptions(&rdr, alloc, &[0]u8{}, .{}, 2 * out.len);
|
||||||
|
defer decomp.deinit();
|
||||||
|
|
||||||
|
return decomp.reader.readSliceShort(out);
|
||||||
|
}
|
||||||
|
pub fn xzDecompress(alloc: std.mem.Allocator, in: []u8, out: []u8) Error!usize {
|
||||||
|
var rdr: Reader = .fixed(in);
|
||||||
|
var decomp: xz.Decompress = .init(&rdr, alloc, &[0]u8{});
|
||||||
|
defer decomp.deinit();
|
||||||
|
|
||||||
|
return decomp.reader.readSliceShort(out);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user