81 Commits

Author SHA1 Message Date
Caleb Gardner c2a3eb420f Fixed some bugs. Caused some bugs 2026-05-28 03:56:43 -05:00
Caleb Gardner eb1b940854 (Mostly) Finished extraction (currently infinitely hanging)
Added DataExtract
Added LookupTable
2026-05-27 19:17:17 -05:00
Caleb Gardner ba2f069a4a More work on re-implementing everything
Finished MetadataReader
Finished DirEntry
Started work on File
Added most important things to Inode
Fixed test.zig
Fixed some build issues causing SEGV
2026-05-27 11:59:47 -05:00
Caleb Gardner 7c4089f826 Re-set & re-write of several things
Implemented new decompression cache to prevent decompressing the same block multiple times
2026-05-27 06:03:54 -05:00
Caleb J. Gardner 4b2b7021c7 Moved & organized decompression
Fully implemented Decompressor vtable
2026-04-02 06:27:34 -05:00
Caleb J. Gardner a1b9828578 Finished (?) decompression restructuring 2026-03-26 06:40:17 -05:00
Caleb J. Gardner 8e4661c4c6 Moving decompression to a vtable interface 2026-03-22 06:35:25 -05:00
Caleb J. Gardner 54aaf30ea5 Working on re-doing decompression 2026-03-21 02:13:36 -05:00
Caleb J. Gardner df22cf6529 Started work on stateful decompression 2026-03-20 01:55:00 -05:00
Caleb J. Gardner 6b5c830234 Actually fix build 2026-03-18 06:17:07 -05:00
Caleb J. Gardner 116234cf9c Fixed build 2026-03-18 06:15:52 -05:00
Caleb J. Gardner 4601e8f323 Updated build 2026-03-18 06:13:19 -05:00
Caleb J. Gardner 50cae8b63d Use zig packed versions of zlib-ng, lz4, and zstd.
Changed use_c_libs to use_zig_decomp so c libraries are now default
2026-03-18 05:24:58 -05:00
Caleb J. Gardner 8b8c9a772f Added -Ddebug build option
Re-added errors from settings xattr values
2026-03-09 00:16:19 -05:00
Caleb Gardner f563648b20 Merge pull request #4 from CalebQ42/inode_finish
Re-do much of extraction
2026-03-08 22:36:37 -05:00
Caleb Gardner 8d4f3d72f8 Merge branch 'main' into inode_finish 2026-03-08 22:34:50 -05:00
Caleb J. Gardner 0b6129f1ae Switch to zlib-ng 2026-03-08 22:15:28 -05:00
Caleb J. Gardner 7308faad36 Moved kv free's in setMetadata 2026-03-05 12:54:25 -06:00
Caleb J. Gardner c9499251f8 Moved lookup tables into separate struct to fix some race conditions
Fixed lingering issues due to zero work size InodeFinish
Fixed xattrs not applying due to the keys sometimes not being
null-terminated.
Updated performance numbers
2026-03-05 12:20:30 -06:00
Caleb J. Gardner d470ca98e3 Added --force to unsquashfs
Fixing race condition bugs (yay)
2026-03-05 07:04:24 -06:00
Caleb J. Gardner a606f5e11a Move extract logic to util/extract.zig to make it easier to read. 2026-03-05 03:03:37 -06:00
Caleb J. Gardner a4e23a840d Updated performance values in README.
Added ability to ignore xattrs & permissions.
Ignore setting xattr errors due to an unknown issues.
2026-03-05 03:03:34 -06:00
Caleb J. Gardner 3a10572953 Updated performance values in README.
Added ability to ignore xattrs & permissions.
Ignore setting xattr errors due to an unknown issues.
2026-03-04 13:28:29 -06:00
Caleb J. Gardner edfe919c1b Expirementation with a new way to finish threads. Currently not working. 2026-03-04 06:39:44 -06:00
Caleb J. Gardner 4515610082 Added xattr support (currently untested)
Use stack fallback allocator on extraction for better performance.
2026-03-04 03:24:30 -06:00
Caleb J. Gardner beca6a2ae6 Small tweaks 2026-03-01 02:27:54 -06:00
Caleb J. Gardner 760e11df0b Fixed github workflow 2026-02-28 22:38:47 -06:00
Caleb J. Gardner 5f629df47c alloc-ified many functions.
Updated README
2026-02-28 22:33:57 -06:00
Caleb J. Gardner b0160e005b Some fixes 2026-02-17 05:54:32 -06:00
Caleb J. Gardner b7b99325da Slight improvement to how permissions are applied to folders
unsquashfs Verbose flag
2026-02-13 01:35:20 -06:00
Caleb J. Gardner b22e4d003d Update times 2026-02-12 08:57:36 -06:00
Caleb J. Gardner a41c37fef4 Updated unsquashfs version print out 2026-02-12 06:54:20 -06:00
Caleb J. Gardner 2d079d77f7 Exclusive file creation 2026-02-12 05:19:25 -06:00
Caleb J. Gardner 48f4235875 Fixed threads == 0 causing single threaded extraction.
Set exclusive file creation
2026-02-12 05:08:16 -06:00
Caleb J. Gardner 567dea8a0b Re-fix Action name 2026-02-12 04:28:32 -06:00
Caleb J. Gardner f78e5c7386 Rever to previous Action 2026-02-12 04:27:06 -06:00
Caleb J. Gardner 81e975c0d9 Try doing upload in two separate steps 2026-02-12 04:22:20 -06:00
Caleb J. Gardner fd274a8072 Different try to upload release files 2026-02-12 04:18:19 -06:00
Caleb J. Gardner 50ae79637e Removed broken ZSTD error value 2026-02-12 04:06:18 -06:00
Caleb J. Gardner ee41dc7278 Added import of zstd_errors.h 2026-02-12 03:58:08 -06:00
Caleb J. Gardner c34acebf51 Only link lzo if allow_lzo 2026-02-12 03:46:36 -06:00
Caleb J. Gardner bdbda29d39 Fixed wrong zig action line 2026-02-12 03:42:48 -06:00
Caleb J. Gardner ca4e867ddc Use setup zig Action 2026-02-12 03:39:05 -06:00
Caleb J. Gardner b066835066 Update apt 2026-02-12 02:51:01 -06:00
Caleb J. Gardner 2363bd7d10 sudo apt Actions 2026-02-12 02:43:55 -06:00
Caleb J. Gardner e619005b77 Fix action 2026-02-12 02:40:37 -06:00
Caleb J. Gardner 28b44891b3 Forgot Zig itself to the build 2026-02-12 02:32:34 -06:00
Caleb J. Gardner 4829c802a3 Clean version string 2026-02-12 02:32:06 -06:00
Caleb Gardner 570db9632a GH Action to create a release when release tagged 2026-02-12 02:30:21 -06:00
Caleb J. Gardner 0076294675 Small fix to testing to check if libc is linked 2026-02-12 02:19:38 -06:00
Caleb J. Gardner fd9e3d595b A bit of README cleanup
Added allow_lzo build option due to lzo build issues
2026-02-11 06:57:52 -06:00
Caleb J. Gardner b8189490eb Small change/fix when applying permissions 2026-02-09 13:50:56 -06:00
Caleb J. Gardner 6adc1d5c0c Fixes for threaded extraction.
Archive.extract now uses threaded extraction
2026-02-09 13:45:38 -06:00
Caleb J. Gardner 5ec12b5786 Finished adding multi-threaded extraction.
Added option in unsquashfs to specify the number of threads used.
Changed some functions to accept an allocator instead of just using
Archive's
Fixed run_tests.sh due to new c libraries
2026-02-08 15:14:35 -06:00
Caleb J. Gardner b892adacd7 Some work for threaded extraction 2026-02-08 10:27:35 -06:00
Caleb J. Gardner 2760ad6ccb Finished adding (untested) C decompression libraries. 2026-02-08 06:52:14 -06:00
Caleb J. Gardner 61311433b9 unsquashfs --help 2026-02-07 17:20:03 -06:00
Caleb J. Gardner 053d64a954 Some cleanup to build zon.
Minor start on threaded extraction
2026-02-07 17:20:03 -06:00
Caleb Gardner 0e0222cd02 Fix formatting for build flags in README 2026-02-07 11:34:34 -06:00
Caleb Gardner 9c0dfbadc2 Merge pull request #2 from CalebQ42/extract_expirement
Extract expirement
2026-02-07 11:31:51 -06:00
Caleb J. Gardner db2fb4b9f2 Fixed compilation when using zig test.
Remove option for static since -static should suffice in most situations
2026-02-07 11:24:58 -06:00
Caleb J. Gardner 067eaa87c2 You can now set when building to use c or zig libraries. 2026-02-07 10:58:32 -06:00
Caleb J. Gardner b64a3ec44a Archive.extract now directly uses Inode instead of File. 2026-02-07 06:57:55 -06:00
Caleb J. Gardner 704215e1a9 Remove updateTimes because I was running into integer overlow issues. 2026-02-07 06:42:34 -06:00
Caleb J. Gardner bcfd983f8d Fixed a handful of errors when extracting.
Fixed issues with unsquashfs
2026-02-07 06:28:27 -06:00
Caleb J. Gardner 75502da1d0 Remove DecompMgr in favor of a much simpler fn ptr.
Moved more functionality to Inode instead of File.
Started doing some optimization around allocation.
Slight rework of ExtractionOptions.
2026-02-07 05:09:17 -06:00
Caleb J. Gardner a316ba569f Renamed File.dev to File.devNum 2026-02-06 23:22:21 -06:00
Caleb J. Gardner a0f3f45885 Unsquashfs 2026-02-06 09:51:27 -06:00
Caleb J. Gardner f771ef7623 Work on extraction 2026-02-06 06:56:28 -06:00
Caleb J. Gardner 0d2576f5ee Fixed tables getting re-created 2026-01-31 06:39:41 -06:00
Caleb J. Gardner a76803aad1 Comments! 2026-01-31 05:14:00 -06:00
Caleb J. Gardner 1ff1e91d5e Added ExtractCompleteArchive test 2026-01-29 22:23:46 -06:00
Caleb J. Gardner 2bcbc16613 Extraction Finished-ish 2026-01-29 06:46:52 -06:00
Caleb J. Gardner 3c98cf2cdb EXTRACTION 2026-01-28 11:19:38 -06:00
Caleb J. Gardner 2c392cf250 Fixed a few bugs preventing basic functions
Worked on extraction, including creating DataReader
Added proper access to id, fragment, and export tables
2026-01-28 06:55:01 -06:00
Caleb J. Gardner 5d4e7b1435 Fixed things 2026-01-25 07:06:30 -06:00
Caleb J. Gardner 7aed59b5b1 Some fixes 2026-01-17 05:30:19 -06:00
Caleb J. Gardner f3fb8a128f Inodes! ExtractionOptions! Files! Directories! 2026-01-16 06:53:10 -06:00
Caleb J. Gardner 23bb19644b Finished (?) decompression, metadata reader, and Table. 2026-01-15 11:22:02 -06:00
Caleb J. Gardner ed14f13d9a Some work on tests & metadata 2026-01-15 06:56:43 -06:00
Caleb J. Gardner 428f938c3a Restart (once again) 2026-01-15 06:40:59 -06:00
44 changed files with 2130 additions and 1613 deletions
+31
View File
@@ -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
View File
@@ -2,3 +2,4 @@ testing/
.zig-cache/
zig-out/
zig-pkg/
+25
View File
@@ -0,0 +1,25 @@
// Project-local debug tasks
//
// For more documentation on how to configure debug tasks,
// see: https://zed.dev/docs/debugger
[
{
"label": "Build & Run",
"adapter": "CodeLLDB",
"request": "launch",
"build": {
"command": "zig",
"args": ["build", "-Duse_c_libs=true", "-Ddebug=true"],
},
"program": "zig-out/bin/unsquashfs",
"args": [
"--force",
"-d",
"testing/TestExtractUnsquashfs",
"testing/LinuxPATest.sfs",
],
},
]
+18
View File
@@ -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 -1
View File
@@ -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
+71 -3
View File
@@ -1,7 +1,75 @@
# zig-squashfs
Messing around with zig via making a squashfs library. May amount to something. Or not.
This is my experiments to learn Zig. Might amount to something. Might not.
## Current state
A library and application to decompress or view squashfs archives.
Performance is reatively bad (when compared to the official [squashfs-tools](https://github.com/plougher/squashfs-tools), but the basics should fully work.
## 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;
```
+154 -34
View File
@@ -1,53 +1,173 @@
const std = @import("std");
/// version if version isn't provided during build
const def_version = "0.0.0+testing";
const Build = std.Build;
const Step = Build.Step;
pub fn build(b: *std.Build) !void {
const opt = b.addOptions();
const ver = b.option([]const u8, "version", "sematic version") orelse def_version;
const sem_ver = try std.SemanticVersion.parse(ver);
opt.addOption(std.SemanticVersion, "version", sem_ver);
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(.{});
const optimize = b.standardOptimizeOption(.{});
const lib_mod = b.createModule(.{
.root_source_file = b.path("src/root.zig"),
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,
});
const lib = b.addLibrary(.{
.linkage = .static,
.name = "zig_squashfs",
.root_module = lib_mod,
.version = sem_ver,
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 exe_mod = b.createModule(.{
.root_source_file = b.path("src/bin/unsquashfs.zig"),
.target = target,
.optimize = optimize,
});
exe_mod.addImport("squashfs", lib_mod);
exe_mod.addOptions("config", opt);
const deps = try getDependencies(b, target, optimize, allow_lzo, dynamic);
for (deps) |d|
lib.root_module.linkLibrary(d);
const exe = b.addExecutable(.{
.linkage = .static,
.name = "unsquashfs",
.root_module = exe_mod,
.version = sem_ver,
.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 lib_unit_tests = b.addTest(.{
.root_module = lib_mod,
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
});
const exe_unit_test = b.addTest(.{
.root_module = exe_mod,
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 run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
const run_exe_unit_tests = b.addRunArtifact(exe_unit_test);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_lib_unit_tests.step);
test_step.dependOn(&run_exe_unit_tests.step);
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);
}
+24 -49
View File
@@ -1,55 +1,30 @@
.{
.name = .zig_squashfs,
.version = "0.0.1",
.fingerprint = 0x527960c72c03ffe3, // Changing this has security and trust implications.
.minimum_zig_version = "0.14.0",
// This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`.
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.name = .squashfs,
.version = "0.0.6",
.fingerprint = 0x37ba29474b87f145, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.1",
.dependencies = .{
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
//.example = .{
// // When updating this field to a new URL, be sure to delete the corresponding
// // `hash`, otherwise you are communicating that you expect to find the old hash at
// // the new URL. If the contents of a URL change this will result in a hash mismatch
// // which will prevent zig from using it.
// .url = "https://example.com/foo.tar.gz",
//
// // This is computed from the file contents of the directory of files that is
// // obtained after fetching `url` and applying the inclusion rules given by
// // `paths`.
// //
// // This field is the source of truth; packages do not come from a `url`; they
// // come from a `hash`. `url` is just one of many possible mirrors for how to
// // obtain a package matching this `hash`.
// //
// // Uses the [multihash](https://multiformats.io/multihash/) format.
// .hash = "...",
//
// // When this is provided, the package is found in a directory relative to the
// // build root. In this case the package's hash is irrelevant and therefore not
// // computed. This field and `url` are mutually exclusive.
// .path = "foo",
//
// // When this is set to `true`, a package is declared to be lazily
// // fetched. This makes the dependency only get fetched if it is
// // actually used.
// .lazy = false,
//},
.zlib_ng = .{
.url = "git+https://github.com/CalebQ42/zig-zlib-ng#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",
},
},
// Specifies the set of files and directories that are included in this package.
// Only files and directories listed here are included in the `hash` that
// is computed for this package. Only files listed here will remain on disk
// when using the zig package manager. As a rule of thumb, one should list
// files required for compilation plus any license(s).
// Paths are relative to the build root. Use the empty string (`""`) to refer to
// the build root itself.
// A directory listed here means that all files within, recursively, are included.
.paths = .{
"build.zig",
"build.zig.zon",
+2
View File
@@ -0,0 +1,2 @@
[tools]
zig = "0.16.0"
Executable
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
zig test \
-lc \
-lz \
-llzma \
-lminilzo \
-llz4 \
-lzstd \
src/test.zig
+100
View File
@@ -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,
);
}
+116 -121
View File
@@ -1,146 +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");
const help_msg =
\\Basic Usage: zig-unsquashfs [Options] SQUASHFS_FILE <EXTRACT_LOCATION>
//TODO: Add more options
const help_mgs =
\\
\\General options:
\\ -e <path> Path to a file or directory inside the archive to extract instead of the whole archive.
\\ Can be given multiple times.
\\ -o <bytes> Skip <bytes> before reading from the archive.
\\ -v Verbose output.
\\Usage: unsquashfs [options] <archive>
\\
\\Extraction options:
\\ --unbreak-symlinks Attempt extract symlink targets along with symlinks. Will not place files outside of the extraction location.
\\ -us Same as --unbreak-symlinks
\\ --deref-symlinks Replace symlink files with their target.
\\ -ds Same as --deref-symlinks
\\ -p <#> Use at most # of processors. Defaults to logical core count.
\\Options:
\\ -d <location> Extract to the given location instead of "squashfs-root"
\\
\\Listing Options:
\\ -l List files instead of extracting. When used, you do not need to specify an extraction location.
\\ -ll Similiar to -l, but with file attributes.
\\ -lln Similiar to -ll, but with numeric uids and gids.
\\ -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)
\\
\\Other:
\\ --help Prints this help message.
\\ -h Same as --help
\\ --version Print version number.
\\ -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 stdout = std.io.getStdOut();
const errors = error{InvalidArguments};
var extr_files: std.ArrayList([]const u8) = undefined;
var archive: []const u8 = "";
var extLoc: []const u8 = "squashfs-root";
var offset: u64 = 0;
var threads: u32 = 0;
var verbose: bool = false;
var unbreak: bool = false;
var deref: bool = false;
var processors: u16 = 0;
var list: ListTypes = .None;
var ignore_xattrs: bool = false;
var ignore_permissions: bool = false;
var force: bool = false;
var filename: []const u8 = "";
var extr_location: []const u8 = "";
pub fn main(init: std.process.Init) !void {
const alloc = init.gpa;
const io = init.io;
const ListTypes = enum {
None,
List,
ListAttr,
ListNumeric,
};
var stdout = Io.File.stdout();
var out = stdout.writer(io, &[0]u8{});
defer out.interface.flush() catch {};
pub fn main() !void {
const alloc = std.heap.smp_allocator;
extr_files = .init(alloc);
defer extr_files.deinit();
var args = std.process.argsWithAllocator(alloc) catch {
_ = try stdout.writeAll("Unable to allocate memory");
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,
};
defer args.deinit();
_ = args.next();
while (args.next()) |arg| {
if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
_ = try stdout.writeAll(help_msg);
return;
} else if (std.mem.eql(u8, arg, "--version")) {
try config.version.format("", .{}, stdout.writer());
_ = try stdout.write("\n");
return;
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;
} else if (std.mem.eql(u8, arg, "--unbreak-symlinks") or std.mem.eql(u8, arg, "-us")) {
unbreak = true;
} else if (std.mem.eql(u8, arg, "--deref-symlinks") or std.mem.eql(u8, arg, "-ds")) {
deref = true;
} else if (std.mem.eql(u8, arg, "-l")) {
list = .List;
} else if (std.mem.eql(u8, arg, "-ll")) {
list = .ListAttr;
} else if (std.mem.eql(u8, arg, "-lln")) {
list = .ListNumeric;
} else if (std.mem.eql(u8, arg, "-e")) {
const next = args.next();
if (next == null) {
_ = try stdout.writeAll("path required after -e\n");
return;
}
try extr_files.append(next.?);
} else if (std.mem.eql(u8, arg, "-o")) {
const next = args.next();
if (next == null) {
_ = try stdout.writeAll("offset required after -o\n");
return;
}
offset = try std.fmt.parseInt(u64, next.?, 10);
} else if (std.mem.eql(u8, arg, "-p")) {
const next = args.next();
if (next == null) {
_ = try stdout.writeAll("number required after -p\n");
return;
}
processors = try std.fmt.parseInt(u16, next.?, 10);
} else if (filename.len == 0) {
filename = arg;
} else if (extr_location.len == 0) {
extr_location = arg;
} else {
_ = try stdout.writeAll("invalid or too many arguments\n");
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;
}
if (filename.len == 0) {
_ = try stdout.writeAll("no archive given\n");
return;
}
if (list == .None and extr_location.len == 0) {
_ = try stdout.writeAll("no extract location given\n");
return;
}
const fil = try std.fs.cwd().openFile(filename, .{});
defer fil.close();
var th_alloc: std.heap.ThreadSafeAllocator = .{ .child_allocator = std.heap.smp_allocator };
var rdr = squashfs.SfsFile.init(
th_alloc.allocator(),
fil,
offset,
) catch |err| {
try std.fmt.format(stdout.writer(), "Error opening {s} as squashfs: {any}\n", .{ filename, err });
return;
};
defer rdr.deinit();
//TODO: list and extr_files;
var op: squashfs.ExtractionOptions = squashfs.ExtractionOptions.init() catch |err| {
try std.fmt.format(stdout.writer(), "Error setting extraction options: {any}\n", .{err});
return;
};
op.verbose = verbose;
op.dereference_symlinks = deref;
op.unbreak_symlinks = unbreak;
if (processors != 0) op.thread_count = processors;
rdr.extract(op, extr_location) catch |err| {
try std.fmt.format(stdout.writer(), "Error extracting archive: {any}\n", .{err});
return;
};
}
test {
std.testing.refAllDecls(squashfs.Archive);
}
+7
View File
@@ -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>
+67
View File
@@ -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);
}
-70
View File
@@ -1,70 +0,0 @@
const std = @import("std");
const InodeType = @import("inode.zig").Type;
const Compression = @import("superblock.zig").Compression;
const Header = extern struct { //use extern instead of packed, due to bit alignment
count: u32,
block: u32,
num: u32,
};
const RawEntry = struct {
offset: u16,
num_offset: i16,
type: InodeType,
size: u16,
name: []const u8,
pub fn init(alloc: std.mem.Allocator, rdr: anytype) !RawEntry {
var fixed: [8]u8 = undefined;
_ = try rdr.read(&fixed);
const size = std.mem.readInt(u16, fixed[6..8], .little);
const name = try alloc.alloc(u8, size + 1);
_ = try rdr.read(name);
return .{
.offset = std.mem.readInt(u16, fixed[0..2], .little),
.num_offset = std.mem.readInt(i16, fixed[2..4], .little),
.type = @enumFromInt(std.mem.readInt(u16, fixed[4..6], .little)),
.size = size,
.name = name,
};
}
};
pub const Entry = struct {
block: u32,
offset: u16,
num: u32,
type: InodeType,
name: []const u8,
pub fn deinit(self: Entry, alloc: std.mem.Allocator) void {
alloc.free(self.name);
}
};
pub fn readDirectory(alloc: std.mem.Allocator, rdr: anytype, size: u32) ![]Entry {
var entries: std.ArrayList(Entry) = .init(alloc);
errdefer entries.deinit();
var cur_red: u32 = 3; // dir size includes "." & "..", so its actual size is off by 3.
var hdr: Header = undefined;
while (cur_red < size) {
_ = try rdr.read(std.mem.asBytes(&hdr));
cur_red += 12;
try entries.ensureUnusedCapacity(hdr.count + 1);
for (0..hdr.count + 1) |_| {
const raw_ent: RawEntry = try .init(alloc, rdr);
cur_red += 9 + raw_ent.size;
errdefer alloc.free(raw_ent.name);
entries.appendAssumeCapacity(.{
.block = hdr.block,
.offset = raw_ent.offset,
.num = @truncate(@abs(@as(i64, hdr.num) + raw_ent.num_offset)),
.type = raw_ent.type,
.name = raw_ent.name,
});
}
}
return entries.toOwnedSlice();
}
-23
View File
@@ -1,23 +0,0 @@
const std = @import("std");
const Self = @This();
/// Replace symlinks with their targets
dereference_symlinks: bool = false,
/// Always extract a symlink's target if it's part of the archive.
/// May result in the symlink's target being changed.
unbreak_symlinks: bool = false,
/// Do not set file's permissions & owner when extracted.
ignore_permissions: bool = false,
/// Verbose logging
verbose: bool = false,
/// Verbose logging writer. If not set, stdout is used.
verbose_logger: std.io.AnyWriter = std.io.getStdOut().writer().any(),
/// Number of threads used during extraction. Defualts to std.Thread.getCpuCount().
thread_count: usize,
pub fn init() !Self {
return .{
.thread_count = try std.Thread.getCpuCount(),
};
}
+113 -342
View File
@@ -1,355 +1,126 @@
//! A wrapper around an Inode to make common activities easier.
const std = @import("std");
const builtin = @import("builtin");
const Io = std.Io;
const dir = @import("directory.zig");
const DirEntry = dir.Entry;
const Archive = @import("archive.zig");
const DirEntry = @import("dir_entry.zig");
const ExtractionOptions = @import("options.zig");
const Inode = @import("inode.zig");
const SfsReader = @import("reader.zig").SfsReader;
const ToReader = @import("reader/to_read.zig").ToRead;
const ExtractionOptions = @import("extract_options.zig");
const DataReader = @import("reader/data.zig").DataReader;
const Compression = @import("superblock.zig").Compression;
const MetadataReader = @import("reader/metadata.zig").MetadataReader;
const LookupTable = @import("lookup_table.zig");
const MetadataReader = @import("util/metadata.zig");
pub fn File(comptime T: type) type {
return struct {
pub const FileError = error{
NotRegular,
NotDirectory,
NotFound,
};
pub const Error = error{
NotFound,
};
const Self = @This();
const File = @This();
rdr: *SfsReader(T),
// parent: *File(T),
alloc: std.mem.Allocator,
archive: *Archive,
inode: Inode,
name: []const u8,
name: []const u8,
inode: Inode,
/// Directory entries. Only populated on directories.
entries: ?[]DirEntry = null,
/// File reader. Only populated on regular files.
data_reader: ?DataReader(T) = null,
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);
pub fn init(rdr: *SfsReader(T), inode: Inode, name: []const u8) !Self {
const name_cpy: []u8 = try rdr.alloc.alloc(u8, name.len);
@memcpy(name_cpy, name);
var out = Self{
.rdr = rdr,
.inode = inode,
.name = name_cpy,
};
switch (inode.data) {
.dir => |d| {
var meta = MetadataReader(T).init(
rdr.alloc,
rdr.super.comp,
rdr.rdr,
d.block + rdr.super.dir_start,
);
try meta.skip(d.offset);
out.entries = try dir.readDirectory(rdr.alloc, &meta, d.size);
},
.ext_dir => |d| {
var meta = MetadataReader(T).init(
rdr.alloc,
rdr.super.comp,
rdr.rdr,
d.block + rdr.super.dir_start,
);
try meta.skip(d.offset);
out.entries = try dir.readDirectory(rdr.alloc, &meta, d.size);
},
.file => |f| {
out.data_reader = try .init(rdr, inode);
_ = f;
//TODO: fragments
// if (f.hasFragment()) {
// try out.data_reader.?.addFragment(
// try rdr.frag_table.get(f.frag_idx),
// f.frag_offset,
// );
// }
},
.ext_file => |f| {
out.data_reader = try .init(rdr, inode);
_ = f;
//TODO: Fragments
// if (f.hasFragment()) {
// try out.data_reader.?.addFragment(
// try rdr.frag_table.get(f.frag_idx),
// f.frag_offset,
// );
// }
},
else => {},
}
return out;
}
pub fn initFromRef(rdr: *SfsReader(T), ref: Inode.Ref, name: []const u8) !Self {
var meta: MetadataReader(T) = .init(rdr.alloc, rdr.super.comp, rdr.rdr, ref.block + rdr.super.inode_start);
try meta.skip(ref.offset);
const inode: Inode = try .init(&meta, rdr.alloc, rdr.super.block_size);
return .init(rdr, inode, name);
}
pub fn initFromEntry(rdr: *SfsReader(T), ent: DirEntry) !Self {
var meta: MetadataReader(T) = .init(rdr.alloc, rdr.super.comp, rdr.rdr, ent.block + rdr.super.inode_start);
try meta.skip(ent.offset);
const inode: Inode = try .init(&meta, rdr.alloc, rdr.super.block_size);
return .init(rdr, inode, ent.name);
}
pub fn deinit(self: *Self) void {
self.rdr.alloc.free(self.name);
self.inode.deinit(self.rdr.alloc);
if (self.entries != null) {
for (self.entries.?) |e| {
e.deinit(self.rdr.alloc);
}
self.rdr.alloc.free(self.entries.?);
}
if (self.data_reader != null) {
self.data_reader.?.deinit();
}
}
const new_name = try alloc.alloc(u8, entry.name.len);
errdefer alloc.free(new_name);
@memcpy(new_name, entry.name);
pub fn uid(self: Self) !u32 {
return self.rdr.id_table.get(self.inode.hdr.uid_idx);
}
pub fn gid(self: Self) !u32 {
return self.rdr.id_table.get(self.inode.hdr.uid_idx);
}
return .{
.alloc = alloc,
.archive = archive,
const Reader = std.io.GenericReader(*DataReader(T), anyerror, DataReader(T).read);
pub fn read(self: *Self, buf: []u8) !usize {
if (self.data_reader == null) return FileError.NotRegular;
return self.data_reader.?.read(buf);
}
pub fn reader(self: *Self) !Reader {
if (self.data_reader == null) return FileError.NotRegular;
return self.data_reader.?.reader();
}
pub fn open(self: Self, path: []const u8) !Self {
if (self.entries == null) return FileError.NotDirectory;
if (path.len == 0) return self;
const idx = std.mem.indexOf(u8, path, "/") orelse path.len;
if (idx == 0) return self.open(path[1..]);
const name = path[0..idx];
for (self.entries.?) |e| {
if (std.mem.eql(u8, e.name, name)) {
var fil: Self = try .initFromEntry(self.rdr, e);
if (idx >= path.len - 1) return fil;
defer fil.deinit();
return fil.open(path[idx + 1 ..]);
}
}
return FileError.NotFound;
}
pub fn iterate(self: Self) Iterator {
return .{
.rdr = self.rdr,
.entries = self.entries.?,
};
}
const Iterator = struct {
rdr: *SfsReader(T),
entries: []DirEntry,
idx: u32 = 0,
pub fn next(self: *Iterator) !?File(T) {
if (self.idx >= self.entries.len) return null;
const out = try Self.initFromEntry(self.rdr, self.entries[self.idx]);
self.idx += 1;
return out;
}
pub fn reset(self: *Iterator) void {
self.idx = 0;
}
};
const WaitGroup = std.Thread.WaitGroup;
const Pool = std.Thread.Pool;
const Mutex = std.Thread.Mutex;
pub const ExtractError = error{FileExists};
pub fn extract(self: *Self, op: ExtractionOptions, path: []const u8) !void {
var wg: WaitGroup = .{};
var pol: Pool = undefined;
try pol.init(.{
.n_jobs = op.thread_count,
.allocator = self.rdr.alloc,
});
defer pol.deinit();
var errs: std.ArrayList(anyerror) = .init(self.rdr.alloc);
defer errs.deinit();
try self.extractInode(op, &wg, &errs, &pol, self.inode, path);
wg.wait();
if (errs.items.len > 0) return errs.items[0];
}
fn extractInode(
self: *Self,
op: ExtractionOptions,
wg: *WaitGroup,
errs: *std.ArrayList(anyerror),
pol: *Pool,
inode: Inode,
path: []const u8,
) !void {
wg.start();
defer wg.finish(); //TODO: When everthing is threaded, this will need to be handled by the threads, not here.
switch (inode.hdr.type) {
.file, .ext_file => {
var fil = try std.fs.cwd().createFile(path, .{});
defer fil.close();
var data: DataReader(T) = try .init(self.rdr, inode);
defer data.deinit();
try data.writeTo(fil); // TODO: Thread
const fil_uid = self.rdr.id_table.get(inode.hdr.uid_idx) catch |err| {
if (op.verbose) {
std.fmt.format(op.verbose_logger, "error getting uid {} from table: {}\n", .{ inode.hdr.uid_idx, err }) catch {};
}
return;
};
const fil_gid = self.rdr.id_table.get(inode.hdr.gid_idx) catch |err| {
if (op.verbose) {
std.fmt.format(op.verbose_logger, "error getting gid {} from table: {}\n", .{ inode.hdr.gid_idx, err }) catch {};
}
return;
};
fil.chmod(inode.hdr.perm) catch |err| {
if (op.verbose) {
std.fmt.format(op.verbose_logger, "error chmod {s}: {}\n", .{ path, err }) catch {};
}
return;
};
fil.chown(fil_uid, fil_gid) catch |err| {
if (op.verbose) {
std.fmt.format(op.verbose_logger, "error chmod {s}: {}\n", .{ path, err }) catch {};
}
return;
};
//TODO: update mtime.
},
.dir, .ext_dir => {
std.fs.cwd().makeDir(path) catch |err| {
if (err != std.fs.Dir.MakeError.PathAlreadyExists) {
return err;
}
};
var dir_block: u32 = 0;
var dir_offset: u16 = 0;
var dir_size: u32 = 0;
switch (inode.data) {
.dir => |d| {
dir_block = d.block;
dir_offset = d.offset;
dir_size = d.size;
},
.ext_dir => |d| {
dir_block = d.block;
dir_offset = d.offset;
dir_size = d.size;
},
else => unreachable,
}
var meta: MetadataReader(T) = .init(self.rdr.alloc, self.rdr.super.comp, self.rdr.rdr, dir_block + self.rdr.super.dir_start);
try meta.skip(dir_offset);
const entries = try dir.readDirectory(self.rdr.alloc, &meta, dir_size);
defer self.rdr.alloc.free(entries);
for (entries) |ent| {
defer ent.deinit(self.rdr.alloc);
var new_path: []u8 = undefined;
if (path[path.len - 1] == '/') {
new_path = self.rdr.alloc.alloc(u8, path.len + ent.name.len) catch |err| {
if (op.verbose) {
std.fmt.format(op.verbose_logger, "error allocating memory: {}\n", .{err}) catch {};
}
errs.append(err) catch {};
continue;
};
@memcpy(new_path[0..path.len], path);
@memcpy(new_path[path.len..], ent.name);
} else {
new_path = self.rdr.alloc.alloc(u8, path.len + ent.name.len + 1) catch |err| {
if (op.verbose) {
std.fmt.format(op.verbose_logger, "error allocating memory: {}\n", .{err}) catch {};
}
errs.append(err) catch {};
continue;
};
@memcpy(new_path[0..path.len], path);
new_path[path.len] = '/';
@memcpy(new_path[path.len + 1 ..], ent.name);
}
defer self.rdr.alloc.free(new_path);
meta = .init(self.rdr.alloc, self.rdr.super.comp, self.rdr.rdr, ent.block + self.rdr.super.inode_start);
meta.skip(ent.offset) catch |err| {
if (op.verbose) {
std.fmt.format(op.verbose_logger, "error reading inode: {}\n", .{err}) catch {};
}
errs.append(err) catch {};
continue;
};
const new_inode = Inode.init(&meta, self.rdr.alloc, self.rdr.super.block_size) catch |err| {
if (op.verbose) {
std.fmt.format(op.verbose_logger, "error reading inode: {}\n", .{err}) catch {};
}
errs.append(err) catch {};
continue;
};
defer new_inode.deinit(self.rdr.alloc);
self.extractInode(op, wg, errs, pol, new_inode, new_path) catch |err| {
if (op.verbose) {
std.fmt.format(op.verbose_logger, "error extracting {s}: {}\n", .{ new_path, err }) catch {};
}
errs.append(err) catch {};
continue;
};
}
var fil = std.fs.cwd().openDir(path, .{ .iterate = true }) catch |err| {
if (op.verbose) {
std.fmt.format(op.verbose_logger, "error openning {s} to set permissions: {}\n", .{ path, err }) catch {};
}
return;
};
const fil_uid = self.rdr.id_table.get(inode.hdr.uid_idx) catch |err| {
if (op.verbose) {
std.fmt.format(op.verbose_logger, "error getting uid {} from table: {}\n", .{ inode.hdr.uid_idx, err }) catch {};
}
return;
};
const fil_gid = self.rdr.id_table.get(inode.hdr.gid_idx) catch |err| {
if (op.verbose) {
std.fmt.format(op.verbose_logger, "error getting gid {} from table: {}\n", .{ inode.hdr.gid_idx, err }) catch {};
}
return;
};
fil.chmod(inode.hdr.perm) catch |err| {
if (op.verbose) {
std.fmt.format(op.verbose_logger, "error chmod {s}: {}\n", .{ path, err }) catch {};
}
return;
};
fil.chown(fil_uid, fil_gid) catch |err| {
if (op.verbose) {
std.fmt.format(op.verbose_logger, "error chmod {s}: {}\n", .{ path, err }) catch {};
}
return;
};
},
// .symlink, .ext_symlink => {},
else => {
std.debug.print("TODO: {}\n", .{inode.hdr.type});
},
}
}
.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,
);
}
+7
View File
@@ -0,0 +1,7 @@
const BlockSize = @import("inode_data/file.zig").BlockSize;
pub const FragEntry = extern struct {
start: u64,
size: BlockSize,
_: u32,
};
-7
View File
@@ -1,7 +0,0 @@
const BlockSize = @import("inode/file.zig").BlockSize;
pub const FragEntry = packed struct {
block: u64,
size: BlockSize,
_: u32,
};
+385 -44
View File
@@ -1,12 +1,23 @@
//! A file-system object. Represents a File or directory.
const std = @import("std");
const Io = std.Io;
const Reader = Io.Reader;
const dir = @import("inode/dir.zig");
const file = @import("inode/file.zig");
const misc = @import("inode/misc.zig");
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 {
offset: u16,
block: u32,
pub const Ref = packed struct(u64) {
block_offset: u16,
block_start: u32,
_: u16,
};
@@ -27,62 +38,96 @@ pub const Type = enum(u16) {
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 {
type: Type,
perm: u16,
inode_type: Type,
permissions: u16,
uid_idx: u16,
gid_idx: u16,
mod_time: u32,
num: u32,
};
pub const Data = union(enum) {
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 Error = error{
NotDirectory,
};
const Self = @This();
const Inode = @This();
hdr: Header,
data: Data,
pub fn init(rdr: anytype, alloc: std.mem.Allocator, block_size: u32) !Self {
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.read(std.mem.asBytes(&hdr));
const data: Data = switch (hdr.type) {
.dir => .{ .dir = try .init(rdr) },
.file => .{ .file = try .init(rdr, alloc, block_size) },
.symlink => .{ .symlink = try .init(rdr, alloc) },
.block_dev => .{ .block_dev = try .init(rdr) },
.char_dev => .{ .char_dev = try .init(rdr) },
.fifo => .{ .fifo = try .init(rdr) },
.socket => .{ .socket = try .init(rdr) },
.ext_dir => .{ .ext_dir = try .init(rdr) },
.ext_file => .{ .ext_file = try .init(rdr, alloc, block_size) },
.ext_symlink => .{ .ext_symlink = try .init(rdr, alloc) },
.ext_block_dev => .{ .ext_block_dev = try .init(rdr) },
.ext_char_dev => .{ .ext_char_dev = try .init(rdr) },
.ext_fifo => .{ .ext_fifo = try .init(rdr) },
.ext_socket => .{ .ext_socket = try .init(rdr) },
};
try rdr.readSliceEndian(Header, @ptrCast(&hdr), .little);
return .{
.hdr = hdr,
.data = data,
.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: Self, alloc: std.mem.Allocator) void {
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),
@@ -91,3 +136,299 @@ pub fn deinit(self: Self, alloc: std.mem.Allocator) void {
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, "/"));
}
-31
View File
@@ -1,31 +0,0 @@
const std = @import("std");
pub const Dir = packed struct {
block: u32,
hard_link: u32,
size: u16,
offset: u16,
parent_num: u32,
pub fn init(rdr: anytype) !Dir {
var out: Dir = undefined;
_ = try rdr.read(std.mem.asBytes(&out));
return out;
}
};
pub const ExtDir = packed struct {
hard_link: u32,
size: u32,
block: u32,
parent_num: u32,
idx_count: u16,
offset: u16,
xattr_idx: u32,
pub fn init(rdr: anytype) !ExtDir {
var out: ExtDir = undefined;
_ = try rdr.read(std.mem.asBytes(&out));
return out;
}
};
-77
View File
@@ -1,77 +0,0 @@
const std = @import("std");
pub const BlockSize = packed struct {
size: u24,
uncompressed: bool,
_: u7,
};
pub const File = struct {
block: u32,
frag_idx: u32,
frag_offset: u32,
size: u32,
block_sizes: []BlockSize,
pub fn init(rdr: anytype, alloc: std.mem.Allocator, block_size: u32) !File {
var fixed: [16]u8 = undefined;
_ = try rdr.read(&fixed);
const frag_idx = std.mem.readInt(u32, fixed[4..8], .little);
const size = std.mem.readInt(u32, fixed[12..16], .little);
var blocks: u32 = size / block_size;
if (size % block_size > 0 and frag_idx == 0xffffffff) {
blocks += 1;
}
const block_sizes = try alloc.alloc(BlockSize, blocks);
errdefer alloc.free(block_sizes);
_ = try rdr.read(std.mem.sliceAsBytes(block_sizes));
return .{
.block = std.mem.readInt(u32, fixed[0..4], .little),
.frag_idx = frag_idx,
.frag_offset = std.mem.readInt(u32, fixed[8..12], .little),
.size = size,
.block_sizes = block_sizes,
};
}
pub fn hasFragment(self: File) bool {
return self.frag_idx != 0xffffffff;
}
};
pub const ExtFile = struct {
block: u64,
size: u64,
sparse: u64,
hard_link: u32,
frag_idx: u32,
frag_offset: u32,
xattr_idx: u32,
block_sizes: []BlockSize,
pub fn init(rdr: anytype, alloc: std.mem.Allocator, block_size: u32) !ExtFile {
var fixed: [40]u8 = undefined;
_ = try rdr.read(&fixed);
const size = std.mem.readInt(u64, fixed[8..16], .little);
const frag_idx = std.mem.readInt(u32, fixed[28..32], .little);
var blocks: u32 = @truncate(size / block_size);
if (size % block_size > 0 and frag_idx == 0xffffffff) {
blocks += 1;
}
const block_sizes = try alloc.alloc(BlockSize, blocks);
errdefer alloc.free(block_sizes);
_ = try rdr.read(std.mem.sliceAsBytes(block_sizes));
return .{
.block = std.mem.readInt(u64, fixed[0..8], .little),
.size = size,
.sparse = std.mem.readInt(u64, fixed[16..24], .little),
.hard_link = std.mem.readInt(u32, fixed[24..28], .little),
.frag_idx = frag_idx,
.frag_offset = std.mem.readInt(u32, fixed[32..36], .little),
.xattr_idx = std.mem.readInt(u32, fixed[36..40], .little),
.block_sizes = block_sizes,
};
}
pub fn hasFragment(self: ExtFile) bool {
return self.frag_idx != 0xffffffff;
}
};
-87
View File
@@ -1,87 +0,0 @@
const std = @import("std");
pub const Symlink = struct {
hard_link: u32,
// size: u32,
target: []const u8,
pub fn init(rdr: anytype, alloc: std.mem.Allocator) !Symlink {
var fixed: [8]u8 = undefined;
_ = try rdr.read(&fixed);
const size = std.mem.readInt(u32, fixed[4..8], .little);
const target = try alloc.alloc(u8, size);
errdefer alloc.free(target);
_ = try rdr.read(target);
return .{
.hard_link = std.mem.readInt(u32, fixed[0..4], .little),
.target = target,
};
}
};
pub const ExtSymlink = struct {
hard_link: u32,
// size: u32,
target: []const u8,
xattr_idx: u32,
pub fn init(rdr: anytype, alloc: std.mem.Allocator) !ExtSymlink {
var fixed: [8]u8 = undefined;
_ = try rdr.read(&fixed);
const size = std.mem.readInt(u32, fixed[4..8], .little);
const target = try alloc.alloc(u8, size);
errdefer alloc.free(target);
_ = try rdr.read(target);
var xattr_idx: u32 = 0;
_ = try rdr.read(std.mem.asBytes(&xattr_idx));
return .{
.hard_link = std.mem.readInt(u32, fixed[0..4], .little),
.target = target,
.xattr_idx = xattr_idx,
};
}
};
pub const Dev = packed struct {
hard_link: u32,
device: u32,
pub fn init(rdr: anytype) !Dev {
var out: Dev = undefined;
_ = try rdr.read(std.mem.asBytes(&out));
return out;
}
};
pub const ExtDev = packed struct {
hard_link: u32,
device: u32,
xattr_idx: u32,
pub fn init(rdr: anytype) !ExtDev {
var out: ExtDev = undefined;
_ = try rdr.read(std.mem.asBytes(&out));
return out;
}
};
pub const IPC = packed struct {
hard_link: u32,
pub fn init(rdr: anytype) !IPC {
var out: IPC = undefined;
_ = try rdr.read(std.mem.asBytes(&out));
return out;
}
};
pub const ExtIPC = packed struct {
hard_link: u32,
xattr_idx: u32,
pub fn init(rdr: anytype) !ExtIPC {
var out: ExtIPC = undefined;
_ = try rdr.read(std.mem.asBytes(&out));
return out;
}
};
+32
View File
@@ -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;
}
};
+91
View File
@@ -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);
}
};
+98
View File
@@ -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;
}
};
+124
View File
@@ -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;
}
+30
View File
@@ -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(),
};
}
-85
View File
@@ -1,85 +0,0 @@
const std = @import("std");
const Inode = @import("inode.zig");
const File = @import("file.zig").File;
const Table = @import("table.zig").Table;
const PRead = @import("reader/p_read.zig").PRead;
const FragEntry = @import("fragment.zig").FragEntry;
const Superblock = @import("superblock.zig").Superblock;
const ExtractionOptions = @import("extract_options.zig");
const MetadataReader = @import("reader/metadata.zig").MetadataReader;
pub const SfsError = error{
NotExportable,
};
pub fn SfsReader(comptime T: type) type {
comptime std.debug.assert(std.meta.hasFn(T, "pread"));
return struct {
const Self = @This();
alloc: std.mem.Allocator,
rdr: PRead(T),
super: Superblock = undefined,
/// ID table. Can be accessed directly
id_table: Table(u32, T) = undefined,
/// Fragment table. Can be accessed directly
frag_table: Table(FragEntry, T) = undefined,
/// Export table. Each element is an inode referce.
/// If accessing directly, keep in mind, the table starts at inode 1, as such it's recommended to use the InodeAt function instead.
export_table: Table(Inode.Ref, T) = undefined,
pub fn init(alloc: std.mem.Allocator, rdr: T, offset: u64) !Self {
var out: Self = .{
.alloc = alloc,
.rdr = .init(rdr, offset),
};
_ = try rdr.pread(std.mem.asBytes(&out.super), 0);
out.frag_table = .init(alloc, out.rdr, out.super.comp, out.super.frag_start, out.super.frag_count);
out.id_table = .init(alloc, out.rdr, out.super.comp, out.super.id_start, out.super.id_count);
out.export_table = .init(alloc, out.rdr, out.super.comp, out.super.export_start, out.super.inode_count);
return out;
}
pub fn deinit(self: *Self) void {
self.id_table.deinit();
self.frag_table.deinit();
self.export_table.deinit();
}
/// A representation of the archives root folder.
pub fn root(self: *Self) !File(T) {
return .initFromRef(self, self.super.root_ref, "");
}
/// Get the file at path. Equivelent to calling open on the root File.
pub fn open(self: *Self, path: []const u8) !File(T) {
var rt = try self.root();
if (path.len == 0 or (path.len == 1 and path[0] == '/') or path.len == 1 and path[0] == '.') return rt;
defer rt.deinit();
return rt.open(path);
}
/// Extract the entire archive to the given path & with the given options.
/// Equivelent to calling extract on the root File.
pub fn extract(self: *Self, op: ExtractionOptions, path: []const u8) !void {
var rt = try self.root();
defer rt.deinit();
return rt.extract(op, path);
}
/// Returns the Inode with the given Inode Number.
/// Requires the archive to have an export table.
pub fn inodeAt(self: Self, num: u32) !Inode {
if (!self.super.flags.has_export) return SfsError.NotExportable;
const ref = try self.export_table.get(num - 1);
var meta = MetadataReader(T).init(
self.alloc,
self.super.comp,
self.rdr,
self.super.inode_start + ref.block,
);
try meta.skip(ref.offset);
return .init(meta, self.alloc, self.super.block_size);
}
};
}
-271
View File
@@ -1,271 +0,0 @@
const std = @import("std");
const Inode = @import("../inode.zig");
const PRead = @import("p_read.zig").PRead;
const SfsReader = @import("../reader.zig").SfsReader;
const FragEntry = @import("../fragment.zig").FragEntry;
const BlockSize = @import("../inode/file.zig").BlockSize;
const Compression = @import("../superblock.zig").Compression;
const DataReaderError = error{
EOF,
InvalidIndex,
ExtractionActive,
};
const DecompCompletion = struct {
errs: std.ArrayList(anyerror),
map: std.AutoArrayHashMap(usize, []u8),
mut: std.Thread.Mutex = .{},
cond: std.Thread.Condition = .{},
fn init(alloc: std.mem.Allocator) DecompCompletion {
return .{
.errs = .init(alloc),
.map = .init(alloc),
};
}
fn deinit(self: *DecompCompletion) void {
self.errs.deinit();
self.map.deinit();
}
fn clear(self: *DecompCompletion) void {
self.mut.lock();
defer self.mut.unlock();
self.errs.clearAndFree();
self.map.clearAndFree();
}
fn add(self: *DecompCompletion, idx: usize, data: []u8) !void {
self.mut.lock();
defer self.mut.unlock();
defer self.cond.signal();
try self.map.put(idx, data);
}
fn addErr(self: *DecompCompletion, err: anyerror) void {
self.mut.lock();
defer self.mut.unlock();
defer self.cond.signal();
self.errs.append(err) catch {};
}
fn getBlock(self: *DecompCompletion, idx: usize) ?[]u8 {
const res = self.map.fetchSwapRemove(idx);
if (res == null) return null;
return res.?.value;
}
fn hasErrs(self: DecompCompletion) bool {
return self.errs.items.len > 0;
}
fn condWait(self: *DecompCompletion) void {
self.cond.wait(&self.mut);
}
};
pub fn DataReader(comptime T: type) type {
return struct {
const Self = @This();
alloc: std.mem.Allocator,
rdr: PRead(T),
comp: Compression,
block_size: u32,
sizes: []BlockSize,
offsets: []u64,
file_size: u64,
frag: ?[]u8 = null,
completion: DecompCompletion,
pub fn init(rdr: *SfsReader(T), inode: Inode) !Self {
var sizes: []BlockSize = undefined;
var file_size: u64 = 0;
var offsets: []u64 = undefined;
switch (inode.data) {
.file => |f| {
sizes = f.block_sizes;
file_size = f.size;
offsets = try rdr.alloc.alloc(u64, sizes.len);
if (sizes.len > 0) offsets[0] = f.block;
},
.ext_file => |f| {
sizes = f.block_sizes;
file_size = f.size;
offsets = try rdr.alloc.alloc(u64, sizes.len);
if (sizes.len > 0) offsets[0] = f.block;
},
else => unreachable,
}
if (offsets.len > 1) {
for (1..offsets.len) |i| {
offsets[i] = offsets[i - 1] + sizes[i - 1].size;
}
}
return .{
.alloc = rdr.alloc,
.rdr = rdr.rdr,
.comp = rdr.super.comp,
.block_size = rdr.super.block_size,
.sizes = sizes,
.offsets = offsets,
.file_size = file_size,
.completion = .init(rdr.alloc),
};
}
pub fn deinit(self: *Self) void {
self.alloc.free(self.offsets);
self.completion.deinit();
}
pub fn addFragment(self: *Self, data: []u8) void {
self.frag = data;
}
pub fn writeTo(self: *Self, wrt: anytype) !void {
comptime std.debug.assert(std.meta.hasFn(@TypeOf(wrt), "write") or std.meta.hasFn(@TypeOf(wrt), "pwrite"));
var write_thr = try std.Thread.spawn(
.{ .allocator = self.alloc },
writeThread,
.{ self, wrt, null, null },
);
defer self.completion.clear();
for (0..self.numBlocks()) |i| {
var thr = std.Thread.spawn(
.{ .allocator = self.alloc },
decompThread,
.{ self, i },
) catch |err| {
self.completion.addErr(err);
continue;
};
thr.detach();
}
write_thr.join();
if (self.completion.hasErrs()) return self.completion.errs.items[0];
}
pub fn writeToNoBlock(self: *Self, wrt: anytype, comptime finish: anytype, finish_args: anytype) !void {
comptime std.debug.assert(std.meta.hasFn(@TypeOf(wrt), "write") or std.meta.hasFn(@TypeOf(wrt), "pwrite"));
errdefer self.completion.clear();
var write_thr = try std.Thread.spawn(
.{ .allocator = self.alloc },
writeThread,
.{ self, wrt, finish, finish_args },
);
write_thr.detach();
for (0..self.numBlocks()) |i| {
var thr = std.Thread.spawn(
.{ .allocator = self.alloc },
decompThread,
.{ self, i },
) catch |err| {
self.completion.addErr(err);
continue;
};
thr.detach();
}
}
fn numBlocks(self: Self) usize {
var out = self.sizes.len;
if (self.frag != null) out += 1;
return out;
}
/// Returns the decompressed data block at the given idx.
/// If the block is sparse (filled with 0s), a zero length slice is returned.
fn blockAt(self: Self, idx: usize) ![]u8 {
if (idx >= self.numBlocks()) return DataReaderError.InvalidIndex;
const size = self.sizes[idx];
if (size.size == 0) return &[0]u8{};
const block = try self.alloc.alloc(u8, blk: {
if (idx == self.numBlocks() - 1) break :blk self.file_size % self.block_size;
break :blk self.block_size;
});
if (idx == self.sizes.len and self.frag != null) {
@memcpy(block, self.frag.?);
return block;
}
if (size.uncompressed) {
_ = try self.rdr.pread(block, self.offsets[idx]);
return block;
}
_ = try self.comp.decompress(
1024 * 1024,
self.alloc,
self.rdr.readerAt(self.offsets[idx]).reader(),
block,
);
return block;
}
fn writeThread(
self: *Self,
wrt: anytype,
comptime finish: anytype,
finish_args: anytype,
) void {
var cur_idx: usize = 0;
self.completion.mut.lock();
defer self.completion.mut.unlock();
while (cur_idx < self.numBlocks() and !self.completion.hasErrs()) {
self.completion.condWait();
if (self.completion.hasErrs()) break;
if (comptime std.meta.hasFn(@TypeOf(wrt), "pwrite")) {
for (self.completion.map.keys()) |_| {
const k = self.completion.map.keys()[0];
const blk = self.completion.getBlock(k).?;
defer self.alloc.free(blk);
if (blk.len > 0) {
_ = wrt.pwrite(blk, self.block_size * k) catch |err| {
self.completion.addErr(err);
break;
};
} else {
_ = wrt.pwrite(&[1]u8{0}, (self.block_size * (k + 1)) - 1) catch |err| {
self.completion.addErr(err);
break;
};
}
cur_idx += 1;
}
continue;
} else {
while (self.completion.getBlock(cur_idx)) |blk| {
defer self.alloc.free(blk);
if (blk.len > 0) {
_ = wrt.write(blk) catch |err| {
self.completion.addErr(err);
break;
};
} else {
const blank: [1024 * 1024]u8 = [1]u8{0} ** (1024 * 1024);
_ = wrt.write(blank[0..self.block_size]) catch |err| {
self.completion.addErr(err);
break;
};
}
cur_idx += 1;
}
}
}
if (comptime @TypeOf(finish) != @TypeOf(null) and @TypeOf(finish_args) != @TypeOf(null)) @call(.auto, finish, finish_args);
}
fn decompThread(
self: *Self,
idx: usize,
) void {
if (self.completion.hasErrs()) return;
defer self.completion.cond.signal();
const block = self.blockAt(idx) catch |err| {
self.completion.addErr(err);
return;
};
self.completion.add(idx, block) catch |err| {
self.completion.addErr(err);
};
}
};
}
-82
View File
@@ -1,82 +0,0 @@
const std = @import("std");
const PRead = @import("p_read.zig").PRead;
const Compression = @import("../superblock.zig").Compression;
const MetaHeader = packed struct {
size: u15,
uncompressed: bool,
};
pub fn MetadataReader(comptime T: type) type {
comptime std.debug.assert(std.meta.hasFn(T, "read"));
return struct {
const Self = @This();
alloc: std.mem.Allocator,
comp: Compression,
rdr: PRead(T),
offset: u64,
block: [8192]u8 = undefined,
block_size: usize = 0,
block_offset: u32 = 0,
pub fn init(alloc: std.mem.Allocator, comp: Compression, rdr: PRead(T), offset: u64) Self {
return .{
.alloc = alloc,
.comp = comp,
.rdr = rdr,
.offset = offset,
};
}
fn readNextBlock(self: *Self) !void {
var hdr: MetaHeader = undefined;
_ = try self.rdr.pread(std.mem.asBytes(&hdr), self.offset);
self.offset += 2;
if (hdr.uncompressed) {
self.block_size = try self.rdr.pread(self.block[0..hdr.size], self.offset);
} else {
self.block_size = try self.comp.decompress(
8192,
self.alloc,
self.rdr.readerAt(self.offset).reader(),
&self.block,
);
}
self.offset += hdr.size;
self.block_offset = 0;
}
pub fn skip(self: *Self, offset: u32) !void {
var skipped: u32 = 0;
var hdr: MetaHeader = undefined;
while (offset - skipped >= 8192) {
_ = try self.rdr.pread(std.mem.asBytes(&hdr), self.offset);
self.offset += 2 + hdr.size;
skipped += 8192;
}
var to_skip: u32 = 0;
while (skipped < offset) {
if (self.block_offset >= self.block_size) try self.readNextBlock();
to_skip = @min(self.block_size - self.block_offset, offset - skipped);
self.block_offset += to_skip;
skipped += to_skip;
}
}
pub fn read(self: *Self, buf: []u8) !usize {
var cur_red: usize = 0;
var to_read: usize = 0;
while (cur_red < buf.len) {
if (self.block_offset >= self.block_size) try self.readNextBlock();
to_read = @min(buf.len - cur_red, self.block_size - self.block_offset);
@memcpy(buf[cur_red .. cur_red + to_read], self.block[self.block_offset .. self.block_offset + to_read]);
cur_red += to_read;
self.block_offset += @truncate(to_read);
}
return cur_red;
}
};
}
-29
View File
@@ -1,29 +0,0 @@
const std = @import("std");
const ToRead = @import("to_read.zig").ToRead;
/// A simple wrapper around a type with the pread([]u8, u64) function.
/// Provides a couple useful utility functions.
pub fn PRead(comptime T: type) type {
comptime std.debug.assert(std.meta.hasFn(T, "pread"));
return struct {
const Self = @This();
rdr: T,
offset: u64,
pub fn init(rdr: T, offset: u64) Self {
return .{
.rdr = rdr,
.offset = offset,
};
}
pub fn pread(self: Self, buf: []u8, offset: u64) !usize {
return self.rdr.pread(buf, self.offset + offset);
}
pub fn readerAt(self: Self, offset: u64) ToRead(T) {
return .init(self.rdr, self.offset + offset);
}
};
}
-43
View File
@@ -1,43 +0,0 @@
const std = @import("std");
pub fn ToRead(comptime T: type) type {
comptime std.debug.assert(std.meta.hasFn(T, "pread"));
return struct {
const Self = @This();
pub const Error = anyerror;
rdr: T,
offset: u64,
pub fn init(rdr: T, init_offset: u64) Self {
return .{
.rdr = rdr,
.offset = init_offset,
};
}
pub fn read(self: *Self, buf: []u8) !usize {
const red = try self.rdr.pread(buf, self.offset);
self.offset += red;
return red;
}
pub fn readAll(self: *Self, buf: []u8) !usize {
var cur_red = try self.read(buf);
if (cur_red == 0) return cur_red;
var res: usize = 0;
while (cur_red < buf.len) {
res = try self.read(buf[cur_red..]);
if (res == 0) break;
cur_red += res;
}
return cur_red;
}
const Reader = std.io.GenericReader(*Self, anyerror, read);
pub fn reader(self: anytype) Reader {
return .{
.context = @constCast(self),
};
}
};
}
+5 -60
View File
@@ -1,62 +1,7 @@
const std = @import("std");
pub const Archive = @import("archive.zig");
pub const ExtractionOptions = @import("options.zig");
const Test = @import("test.zig");
pub const SfsReader = @import("reader.zig").SfsReader;
pub const ExtractionOptions = @import("extract_options.zig");
pub const SfsFile = SfsReader(std.fs.File);
const test_archive = "testing/LinuxPATest.sfs";
test "OpenFile" {
const sfs_fil = try std.fs.cwd().openFile(test_archive, .{});
defer sfs_fil.close();
var rdr: SfsFile = try .init(std.testing.allocator, sfs_fil, 0);
defer rdr.deinit();
_ = try rdr.frag_table.get(rdr.super.frag_count - 1);
_ = try rdr.id_table.get(rdr.super.id_count - 1);
_ = try rdr.export_table.get(rdr.super.inode_count - 1);
std.debug.print("{}\n", .{rdr.super});
var root = try rdr.root();
defer root.deinit();
var iter = root.iterate();
while (true) {
var f = try iter.next();
if (f == null) break;
defer f.?.deinit();
std.debug.print("{s}\n", .{f.?.name});
}
std.debug.print("Finished OpenFile test\n", .{});
}
test "ExtractSingleFile" {
const single_file = "PortableApps/Notepad++Portable/App/Notepad++/doLocalConf.xml";
const single_file_extr_loc = "testing/doLocalConf.xml";
std.fs.cwd().deleteFile(single_file_extr_loc) catch {};
const sfs_fil = try std.fs.cwd().openFile(test_archive, .{});
defer sfs_fil.close();
var rdr: SfsFile = try .init(std.testing.allocator, sfs_fil, 0);
defer rdr.deinit();
var fil = try rdr.open(single_file);
defer fil.deinit();
var op: ExtractionOptions = try .init();
op.verbose = true;
try fil.extract(op, single_file_extr_loc);
std.debug.print("Finished ExtractSingleFile test\n", .{});
}
test "ExtractAll" {
const extr_dir = "testing/testExtract";
std.fs.cwd().deleteTree(extr_dir) catch {};
const sfs_fil = try std.fs.cwd().openFile(test_archive, .{});
defer sfs_fil.close();
var rdr: SfsFile = try .init(std.testing.allocator, sfs_fil, 0);
defer rdr.deinit();
const op: ExtractionOptions = try .init();
// op.verbose = true;
try rdr.extract(op, extr_dir);
std.debug.print("Finished ExtractAll test\n", .{});
test {
@import("std").testing.refAllDecls(Test);
}
+63
View File
@@ -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;
}
};
-79
View File
@@ -1,79 +0,0 @@
const std = @import("std");
const InodeRef = @import("inode.zig").Ref;
pub const Superblock = packed struct {
magic: u32,
inode_count: u32,
mod_time: u32,
block_size: u32,
frag_count: u32,
comp: Compression,
block_log: u16,
flags: packed struct {
_: u4,
id_uncomp: bool,
comp_options: bool,
no_xattr: bool,
xattr_uncomp: bool,
has_export: bool,
de_dupe: bool,
frag_always: bool,
no_frag: bool,
frag_uncomp: bool,
check: bool,
data_uncomp: bool,
inode_uncomp: bool,
},
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,
};
pub const DecompressError = error{
LzoUnavailable,
Lz4Unavailable,
};
pub const Compression = enum(u16) {
gzip = 1,
lzma,
lzo,
xz,
lz4,
zstd,
pub fn decompress(self: Compression, comptime max_size: u32, alloc: std.mem.Allocator, source: anytype, dest: []u8) !usize {
switch (self) {
.gzip => {
var decomp = std.compress.zlib.decompressor(source);
return decomp.read(dest);
},
.lzma => {
var decomp = try std.compress.lzma.decompress(alloc, source);
defer decomp.deinit();
return decomp.read(dest);
},
.lzo => return DecompressError.LzoUnavailable,
.xz => {
var decomp = try std.compress.xz.decompress(alloc, source);
defer decomp.deinit();
return decomp.read(dest);
},
.lz4 => return DecompressError.Lz4Unavailable,
.zstd => {
var window: [max_size]u8 = undefined;
var decomp = std.compress.zstd.decompressor(source, .{ .window_buffer = &window });
return decomp.read(dest);
},
}
}
};
-75
View File
@@ -1,75 +0,0 @@
const std = @import("std");
const PRead = @import("reader/p_read.zig").PRead;
const Compression = @import("superblock.zig").Compression;
const MetadataReader = @import("reader/metadata.zig").MetadataReader;
pub const TableError = error{
InvalidIndex,
};
pub fn Table(comptime T: type, comptime R: type) type {
comptime std.debug.assert(std.meta.hasFn(R, "pread"));
return struct {
const Self = @This();
alloc: std.mem.Allocator,
rdr: PRead(R),
comp: Compression,
offset: u64,
table_count: u32,
mut: std.Thread.RwLock = .{},
table: []T = &[0]T{},
pub fn init(alloc: std.mem.Allocator, rdr: PRead(R), comp: Compression, offset: u64, table_count: u32) Self {
return .{
.alloc = alloc,
.rdr = rdr,
.comp = comp,
.offset = offset,
.table_count = table_count,
};
}
pub fn deinit(self: Self) void {
self.alloc.free(self.table);
}
fn resize(self: *Self, to_add: usize) !void {
if (!self.alloc.resize(self.table, self.table.len + to_add)) {
const new_table = try self.alloc.alloc(T, self.table.len + to_add);
@memcpy(new_table[0..self.table.len], self.table);
self.alloc.free(self.table);
self.table = new_table;
}
}
pub fn get(self: *Self, idx: u32) !T {
if (idx >= self.table_count) return TableError.InvalidIndex;
self.mut.lockShared();
defer self.mut.unlockShared();
if (idx >= self.table.len) {
return self.getAndFill(idx);
}
return self.table[idx];
}
fn getAndFill(self: *Self, idx: u32) !T {
self.mut.unlockShared();
defer self.mut.lockShared();
self.mut.lock();
defer self.mut.unlock();
var to_read: usize = 0;
var offset: u64 = 0;
while (idx >= self.table.len) {
to_read = @min(self.table_count - self.table.len, comptime 8192 / @sizeOf(T));
try self.resize(to_read);
_ = try self.rdr.pread(std.mem.asBytes(&offset), self.offset);
self.offset += 8;
var meta: MetadataReader(R) = .init(self.alloc, self.comp, self.rdr, offset);
_ = try meta.read(std.mem.sliceAsBytes(self.table[self.table.len - to_read ..]));
}
return self.table[idx];
}
};
}
+109
View File
@@ -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,
};
+61
View File
@@ -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;
}
+85
View File
@@ -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]);
}
+112
View File
@@ -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;
}
+38
View File
@@ -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,
};
}
+110
View File
@@ -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;
}
+40
View File
@@ -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);
}