71 Commits

Author SHA1 Message Date
Caleb J. Gardner 451e53264d Basic extraction complete 2025-08-16 01:25:38 -05:00
Caleb J. Gardner 99e3b81100 Re-writing extraction (again) 2025-08-15 06:48:55 -05:00
Caleb J. Gardner 1eceb8d899 Futher tweaks & fixes 2025-08-08 06:34:31 -05:00
Caleb Gardner b50f28026f STUFF 2025-08-07 21:45:03 -05:00
Caleb Gardner 1269d3e30d More work on data reader 2025-08-07 20:47:04 -05:00
Caleb J. Gardner b3f4a02b72 More work on the data decompilation 2025-08-01 20:04:09 -05:00
Caleb Gardner ebaacf57a3 MORE for data reader (only a bit) 2025-07-31 12:58:35 -05:00
Caleb Gardner 13f92f2e83 Re-writing data reader. Again. 2025-07-30 20:54:11 -05:00
Caleb Gardner 3fb95dd3fa Started work on data reader 2025-07-29 20:23:27 -05:00
Caleb J. Gardner 9871b0b2c0 Started re-work of data reader 2025-07-29 06:43:28 -05:00
Caleb Gardner 61d194e80a Moving data reader to arrays instead of slices 2025-07-25 06:42:22 -05:00
Caleb Gardner 8c44c77456 Merge pull request #1 from CalebQ42/reset-4
Reset 4
2025-07-25 06:07:55 -05:00
Caleb Gardner a96ad46a6c Things are working (though slowly) 2025-07-22 05:13:34 -05:00
Caleb Gardner d5c50b19f2 Further progress on extraction 2025-07-21 06:40:18 -05:00
Caleb Gardner 8998d28253 Data reader fixes
Re-started extraction logic
2025-07-19 16:40:54 -05:00
Caleb Gardner 9c1d90f60b Reset on extraction logic
Fixed not using MetadataReader for Tables
Fixed MetadataReaders not respecting uncompressed blocks
2025-07-19 06:30:24 -05:00
Caleb Gardner eb214feefa Further progress on extraction 2025-07-18 22:30:30 -05:00
Caleb Gardner de988f083f Finished (?) file extraction 2025-07-18 05:41:27 -05:00
Caleb Gardner b4af1233e5 Started work on file extraction 2025-07-17 22:17:58 -05:00
Caleb Gardner 4d52627d5d Further tweaks & fixes 2025-07-17 09:16:32 -05:00
Caleb Gardner d6b136bc8f Added basic test
Fixed various bugs
2025-07-17 03:50:09 -05:00
Caleb Gardner 87563e43a5 Finished (?) data reader 2025-07-17 02:24:57 -05:00
Caleb Gardner b0dced90bc MORE PROGRESS 2025-07-14 01:40:04 -05:00
Caleb Gardner 69d90242ba Further progress
Mainly focusing on getting things more consistent
2025-07-12 02:13:41 -05:00
Caleb Gardner 61c86c9fea Laying down some framework 2025-07-11 06:45:51 -05:00
Caleb Gardner 23687eabb0 Further progress in parsing format 2025-07-11 04:32:02 -05:00
Caleb Gardner 5c14b7db48 Try 4, lol. 2025-07-09 06:42:02 -05:00
Caleb Gardner 60e183512b (some) listing for zig-unsquashfs 2025-05-31 22:26:20 -05:00
Caleb Gardner fd1f83d855 Added uid & gid 2025-05-31 21:57:18 -05:00
Caleb Gardner d48ed4259e Added parent_path to Files
Should allow for (future) better path parsing
2025-05-31 21:49:09 -05:00
Caleb Gardner 8cc576a7fd Remove random .continue folder 2025-05-29 00:10:23 -05:00
Caleb Gardner 10304139e4 Fix DataReader
Don't use DataExtractor when set to only use 1 thread
2025-05-28 21:17:04 -05:00
Caleb Gardner 4af3e0373e Init data reader for Files
Fixed File.Size()
Don't use extractor for small files
2025-05-28 20:47:28 -05:00
Caleb Gardner 5be59be220 Increase buffer size for decompressTo 2025-05-28 02:49:53 -05:00
Caleb Gardner f122d1b4be Fixed the final, lingering issues.
Updated README
2025-05-28 00:57:12 -05:00
Caleb Gardner e4a6c32528 So close yet so far away 2025-05-27 22:26:58 -05:00
Caleb Gardner c057099591 Fragment data is currently broken, starting to fix 2025-05-27 16:17:20 -05:00
Caleb Gardner 17dbda3326 Nearly there...
Various tweaks & fixes to get extraction working properly
2025-05-27 14:21:40 -05:00
Caleb Gardner 985e2bd7e5 Extraction?
There's still a problem with fragments...
2025-05-26 07:06:03 -05:00
Caleb Gardner 7a4105bebd Added Zig-unsquashfs arg parsing.
Tweaks to build, including the ability to specifiy version
2025-05-26 04:05:53 -05:00
Caleb Gardner b0ecbe16bd Small tweaks 2025-05-25 08:22:33 -05:00
Caleb Gardner c4e2dab3f7 Fixed block/char/fifo types 2025-05-25 06:33:03 -05:00
Caleb Gardner dd452060cb Several fixes for File.extract
Tweaks and fixes.
2025-05-25 05:34:32 -05:00
Caleb Gardner 82011a092c Small tweaks and fixes 2025-05-24 23:30:23 -05:00
Caleb Gardner 3bfd262824 Tweak build 2025-05-24 14:35:53 -05:00
Caleb Gardner 6f02f9f14d Start work on File data extraction 2025-05-23 03:58:26 -05:00
Caleb Gardner 66f6cfa069 Added File.reader() 2025-05-22 06:27:38 -05:00
Caleb Gardner af06021b1b Added some doc 2025-05-21 21:38:05 -05:00
Caleb Gardner 213dfa8b92 Started on zig-unquashfs 2025-05-21 20:50:09 -05:00
Caleb Gardner e91d75458e Fixed table reading 2025-05-21 20:35:45 -05:00
Caleb Gardner 41a6b0d6f3 Mostly finished data reader 2025-05-20 09:15:00 -05:00
Caleb Gardner 128ed9f001 Started work on data readers 2025-05-19 09:49:23 -05:00
Caleb Gardner 1150b0d427 Fixed early file.deinit causing memory leaks 2025-05-19 06:08:43 -05:00
Caleb Gardner 1b0a0221c4 Now just for the memory leaks 2025-05-17 16:16:54 -05:00
Caleb Gardner 9f345e5fdb Almost there? 2025-05-17 14:33:16 -05:00
Caleb Gardner f77c2ecf48 Making things work properly 2025-05-17 09:05:24 -05:00
Caleb Gardner bbf3539dcf Messing around with thing.
Things not working.
2025-05-16 15:16:46 -05:00
Caleb Gardner 43295fb823 Finished? inodes 2025-05-16 11:02:59 -05:00
Caleb Gardner ff2ef6feaa INODES! 2025-05-16 06:59:21 -05:00
Caleb Gardner 986f308c60 Start over to clear my head 2025-05-16 05:55:40 -05:00
Caleb Gardner fc068fdbd9 Tweaking to get stuff workign 2025-05-15 05:59:38 -05:00
Caleb Gardner e010763fc6 Directory table parsing 2025-05-14 22:23:17 -05:00
Caleb Gardner 5daffdafc7 Proper zig project organization 2025-05-14 11:46:09 -05:00
Caleb Gardner 6dd3054006 Started work on File 2025-05-14 06:59:56 -05:00
Caleb Gardner b0c71c59f8 Inode parsing works! 2025-05-14 05:16:12 -05:00
Caleb Gardner 3684a958a0 Almost kind of working 2025-05-14 01:59:19 -05:00
Caleb Gardner a866804853 A bit more work on metadata reader 2025-05-13 22:29:45 -05:00
Caleb Gardner 246d63d48a A bit more messing around. 2025-05-13 10:24:17 -05:00
Caleb Gardner b4848de95d Further work on inode decoding 2025-05-13 00:23:53 -05:00
Caleb Gardner b3a5ff8f94 Starting work on inode decoding 2025-05-12 13:46:46 -05:00
Caleb Gardner 58e89c0981 Initial commit 2025-05-11 09:39:24 -05:00
54 changed files with 1602 additions and 3221 deletions
-31
View File
@@ -1,31 +0,0 @@
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
-25
View File
@@ -1,25 +0,0 @@
// 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",
],
},
]
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2026 Caleb Gardner
Copyright (c) 2025 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
+3 -71
View File
@@ -1,75 +1,7 @@
# zig-squashfs
This is my experiments to learn Zig. Might amount to something. Might not.
Messing around with zig via making a squashfs library. May amount to something. Or not.
A library and application to decompress or view squashfs archives.
## Current state
## 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;
```
Performance is reatively bad (when compared to the official [squashfs-tools](https://github.com/plougher/squashfs-tools), but the basics should fully work.
+27 -87
View File
@@ -1,113 +1,53 @@
const std = @import("std");
/// version if version isn't provided during build
const def_version = "0.0.0+testing";
pub fn build(b: *std.Build) !void {
const use_zig_decomp = b.option(bool, "use_zig_decomp", "Use zig standard library for decompression.") orelse false;
// const allow_lzo = b.option(bool, "allow_lzo", "Compile with lzo support") orelse false;
const debug = b.option(bool, "debug", "Enable options to make debugging easier.") orelse false;
const version_string_option = b.option([]const u8, "version", "Version of the library/binary");
const zig_squashfs_options = b.addOptions();
zig_squashfs_options.addOption(bool, "use_zig_decomp", use_zig_decomp);
// zig_squashfs_options.addOption(bool, "allow_lzo", allow_lzo);
const 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 target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const mod = b.addModule("zig_squashfs", .{
const lib_mod = b.createModule(.{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = if (debug == true) .Debug else optimize,
.link_libc = !use_zig_decomp,
.valgrind = debug,
.error_tracing = debug,
.strip = if (debug == true) false else null,
});
mod.addOptions("config", zig_squashfs_options);
if (!use_zig_decomp) {
var zlib_ng = b.dependency("zlib_ng", .{
.target = target,
.optimize = optimize,
});
mod.linkLibrary(zlib_ng.artifact("zng"));
mod.linkSystemLibrary("lzma", .{ .preferred_link_mode = .static });
var minilzo = b.dependency("minilzo", .{
.target = target,
.optimize = optimize,
const lib = b.addLibrary(.{
.linkage = .static,
.name = "zig_squashfs",
.root_module = lib_mod,
.version = sem_ver,
});
mod.linkLibrary(minilzo.artifact("minilzo"));
var lz4 = b.dependency("lz4", .{
.target = target,
.optimize = optimize,
});
mod.linkLibrary(lz4.artifact("lz4"));
var zstd = b.dependency("zstd", .{
.target = target,
.optimize = optimize,
});
mod.linkLibrary(zstd.artifact("zstd"));
}
var version = version_string_option orelse "0.0.0-testing";
if (version[0] == 'v') version = version[1..];
const unsquashfs_options = b.addOptions();
unsquashfs_options.addOption(
std.SemanticVersion,
"version",
try std.SemanticVersion.parse(version),
);
var exe_mod = b.createModule(.{
const exe_mod = b.createModule(.{
.root_source_file = b.path("src/bin/unsquashfs.zig"),
.target = target,
.optimize = if (debug == true) .Debug else optimize,
.link_libc = !use_zig_decomp,
.imports = &.{
.{ .name = "zig_squashfs", .module = mod },
},
.valgrind = debug,
.error_tracing = debug,
.strip = if (debug == true) false else null,
.optimize = optimize,
});
exe_mod.addOptions("config", unsquashfs_options);
exe_mod.addImport("squashfs", lib_mod);
exe_mod.addOptions("config", opt);
const exe = b.addExecutable(.{
.linkage = .static,
.name = "unsquashfs",
.root_module = exe_mod,
.use_llvm = debug,
});
const lib = b.addLibrary(.{
.name = "squashfs",
.root_module = mod,
.use_llvm = debug,
.version = sem_ver,
});
b.installArtifact(lib);
b.installArtifact(exe);
const mod_tests = b.addTest(.{
.root_module = mod,
.test_runner = .{
.mode = .simple,
.path = b.path("src/test.zig"),
},
const lib_unit_tests = b.addTest(.{
.root_module = lib_mod,
});
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 = mod,
});
const exe_check = b.addExecutable(.{
.name = "unsquashfs",
const exe_unit_test = b.addTest(.{
.root_module = exe_mod,
});
const check = b.step("check", "Check if unsquashfs compiles");
check.dependOn(&lib_check.step);
check.dependOn(&exe_check.step);
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);
}
+49 -20
View File
@@ -1,26 +1,55 @@
.{
.name = .squashfs,
.version = "0.0.6",
.fingerprint = 0x37ba29474b87f145, // Changing this has security and trust implications.
.minimum_zig_version = "0.15.2",
.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.
.dependencies = .{
.zlib_ng = .{
.url = "git+https://github.com/CalebQ42/zig-zlib-ng#5f2f02dfb28acca2517dacbbd09e9b987f57b133",
.hash = "zlib_ng-2.3.3-pre1-2HYS4ClFAABW8KlHMyBHtlNKE3V7kCS8wqfxawG7xeaa",
},
.zstd = .{
.url = "git+https://github.com/allyourcodebase/zstd.git?ref=1.5.7-1#e1a501be57f42c541e8a5597e4b59a074dfd09a3",
.hash = "zstd-1.5.7-1-KEItkAMwAAD6OKY3m0OOmXG7aL-aLUfrDqbP5J5oYapU",
},
.lz4 = .{
.url = "git+https://github.com/allyourcodebase/lz4.git?ref=1.10.0-6#41f52ab227caf9d48cf88c89a4d2946caa12b102",
.hash = "lz4-1.10.0-6-ewyzw-4NAAAWDpY4xpiqr4LQhZQAC0x_rGnW2iPh6jk2",
},
.minilzo = .{
.url = "git+https://github.com/CalebQ42/zig-minilzo.git#7cbae997b91a44d74b7cd6c073584dc9562a6c90",
.hash = "minilzo-2.10.0-Ij7BO8wLAADeWI4Pe4jp8XTDsDaquZR14oZ7_9yKKDWP",
},
// 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,
//},
},
// 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",
-10
View File
@@ -1,10 +0,0 @@
#!/bin/sh
zig test \
-lc \
-lz \
-llzma \
-lminilzo \
-llz4 \
-lzstd \
src/test.zig
-105
View File
@@ -1,105 +0,0 @@
//! A squashfs archive read from a file.
//! Can be used to directly access File's contents or extract to the filesystem.
const std = @import("std");
const File = std.fs.File;
const builtin = @import("builtin");
const config = @import("config");
const cDecomp = @import("decomp/misc_c.zig");
const Decomp = @import("decomp/zig_decomp.zig");
const ExtractionOptions = @import("options.zig");
const Inode = @import("inode.zig");
const InodeRef = Inode.Ref;
const BlockSize = @import("inode_data/file.zig").BlockSize;
const SfsFile = @import("file.zig");
const Superblock = @import("super.zig").Superblock;
const Tables = @import("tables.zig");
const MetadataReader = @import("util/metadata.zig");
const OffsetFile = @import("util/offset_file.zig");
const XattrTable = @import("xattr.zig");
const Archive = @This();
alloc: std.mem.Allocator,
fil: OffsetFile,
super: Superblock,
tables: ?Tables = null,
decomp: @import("decomp/types.zig").Decomp,
/// Default settings using std.Thread.getCpuCount() threads and the minimum of 4gb or half of system memory for memory usage.
pub fn init(alloc: std.mem.Allocator, fil: File, offset: u64) !Archive {
var super: Superblock = undefined;
const red = try fil.pread(@ptrCast(&super), offset);
std.debug.assert(red == @sizeOf(Superblock));
try super.validate();
const off_fil: OffsetFile = .init(fil, offset);
return .{
.alloc = alloc,
.fil = off_fil,
.decomp = if (config.use_zig_decomp)
switch (super.compression) {
.lz4 => return error.Lz4Unsupported,
.lzo => return error.LzoUnsupported,
.gzip => .{ .gzip = .{} },
.lzma => .{ .lzma = .{} },
.xz => .{ .xz = .{} },
.zstd => .{ .zstd = .{} },
}
else switch (super.compression) {
.gzip => .{ .gzip = .init(alloc) },
.zstd => .{ .zstd = .init(alloc) },
.xz => .{ .xz = .init(alloc) },
.lzma => .{ .lzma = .init(alloc) },
.lzo => .{ .lzo = .{} },
.lz4 => .{ .lz4 = .{} },
},
.super = super,
};
}
pub fn deinit(self: *Archive) void {
if (self.tables != null)
self.tables.?.deinit();
self.decomp.deinit();
}
pub fn inode(self: *Archive, alloc: std.mem.Allocator, num: u32) !Inode {
if (self.tables == null)
self.tables = try .init(alloc, self);
const ref = try self.export_table.get(num - 1);
var rdr = try self.fil.readerAt(ref.block_start + self.super.inode_start, &[0]u8{});
var meta: MetadataReader = .init(alloc, &rdr.interface, &self.decomp);
try meta.interface.discardAll(ref.block_offset);
return try .read(alloc, &meta.interface, self.super.block_size);
}
pub fn root(self: *Archive, alloc: std.mem.Allocator) !SfsFile {
if (self.tables == null)
self.tables = try .init(alloc, self);
var rdr = try self.fil.readerAt(self.super.root_ref.block_start + self.super.inode_start, &[0]u8{});
var meta: MetadataReader = .init(alloc, &rdr.interface, self.decomp);
try meta.interface.discardAll(self.super.root_ref.block_offset);
const in: Inode = try .read(alloc, &meta.interface, self.super.block_size);
return .init(self, in, "");
}
pub fn open(self: *Archive, alloc: std.mem.Allocator, path: []const u8) !SfsFile {
if (self.tables == null)
self.tables = try .init(alloc, self);
var root_fil = try self.root(alloc);
defer if (!SfsFile.pathIsSelf(path)) root_fil.deinit();
return root_fil.open(path);
}
pub fn extract(self: Archive, alloc: std.mem.Allocator, path: []const u8, options: ExtractionOptions) !void {
var rdr = try self.fil.readerAt(self.super.root_ref.block_start + self.super.inode_start, &[0]u8{});
var meta: MetadataReader = .init(self.alloc, &rdr.interface, self.decomp);
try meta.interface.discardAll(self.super.root_ref.block_offset);
const in: Inode = try .read(self.alloc, &meta.interface, self.super.block_size);
try in.extractTo(alloc, self, path, options);
}
+119 -106
View File
@@ -1,133 +1,146 @@
const std = @import("std");
const Writer = std.Io.Writer;
const builtin = @import("builtin");
const config = @import("config");
const squashfs = @import("zig_squashfs");
const squashfs = @import("squashfs");
//TODO: Add more options
const help_mgs =
const help_msg =
\\Basic Usage: zig-unsquashfs [Options] SQUASHFS_FILE <EXTRACT_LOCATION>
\\
\\Usage: unsquashfs [options] <archive>
\\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.
\\
\\Options:
\\ -d <location> Extract to the given location instead of "squashfs-root"
\\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.
\\
\\ -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)
\\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.
\\
\\ -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
\\Other:
\\ --help Prints this help message.
\\ -h Same as --help
\\ --version Print version number.
\\
;
const errors = error{InvalidArguments};
const stdout = std.io.getStdOut();
var archive: []const u8 = "";
var extLoc: []const u8 = "squashfs-root";
var extr_files: std.ArrayList([]const u8) = undefined;
var offset: u64 = 0;
var threads: u32 = 0;
var verbose: bool = false;
var ignore_xattrs: bool = false;
var ignore_permissions: bool = false;
var force: bool = false;
var unbreak: bool = false;
var deref: bool = false;
var processors: u16 = 0;
var list: ListTypes = .None;
var filename: []const u8 = "";
var extr_location: []const u8 = "";
const ListTypes = enum {
None,
List,
ListAttr,
ListNumeric,
};
pub fn main() !void {
const alloc = std.heap.smp_allocator;
var stdout = std.fs.File.stdout();
var out = stdout.writer(&[0]u8{});
defer out.interface.flush() catch {};
try handleArgs(alloc, &out.interface);
if (archive.len == 0) {
try out.interface.print("You must provide a squashfs archive\n", .{});
try out.interface.print(help_mgs, .{});
extr_files = .init(alloc);
defer extr_files.deinit();
var args = std.process.argsWithAllocator(alloc) catch {
_ = try stdout.writeAll("Unable to allocate memory");
return;
}
var fil: std.fs.File = try std.fs.cwd().openFile(archive, .{}); //TODO: Handle error gracefully.
defer fil.close();
var arc: squashfs.Archive = try .init(alloc, fil, offset); //TODO: Update when memory size matters. //TODO: Handle error gracefully.
defer arc.deinit();
const options: squashfs.ExtractionOptions = .{
.threads = if (threads == 0) try std.Thread.getCpuCount() else threads,
.verbose = verbose,
.verbose_writer = if (verbose) &out.interface else null,
.ignore_xattr = ignore_xattrs,
.ignore_permissions = ignore_permissions,
};
if (force)
try std.fs.cwd().deleteTree(extLoc);
try arc.extract(alloc, extLoc, options); //TODO: Handle error gracefully.
}
fn handleArgs(alloc: std.mem.Allocator, out: *Writer) !void {
var args = try std.process.argsWithAllocator(alloc);
defer args.deinit();
_ = args.next(); // args[0] is the application launch command.
_ = args.next();
while (args.next()) |arg| {
if (std.mem.eql(u8, arg, "-o")) {
const nxt = args.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 = args.next();
if (nxt == null or nxt.?.len == 0) {
try out.print("-d must be followed by a location\n", .{});
return errors.InvalidArguments;
}
extLoc = nxt.?;
continue;
} else if (std.mem.eql(u8, arg, "-p")) {
const nxt = args.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;
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;
} else if (std.mem.eql(u8, arg, "-v")) {
verbose = true;
continue;
} else if (std.mem.eql(u8, arg, "-dx")) {
ignore_xattrs = true;
continue;
} else if (std.mem.eql(u8, arg, "-dp")) {
ignore_permissions = true;
continue;
} else if (std.mem.eql(u8, arg, "--force")) {
force = true;
continue;
} else if (std.mem.eql(u8, arg, "--version")) {
try out.print("zig-unsquashfs v", .{});
try config.version.format(out);
try out.print("\nBuilt using Zig {s} in {} mode\n", .{ builtin.zig_version_string, builtin.mode });
std.process.exit(0);
return;
} else if (std.mem.eql(u8, arg, "--help")) {
try out.print(help_mgs, .{});
std.process.exit(0);
} 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;
}
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;
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;
}
archive = arg;
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");
return;
}
}
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;
};
}
-8
View File
@@ -1,8 +0,0 @@
pub const c = @cImport({
@cInclude("zlib-ng.h");
@cInclude("lzma.h");
@cInclude("lz4.h");
@cInclude("zstd.h");
@cInclude("zstd_errors.h");
@cInclude("lzo/minilzo.h");
});
-25
View File
@@ -1,25 +0,0 @@
const std = @import("std");
const Decompressor = @This();
pub const Error = error{
OutOfMemory,
BadInput,
OutputTooSmall,
ReadFailed,
WriteFailed,
EndOfStream,
};
vtable: *const struct {
decompress: *const fn (*Decompressor, []u8, []u8) Error!usize = DefaultDecompress,
stateless: *const fn (std.mem.Allocator, []u8, []u8) Error!usize,
},
pub fn decompress(self: *Decompressor, in: []u8, out: []u8) Error!usize {
return self.vtable.decompress(self, in, out);
}
fn DefaultDecompress(self: *Decompressor, in: []u8, out: []u8) Error!usize {
return self.vtable.stateless(std.heap.smp_allocator, in, out);
}
-14
View File
@@ -1,14 +0,0 @@
const std = @import("std");
const c = @import("../../c.zig").c;
const Decompressor = @import("../../decomp.zig");
const Self = @This();
interface: Decompressor = .{ .vtable = &.{ .stateless = stateless } },
fn stateless(_: std.mem.Allocator, in: []u8, out: []u8) Decompressor.Error!usize {
const res = c.LZ4_decompress_fast(in.ptr, out.ptr, @intCast(out.len));
if (res < 0) return Decompressor.Error.ReadFailed;
return @abs(res);
}
-127
View File
@@ -1,127 +0,0 @@
const std = @import("std");
const c = @import("../../c.zig").c;
const Decompressor = @import("../../decomp.zig");
const Self = @This();
alloc: std.mem.Allocator,
streams: std.AutoHashMap(std.Thread.Id, c.lzma_stream),
interface: Decompressor,
pub fn init(alloc: std.mem.Allocator) Self {
return .{
.alloc = alloc,
.streams = .init(alloc),
.interface = .{
.vtable = &.{
.decompress = decompress,
.stateless = stateless,
},
},
};
}
pub fn deinit(self: *Self) void {
self.streams.deinit();
}
fn decompress(decomp: *Decompressor, in: []u8, out: []u8) Decompressor.Error!usize {
var self: *Self = @fieldParentPtr("interface", decomp);
var strm = try self.getOrCreate();
strm.next_in = in.ptr;
strm.avail_in = in.len;
strm.next_out = out.ptr;
strm.avail_out = out.len;
var res = c.lzma_alone_decoder(strm, out.len * 2);
decodeResult(res) catch |err| return lzmaErrToDecompErr(err);
while (res == c.LZMA_OK)
res = c.lzma_code(strm, c.LZMA_RUN);
decodeResult(res) catch |err| return lzmaErrToDecompErr(err);
return strm.total_out;
}
fn stateless(alloc: std.mem.Allocator, in: []u8, out: []u8) Decompressor.Error!usize {
var strm: c.lzma_stream = .{
.allocator = &.{
.alloc = lzmaAlloc,
.free = lzmaFree,
.@"opaque" = @ptrCast(@constCast(&alloc)),
},
.next_in = in.ptr,
.avail_in = in.len,
.next_out = out.ptr,
.avail_out = out.len,
};
var res = c.lzma_alone_decoder(&strm, out.len * 2);
decodeResult(res) catch |err| return lzmaErrToDecompErr(err);
while (res == c.LZMA_OK)
res = c.lzma_code(&strm, c.LZMA_RUN);
decodeResult(res) catch |err| return lzmaErrToDecompErr(err);
return strm.total_out;
}
inline fn getOrCreate(self: *Self) !*c.lzma_stream {
const res = try self.streams.getOrPut(std.Thread.getCurrentId());
if (res.found_existing) return res.value_ptr;
res.value_ptr.* = .{ .allocator = &.{
.alloc = lzmaAlloc,
.free = lzmaFree,
.@"opaque" = @ptrCast(&self.alloc),
} };
return res.value_ptr;
}
inline fn decodeResult(res: usize) Error!void {
return switch (res) {
c.LZMA_OK, c.LZMA_STREAM_END => {},
c.LZMA_NO_CHECK => Error.NoCheck,
c.LZMA_UNSUPPORTED_CHECK => Error.UnsupportedCheck,
c.LZMA_GET_CHECK => Error.GetCheck,
c.LZMA_MEM_ERROR, c.LZMA_MEMLIMIT_ERROR => Error.OutOfMemory,
c.LZMA_FORMAT_ERROR => Error.Format,
c.LZMA_OPTIONS_ERROR => Error.Options,
c.LZMA_DATA_ERROR => Error.Data,
c.LZMA_BUF_ERROR => Error.Buffer,
c.LZMA_PROG_ERROR => Error.Program,
c.LZMA_SEEK_NEEDED => Error.SeekNeeded,
else => Error.Unknown,
};
}
inline fn lzmaErrToDecompErr(err: Error) Decompressor.Error {
return switch (err) {
Error.OutOfMemory => Decompressor.Error.OutOfMemory,
Error.NoCheck => Decompressor.Error.ReadFailed,
Error.UnsupportedCheck => Decompressor.Error.ReadFailed,
Error.GetCheck => Decompressor.Error.ReadFailed,
Error.Format => Decompressor.Error.ReadFailed,
Error.Options => Decompressor.Error.ReadFailed,
Error.Data => Decompressor.Error.ReadFailed,
Error.Buffer => Decompressor.Error.WriteFailed,
Error.Program => Decompressor.Error.ReadFailed,
Error.SeekNeeded => Decompressor.Error.ReadFailed,
else => Decompressor.Error.ReadFailed,
};
}
fn lzmaAlloc(ptr: ?*anyopaque, size: usize, _: usize) callconv(.c) ?*anyopaque {
var alloc: *std.mem.Allocator = @ptrCast(@alignCast(@constCast(ptr)));
return alloc.rawAlloc(size, .@"1", 0);
}
fn lzmaFree(ptr: ?*anyopaque, mem_ptr: ?*anyopaque) callconv(.c) void {
var alloc: *std.mem.Allocator = @ptrCast(@alignCast(@constCast(ptr)));
alloc.rawFree(@ptrCast(mem_ptr), .@"1", 0);
}
const Error = error{
OutOfMemory,
NoCheck,
UnsupportedCheck,
GetCheck,
Format,
Options,
Data,
Buffer,
Program,
SeekNeeded,
Unknown,
};
-67
View File
@@ -1,67 +0,0 @@
const std = @import("std");
const c = @import("../../c.zig").c;
const Decompressor = @import("../../decomp.zig");
const Self = @This();
interface: Decompressor = .{ .vtable = &.{ .stateless = stateless } },
fn stateless(_: std.mem.Allocator, in: []u8, out: []u8) Decompressor.Error!usize {
var out_len: usize = out.len;
const res = c.lzo1x_decompress(in.ptr, in.len, out.ptr, &out_len, null);
decodeError(res) catch |err| return lzoErrToDecompErr(err);
return out_len;
}
inline fn decodeError(res: c_int) Error!void {
return switch (res) {
c.LZO_E_OK => {},
c.LZO_E_EOF_NOT_FOUND => Error.EofNotFound,
c.LZO_E_INPUT_NOT_CONSUMED => Error.InputNotConsumed,
c.LZO_E_INPUT_OVERRUN => Error.InputOverrun,
c.LZO_E_INTERNAL_ERROR => Error.InternalError,
c.LZO_E_INVALID_ALIGNMENT => Error.InvalidAlignment,
c.LZO_E_INVALID_ARGUMENT => Error.InvalidArgument,
c.LZO_E_LOOKBEHIND_OVERRUN => Error.LookbehindOverrun,
c.LZO_E_NOT_COMPRESSIBLE => Error.NotCompressible,
c.LZO_E_NOT_YET_IMPLEMENTED => Error.NotYetImplemented,
c.LZO_E_OUTPUT_NOT_CONSUMED => Error.OutputNotConsumed,
c.LZO_E_OUTPUT_OVERRUN => Error.OutputOverrun,
c.LZO_E_OUT_OF_MEMORY => Error.OutOfMemory,
else => Error.Unknown,
};
}
inline fn lzoErrToDecompErr(err: Error) Decompressor.Error {
return switch (err) {
Error.EofNotFound => Decompressor.Error.ReadFailed,
Error.InputNotConsumed => Decompressor.Error.ReadFailed,
Error.InputOverrun => Decompressor.Error.ReadFailed,
Error.InternalError => Decompressor.Error.ReadFailed,
Error.InvalidAlignment => Decompressor.Error.ReadFailed,
Error.InvalidArgument => Decompressor.Error.ReadFailed,
Error.LookbehindOverrun => Decompressor.Error.ReadFailed,
Error.NotCompressible => Decompressor.Error.ReadFailed,
Error.NotYetImplemented => Decompressor.Error.ReadFailed,
Error.OutputNotConsumed => Decompressor.Error.WriteFailed,
Error.OutputOverrun => Decompressor.Error.WriteFailed,
Error.OutOfMemory => Decompressor.Error.OutOfMemory,
else => Decompressor.Error.ReadFailed,
};
}
const Error = error{
EofNotFound,
InputNotConsumed,
InputOverrun,
InternalError,
InvalidAlignment,
InvalidArgument,
LookbehindOverrun,
NotCompressible,
NotYetImplemented,
OutputNotConsumed,
OutputOverrun,
OutOfMemory,
Unknown,
};
-127
View File
@@ -1,127 +0,0 @@
const std = @import("std");
const c = @import("../../c.zig").c;
const Decompressor = @import("../../decomp.zig");
const Self = @This();
alloc: std.mem.Allocator,
streams: std.AutoHashMap(std.Thread.Id, c.lzma_stream),
interface: Decompressor,
pub fn init(alloc: std.mem.Allocator) Self {
return .{
.alloc = alloc,
.streams = .init(alloc),
.interface = .{
.vtable = &.{
.decompress = decompress,
.stateless = stateless,
},
},
};
}
pub fn deinit(self: *Self) void {
self.streams.deinit();
}
fn decompress(decomp: *Decompressor, in: []u8, out: []u8) Decompressor.Error!usize {
var self: *Self = @fieldParentPtr("interface", decomp);
var strm = try self.getOrCreate();
strm.next_in = in.ptr;
strm.avail_in = in.len;
strm.next_out = out.ptr;
strm.avail_out = out.len;
var res = c.lzma_stream_decoder(strm, out.len * 2, 0);
decodeResult(res) catch |err| return lzmaErrToDecompErr(err);
while (res == c.LZMA_OK)
res = c.lzma_code(strm, c.LZMA_RUN);
decodeResult(res) catch |err| return lzmaErrToDecompErr(err);
return strm.total_out;
}
fn stateless(alloc: std.mem.Allocator, in: []u8, out: []u8) Decompressor.Error!usize {
var strm: c.lzma_stream = .{
.allocator = &.{
.alloc = lzmaAlloc,
.free = lzmaFree,
.@"opaque" = @ptrCast(@constCast(&alloc)),
},
.next_in = in.ptr,
.avail_in = in.len,
.next_out = out.ptr,
.avail_out = out.len,
};
var res = c.lzma_stream_decoder(&strm, out.len * 2, 0);
decodeResult(res) catch |err| return lzmaErrToDecompErr(err);
while (res == c.LZMA_OK)
res = c.lzma_code(&strm, c.LZMA_RUN);
decodeResult(res) catch |err| return lzmaErrToDecompErr(err);
return strm.total_out;
}
inline fn getOrCreate(self: *Self) !*c.lzma_stream {
const res = try self.streams.getOrPut(std.Thread.getCurrentId());
if (res.found_existing) return res.value_ptr;
res.value_ptr.* = .{ .allocator = &.{
.alloc = lzmaAlloc,
.free = lzmaFree,
.@"opaque" = @ptrCast(&self.alloc),
} };
return res.value_ptr;
}
inline fn decodeResult(res: usize) Error!void {
return switch (res) {
c.LZMA_OK, c.LZMA_STREAM_END => {},
c.LZMA_NO_CHECK => Error.NoCheck,
c.LZMA_UNSUPPORTED_CHECK => Error.UnsupportedCheck,
c.LZMA_GET_CHECK => Error.GetCheck,
c.LZMA_MEM_ERROR, c.LZMA_MEMLIMIT_ERROR => Error.OutOfMemory,
c.LZMA_FORMAT_ERROR => Error.Format,
c.LZMA_OPTIONS_ERROR => Error.Options,
c.LZMA_DATA_ERROR => Error.Data,
c.LZMA_BUF_ERROR => Error.Buffer,
c.LZMA_PROG_ERROR => Error.Program,
c.LZMA_SEEK_NEEDED => Error.SeekNeeded,
else => Error.Unknown,
};
}
inline fn lzmaErrToDecompErr(err: Error) Decompressor.Error {
return switch (err) {
Error.OutOfMemory => Decompressor.Error.OutOfMemory,
Error.NoCheck => Decompressor.Error.ReadFailed,
Error.UnsupportedCheck => Decompressor.Error.ReadFailed,
Error.GetCheck => Decompressor.Error.ReadFailed,
Error.Format => Decompressor.Error.ReadFailed,
Error.Options => Decompressor.Error.ReadFailed,
Error.Data => Decompressor.Error.ReadFailed,
Error.Buffer => Decompressor.Error.WriteFailed,
Error.Program => Decompressor.Error.ReadFailed,
Error.SeekNeeded => Decompressor.Error.ReadFailed,
else => Decompressor.Error.ReadFailed,
};
}
fn lzmaAlloc(ptr: ?*anyopaque, size: usize, _: usize) callconv(.c) ?*anyopaque {
var alloc: *std.mem.Allocator = @ptrCast(@alignCast(@constCast(ptr)));
return alloc.rawAlloc(size, .@"1", 0);
}
fn lzmaFree(ptr: ?*anyopaque, mem_ptr: ?*anyopaque) callconv(.c) void {
var alloc: *std.mem.Allocator = @ptrCast(@alignCast(@constCast(ptr)));
alloc.rawFree(@ptrCast(mem_ptr), .@"1", 0);
}
const Error = error{
OutOfMemory,
NoCheck,
UnsupportedCheck,
GetCheck,
Format,
Options,
Data,
Buffer,
Program,
SeekNeeded,
Unknown,
};
-112
View File
@@ -1,112 +0,0 @@
const std = @import("std");
const c = @import("../../c.zig").c;
const Decompressor = @import("../../decomp.zig");
const Self = @This();
alloc: std.mem.Allocator,
streams: std.AutoHashMap(std.Thread.Id, c.zng_stream),
interface: Decompressor,
pub fn init(alloc: std.mem.Allocator) Self {
return .{
.alloc = alloc,
.streams = .init(alloc),
.interface = .{
.vtable = &.{
.decompress = decompress,
.stateless = stateless,
},
},
};
}
pub fn deinit(self: *Self) void {
self.streams.deinit();
}
fn decompress(decomp: *Decompressor, in: []u8, out: []u8) Decompressor.Error!usize {
const self: *Self = @fieldParentPtr("interface", decomp);
var strm = try self.getOrCreate();
strm.next_in = in.ptr;
strm.avail_in = @truncate(in.len);
strm.next_out = out.ptr;
strm.total_out = out.len;
var res = c.zng_inflateReset(strm);
decodeError(res) catch |err| return zlibErrToDecompErr(err);
res = c.zng_inflate(strm, c.Z_FULL_FLUSH);
decodeError(res) catch |err| return zlibErrToDecompErr(err);
return strm.total_out;
}
fn stateless(alloc: std.mem.Allocator, in: []u8, out: []u8) Decompressor.Error!usize {
var strm: c.zng_stream = .{
.zalloc = zalloc,
.zfree = zfree,
.@"opaque" = @constCast(&alloc),
.next_in = in.ptr,
.avail_in = @truncate(in.len),
.next_out = out.ptr,
.total_out = out.len,
};
var res = c.zng_inflateInit(&strm);
decodeError(res) catch |err| return zlibErrToDecompErr(err);
res = c.zng_inflate(&strm, c.Z_FULL_FLUSH);
decodeError(res) catch |err| return zlibErrToDecompErr(err);
return strm.total_out;
}
fn getOrCreate(self: *Self) !*c.zng_stream {
const res = try self.streams.getOrPut(std.Thread.getCurrentId());
if (res.found_existing) return res.value_ptr;
res.value_ptr.* = .{
.zalloc = zalloc,
.zfree = zfree,
.@"opaque" = &self.alloc,
};
return res.value_ptr;
}
fn zalloc(ptr: ?*anyopaque, size: c_uint, len: c_uint) callconv(.c) ?*anyopaque {
var alloc: *std.mem.Allocator = @ptrCast(@alignCast(@constCast(ptr)));
return alloc.rawAlloc(size * len, .@"1", 0);
}
fn zfree(ptr: ?*anyopaque, mem_ptr: ?*anyopaque) callconv(.c) void {
var alloc: *std.mem.Allocator = @ptrCast(@alignCast(@constCast(ptr)));
alloc.rawFree(@ptrCast(mem_ptr), .@"1", 0);
}
inline fn decodeError(res: i32) Error!void {
if (res >= 0) return;
return switch (res) {
c.Z_STREAM_ERROR => Error.Stream,
c.Z_DATA_ERROR => Error.Data,
c.Z_MEM_ERROR => Error.OutOfMemory,
c.Z_BUF_ERROR => Error.Buffer,
c.Z_VERSION_ERROR => Error.Version,
else => Error.Misc,
};
}
inline fn zlibErrToDecompErr(err: Error) Decompressor.Error {
return switch (err) {
Error.OutOfMemory => Decompressor.Error.OutOfMemory,
Error.Misc => Decompressor.Error.ReadFailed,
Error.Stream => Decompressor.Error.ReadFailed,
Error.Data => Decompressor.Error.ReadFailed,
Error.Buffer => Decompressor.Error.WriteFailed,
Error.Version => Decompressor.Error.ReadFailed,
};
}
const Error = error{
OutOfMemory,
Misc,
Stream,
Data,
Buffer,
Version,
};
-165
View File
@@ -1,165 +0,0 @@
const std = @import("std");
const c = @import("../../c.zig").c;
const Decompressor = @import("../../decomp.zig");
const Self = @This();
alloc: std.mem.Allocator,
ctx: std.AutoHashMap(std.Thread.Id, ?*c.ZSTD_DCtx),
interface: Decompressor,
pub fn init(alloc: std.mem.Allocator) Self {
return .{
.alloc = alloc,
.ctx = .init(alloc),
.interface = .{
.vtable = &.{
.decompress = decompress,
.stateless = stateless,
},
},
};
}
pub fn deinit(self: *Self) void {
self.ctx.deinit();
}
fn decompress(decomp: *Decompressor, in: []u8, out: []u8) Decompressor.Error!usize {
var self: *Self = @fieldParentPtr("interface", decomp);
const ctx = try self.getOrCreate();
const res = c.ZSTD_decompressDCtx(ctx, out.ptr, out.len, in.ptr, in.len);
decodeError(res) catch |err| return zstdErrToDecompErr(err);
return res;
}
fn stateless(_: std.mem.Allocator, in: []u8, out: []u8) Decompressor.Error!usize {
const res = c.ZSTD_decompress(out.ptr, out.len, in.ptr, in.len);
decodeError(res) catch |err| return zstdErrToDecompErr(err);
return res;
}
inline fn getOrCreate(self: *Self) !?*c.ZSTD_DCtx {
const res = try self.ctx.getOrPut(std.Thread.getCurrentId());
if (res.found_existing) return res.value_ptr.*;
res.value_ptr.* = c.ZSTD_createDCtx();
return res.value_ptr.*;
}
inline fn decodeError(res: usize) Error!void {
if (c.ZSTD_isError(res) == 0) return;
return switch (c.ZSTD_getErrorCode(res)) {
c.ZSTD_error_prefix_unknown => Error.PrefixUnknown,
c.ZSTD_error_version_unsupported => Error.VersionUnsupported,
c.ZSTD_error_frameParameter_unsupported => Error.FrameParameterUnsupported,
c.ZSTD_error_frameParameter_windowTooLarge => Error.FrameParameterWindowTooLarge,
c.ZSTD_error_corruption_detected => Error.CorruptionDetected,
c.ZSTD_error_checksum_wrong => Error.ChecksumWrong,
c.ZSTD_error_literals_headerWrong => Error.LiteralsHeaderWrong,
c.ZSTD_error_dictionary_corrupted => Error.DictionaryCorrupted,
c.ZSTD_error_dictionary_wrong => Error.DictionaryWrong,
c.ZSTD_error_dictionaryCreation_failed => Error.DictionaryCreationFailed,
c.ZSTD_error_parameter_unsupported => Error.ParameterUnsupported,
c.ZSTD_error_parameter_combination_unsupported => Error.ParameterCombinationUnsupported,
c.ZSTD_error_parameter_outOfBound => Error.ParameterOutOfBound,
c.ZSTD_error_tableLog_tooLarge => Error.TableLogTooLarge,
c.ZSTD_error_maxSymbolValue_tooLarge => Error.MaxSymbolValueTooLarge,
c.ZSTD_error_maxSymbolValue_tooSmall => Error.MaxSymbolValueTooSmall,
c.ZSTD_error_cannotProduce_uncompressedBlock => Error.CannotProduceUncompressedBlock,
c.ZSTD_error_stabilityCondition_notRespected => Error.StabilityConditionNotRespected,
c.ZSTD_error_stage_wrong => Error.StageWrong,
c.ZSTD_error_init_missing => Error.InitMissing,
c.ZSTD_error_memory_allocation => Error.MemoryAllocation,
c.ZSTD_error_workSpace_tooSmall => Error.WorkSpaceTooSmall,
c.ZSTD_error_dstSize_tooSmall => Error.DstSizeTooSmall,
c.ZSTD_error_srcSize_wrong => Error.SrcSizeWrong,
c.ZSTD_error_dstBuffer_null => Error.DstBufferNull,
c.ZSTD_error_noForwardProgress_destFull => Error.NoForwardProgressDestFull,
c.ZSTD_error_noForwardProgress_inputEmpty => Error.NoForwardProgressInputEmpty,
c.ZSTD_error_frameIndex_tooLarge => Error.FrameIndexTooLarge,
c.ZSTD_error_seekableIO => Error.SeekableIo,
c.ZSTD_error_dstBuffer_wrong => Error.DstBufferWrong,
c.ZSTD_error_srcBuffer_wrong => Error.SrcBufferWrong,
c.ZSTD_error_sequenceProducer_failed => Error.SequenceProducerFailed,
c.ZSTD_error_externalSequences_invalid => Error.ExternalSequencesInvalid,
else => Error.Generic,
};
}
inline fn zstdErrToDecompErr(err: Error) Decompressor.Error {
return switch (err) {
Error.OutOfMemory => Decompressor.Error.OutOfMemory,
Error.Generic => Decompressor.Error.ReadFailed,
Error.PrefixUnknown => Decompressor.Error.ReadFailed,
Error.VersionUnsupported => Decompressor.Error.ReadFailed,
Error.FrameParameterUnsupported => Decompressor.Error.ReadFailed,
Error.FrameParameterWindowTooLarge => Decompressor.Error.ReadFailed,
Error.CorruptionDetected => Decompressor.Error.ReadFailed,
Error.ChecksumWrong => Decompressor.Error.ReadFailed,
Error.LiteralsHeaderWrong => Decompressor.Error.ReadFailed,
Error.DictionaryCorrupted => Decompressor.Error.ReadFailed,
Error.DictionaryWrong => Decompressor.Error.ReadFailed,
Error.DictionaryCreationFailed => Decompressor.Error.ReadFailed,
Error.ParameterUnsupported => Decompressor.Error.ReadFailed,
Error.ParameterCombinationUnsupported => Decompressor.Error.ReadFailed,
Error.ParameterOutOfBound => Decompressor.Error.ReadFailed,
Error.TableLogTooLarge => Decompressor.Error.ReadFailed,
Error.MaxSymbolValueTooLarge => Decompressor.Error.ReadFailed,
Error.MaxSymbolValueTooSmall => Decompressor.Error.ReadFailed,
Error.CannotProduceUncompressedBlock => Decompressor.Error.ReadFailed,
Error.StabilityConditionNotRespected => Decompressor.Error.ReadFailed,
Error.StageWrong => Decompressor.Error.ReadFailed,
Error.InitMissing => Decompressor.Error.ReadFailed,
Error.MemoryAllocation => Decompressor.Error.OutOfMemory,
Error.WorkSpaceTooSmall => Decompressor.Error.WriteFailed,
Error.DstSizeTooSmall => Decompressor.Error.WriteFailed,
Error.SrcSizeWrong => Decompressor.Error.ReadFailed,
Error.DstBufferNull => Decompressor.Error.WriteFailed,
Error.NoForwardProgressDestFull => Decompressor.Error.WriteFailed,
Error.NoForwardProgressInputEmpty => Decompressor.Error.ReadFailed,
Error.FrameIndexTooLarge => Decompressor.Error.ReadFailed,
Error.SeekableIo => Decompressor.Error.ReadFailed,
Error.DstBufferWrong => Decompressor.Error.WriteFailed,
Error.SrcBufferWrong => Decompressor.Error.ReadFailed,
Error.SequenceProducerFailed => Decompressor.Error.ReadFailed,
Error.ExternalSequencesInvalid => Decompressor.Error.ReadFailed,
};
}
const Error = error{
OutOfMemory,
Generic,
PrefixUnknown,
VersionUnsupported,
FrameParameterUnsupported,
FrameParameterWindowTooLarge,
CorruptionDetected,
ChecksumWrong,
LiteralsHeaderWrong,
DictionaryCorrupted,
DictionaryWrong,
DictionaryCreationFailed,
ParameterUnsupported,
ParameterCombinationUnsupported,
ParameterOutOfBound,
TableLogTooLarge,
MaxSymbolValueTooLarge,
MaxSymbolValueTooSmall,
CannotProduceUncompressedBlock,
StabilityConditionNotRespected,
StageWrong,
InitMissing,
MemoryAllocation,
WorkSpaceTooSmall,
DstSizeTooSmall,
SrcSizeWrong,
DstBufferNull,
NoForwardProgressDestFull,
NoForwardProgressInputEmpty,
FrameIndexTooLarge,
SeekableIo,
DstBufferWrong,
SrcBufferWrong,
SequenceProducerFailed,
ExternalSequencesInvalid,
};
-43
View File
@@ -1,43 +0,0 @@
const config = @import("config");
const Decompressor = @import("../decomp.zig");
const cLz4 = @import("c/lz4.zig");
const cLzma = @import("c/lzma.zig");
const cLzo = @import("c/lzo.zig");
const cXz = @import("c/xz.zig");
const cZlib = @import("c/zlib.zig");
const cZstd = @import("c/zstd.zig");
const zigLzma = @import("zig/lzma.zig");
const zigXz = @import("zig/xz.zig");
const zigZlib = @import("zig/zstd.zig");
const zigZstd = @import("zig/zstd.zig");
pub const Decomp = union(enum) {
gzip: if (config.use_zig_decomp) zigZlib else cZlib,
lzma: if (config.use_zig_decomp) zigLzma else cLzma,
lzo: if (config.use_zig_decomp) void else cLzo,
xz: if (config.use_zig_decomp) zigXz else cXz,
lz4: if (config.use_zig_decomp) void else cLz4,
zstd: if (config.use_zig_decomp) zigZstd else cZstd,
pub fn deinit(self: *Decomp) void {
switch (self) {
.gzip => self.gzip.deinit(),
.lzma => self.lzma.deinit(),
.xz => self.xz.deinit(),
.zstd => self.zstd.deinit(),
else => {},
}
}
pub fn decompressor(self: *Decomp) *Decompressor {
return switch (self) {
.gzip => &self.gzip.interface,
.lzma => &self.lzma.interface,
.lzo => &self.lzo.interface,
.xz => &self.xz.interface,
.lz4 => &self.lz4.interface,
.zstd => &self.zstd.interface,
};
}
};
-19
View File
@@ -1,19 +0,0 @@
const std = @import("std");
const lzma = std.compress.lzma;
const Reader = std.Io.Reader;
const Decompressor = @import("../../decomp.zig");
const Self = @This();
interface: Decompressor = .{ .vtable = &.{ .stateless = stateless } },
fn stateless(alloc: std.mem.Allocator, in: []u8, out: []u8) Decompressor.Error!usize {
var rdr: Reader = .fixed(in);
var decomp = try lzma.decompress(alloc, rdr.adaptToOldInterface());
defer decomp.deinit();
return decomp.read(out) catch |err| switch (err) {
error.CorruptInput, error.EndOfStream, error.Overflow => return Decompressor.Error.ReadFailed,
else => return err,
};
}
-25
View File
@@ -1,25 +0,0 @@
const std = @import("std");
const xz = std.compress.xz;
const Reader = std.Io.Reader;
const Decompressor = @import("../../decomp.zig");
const Self = @This();
interface: Decompressor = .{ .vtable = &.{ .stateless = stateless } },
fn stateless(alloc: std.mem.Allocator, in: []u8, out: []u8) Decompressor.Error!usize {
var rdr: Reader = .fixed(in);
var decomp = try xz.decompress(alloc, rdr.adaptToOldInterface());
defer decomp.deinit();
return decomp.read(out) catch |err| switch (err) {
error.CorruptInput,
error.EndOfStream,
error.EndOfStreamWithNoError,
error.WrongChecksum,
error.Unsupported,
error.Overflow,
=> Decompressor.Error.ReadFailed,
else => return err,
};
}
-18
View File
@@ -1,18 +0,0 @@
const std = @import("std");
const Reader = std.Io.Reader;
const flate = std.compress.flate;
const Decompressor = @import("../../decomp.zig");
const Self = @This();
interface: Decompressor = .{ .vtable = &.{ .stateless = stateless } },
fn stateless(alloc: std.mem.Allocator, in: []u8, out: []u8) Decompressor.Error!usize {
const buf = try alloc.alloc(u8, out.len);
defer alloc.free(buf);
var rdr: Reader = .fixed(in);
var decomp = flate.Decompress.init(&rdr, .zlib, buf);
return decomp.reader.readSliceShort(out);
}
-18
View File
@@ -1,18 +0,0 @@
const std = @import("std");
const Reader = std.Io.Reader;
const zstd = std.compress.zstd;
const Decompressor = @import("../../decomp.zig");
const Self = @This();
interface: Decompressor = .{ .vtable = &.{ .stateless = stateless } },
fn stateless(alloc: std.mem.Allocator, in: []u8, out: []u8) Decompressor.Error!usize {
const buf = try alloc.alloc(u8, out.len * 2);
defer alloc.free(buf);
var rdr: Reader = .fixed(in);
var decomp = zstd.Decompress.init(&rdr, buf, .{ .window_len = @min(out.len, zstd.default_window_len) });
return decomp.reader.readSliceShort(out);
}
-60
View File
@@ -1,60 +0,0 @@
//! Directory entry from the directory table.
const std = @import("std");
const Reader = std.Io.Reader;
const InodeType = @import("inode.zig").InodeType;
const Entry = @This();
const Header = extern struct { // use extern due to bad alignment with packed.
count: u32,
block_start: u32,
num: u32,
};
const RawEntry = packed struct {
offset: u16,
inode_offset: i16,
inode_type: InodeType,
name_size: u16,
};
block_start: u32,
block_offset: u16,
num: u32,
inode_type: InodeType,
name: []const u8,
pub fn readDir(alloc: std.mem.Allocator, rdr: *Reader, size: u32) ![]Entry {
var cur_red: u32 = 3; // start at 3 due to "." & ".." being counted in the dir size.
var hdr: Header = undefined;
var raw: RawEntry = undefined;
var out: std.ArrayList(Entry) = try .initCapacity(alloc, 100); // Start out with a decent capacity instead of needing to allocate per header.
errdefer out.deinit(alloc);
while (cur_red < size) {
try rdr.readSliceEndian(Header, @ptrCast(&hdr), .little);
cur_red += @sizeOf(Header);
try out.ensureUnusedCapacity(alloc, hdr.count + 1);
for (0..hdr.count + 1) |_| {
try rdr.readSliceEndian(RawEntry, @ptrCast(&raw), .little);
const name = try alloc.alloc(u8, raw.name_size + 1);
errdefer alloc.free(name);
try rdr.readSliceEndian(u8, name, .little);
const val = out.addOneAssumeCapacity();
val.* = .{
.block_start = hdr.block_start,
.block_offset = raw.offset,
.num = @abs(hdr.num + raw.offset),
.inode_type = raw.inode_type,
.name = name,
};
cur_red += @sizeOf(RawEntry) + raw.name_size + 1;
}
}
return out.toOwnedSlice(alloc);
}
pub fn deinit(self: Entry, alloc: std.mem.Allocator) void {
alloc.free(self.name);
}
+70
View File
@@ -0,0 +1,70 @@
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
@@ -0,0 +1,23 @@
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(),
};
}
+329 -176
View File
@@ -1,202 +1,355 @@
//! A file/directory within the squashfs archive.
const std = @import("std");
const File = std.fs.File;
const WaitGroup = std.Thread.WaitGroup;
const Mutex = std.Thread.Mutex;
const builtin = @import("builtin");
const Archive = @import("archive.zig");
const DirEntry = @import("dir_entry.zig");
const ExtractionOptions = @import("options.zig");
const dir = @import("directory.zig");
const DirEntry = dir.Entry;
const Inode = @import("inode.zig");
const BlockSize = @import("inode_data/file.zig").BlockSize;
const DataReader = @import("util/data.zig");
const MetadataReader = @import("util/metadata.zig");
const 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 FileError = error{
pub fn File(comptime T: type) type {
return struct {
pub const FileError = error{
NotRegular,
NotDirectory,
NotRegularFile,
NotSymlink,
NotDevice,
NotFound,
ExtractionPathExists,
};
};
const SfsFile = @This();
const Self = @This();
archive: *Archive,
rdr: *SfsReader(T),
// parent: *File(T),
inode: Inode,
name: []const u8,
inode: Inode,
name: []const u8,
/// Initialize a new File.
/// name is copied to the File so can be safely freed afterwards.
pub fn init(archive: *Archive, inode: Inode, name: []const u8) !SfsFile {
const new_name = try archive.allocator().alloc(u8, name.len);
@memcpy(new_name, name);
return .{
.archive = archive,
/// Directory entries. Only populated on directories.
entries: ?[]DirEntry = null,
/// File reader. Only populated on regular files.
data_reader: ?DataReader(T) = null,
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 = new_name,
.name = name_cpy,
};
}
pub fn fromEntry(archive: *Archive, entry: DirEntry) !SfsFile {
var rdr = try archive.fil.readerAt(entry.block_start + archive.super.inode_start, &[0]u8{});
var meta: MetadataReader = .init(archive.allocator(), &rdr.interface, archive.decomp);
try meta.interface.discardAll(entry.block_offset);
const inode: Inode = try .read(archive.allocator(), &meta.interface, archive.super.block_size);
errdefer inode.deinit(archive.allocator());
return .init(archive, inode, entry.name);
}
pub fn deinit(self: SfsFile) void {
var alloc = self.archive.allocator();
alloc.free(self.name);
self.inode.deinit(alloc);
}
fn getEntries(self: SfsFile) ![]DirEntry {
return self.inode.dirEntries(self.archive.allocator(), self.archive.*);
}
pub fn ownerUid(self: SfsFile) !u16 {
return self.archive.id(self.inode.hdr.uid_idx);
}
pub fn ownerGid(self: SfsFile) !u16 {
return self.archive.id(self.inode.hdr.gid_idx);
}
pub fn permissions(self: SfsFile) u16 {
return self.inode.hdr.permissions;
}
pub fn isRegular(self: SfsFile) bool {
return switch (self.inode.hdr.inode_type) {
.file, .ext_file => true,
else => false,
};
}
/// The returned DataReader will no longer work if the File's deinit function is called
/// or, more specifically, it's inode's deinit function is called.
pub fn dataReader(self: SfsFile) !DataReader {
return self.inode.dataReader(self.archive);
}
pub fn isDir(self: SfsFile) bool {
return switch (self.inode.hdr.inode_type) {
.dir, .ext_dir => true,
else => false,
};
}
pub fn iterate(self: SfsFile) !Iterator {
if (!self.isDir()) return FileError.NotDirectory;
return .{
.entries = try self.getEntries(),
.archive = self.archive,
};
}
/// Open a sub-file/folder within a directory at the given path.
/// If path is "", ".", "/", or "./", this File is returned.
pub fn open(self: SfsFile, alloc: std.mem.Allocator, path: []const u8) !SfsFile {
if (!self.isDir()) return FileError.NotDirectory;
if (pathIsSelf(path)) return self;
// Recursively stip ending & leading path separators.
if (path[0] == '/') return self.open(path[1..]);
if (path[path.len - 1] == '/') return self.open(path[0 .. path.len - 1]);
const idx = std.mem.indexOf(u8, path, "/") orelse path.len;
const first_element = path[0..idx];
if (std.mem.eql(u8, first_element, ".")) return self.open(path[idx + 1 ..]);
const entries = try self.getEntries();
defer {
for (entries) |e| {
e.deinit(alloc);
}
alloc.free(entries);
}
var cur_slice = entries;
var split = cur_slice.len / 2;
while (cur_slice.len > 0) {
split = cur_slice.len / 2;
const comp = std.mem.order(u8, first_element, cur_slice[split].name);
switch (comp) {
.eq => {
var fil: SfsFile = try .fromEntry(self.archive, cur_slice[split]);
if (idx == path.len) {
return fil;
}
defer fil.deinit();
return fil.open(alloc, path[idx + 1 ..]);
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);
},
.lt => cur_slice = cur_slice[0..split],
.gt => cur_slice = cur_slice[split + 1 ..],
.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();
}
}
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);
}
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 isSymlink(self: SfsFile) bool {
return switch (self.inode.hdr.inode_type) {
.symlink, .ext_symlink => true,
else => false,
}
pub fn iterate(self: Self) Iterator {
return .{
.rdr = self.rdr,
.entries = self.entries.?,
};
}
pub fn symlinkPath(self: SfsFile) ![]const u8 {
if (!self.isSymlink()) return FileError.NotSymlink;
return switch (self.inode.data) {
.symlink => |s| s.target,
.ext_symlink => |s| s.target,
else => unreachable,
};
}
}
/// Check if the File is a block or character device.
pub fn isDevice(self: SfsFile) bool {
return switch (self.inode.hdr.inode_type) {
.block_dev, .char_dev, .ext_block_dev, .ext_char_dev => true,
else => false,
};
}
/// If the File is a block or character device, get's it's device number.
pub fn devNum(self: SfsFile) !u32 {
if (!self.isDevice()) return FileError.NotDevice;
return switch (self.inode.data) {
.block_dev, .char_dev => |d| d.dev,
.ext_block_dev, .ext_char_dev => |d| d.dev,
else => unreachable,
};
}
/// Extract the given File to the path. If File is a regular file, the path must be a directory or not exist.
/// If the gievn path is a folder, the File's contents will be extracted within.
pub fn extract(self: *SfsFile, alloc: std.mem.Allocator, path: []const u8, options: ExtractionOptions) !void {
return self.inode.extractToThreaded(alloc, self.archive, path, options);
}
/// Utility function.
pub fn pathIsSelf(path: []const u8) bool {
if (path.len == 0) return true;
if (path.len == 1 and (path[0] == '/' or path[0] == '.')) return true;
if (path.len == 2 and (path[0] == '.' and path[1] == '/')) return true;
return false;
}
pub const Iterator = struct {
const Iterator = struct {
rdr: *SfsReader(T),
entries: []DirEntry,
archive: *Archive,
idx: u32 = 0,
pub fn next(self: *Iterator) !?SfsFile {
pub fn next(self: *Iterator) !?File(T) {
if (self.idx >= self.entries.len) return null;
defer self.idx += 1;
return try SfsFile.fromEntry(self.archive, self.entries[self.idx]);
const out = try Self.initFromEntry(self.rdr, self.entries[self.idx]);
self.idx += 1;
return out;
}
pub fn deinit(self: Iterator) void {
var alloc = self.archive.allocator();
for (self.entries) |e| {
e.deinit(alloc);
pub fn reset(self: *Iterator) void {
self.idx = 0;
}
alloc.free(self.entries);
};
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});
},
}
}
};
}
+7
View File
@@ -0,0 +1,7 @@
const BlockSize = @import("inode/file.zig").BlockSize;
pub const FragEntry = packed struct {
block: u64,
size: BlockSize,
_: u32,
};
+38 -134
View File
@@ -1,32 +1,16 @@
//! A file-system object. Represents a File or directory.
const std = @import("std");
const Reader = std.Io.Reader;
const WaitGroup = std.Thread.WaitGroup;
const Pool = std.Thread.Pool;
const Mutex = std.Thread.Mutex;
const Archive = @import("archive.zig");
const DirEntry = @import("dir_entry.zig");
const ExtractionOptions = @import("options.zig");
const dir = @import("inode_data/dir.zig");
const file = @import("inode_data/file.zig");
const misc = @import("inode_data/misc.zig");
const Tables = @import("tables.zig");
const DataReader = @import("util/data.zig");
const ThreadedDataReader = @import("util/data_threaded.zig");
const InodeExtract = @import("util/extract.zig");
const InodeFinish = @import("util/inode_finish.zig");
const FinishUnion = InodeFinish.FinishUnion;
const MetadataReader = @import("util/metadata.zig");
const dir = @import("inode/dir.zig");
const file = @import("inode/file.zig");
const misc = @import("inode/misc.zig");
pub const Ref = packed struct {
block_offset: u16,
block_start: u32,
offset: u16,
block: u32,
_: u16,
};
pub const InodeType = enum(u16) {
pub const Type = enum(u16) {
dir = 1,
file,
symlink,
@@ -43,7 +27,16 @@ pub const InodeType = enum(u16) {
ext_socket,
};
pub const InodeData = union(InodeType) {
pub const Header = packed struct {
type: Type,
perm: 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,
@@ -60,51 +53,36 @@ pub const InodeData = union(InodeType) {
ext_socket: misc.ExtIPC,
};
pub const Header = packed struct {
inode_type: InodeType,
permissions: u16,
uid_idx: u16,
gid_idx: u16,
mod_time: u32,
num: u32,
};
const Inode = @This();
const Self = @This();
hdr: Header,
data: InodeData,
data: Data,
pub fn read(alloc: std.mem.Allocator, rdr: *Reader, block_size: u32) !Inode {
pub fn init(rdr: anytype, alloc: std.mem.Allocator, block_size: u32) !Self {
var hdr: Header = undefined;
try rdr.readSliceEndian(Header, @ptrCast(&hdr), .little);
_ = 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) },
};
return .{
.hdr = hdr,
.data = switch (hdr.inode_type) {
.dir => .{ .dir = try .read(rdr) },
.file => .{ .file = try .read(alloc, rdr, block_size) },
.symlink => .{ .symlink = try .read(alloc, rdr) },
.block_dev => .{ .block_dev = try .read(rdr) },
.char_dev => .{ .char_dev = try .read(rdr) },
.fifo => .{ .fifo = try .read(rdr) },
.socket => .{ .socket = try .read(rdr) },
.ext_dir => .{ .ext_dir = try .read(rdr) },
.ext_file => .{ .ext_file = try .read(alloc, rdr, block_size) },
.ext_symlink => .{ .ext_symlink = try .read(alloc, rdr) },
.ext_block_dev => .{ .ext_block_dev = try .read(rdr) },
.ext_char_dev => .{ .ext_char_dev = try .read(rdr) },
.ext_fifo => .{ .ext_fifo = try .read(rdr) },
.ext_socket => .{ .ext_socket = try .read(rdr) },
},
.data = data,
};
}
pub fn readFromEntry(alloc: std.mem.Allocator, archive: Archive, entry: DirEntry) !Inode {
var rdr = try archive.fil.readerAt(archive.super.inode_start + entry.block_start, &[0]u8{});
var meta: MetadataReader = .init(alloc, &rdr.interface, archive.decomp);
try meta.interface.discardAll(entry.block_offset);
return read(alloc, &meta.interface, archive.super.block_size);
}
pub fn deinit(self: Inode, alloc: std.mem.Allocator) void {
pub fn deinit(self: Self, alloc: std.mem.Allocator) void {
switch (self.data) {
.file => |f| alloc.free(f.block_sizes),
.ext_file => |f| alloc.free(f.block_sizes),
@@ -113,77 +91,3 @@ pub fn deinit(self: Inode, alloc: std.mem.Allocator) void {
else => {},
}
}
/// Get the data reader for a file inode.
pub fn dataReader(self: Inode, alloc: std.mem.Allocator, archive: Archive, tables: *Tables) !DataReader {
return switch (self.hdr.inode_type) {
.file => readerFromData(alloc, archive, tables, self.data.file),
.ext_file => readerFromData(alloc, archive, tables, self.data.ext_file),
else => error.NotRegularFile,
};
}
fn readerFromData(alloc: std.mem.Allocator, archive: Archive, tables: *Tables, data: anytype) !DataReader {
var out: DataReader = .init(alloc, archive, data.block_sizes, data.block_start, data.size);
if (data.frag_idx != 0xFFFFFFFF)
out.addFragment(try tables.frag_table.get(data.frag_idx), data.frag_block_offset);
return out;
}
/// Get the directory entries for a directory inode.
pub fn dirEntries(self: Inode, alloc: std.mem.Allocator, archive: Archive) ![]DirEntry {
return switch (self.hdr.inode_type) {
.dir => entriesFromData(alloc, archive, self.data.dir),
.ext_dir => entriesFromData(alloc, archive, self.data.ext_dir),
else => error.NotDirectory,
};
}
fn entriesFromData(alloc: std.mem.Allocator, archive: Archive, data: anytype) ![]DirEntry {
var rdr = try archive.fil.readerAt(archive.super.dir_start + data.block_start, &[0]u8{});
var meta: MetadataReader = .init(alloc, &rdr.interface, archive.decomp);
try meta.interface.discardAll(data.block_offset);
return DirEntry.readDir(alloc, &meta.interface, data.size);
}
/// Returns the xattr index for the given inode. If the inode isn't an extended variant or doesn't have any, the u32 max is returned (0xFFFFFFFF).
pub fn xattrIdx(self: Inode) u32 {
return switch (self.data) {
.ext_dir => |d| d.xattr_id,
.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 => 0xFFFFFFFF,
};
}
/// Applies the Inode's metadata to the given File.
/// Mod time is always set, but permissions and xattrs are set based on the given ExtractionOptions.
pub fn setMetadata(self: Inode, alloc: std.mem.Allocator, tables: *Tables, fil: std.fs.File, options: ExtractionOptions) !void {
const time = @as(i128, self.hdr.mod_time) * 1000000000;
try fil.updateTimes(time, time);
if (!options.ignore_permissions) {
try fil.chmod(self.hdr.permissions);
try fil.chown(try tables.id_table.get(self.hdr.uid_idx), try tables.id_table.get(self.hdr.gid_idx));
}
if (!options.ignore_xattr) {
const idx = self.xattrIdx();
if (idx == 0xFFFFFFFF) return;
const xattrs = try tables.xattr_table.get(alloc, idx);
defer alloc.free(xattrs);
for (xattrs) |kv| {
const res = std.os.linux.fsetxattr(fil.handle, kv.key, kv.value.ptr, kv.value.len, 0);
alloc.free(kv.key);
alloc.free(kv.value);
if (res != 0) {
if (options.verbose)
options.verbose_writer.?.print("fsetxattr has result of: {}\n", .{res}) catch {};
return error.SetXattr;
}
}
}
}
/// Extract the inode to the given path.
pub fn extractTo(self: Inode, alloc: std.mem.Allocator, archive: Archive, path: []const u8, options: ExtractionOptions) !void {
return InodeExtract.extractTo(alloc, self, archive, path, options);
}
+31
View File
@@ -0,0 +1,31 @@
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
@@ -0,0 +1,77 @@
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
@@ -0,0 +1,87 @@
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
@@ -1,32 +0,0 @@
const Reader = @import("std").Io.Reader;
pub const Dir = packed struct {
block_start: u32,
hard_links: u32,
size: u16,
block_offset: u16,
parent_num: u32,
pub fn read(rdr: *Reader) !Dir {
var d: Dir = undefined;
try rdr.readSliceEndian(Dir, @ptrCast(&d), .little);
return d;
}
};
pub const ExtDir = packed struct {
hard_links: u32,
size: u32,
block_start: u32,
parent_num: u32,
idx_count: u16,
block_offset: u16,
xattr_id: u32,
// index: []DirIndex
pub fn read(rdr: *Reader) !ExtDir {
var d: ExtDir = undefined;
try rdr.readSliceEndian(Dir, @ptrCast(&d), .little);
return d;
}
};
-76
View File
@@ -1,76 +0,0 @@
const std = @import("std");
const Reader = std.Io.Reader;
pub const BlockSize = packed struct {
size: u24,
uncompressed: bool,
_: u7,
};
pub const File = struct {
block_start: u32, // bytes 0-3
frag_idx: u32, // bytes 4-7
frag_block_offset: u32, // bytes 8-11
size: u32, // bytes 12-15
block_sizes: []BlockSize,
pub fn read(alloc: std.mem.Allocator, rdr: *Reader, block_size: u32) !File {
var start: [16]u8 = undefined;
try rdr.readSliceAll(&start);
const frag_idx: u32 = std.mem.readInt(u32, start[4..8], .little);
const size: u32 = std.mem.readInt(u32, start[12..16], .little);
var num_blocks: u32 = size / block_size;
if (size % block_size != 0 and frag_idx == 0xFFFFFFFF) num_blocks += 1;
const sizes = try alloc.alloc(BlockSize, num_blocks);
errdefer alloc.free(sizes);
try rdr.readSliceEndian(BlockSize, sizes, .little);
return .{
.block_start = std.mem.readInt(u32, start[0..4], .little),
.frag_idx = frag_idx,
.frag_block_offset = std.mem.readInt(u32, start[8..12], .little),
.size = size,
.block_sizes = sizes,
};
}
pub fn deinit(self: File, alloc: std.mem.Allocator) void {
alloc.free(self.block_sizes);
}
};
pub const ExtFile = struct {
block_start: u64, // bytes 0-7
size: u64, // bytes 8-15
sparse: u64, // bytes 16-23
hard_links: u32, // bytes 24-27
frag_idx: u32, // bytes 28-31
frag_block_offset: u32, // bytes 32-35
xattr_idx: u32, // bytes 36-39
block_sizes: []BlockSize,
pub fn read(alloc: std.mem.Allocator, rdr: *Reader, block_size: u32) !ExtFile {
var start: [40]u8 = undefined;
try rdr.readSliceAll(&start);
const frag_idx: u32 = std.mem.readInt(u32, start[28..32], .little);
const size: u64 = std.mem.readInt(u64, start[8..16], .little);
var num_blocks: u32 = @truncate(size / block_size);
if (size % block_size != 0 and frag_idx == 0xFFFFFFFF) num_blocks += 1;
const sizes = try alloc.alloc(BlockSize, num_blocks);
errdefer alloc.free(sizes);
try rdr.readSliceEndian(BlockSize, sizes, .little);
return .{
.block_start = std.mem.readInt(u64, start[0..8], .little),
.size = size,
.sparse = std.mem.readInt(u64, start[16..24], .little),
.hard_links = std.mem.readInt(u32, start[24..28], .little),
.frag_idx = frag_idx,
.frag_block_offset = std.mem.readInt(u32, start[32..36], .little),
.xattr_idx = std.mem.readInt(u32, start[36..40], .little),
.block_sizes = sizes,
};
}
pub fn deinit(self: ExtFile, alloc: std.mem.Allocator) void {
alloc.free(self.block_sizes);
}
};
-98
View File
@@ -1,98 +0,0 @@
const std = @import("std");
const Reader = std.Io.Reader;
pub const Symlink = struct {
hard_links: u32,
target: []const u8,
pub fn read(alloc: std.mem.Allocator, rdr: *Reader) !Symlink {
var start: [8]u8 = undefined;
try rdr.readSliceAll(&start);
const target_size = std.mem.readInt(u32, start[4..8], .little);
const target = try alloc.alloc(u8, target_size + 1);
errdefer alloc.free(target);
try rdr.readSliceEndian(u8, target, .little);
return .{
.hard_links = std.mem.readInt(u32, start[0..4], .little),
.target = target,
};
}
pub fn deinit(self: Symlink, alloc: std.mem.Allocator) void {
alloc.free(self.target);
}
};
pub const ExtSymlink = struct {
hard_links: u32,
target: []const u8,
xattr_idx: u32,
pub fn read(alloc: std.mem.Allocator, rdr: *Reader) !ExtSymlink {
var start: [8]u8 = undefined;
try rdr.readSliceAll(&start);
const target_size = std.mem.readInt(u32, start[4..8], .little);
const target = try alloc.alloc(u8, target_size + 1);
errdefer alloc.free(target);
try rdr.readSliceEndian(u8, target, .little);
var xattr_idx: u32 = undefined;
try rdr.readSliceEndian(u32, @ptrCast(&xattr_idx), .little);
return .{
.hard_links = std.mem.readInt(u32, start[0..4], .little),
.target = target,
.xattr_idx = xattr_idx,
};
}
pub fn deinit(self: ExtSymlink, alloc: std.mem.Allocator) void {
alloc.free(self.target);
}
};
/// A block or character device.
pub const Dev = packed struct {
hard_links: u32,
dev: u32,
pub fn read(rdr: *Reader) !Dev {
var d: Dev = undefined;
try rdr.readSliceEndian(Dev, @ptrCast(&d), .little);
return d;
}
};
/// An extended block or character device.
pub const ExtDev = packed struct {
hard_links: u32,
dev: u32,
xattr_idx: u32,
pub fn read(rdr: *Reader) !ExtDev {
var d: ExtDev = undefined;
try rdr.readSliceEndian(ExtDev, @ptrCast(&d), .little);
return d;
}
};
/// A socket or FIFO file.
pub const IPC = packed struct {
hard_links: u32,
pub fn read(rdr: *Reader) !IPC {
var d: IPC = undefined;
try rdr.readSliceEndian(IPC, @ptrCast(&d), .little);
return d;
}
};
/// An extended socket or FIFO file.
pub const ExtIPC = packed struct {
hard_links: u32,
xattr_idx: u32,
pub fn read(rdr: *Reader) !ExtIPC {
var d: ExtIPC = undefined;
try rdr.readSliceEndian(ExtIPC, @ptrCast(&d), .little);
return d;
}
};
-33
View File
@@ -1,33 +0,0 @@
//! Options for file/directory extraction.
const std = @import("std");
const Writer = std.Io.Writer;
const ExtractionOptions = @This();
/// The number of threads used for extraction. 0 implies single threaded.
threads: usize = 1,
/// Don't set the file's owner & permissions after extraction
ignore_permissions: bool = false,
/// Don't set xattr values. Currently xattrs are never set anyway.
ignore_xattr: bool = false,
/// Replace symlinks with their target.
dereference_symlinks: bool = false,
/// Verbose logging. If true, verbose_writer must be set
verbose: bool = false,
/// Where to print verbose log.
verbose_writer: ?*Writer = null,
pub const SingleThreadedDefault: ExtractionOptions = .{};
pub fn Default() !ExtractionOptions {
return .{
.threads = try std.Thread.getCpuCount(),
};
}
pub fn VerboseDefault(wrt: *Writer) !ExtractionOptions {
return .{
.verbose = true,
.verbose_writer = wrt,
.threads = try std.Thread.getCpuCount(),
};
}
+85
View File
@@ -0,0 +1,85 @@
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
@@ -0,0 +1,271 @@
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
@@ -0,0 +1,82 @@
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
@@ -0,0 +1,29 @@
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
@@ -0,0 +1,43 @@
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),
};
}
};
}
+62 -2
View File
@@ -1,2 +1,62 @@
pub const Archive = @import("archive.zig");
pub const ExtractionOptions = @import("options.zig");
const std = @import("std");
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", .{});
}
-69
View File
@@ -1,69 +0,0 @@
const std = @import("std");
const math = std.math;
const InodeRef = @import("inode.zig").Ref;
const SQUASHFS_MAGIC: u32 = std.mem.readInt(u32, "hsqs", .little);
const SuperblockError = error{
InvalidMagic,
InvalidBlockLog,
InvalidVersion,
InvalidCheck,
};
/// A squashfs Superblock
pub const Superblock = packed struct {
magic: u32,
inode_count: u32,
mod_time: u32,
block_size: u32,
frag_count: u32,
compression: enum(u16) {
gzip = 1,
lzma,
lzo,
xz,
lz4,
zstd,
},
block_log: u16,
flags: packed struct {
inode_uncompressed: bool,
data_uncompressed: bool,
check: bool,
frag_uncompressed: bool,
fragment_never: bool,
fragment_always: bool,
duplicates: bool,
exportable: bool,
xattr_uncompressed: bool,
xattr_never: bool,
compression_options: bool,
ids_uncompressed: bool,
_: u4,
},
id_count: u16,
ver_maj: u16,
ver_min: u16,
root_ref: InodeRef,
size: u64,
id_start: u64,
xattr_start: u64,
inode_start: u64,
dir_start: u64,
frag_start: u64,
export_start: u64,
/// Validate the Superblock. If an error is returned, it's likely the archive is corrupted or not a squashfs archive.
pub fn validate(self: Superblock) !void {
if (self.magic != SQUASHFS_MAGIC)
return SuperblockError.InvalidMagic;
if (self.flags.check)
return SuperblockError.InvalidCheck;
if (self.ver_maj != 4 or self.ver_min != 0)
return SuperblockError.InvalidVersion;
if (math.log2(self.block_size) != self.block_log)
return SuperblockError.InvalidBlockLog;
}
};
+79
View File
@@ -0,0 +1,79 @@
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
@@ -0,0 +1,75 @@
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];
}
};
}
-108
View File
@@ -1,108 +0,0 @@
const std = @import("std");
const Mutex = std.Thread.Mutex;
const Archive = @import("archive.zig");
const BlockSize = @import("inode_data/file.zig").BlockSize;
const InodeRef = @import("inode.zig").Ref;
const Superblock = @import("super.zig").Superblock;
const MetadataReader = @import("util/metadata.zig");
const OffsetFile = @import("util/offset_file.zig");
const XattrTable = @import("xattr.zig");
const Decompressor = @import("decomp.zig");
/// Information about a fragment section. Multiple fragments are contained in the block described by a single FragEntry.
/// The offset into the block and fragment size is stored in the file's inode.
pub const FragEntry = packed struct {
start: u64,
size: BlockSize,
_: u32,
};
const Tables = @This();
frag_table: Table(FragEntry),
id_table: Table(u16),
export_table: Table(InodeRef),
xattr_table: XattrTable,
pub fn init(alloc: std.mem.Allocator, archive: Archive) !Tables {
return .{
.frag_table = try .init(alloc, archive.fil, archive.decomp, archive.super.frag_start, archive.super.frag_count),
.id_table = try .init(alloc, archive.fil, archive.decomp, archive.super.id_start, archive.super.id_count),
.export_table = try .init(alloc, archive.fil, archive.decomp, archive.super.export_start, archive.super.inode_count),
.xattr_table = try .init(alloc, archive.fil, archive.decomp, archive.super.xattr_start),
};
}
pub fn deinit(self: *Tables) void {
self.frag_table.deinit();
self.id_table.deinit();
self.export_table.deinit();
self.xattr_table.deinit();
}
/// A two-layer metadata table.
pub fn Table(T: anytype) type {
return struct {
const Self = @This();
const VALS_PER_BLOCK = 8192 / @sizeOf(T);
alloc: std.mem.Allocator,
fil: OffsetFile,
decomp: *Decompressor,
tab_start: u64,
tab: std.AutoHashMap(u32, []T),
values: u32,
mut: Mutex = .{},
pub fn init(alloc: std.mem.Allocator, fil: OffsetFile, decomp: *Decompressor, tab_start: u64, values: u32) !Self {
return .{
.alloc = alloc,
.fil = fil,
.decomp = decomp,
.tab_start = tab_start,
.tab = .init(alloc),
.values = values,
};
}
pub fn deinit(self: *Self) void {
var iter = self.tab.valueIterator();
while (iter.next()) |s| {
self.alloc.free(s.*);
}
self.tab.deinit();
}
pub fn get(self: *Self, idx: u32) !T {
if (idx >= self.values) return error.InvalidIndex;
const block_num = idx / VALS_PER_BLOCK;
const idx_offset = idx - (block_num * VALS_PER_BLOCK);
if (self.tab.contains(block_num)) {
const block = self.tab.get(block_num).?;
return block[idx_offset];
}
self.mut.lock();
defer self.mut.unlock();
// Double check in case of race condition..
if (self.tab.contains(block_num)) {
const block = self.tab.get(block_num).?;
return block[idx_offset];
}
const is_last = (self.values - 1) / VALS_PER_BLOCK == block_num;
const slice_size = if (is_last) self.values - (block_num * VALS_PER_BLOCK) else VALS_PER_BLOCK;
const slice = try self.alloc.alloc(T, slice_size);
var rdr = try self.fil.readerAt(self.tab_start + (8 * block_num), &[0]u8{});
var offset: u64 = 0;
try rdr.interface.readSliceEndian(u64, @ptrCast(&offset), .little);
rdr = try self.fil.readerAt(offset, &[0]u8{});
var meta: MetadataReader = .init(self.alloc, &rdr.interface, self.decomp);
try meta.interface.readSliceEndian(T, @ptrCast(slice), .little);
try self.tab.put(block_num, slice);
return slice[idx_offset];
}
};
}
-84
View File
@@ -1,84 +0,0 @@
const std = @import("std");
const stuff = @import("builtin");
const Archive = @import("archive.zig");
const Superblock = @import("super.zig").Superblock;
const TestArchive = "testing/LinuxPATest.sfs";
test "Basics" {
var fil = try std.fs.cwd().openFile(TestArchive, .{});
defer fil.close();
var sfs: Archive = try .init(std.testing.allocator, fil);
defer sfs.deinit();
if (sfs.super != LinuxPATestCorrectSuperblock) {
std.debug.print("Superblock wrong\nShould be: {}\n\nis: {}\n", .{ LinuxPATestCorrectSuperblock, sfs.super });
return error.BadSuperblock;
}
}
const TestFile = "Start.exe";
const TestFileExtractLocation = "testing/Start.exe";
test "ExtractSingleFile" {
std.fs.cwd().deleteFile(TestFileExtractLocation) catch {};
var fil = try std.fs.cwd().openFile(TestArchive, .{});
defer fil.close();
var sfs: Archive = try .init(std.testing.allocator, fil);
defer sfs.deinit();
var test_fil = try sfs.open(TestFile);
defer test_fil.deinit();
try test_fil.extract(TestFileExtractLocation, .Default);
//TODO: validate extracted file.
}
const TestFullExtractLocation = "testing/TestExtract";
test "ExtractCompleteArchive" {
std.fs.cwd().deleteTree(TestFullExtractLocation) catch {};
var fil = try std.fs.cwd().openFile(TestArchive, .{});
defer fil.close();
var sfs: Archive = try .init(std.testing.allocator, fil);
defer sfs.deinit();
try sfs.extract(TestFullExtractLocation, .Default);
}
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,
};
-174
View File
@@ -1,174 +0,0 @@
//! A reader for a regular file.
const std = @import("std");
const Reader = std.Io.Reader;
const Writer = std.Io.Writer;
const Limit = std.Io.Limit;
const Archive = @import("../archive.zig");
const DecompFn = @import("../decomp.zig").DecompFn;
const BlockSize = @import("../inode_data/file.zig").BlockSize;
const FragEntry = @import("../tables.zig").FragEntry;
const OffsetFile = @import("offset_file.zig");
const DataReader = @This();
alloc: std.mem.Allocator,
fil: OffsetFile,
decomp: DecompFn,
block_size: u32,
blocks: []BlockSize,
frag: ?FragEntry = null, // TODO: do something better?
frag_offset: u32 = 0,
size: u64,
interface: Reader,
cur_offset: u64,
block_idx: u32 = 0,
pub fn init(alloc: std.mem.Allocator, archive: Archive, blocks: []BlockSize, start: u64, size: u64) DataReader {
return .{
.alloc = alloc,
.fil = archive.fil,
.decomp = archive.decomp,
.block_size = archive.super.block_size,
.blocks = blocks,
.size = size,
.cur_offset = start,
.interface = .{
.end = 0,
.seek = 0,
.buffer = &[0]u8{},
.vtable = &.{
.stream = stream,
.discard = discard,
.readVec = readVec,
},
},
};
}
pub fn deinit(self: *DataReader) void {
self.alloc.free(self.interface.buffer);
self.interface.end = 0;
self.interface.seek = 0;
}
pub fn addFragment(self: *DataReader, entry: FragEntry, frag_offset: u32) void {
self.frag = entry;
self.frag_offset = frag_offset;
}
fn numBlocks(self: DataReader) usize {
var res = self.blocks.len;
if (self.frag != null) res += 1;
return res;
}
fn advance(self: *DataReader) !void {
if (self.block_idx > self.blocks.len or (self.block_idx == self.blocks.len and self.frag == null)) {
if (self.interface.buffer.len > 0) {
self.alloc.free(self.interface.buffer);
self.interface.buffer = &[0]u8{};
self.interface.end = 0;
self.interface.seek = 0;
}
return Reader.Error.EndOfStream;
}
defer self.block_idx += 1;
const cur_block_size = if (self.block_idx == self.numBlocks() - 1) self.size % self.block_size else self.block_size;
try self.resizeBuffer(cur_block_size);
self.interface.seek = 0;
self.interface.end = cur_block_size;
if (self.block_idx == self.blocks.len) { // fragment
var rdr = try self.fil.readerAt(self.frag.?.start, &[0]u8{});
if (self.frag.?.size.uncompressed) {
try rdr.interface.discardAll(self.frag_offset);
try rdr.interface.readSliceAll(self.interface.buffer);
return;
}
const tmp_buf = try self.alloc.alloc(u8, self.frag.?.size.size);
defer self.alloc.free(tmp_buf);
try rdr.interface.readSliceAll(tmp_buf);
const needed_block = try self.alloc.alloc(u8, self.block_size);
defer self.alloc.free(needed_block);
_ = try self.decomp(self.alloc, tmp_buf, needed_block);
@memcpy(self.interface.buffer, needed_block[self.frag_offset .. self.frag_offset + cur_block_size]);
return;
}
const block = self.blocks[self.block_idx];
if (block.size == 0) {
@memset(self.interface.buffer, 0);
return;
}
var rdr = try self.fil.readerAt(self.cur_offset, &[0]u8{});
self.cur_offset += block.size;
if (block.uncompressed) {
try rdr.interface.readSliceAll(self.interface.buffer);
return;
}
const tmp_buf = try self.alloc.alloc(u8, block.size);
defer self.alloc.free(tmp_buf);
try rdr.interface.readSliceAll(tmp_buf);
_ = try self.decomp(self.alloc, tmp_buf, self.interface.buffer);
}
/// Does not guarentee that data currently in the buffer is retained.
fn resizeBuffer(self: *DataReader, size: usize) !void {
if (self.interface.buffer.len == size) return;
if (!self.alloc.resize(self.interface.buffer, size)) {
self.alloc.free(self.interface.buffer);
self.interface.buffer = self.alloc.alloc(u8, size) catch |err| {
self.interface.buffer = &[0]u8{};
return err;
};
} else {
self.interface.buffer.len = size;
}
}
fn stream(rdr: *Reader, wrt: *Writer, limit: Limit) Reader.StreamError!usize {
var self: *DataReader = @alignCast(@fieldParentPtr("interface", rdr));
if (rdr.seek >= rdr.end) self.advance() catch |err| {
if (err == error.EndOfStream) return error.EndOfStream;
std.log.err("Error advancing data reader: {}\n", .{err});
return Reader.Error.ReadFailed;
};
if (limit == .nothing) return 0;
const to_read = @min(rdr.end - rdr.seek, @intFromEnum(limit));
const res = try wrt.write(rdr.buffer[rdr.seek .. rdr.seek + to_read]);
rdr.seek += res;
return res;
}
fn discard(rdr: *Reader, limit: Limit) Reader.Error!usize {
var self: *DataReader = @alignCast(@fieldParentPtr("interface", rdr));
if (rdr.seek >= rdr.end) self.advance() catch |err| {
if (err == error.EndOfStream) return error.EndOfStream;
std.log.err("Error advancing data reader: {}\n", .{err});
return Reader.Error.ReadFailed;
};
if (limit == .nothing) return 0;
const to_adv = @min(rdr.end - rdr.seek, @intFromEnum(limit));
rdr.seek += to_adv;
return to_adv;
}
fn readVec(rdr: *Reader, vec: [][]u8) Reader.Error!usize {
var self: *DataReader = @alignCast(@fieldParentPtr("interface", rdr));
if (rdr.seek >= rdr.end) self.advance() catch |err| {
if (err == error.EndOfStream) return error.EndOfStream;
std.log.err("Error advancing data reader: {}\n", .{err});
return Reader.Error.ReadFailed;
};
var cur_red: usize = 0;
for (vec) |s| {
const to_copy: usize = @min(rdr.end - rdr.seek, s.len);
@memcpy(s[0..to_copy], rdr.buffer[rdr.seek .. rdr.seek + to_copy]);
rdr.seek += to_copy;
cur_red += to_copy;
if (rdr.end == rdr.seek) break;
}
return cur_red;
}
-207
View File
@@ -1,207 +0,0 @@
//! Similiar to DataReader, but set-up for threaded writing to files.
const std = @import("std");
const Reader = std.Io.Reader;
const Writer = std.Io.Writer;
const Limit = std.Io.Limit;
const WaitGroup = std.Thread.WaitGroup;
const Pool = std.Thread.Pool;
const Archive = @import("../archive.zig");
const DecompFn = @import("../decomp.zig").DecompFn;
const BlockSize = @import("../inode_data/file.zig").BlockSize;
const FragEntry = @import("../tables.zig").FragEntry;
const InodeFinish = @import("inode_finish.zig");
const OffsetFile = @import("offset_file.zig");
const ThreadedDataReader = @This();
alloc: std.mem.Allocator,
fil: OffsetFile,
decomp: DecompFn,
block_size: u32,
blocks: []BlockSize,
frag: ?FragEntry = null, // TODO: do something better?
frag_offset: u32 = 0,
size: u64,
num_blocks: usize,
start_offset: u64,
pub fn init(alloc: std.mem.Allocator, archive: Archive, blocks: []BlockSize, start: u64, size: u64) ThreadedDataReader {
return .{
.alloc = alloc,
.fil = archive.fil,
.decomp = archive.decomp,
.block_size = archive.super.block_size,
.blocks = blocks,
.size = size,
.num_blocks = blocks.len,
.start_offset = start,
};
}
pub fn addFragment(self: *ThreadedDataReader, entry: FragEntry, frag_offset: u32) void {
self.frag = entry;
self.frag_offset = frag_offset;
self.num_blocks = self.blocks.len + 1;
}
/// Extract the data to the file threadedly, using pool to spawn threads.
/// If errors occur, they are set to finish.out_err.
pub fn extractThreaded(self: ThreadedDataReader, file: std.fs.File, pool: *Pool, finish: *InodeFinish) void {
var cur_write_offset: u64 = 0;
var cur_read_offset: u64 = self.start_offset;
for (0..self.blocks.len) |i| {
const cur_block_size = if (i == self.num_blocks - 1) self.size % self.block_size else self.block_size;
pool.spawn(workThreadBlocks, .{ self, file, cur_write_offset, cur_read_offset, self.blocks[i], cur_block_size, finish }) catch |res_err| {
finish.logError("Can't spawn pool task: {}", .{res_err});
finish.out_err.* = res_err;
finish.finish();
};
cur_write_offset += cur_block_size;
cur_read_offset += self.blocks[i].size;
}
if (self.frag != null)
pool.spawn(workThreadFragment, .{ self, file, cur_write_offset, finish }) catch |res_err| {
finish.logError("Can't spawn pool task: {}", .{res_err});
finish.out_err.* = res_err;
finish.finish();
};
}
fn workThreadBlocks(
self: ThreadedDataReader,
fil: std.fs.File,
write_offset: u64,
read_offset: u64,
block: BlockSize,
cur_block_size: u64,
finish: *InodeFinish,
) void {
defer finish.finish();
var wrt = fil.writer(&[0]u8{});
wrt.seekTo(write_offset) catch |err| {
finish.logError("Error seeking file writer: {}", .{err});
finish.out_err.* = err;
return;
};
defer wrt.interface.flush() catch |err| {
finish.logError("Error flushing file writer: {}", .{err});
finish.out_err.* = err;
};
if (block.size == 0) {
wrt.interface.splatByteAll(0, cur_block_size) catch |err| {
finish.logError("Error writing zeroes: {}", .{err});
finish.out_err.* = err;
return;
};
return;
}
var rdr = self.fil.readerAt(read_offset, &[0]u8{}) catch |err| {
finish.logError("Error creating file reader: {}", .{err});
finish.out_err.* = err;
return;
};
if (block.uncompressed) {
rdr.interface.streamExact(&wrt.interface, block.size) catch |err| {
finish.logError("Error streaming data: {}", .{err});
finish.out_err.* = err;
return;
};
return;
}
// TODO: shared buffers
const read_buf = self.alloc.alloc(u8, block.size) catch |err| {
finish.logError("Error creating reader buffer: {}", .{err});
finish.out_err.* = err;
return;
};
defer self.alloc.free(read_buf);
rdr.interface.readSliceAll(read_buf) catch |err| {
finish.logError("Error reading data into reader buffer: {}", .{err});
finish.out_err.* = err;
return;
};
// TODO: shared buffers
const res_buf = self.alloc.alloc(u8, cur_block_size) catch |err| {
finish.logError("Error creating result buffer: {}", .{err});
finish.out_err.* = err;
return;
};
defer self.alloc.free(res_buf);
_ = self.decomp(self.alloc, read_buf, res_buf) catch |err| {
finish.logError("Error decompressing data block: {}", .{err});
finish.out_err.* = err;
return;
};
wrt.interface.writeAll(res_buf) catch |err| {
finish.logError("Error writing to file: {}", .{err});
finish.out_err.* = err;
return;
};
}
fn workThreadFragment(self: ThreadedDataReader, fil: std.fs.File, write_offset: u64, finish: *InodeFinish) void {
defer finish.finish();
var wrt = fil.writer(&[0]u8{});
wrt.seekTo(write_offset) catch |err| {
finish.logError("Error seeking file writer for file fragment: {}", .{err});
finish.out_err.* = err;
return;
};
defer wrt.interface.flush() catch |err| {
finish.out_err.* = err;
};
var rdr = self.fil.readerAt(self.frag.?.start, &[0]u8{}) catch |err| {
finish.logError("Error creating file reader for file fragment: {}", .{err});
finish.out_err.* = err;
return;
};
if (self.frag.?.size.uncompressed) {
rdr.interface.discardAll(self.frag_offset) catch |err| {
finish.logError("Error discarding useless fragment data: {}", .{err});
finish.out_err.* = err;
return;
};
rdr.interface.streamExact(&wrt.interface, self.size % self.block_size) catch |err| {
finish.logError("Error streaming fragment data: {}", .{err});
finish.out_err.* = err;
return;
};
return;
}
const tmp_buf = self.alloc.alloc(u8, self.frag.?.size.size) catch |err| {
finish.logError("Error creating a temporary buffer for a file fragment: {}", .{err});
finish.out_err.* = err;
return;
};
defer self.alloc.free(tmp_buf);
rdr.interface.readSliceAll(tmp_buf) catch |err| {
finish.logError("Error reading data into fragment buffer: {}", .{err});
finish.out_err.* = err;
return;
};
const needed_block = self.alloc.alloc(u8, self.block_size) catch |err| {
finish.logError("Error allocating fragment decompression results: {}", .{err});
finish.out_err.* = err;
return;
};
defer self.alloc.free(needed_block);
_ = self.decomp(self.alloc, tmp_buf, needed_block) catch |err| {
finish.logError("Error decompressing fragment: {}", .{err});
finish.out_err.* = err;
return;
};
wrt.interface.writeAll(needed_block[self.frag_offset .. self.frag_offset + (self.size % self.block_size)]) catch |err| {
finish.logError("Error writing fragment: {}", .{err});
finish.out_err.* = err;
return;
};
}
-353
View File
@@ -1,353 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const Pool = std.Thread.Pool;
const WaitGroup = std.Thread.WaitGroup;
const Archive = @import("../archive.zig");
const DirEntry = @import("../dir_entry.zig");
const Inode = @import("../inode.zig");
const ExtractionOptions = @import("../options.zig");
const Tables = @import("../tables.zig");
const InodeFinish = @import("inode_finish.zig");
const FinishUnion = InodeFinish.FinishUnion;
const ThreadedDataReader = @import("data_threaded.zig");
// 1 MB
const STACK_ALLOC_SIZE = 1024 * 1024;
pub fn extractTo(
allocator: Allocator,
inode: Inode,
archive: Archive,
path: []const u8,
options: ExtractionOptions,
) !void {
if (path[path.len - 1] == '/')
return extractTo(allocator, inode, archive, path[0 .. path.len - 2], options);
var stack_alloc = std.heap.stackFallback(STACK_ALLOC_SIZE, allocator);
var arena: std.heap.ArenaAllocator = .init(stack_alloc.get());
defer arena.deinit();
if (options.threads <= 1) {
const alloc = arena.allocator();
var tables: Tables = try .init(alloc, archive);
return extractSingleThread(arena.allocator(), inode, archive, &tables, path, options);
}
var thread_alloc = std.heap.ThreadSafeAllocator{ .child_allocator = arena.allocator() };
const alloc = thread_alloc.allocator();
var tables: Tables = try .init(alloc, archive);
var pool_alloc = std.heap.stackFallback(10 * 1024, alloc);
var pool: Pool = undefined;
try pool.init(.{ .allocator = pool_alloc.get(), .n_jobs = options.threads - 1 });
var wg: WaitGroup = .{};
var err: ?anyerror = null;
wg.start();
try pool.spawn(extractMultiThread, .{
alloc,
inode,
archive,
&tables,
path,
options,
&pool,
FinishUnion{ .wg = &wg },
&err,
});
pool.waitAndWork(&wg);
if (err != null) return err.?;
}
fn extractSingleThread(
alloc: Allocator,
inode: Inode,
archive: Archive,
tables: *Tables,
path: []const u8,
options: ExtractionOptions,
) !void {
switch (inode.hdr.inode_type) {
.dir, .ext_dir => {
_ = std.fs.cwd().makeDir(path) catch |err| switch (err) {
std.fs.Dir.MakeError.PathAlreadyExists => {},
else => return err,
};
// Currently we are ignoring any deinit or free calls since we know we are under an ArenaAllocator.
// Possibly in the future, do some simple math to see if it would be safe to ONLY deinit via Arena,
// otherwise be more conscientious about freeing memory.
// For now, this is good enough.
const entries = try inode.dirEntries(alloc, archive);
for (entries) |ent| {
const sub_inode: Inode = try .readFromEntry(alloc, archive, ent);
const new_path = try std.mem.concat(alloc, u8, &[_][]const u8{ path, "/", ent.name });
try extractSingleThread(alloc, sub_inode, archive, tables, new_path, options);
}
const fil = try std.fs.cwd().openFile(path, .{});
defer fil.close();
try inode.setMetadata(alloc, tables, fil, options);
},
.file, .ext_file => {
var fil = try std.fs.cwd().createFile(path, .{ .exclusive = true });
defer fil.close();
var wrt = fil.writer(&[0]u8{});
var dat_rdr = try inode.dataReader(alloc, archive, tables);
defer dat_rdr.deinit();
_ = try dat_rdr.interface.streamRemaining(&wrt.interface);
try wrt.interface.flush();
try inode.setMetadata(alloc, tables, fil, options);
},
.symlink, .ext_symlink => return extractSymlink(inode, path),
else => return extractDeviceAndIPC(inode, alloc, tables, path, options),
}
}
fn extractMultiThread(
alloc: Allocator,
inode: Inode,
archive: Archive,
tables: *Tables,
path: []const u8,
options: ExtractionOptions,
pool: *Pool,
fin: FinishUnion,
err: *?anyerror,
) void {
if (err.* != null) {
fin.finish();
return;
}
switch (inode.hdr.inode_type) {
.dir, .ext_dir => {
_ = std.fs.cwd().makeDir(path) catch |res_err| switch (res_err) {
std.fs.Dir.MakeError.PathAlreadyExists => {},
else => {
err.* = res_err;
fin.finish();
return;
},
};
// Currently we are ignoring any deinit or free calls since we know we are under an ArenaAllocator.
// Possibly in the future, do some simple math to see if it would be safe to ONLY deinit via Arena,
// otherwise be more conscientious about freeing memory.
// For now, this is good enough.
const entries = inode.dirEntries(alloc, archive) catch |res_err| {
err.* = res_err;
fin.finish();
return;
};
if (entries.len == 0) {
fin.finish();
return;
}
var dir_fin = InodeFinish.create(
alloc,
inode,
path,
tables,
options,
fin,
err,
null,
entries.len,
) catch |res_err| {
err.* = res_err;
fin.finish();
return;
};
for (entries) |ent| {
if (ent.inode_type == .dir) {
extractEntry(
alloc,
ent,
archive,
tables,
path,
options,
pool,
.{ .fin = dir_fin },
err,
);
continue;
}
pool.spawn(
extractEntry,
.{ alloc, ent, archive, tables, path, options, pool, FinishUnion{ .fin = dir_fin }, err },
) catch |res_err| {
err.* = res_err;
dir_fin.finish();
return;
};
}
},
.file, .ext_file => {
const fil = std.fs.cwd().createFile(path, .{ .exclusive = true }) catch |res_err| {
if (options.verbose)
options.verbose_writer.?.print("Can't create file at {s}: {}\n", .{ path, res_err }) catch {};
err.* = res_err;
fin.finish();
return;
};
var data_rdr = threadedDataReader(inode, alloc, archive, tables) catch |res_err| {
if (options.verbose)
options.verbose_writer.?.print("Can't create data reader for inode #{} (extracting to {s}): {}\n", .{ inode.hdr.num, path, res_err }) catch {};
err.* = res_err;
fin.finish();
return;
};
if (data_rdr == null) {
inode.setMetadata(alloc, tables, fil, options) catch |res_err| {
if (options.verbose)
options.verbose_writer.?.print("Can't set metadata to {s}: {}\n", .{ path, res_err }) catch {};
err.* = res_err;
};
fin.finish();
return;
}
const file_fin = InodeFinish.create(
alloc,
inode,
path,
tables,
options,
fin,
err,
fil,
data_rdr.?.num_blocks,
) catch |res_err| {
if (options.verbose)
options.verbose_writer.?.print("Can't create callback for inode #{} (extracting to {s}): {}\n", .{ inode.hdr.num, path, res_err }) catch {};
err.* = res_err;
fin.finish();
return;
};
data_rdr.?.extractThreaded(fil, pool, file_fin);
},
.symlink, .ext_symlink => {
extractSymlink(inode, path) catch |res_err| {
err.* = res_err;
};
fin.finish();
},
else => {
extractDeviceAndIPC(inode, alloc, tables, path, options) catch |res_err| {
err.* = res_err;
};
fin.finish();
},
}
}
fn extractEntry(
alloc: Allocator,
ent: DirEntry,
archive: Archive,
tables: *Tables,
path: []const u8,
options: ExtractionOptions,
pool: *Pool,
fin: FinishUnion,
err: *?anyerror,
) void {
const new_path = std.mem.concat(alloc, u8, &[_][]const u8{ path, "/", ent.name }) catch |res_err| {
err.* = res_err;
fin.finish();
return;
};
const inode = Inode.readFromEntry(alloc, archive, ent) catch |res_err| {
err.* = res_err;
fin.finish();
return;
};
extractMultiThread(alloc, inode, archive, tables, new_path, options, pool, fin, err);
}
/// Get a threaded data reader for a file inode.
fn threadedDataReader(self: Inode, alloc: std.mem.Allocator, archive: Archive, tables: *Tables) !?ThreadedDataReader {
return switch (self.hdr.inode_type) {
.file => threadedReaderFromData(alloc, archive, tables, self.data.file),
.ext_file => threadedReaderFromData(alloc, archive, tables, self.data.ext_file),
else => error.NotRegularFile,
};
}
fn threadedReaderFromData(alloc: std.mem.Allocator, archive: Archive, tables: *Tables, data: anytype) !?ThreadedDataReader {
if (data.block_sizes.len == 0 and data.frag_idx == 0xFFFFFFFF) return null;
var out: ThreadedDataReader = .init(alloc, archive, data.block_sizes, data.block_start, data.size);
if (data.frag_idx != 0xFFFFFFFF)
out.addFragment(try tables.frag_table.get(data.frag_idx), data.frag_block_offset);
return out;
}
/// Creates the symlink described by the inode.
/// Sets metadata.
fn extractSymlink(self: Inode, path: []const u8) !void {
const target = switch (self.data) {
.symlink => |s| s.target,
.ext_symlink => |s| s.target,
else => unreachable,
};
try std.fs.cwd().symLink(target, path, .{});
}
/// Creates the device described by the inode.
/// Sets metadata.
fn extractDeviceAndIPC(self: Inode, alloc: std.mem.Allocator, tables: *Tables, path: []const u8, options: ExtractionOptions) !void {
var mode: u32 = undefined;
var dev: u32 = 0;
switch (self.data) {
.char_dev => |d| {
mode = std.posix.S.IFCHR;
dev = d.dev;
},
.ext_char_dev => |d| {
mode = std.posix.S.IFCHR;
dev = d.dev;
},
.block_dev => |d| {
mode = std.posix.S.IFBLK;
dev = d.dev;
},
.ext_block_dev => |d| {
mode = std.posix.S.IFBLK;
dev = d.dev;
},
.fifo, .ext_fifo => mode = std.posix.S.IFIFO,
.socket, .ext_socket => mode = std.posix.S.IFSOCK,
else => unreachable,
}
const res: std.os.linux.E = @enumFromInt(std.os.linux.mknod(@ptrCast(path), mode, dev));
switch (res) {
.SUCCESS => {},
.ACCES => return std.fs.Dir.MakeError.AccessDenied,
.DQUOT => return std.fs.Dir.MakeError.DiskQuota,
.EXIST => return std.fs.Dir.MakeError.PathAlreadyExists,
.FAULT, .NOENT => return std.fs.Dir.MakeError.BadPathName,
.LOOP => return std.fs.Dir.MakeError.SymLinkLoop,
.NAMETOOLONG => return std.fs.Dir.MakeError.NameTooLong,
.NOMEM => return std.fs.Dir.MakeError.SystemResources,
.NOSPC => return std.fs.Dir.MakeError.NoSpaceLeft,
.NOTDIR => return std.fs.Dir.MakeError.NotDir,
.PERM => return std.fs.Dir.MakeError.PermissionDenied,
.ROFS => return std.fs.Dir.MakeError.ReadOnlyFileSystem,
else => return blk: {
std.debug.print("unhandled mknod result: {}\n", .{res});
break :blk std.fs.Dir.MakeError.Unexpected;
},
}
var fil = try std.fs.cwd().openFile(path, .{});
defer fil.close();
try self.setMetadata(alloc, tables, fil, options);
}
-101
View File
@@ -1,101 +0,0 @@
const std = @import("std");
const WaitGroup = std.Thread.WaitGroup;
const Mutex = std.Thread.Mutex;
const Archive = @import("../archive.zig");
const Inode = @import("../inode.zig");
const ExtractionOptions = @import("../options.zig");
const Tables = @import("../tables.zig");
const InodeFinish = @This();
const FinishEnum = enum {
wg,
fin,
};
pub const FinishUnion = union(FinishEnum) {
wg: *WaitGroup,
fin: *InodeFinish,
pub fn finish(self: FinishUnion) void {
switch (self) {
.wg => |wg| wg.finish(),
.fin => |fin| fin.finish(),
}
}
};
alloc: std.mem.Allocator,
inode: Inode,
path: []const u8,
tables: *Tables,
options: ExtractionOptions,
parent_finish: FinishUnion,
fil: ?std.fs.File,
out_err: *?anyerror,
wg: WaitGroup = .{},
mut: Mutex = .{},
pub fn create(
alloc: std.mem.Allocator,
inode: Inode,
path: []const u8,
tables: *Tables,
options: ExtractionOptions,
parent_finish: FinishUnion,
out_err: *?anyerror,
fil: ?std.fs.File,
work_size: usize,
) !*InodeFinish {
if (work_size == 0)
return error.InvalidWorkSize;
const out = try alloc.create(InodeFinish);
errdefer alloc.destroy(out);
out.* = .{
.alloc = alloc,
.inode = inode,
.path = path,
.tables = tables,
.options = options,
.parent_finish = parent_finish,
.out_err = out_err,
.fil = fil,
};
out.wg.startMany(work_size);
return out;
}
pub fn logError(self: *InodeFinish, comptime fmt: []const u8, args: anytype) void {
if (self.options.verbose)
self.options.verbose_writer.?.print(fmt, args) catch {};
}
pub fn finish(self: *InodeFinish) void {
self.mut.lock();
{
defer self.mut.unlock();
self.wg.finish();
if (!self.wg.isDone()) return;
}
defer {
self.parent_finish.finish();
self.alloc.destroy(self);
}
if (self.fil == null)
self.fil = std.fs.cwd().openFile(self.path, .{}) catch |err| {
if (self.options.verbose)
self.options.verbose_writer.?.print("Error opening {s} to set metadata: {}\n", .{ self.path, err }) catch {};
self.out_err.* = err;
return;
};
defer self.fil.?.close();
self.inode.setMetadata(self.alloc, self.tables, self.fil.?, self.options) catch |err| {
if (self.options.verbose)
self.options.verbose_writer.?.print("Error setting metadata to {s}: {}\n", .{ self.path, err }) catch {};
self.out_err.* = err;
return;
};
}
-41
View File
@@ -1,41 +0,0 @@
const std = @import("std");
const OffsetFile = @import("offset_file.zig");
const MetadataCache = @This();
alloc: std.mem.Allocator,
buf: []u8,
fixed_alloc: std.heap.FixedBufferAllocator,
cache: std.AutoArrayHashMap(u64, [8192]u8),
mut: std.Thread.Mutex = .{},
cache_mut: std.AutoArrayHashMap(u64, std.Thread.Mutex),
fil: OffsetFile,
pub fn init(alloc: std.mem.Allocator, cache_size: u64) !MetadataCache {}
pub fn deinit(self: *MetadataCache) void {
self.mut.lock();
defer self.mut.unlock();
self.cache.deinit();
self.cache_mut.deinit();
self.alloc.free(self.buf);
}
pub fn getChunk(self: *MetadataCache, offset: u64) ![8192]u8 {
var res = self.cache.get(offset);
if (res != null) return res.?;
var offset_mut = blk: {
self.mut.lock();
defer self.mut.unlock();
const mut = try self.cache_mut.getOrPut(offset);
if (!mut.found_existing)
mut.value_ptr.* = .{};
break :blk mut.value_ptr;
};
offset_mut.lock();
defer offset_mut.unlock();
}
-97
View File
@@ -1,97 +0,0 @@
const std = @import("std");
const Reader = std.Io.Reader;
const Writer = std.Io.Writer;
const Limit = std.Io.Limit;
const StreamError = std.Io.Reader.StreamError;
const DecompFn = @import("../decomp.zig").DecompFn;
const BlockHeader = packed struct {
size: u15,
uncompressed: bool,
};
const This = @This();
alloc: std.mem.Allocator,
rdr: *Reader,
decomp: DecompFn,
buf: [8192]u8 = undefined,
interface: Reader,
err: ?anyerror = null,
pub fn init(alloc: std.mem.Allocator, rdr: *Reader, decomp: DecompFn) This {
return .{
.alloc = alloc,
.rdr = rdr,
.decomp = decomp,
.interface = .{
.buffer = &[0]u8{},
.end = 0,
.seek = 0,
.vtable = &.{
.stream = stream,
.discard = discard,
.readVec = readVec,
},
},
};
}
fn advance(self: *This) !void {
self.interface.seek = 0;
var hdr: BlockHeader = undefined;
try self.rdr.readSliceEndian(BlockHeader, @ptrCast(&hdr), .little);
if (hdr.uncompressed) {
try self.rdr.readSliceEndian(u8, self.buf[0..hdr.size], .little);
self.interface.end = hdr.size;
self.interface.buffer = self.buf[0..hdr.size];
return;
}
var tmp_buf: [8192]u8 = undefined;
try self.rdr.readSliceAll(tmp_buf[0..hdr.size]);
self.interface.end = try self.decomp(self.alloc, tmp_buf[0..hdr.size], &self.buf);
self.interface.buffer = self.buf[0..self.interface.end];
}
fn stream(rdr: *Reader, wrt: *Writer, limit: Limit) StreamError!usize {
const self: *This = @fieldParentPtr("interface", rdr);
if (rdr.end == rdr.seek) self.advance() catch |err| {
self.err = err;
return StreamError.ReadFailed;
};
if (@intFromEnum(limit) == 0) return 0;
const to_write = @min(rdr.end - rdr.seek, @intFromEnum(limit));
const wrote = try wrt.write(self.buf[rdr.seek .. rdr.seek + to_write]);
self.interface.seek += wrote;
return wrote;
}
fn discard(rdr: *Reader, limit: Limit) error{ EndOfStream, ReadFailed }!usize {
const self: *This = @fieldParentPtr("interface", rdr);
if (rdr.end == rdr.seek) self.advance() catch |err| {
self.err = err;
return StreamError.ReadFailed;
};
if (@intFromEnum(limit) == 0) return 0;
const to_skip = @min(rdr.end - rdr.seek, @intFromEnum(limit));
rdr.seek += to_skip;
return to_skip;
}
fn readVec(rdr: *Reader, vec: [][]u8) error{ EndOfStream, ReadFailed }!usize {
const self: *This = @fieldParentPtr("interface", rdr);
if (rdr.end == rdr.seek) self.advance() catch |err| {
self.err = err;
return StreamError.ReadFailed;
};
var cur_red: usize = 0;
for (vec) |s| {
const to_copy: usize = @min(rdr.end - rdr.seek, s.len);
@memcpy(s[0..to_copy], self.buf[rdr.seek .. rdr.seek + to_copy]);
rdr.seek += to_copy;
cur_red += to_copy;
if (rdr.end == rdr.seek) break;
}
return cur_red;
}
-20
View File
@@ -1,20 +0,0 @@
//! A File where it's meaningful (to us) content starts at a given offset.
const std = @import("std");
const File = std.fs.File;
const Reader = std.fs.File.Reader;
const OffsetFile = @This();
fil: File,
offset: u64,
pub fn init(fil: File, init_offset: u64) OffsetFile {
return .{ .fil = fil, .offset = init_offset };
}
pub fn readerAt(self: OffsetFile, offset: u64, buffer: []u8) !Reader {
var rdr = self.fil.reader(buffer);
try rdr.seekTo(self.offset + offset);
return rdr;
}
-117
View File
@@ -1,117 +0,0 @@
const std = @import("std");
const Decompressor = @import("decomp.zig");
const Table = @import("tables.zig").Table;
const MetadataReader = @import("util/metadata.zig");
const OffsetFile = @import("util/offset_file.zig");
const Ref = packed struct {
block_offset: u16,
block_start: u32,
_: u16,
};
const Entry = packed struct {
ref: Ref,
count: u32,
size: u32,
};
const KeyPrefix = enum(u8) {
user,
trusted,
security,
};
const KeyRaw = packed struct {
type: packed struct {
prefix: KeyPrefix,
out_of_line: bool,
_: u7,
},
name_size: u16,
};
pub const KeyValue = struct {
key: [:0]u8,
value: []u8,
};
const XattrTable = @This();
alloc: std.mem.Allocator,
fil: OffsetFile,
decomp: *Decompressor,
count: u32,
start: u64,
table: Table(Entry),
pub fn init(alloc: std.mem.Allocator, fil: OffsetFile, decomp: *Decompressor, table_start: u64) !XattrTable {
var info = packed struct {
start: u64 = undefined,
count: u32 = undefined,
_: u32 = undefined,
}{};
var rdr = try fil.readerAt(table_start, &[0]u8{});
try rdr.interface.readSliceEndian(@TypeOf(info), @ptrCast(&info), .little);
return .{
.alloc = alloc,
.fil = fil,
.decomp = decomp,
.count = info.count,
.start = info.start,
.table = try .init(alloc, fil, decomp, table_start + 16, info.count),
};
}
pub fn deinit(self: *XattrTable) void {
self.table.deinit();
}
pub fn get(self: *XattrTable, alloc: std.mem.Allocator, idx: u32) ![]KeyValue {
const entry: Entry = try self.table.get(idx);
const out = try alloc.alloc(KeyValue, entry.count);
for (out) |*kv| {
var rdr = try self.fil.readerAt(self.start + entry.ref.block_start, &[0]u8{});
var meta: MetadataReader = .init(alloc, &rdr.interface, self.decomp);
try meta.interface.discardAll(entry.ref.block_offset);
var key_raw: KeyRaw = undefined;
try meta.interface.readSliceEndian(KeyRaw, @ptrCast(&key_raw), .little);
switch (key_raw.type.prefix) {
.user => {
kv.key = @ptrCast(try alloc.alloc(u8, key_raw.name_size + 5 + 1));
@memcpy(kv.key[0..5], "user.");
try meta.interface.readSliceAll(kv.key[5 .. kv.key.len - 1]);
kv.key[kv.key.len - 1] = 0;
},
.security => {
kv.key = @ptrCast(try alloc.alloc(u8, key_raw.name_size + 9 + 1));
@memcpy(kv.key[0..9], "security.");
try meta.interface.readSliceAll(kv.key[9 .. kv.key.len - 1]);
kv.key[kv.key.len - 1] = 0;
},
.trusted => {
kv.key = @ptrCast(try alloc.alloc(u8, key_raw.name_size + 8 + 1));
@memcpy(kv.key[0..8], "trusted.");
try meta.interface.readSliceAll(kv.key[8 .. kv.key.len - 1]);
kv.key[kv.key.len - 1] = 0;
},
}
if (key_raw.type.out_of_line) {
try meta.interface.discardAll(4);
var ref: Ref = undefined;
try meta.interface.readSliceEndian(Ref, @ptrCast(&ref), .little);
rdr = try self.fil.readerAt(self.start + ref.block_start, &[0]u8{});
meta = .init(alloc, &rdr.interface, self.decomp);
try meta.interface.discardAll(ref.block_offset);
}
var value_size: u32 = undefined;
try meta.interface.readSliceEndian(u32, @ptrCast(&value_size), .little);
kv.value = try alloc.alloc(u8, value_size);
try meta.interface.readSliceAll(kv.value);
}
return out;
}