1a8838b544
Changed all *const Decompressor to *Decompressor Changed all decompressors to only stateless (my queues are borked)
289 lines
8.6 KiB
Zig
289 lines
8.6 KiB
Zig
const std = @import("std");
|
|
const Io = std.Io;
|
|
|
|
const Decomp = @import("decomp.zig");
|
|
const ExtractionOptions = @import("options.zig");
|
|
const File = @import("file.zig");
|
|
const Inode = @import("inode.zig");
|
|
const LookupTable = @import("lookup_table.zig");
|
|
const Decompressor = @import("util/decompressor.zig");
|
|
const MetadataReader = @import("util/metadata.zig");
|
|
const Utils = @import("util/misc.zig");
|
|
const OffsetFile = @import("util/offset_file.zig");
|
|
|
|
const Archive = @This();
|
|
|
|
file: OffsetFile,
|
|
super: Superblock,
|
|
|
|
stateless_decomp: Decompressor,
|
|
|
|
pub fn init(io: Io, file: std.Io.File, offset: u64) !Archive {
|
|
var rdr = file.reader(io, &[0]u8{});
|
|
try rdr.seekTo(offset);
|
|
var super: Superblock = undefined;
|
|
try rdr.interface.readSliceEndian(Superblock, @ptrCast(&super), .little);
|
|
try super.validate();
|
|
|
|
return .{
|
|
.file = try .init(io, file, super.size, offset),
|
|
.super = super,
|
|
|
|
.stateless_decomp = try Decomp.StatelessDecomp(super.compression),
|
|
};
|
|
}
|
|
pub fn deinit(self: *Archive, io: Io) void {
|
|
self.file.deinit(io);
|
|
}
|
|
|
|
/// The root folder of the Archive. Used to open other Files.
|
|
pub fn root(self: *Archive, alloc: std.mem.Allocator) !File {
|
|
const root_inode = try Utils.inodeFromRef(
|
|
alloc,
|
|
self.file,
|
|
&self.stateless_decomp,
|
|
self.super.inode_start,
|
|
self.super.block_size,
|
|
self.super.root_ref,
|
|
);
|
|
return .init(alloc, self.*, root_inode, "");
|
|
}
|
|
/// Opens a File within the archive.
|
|
pub fn open(self: *Archive, alloc: std.mem.Allocator, io: Io, filepath: []const u8) !File {
|
|
var root_file = try self.root(alloc);
|
|
const path = std.mem.trim(u8, filepath, "/");
|
|
if (Utils.pathIsSelf(path))
|
|
return root_file;
|
|
defer root_file.deinit();
|
|
return root_file.open(alloc, io, filepath);
|
|
}
|
|
|
|
/// Returns the inode with the given inode number.
|
|
/// Requires that the archive is exportable (has an export lookup table).
|
|
pub fn inode(self: *Archive, alloc: std.mem.Allocator, io: Io, num: u32) !Inode {
|
|
if (!self.super.flags.exportable)
|
|
return error.NotExportable;
|
|
const ref = try LookupTable.lookupValue(
|
|
Inode.Ref,
|
|
alloc,
|
|
io,
|
|
&self.stateless_decomp,
|
|
self.file,
|
|
self.super.export_start,
|
|
num + 1,
|
|
);
|
|
return Utils.inodeFromRef(
|
|
alloc,
|
|
io,
|
|
self.file,
|
|
&self.stateless_decomp,
|
|
self.super.inode_start,
|
|
self.super.block_size,
|
|
ref,
|
|
);
|
|
}
|
|
/// Returns a value at the given index from the Archive's id (uid/gid) table.
|
|
pub fn idTable(self: *Archive, alloc: std.mem.Allocator, io: Io, idx: u32) !u16 {
|
|
return LookupTable.lookupValue(
|
|
u16,
|
|
alloc,
|
|
io,
|
|
&self.stateless_decomp,
|
|
self.file,
|
|
self.super.id_start,
|
|
idx,
|
|
);
|
|
}
|
|
|
|
/// Extract the entire archive contents to the given directory.
|
|
pub fn extract(self: *Archive, alloc: std.mem.Allocator, io: Io, extract_dir: []const u8, options: ExtractionOptions) !void {
|
|
const root_inode = try Utils.inodeFromRef(
|
|
alloc,
|
|
self.file,
|
|
&self.stateless_decomp,
|
|
self.super.inode_start,
|
|
self.super.block_size,
|
|
self.super.root_ref,
|
|
);
|
|
return root_inode.extract(alloc, io, self.file, self.super, extract_dir, options);
|
|
}
|
|
|
|
// Superblock
|
|
|
|
const SQUASHFS_MAGIC: u32 = std.mem.readInt(u32, "hsqs", .little);
|
|
|
|
const SuperblockError = error{
|
|
InvalidMagic,
|
|
InvalidBlockLog,
|
|
InvalidVersion,
|
|
InvalidCheck,
|
|
};
|
|
|
|
/// A squashfs Superblock
|
|
pub const Superblock = extern struct {
|
|
magic: u32,
|
|
inode_count: u32,
|
|
mod_time: u32,
|
|
block_size: u32,
|
|
frag_count: u32,
|
|
compression: Decomp.Enum,
|
|
block_log: u16,
|
|
flags: packed struct(u16) {
|
|
inode_uncompressed: bool,
|
|
data_uncompressed: bool,
|
|
check: bool,
|
|
frag_uncompressed: bool,
|
|
fragment_never: bool,
|
|
fragment_always: bool,
|
|
duplicates: bool,
|
|
exportable: bool,
|
|
xattr_uncompressed: bool,
|
|
xattr_never: bool,
|
|
compression_options: bool,
|
|
ids_uncompressed: bool,
|
|
_: u4,
|
|
},
|
|
id_count: u16,
|
|
ver_maj: u16,
|
|
ver_min: u16,
|
|
root_ref: Inode.Ref,
|
|
size: u64,
|
|
id_start: u64,
|
|
xattr_start: u64,
|
|
inode_start: u64,
|
|
dir_start: u64,
|
|
frag_start: u64,
|
|
export_start: u64,
|
|
|
|
/// Validate the Superblock. If an error is returned, it's likely the archive is corrupted or not a squashfs archive.
|
|
pub fn validate(self: Superblock) !void {
|
|
if (self.magic != SQUASHFS_MAGIC)
|
|
return SuperblockError.InvalidMagic;
|
|
if (self.flags.check)
|
|
return SuperblockError.InvalidCheck;
|
|
if (self.ver_maj != 4 or self.ver_min != 0)
|
|
return SuperblockError.InvalidVersion;
|
|
if (std.math.log2(self.block_size) != self.block_log)
|
|
return SuperblockError.InvalidBlockLog;
|
|
}
|
|
};
|
|
|
|
// Tests
|
|
|
|
const TestArchive = "testing/LinuxPATest.sfs";
|
|
|
|
test "Basics" {
|
|
std.debug.print("Starting test: Basics...\n", .{});
|
|
|
|
const alloc = std.testing.allocator;
|
|
const io = std.testing.io;
|
|
|
|
var fil = try Io.Dir.cwd().openFile(io, TestArchive, .{});
|
|
defer fil.close(io);
|
|
var sfs: Archive = try .init(io, fil, 0);
|
|
defer sfs.deinit(io);
|
|
try std.testing.expectEqualDeep(sfs.super, LinuxPATestCorrectSuperblock);
|
|
const root_file = try sfs.root(alloc);
|
|
defer root_file.deinit();
|
|
}
|
|
|
|
const TestFile = "Start.exe";
|
|
const TestFileExtractLocation = "testing/Start.exe";
|
|
|
|
test "ExtractSingleFile" {
|
|
std.debug.print("Starting test: ExtractSingleFile...\n", .{});
|
|
|
|
const alloc = std.testing.allocator;
|
|
const io = std.testing.io;
|
|
|
|
Io.Dir.cwd().deleteFile(io, TestFileExtractLocation) catch {};
|
|
var fil = try Io.Dir.cwd().openFile(io, TestArchive, .{});
|
|
defer fil.close(io);
|
|
var sfs: Archive = try .init(io, fil, 0);
|
|
defer sfs.deinit(io);
|
|
var test_fil = try sfs.open(alloc, io, TestFile);
|
|
defer test_fil.deinit();
|
|
try test_fil.extract(alloc, io, TestFileExtractLocation, .default);
|
|
//TODO: validate extracted file.
|
|
}
|
|
|
|
const TestFullExtractLocation = "testing/TestExtract";
|
|
|
|
test "ExtractCompleteArchive" {
|
|
std.debug.print("Starting test: ExtractCompleteArchive...\n", .{});
|
|
|
|
const alloc = std.testing.allocator;
|
|
const io = std.testing.io;
|
|
|
|
Io.Dir.cwd().deleteTree(io, TestFullExtractLocation) catch {};
|
|
var fil = try Io.Dir.cwd().openFile(io, TestArchive, .{});
|
|
defer fil.close(io);
|
|
var sfs: Archive = try .init(io, fil, 0);
|
|
defer sfs.deinit(io);
|
|
try sfs.extract(alloc, io, TestFullExtractLocation, .default);
|
|
}
|
|
|
|
test "ExtractCompleteArchiveSingleThreaded" {
|
|
std.debug.print("Starting test: ExtractCompleteArchive...\n", .{});
|
|
|
|
const alloc = std.testing.allocator;
|
|
const io = std.testing.io;
|
|
|
|
var fil = try Io.Dir.cwd().openFile(io, TestArchive, .{});
|
|
defer fil.close(io);
|
|
{
|
|
std.debug.print("First testing using Threaded.global_single_threaded...\n", .{});
|
|
Io.Dir.cwd().deleteTree(io, TestFullExtractLocation) catch {};
|
|
var sfs: Archive = try .init(Io.Threaded.global_single_threaded.io(), fil, 0);
|
|
defer sfs.deinit(Io.Threaded.global_single_threaded.io());
|
|
try sfs.extract(alloc, Io.Threaded.global_single_threaded.io(), TestFullExtractLocation, .default);
|
|
}
|
|
{
|
|
std.debug.print("Next testing using ExtractionOptions.single_threaded...\n", .{});
|
|
Io.Dir.cwd().deleteTree(io, TestFullExtractLocation) catch {};
|
|
var sfs: Archive = try .init(io, fil, 0);
|
|
defer sfs.deinit(io);
|
|
try sfs.extract(alloc, io, TestFullExtractLocation, .default_single_threaded);
|
|
}
|
|
}
|
|
|
|
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,
|
|
};
|