Fleshing out more of the library.

Mainly adding functions to File
This commit is contained in:
Caleb J. Gardner
2026-04-07 15:41:30 -05:00
parent eec468ff17
commit 3e97aabe53
9 changed files with 371 additions and 43 deletions
+1 -1
View File
@@ -108,6 +108,6 @@ pub fn build(b: *std.Build) !void {
.root_module = exe_mod, .root_module = exe_mod,
}); });
const check = b.step("check", "Check if unsquashfs compiles"); const check = b.step("check", "Check if unsquashfs compiles");
check.dependOn(&lib_check.step);
check.dependOn(&exe_check.step); check.dependOn(&exe_check.step);
check.dependOn(&lib_check.step);
} }
+23 -12
View File
@@ -42,25 +42,20 @@ pub fn init(fil: std.fs.File, offset: u64) !Archive {
}; };
} }
pub fn extract(self: Archive, alloc: std.mem.Allocator, path: []const u8, options: ExtractionOptions) !void {
_ = self;
_ = alloc;
_ = path;
_ = options;
return error.TODO;
}
pub fn root(self: Archive, alloc: std.mem.Allocator) !File { pub fn root(self: Archive, alloc: std.mem.Allocator) !File {
return .{ return .{
.alloc = alloc, .file = self.file,
.super = self.super.toMinimal(),
.decomp = self.stateless_decomp.statelessCopy(alloc),
.inode = try Utils.refToInode( .inode = try Utils.readInode(
alloc, alloc,
&self.stateless_decomp, &self.stateless_decomp,
self.file, self.file,
self.super.inode_start, self.super.inode_start,
self.super.block_size, self.super.block_size,
self.super.root_ref, self.super.root_ref.block_start,
self.super.root_ref.block_offset,
), ),
.name = "", .name = "",
}; };
@@ -80,7 +75,23 @@ pub fn id(self: Archive, idx: u32) !u16 {
} }
pub fn inode(self: Archive, alloc: std.mem.Allocator, inode_num: u32) !Inode { pub fn inode(self: Archive, alloc: std.mem.Allocator, inode_num: u32) !Inode {
const ref = try LookupTable.stateless(Inode.Ref, self.file, &self.stateless_decomp, self.super.export_start, inode_num - 1); const ref = try LookupTable.stateless(Inode.Ref, self.file, &self.stateless_decomp, self.super.export_start, inode_num - 1);
return Utils.refToInode(alloc, &self.stateless_decomp, self.file, self.super.inode_start, self.super.block_size, ref); return Utils.readInode(
alloc,
&self.stateless_decomp,
self.file,
self.super.inode_start,
self.super.block_size,
ref.block_start,
ref.block_offset,
);
}
pub fn extract(self: Archive, alloc: std.mem.Allocator, path: []const u8, options: ExtractionOptions) !void {
_ = self;
_ = alloc;
_ = path;
_ = options;
return error.TODO;
} }
// Superblock // Superblock
+5
View File
@@ -17,6 +17,11 @@ vtable: *const struct {
stateless: StatelessDecomp, stateless: StatelessDecomp,
}, },
/// Create a copy of the decompressor using it's stateless function and the new allocator.
pub fn statelessCopy(self: Decompressor, alloc: std.mem.Allocator) Decompressor {
return &.{ .alloc = alloc, .vtable = &.{ .stateless = self.vtable.stateless } };
}
pub fn decompress(self: *const Decompressor, in: []u8, out: []u8) Error!usize { pub fn decompress(self: *const Decompressor, in: []u8, out: []u8) Error!usize {
return self.vtable.decompress(self, in, out); return self.vtable.decompress(self, in, out);
} }
+4
View File
@@ -42,6 +42,10 @@ pub const Entry = struct {
num: u32, num: u32,
inode_type: InodeType, inode_type: InodeType,
name: []u8, name: []u8,
pub fn deinit(self: Entry, alloc: std.mem.Allocator) void {
alloc.free(self.name);
}
}; };
// extern instead of packed due to alignment issues (packed will read it as 16 bytes instead of 12). // extern instead of packed due to alignment issues (packed will read it as 16 bytes instead of 12).
+82 -27
View File
@@ -3,8 +3,12 @@ const std = @import("std");
const Archive = @import("archive.zig"); const Archive = @import("archive.zig");
const Decompressor = @import("decomp.zig"); const Decompressor = @import("decomp.zig");
const Directory = @import("directory.zig"); const Directory = @import("directory.zig");
const ExtractionOptions = @import("options.zig");
const Inode = @import("inode.zig"); const Inode = @import("inode.zig");
const DataReader = @import("util/data_reader.zig");
const FileIter = @import("util/iter.zig");
const MetadataReader = @import("util/metadata.zig"); const MetadataReader = @import("util/metadata.zig");
const OffsetFile = @import("util/offset_file.zig");
const Utils = @import("util/utils.zig"); const Utils = @import("util/utils.zig");
pub const Error = error{ pub const Error = error{
@@ -14,9 +18,8 @@ pub const Error = error{
const File = @This(); const File = @This();
alloc: std.mem.Allocator, file: OffsetFile,
super: Archive.MinimalSuperblock,
superblock: Archive.MinimalSuperblock,
decomp: Decompressor, decomp: Decompressor,
name: []const u8, name: []const u8,
@@ -26,44 +29,96 @@ pub fn init(alloc: std.mem.Allocator, archive: Archive, entry: Directory.Entry)
const new_name = try alloc.alloc(u8, entry.name.len); const new_name = try alloc.alloc(u8, entry.name.len);
errdefer alloc.free(new_name); errdefer alloc.free(new_name);
@memcpy(new_name, entry.name); @memcpy(new_name, entry.name);
var rdr = archive.file.readerAt(archive.super.inode_start + entry.block_start, &[0]u8{});
var meta: MetadataReader = .init(&rdr.interface, &archive.stateless_decomp);
try meta.interface.discardAll(entry.block_offset);
return .{ return .{
.alloc = alloc, .file = archive.file,
.superblock = archive.super, .super = archive.super,
.decomp = .{ .decomp = .{
.alloc = alloc, .alloc = alloc,
.vtable = &.{ .stateless = archive.stateless_decomp.vtable.stateless }, .vtable = &.{ .stateless = archive.stateless_decomp.vtable.stateless },
}, },
.name = new_name, .name = new_name,
.inode = try .read(alloc, &meta.interface, archive.super.block_size), .inode = try Utils.readInode(
alloc,
&archive.decomp,
archive.file,
archive.super.inode_start,
archive.super.block_size,
entry.block_start,
entry.block_offset,
),
}; };
} }
pub fn deinit(self: File) void { pub fn deinit(self: File) void {
self.alloc.free(self.name); self.decomp.alloc.free(self.name);
self.inode.deinit(self.decomp.alloc);
} }
/// Opens a sub-directory. If the given path is "", ".", "/", or "./", a copy of the File is returned. // Directory functions
pub fn open(self: File, alloc: std.mem.Allocator, path: []const u8) !File {
pub fn isDir(self: File) bool {
return switch (self.inode.hdr.inode_type) {
.dir, .ext_dir => true,
else => false,
};
}
/// Opens a sub-file. If the given path is "" or "." (after trimming /) a copy of the File is returned.
pub fn open(self: File, alloc: std.mem.Allocator, filepath: []const u8) !File {
switch (self.inode.hdr.inode_type) { switch (self.inode.hdr.inode_type) {
.dir, .ext_dir => {}, .dir, .ext_dir => {
var res = try self.inode.findInode(
alloc,
&self.decomp,
self.file,
self.super.dir_start,
self.super.inode_start,
self.super.block_size,
filepath,
);
if (res.name.len == 0) {
res.name = try alloc.alloc(u8, self.name.len);
@memcpy(res.name, self.name);
}
return .{
.file = self.file,
.super = self.super,
.decomp = .{
.alloc = alloc,
.vtable = &.{ .stateless = self.decomp.vtable.stateless },
},
.name = res.name,
.inode = res.inode,
};
},
else => Error.NotDirectory, else => Error.NotDirectory,
} }
if (Utils.pathIsSelf(path)) { }
const new_name = try alloc.alloc(u8, self.name.len); pub fn iter(self: File, alloc: std.mem.Allocator) !FileIter {
@memcpy(new_name, self.name); return .{
return .{ .alloc = alloc,
.alloc = alloc, .entries = try self.inode.readDirectory(alloc, &self.decomp, self.file, self.super.dir_start),
.superblock = self.superblock, };
.decomp = .{ }
.alloc = alloc,
.vtable = &.{ .stateless = self.decomp.vtable.stateless },
},
.name = new_name,
.inode = self.inode,
};
}
// Regular file functions
pub fn isRegularFile(self: File) bool {
return switch (self.inode.hdr.inode_type) {
.file, .ext_file => true,
else => false,
};
}
pub fn dataReader(self: File, alloc: std.mem.Allocator) !DataReader {
if (!self.isRegularFile()) return Error.NotRegularFile;
_ = alloc;
return error.TODO;
}
// Universal functions
pub fn extract(self: File, alloc: std.mem.Allocator, path: []const u8, options: ExtractionOptions) !void {
_ = self;
_ = alloc;
_ = path;
_ = options;
return error.TODO; return error.TODO;
} }
+198
View File
@@ -1,10 +1,19 @@
//! This is the raw squashfs representation of a file/directory.
//! Most of the time using File is a better experience and using Inodes directory
//! is only required for more technical use cases.
const std = @import("std"); const std = @import("std");
const Reader = std.Io.Reader; const Reader = std.Io.Reader;
const Decompressor = @import("decomp.zig");
const Directory = @import("directory.zig");
const Dir = @import("inode/dir.zig"); const Dir = @import("inode/dir.zig");
const File = @import("inode/file.zig"); const File = @import("inode/file.zig");
const Misc = @import("inode/misc.zig"); const Misc = @import("inode/misc.zig");
const Sym = @import("inode/sym.zig"); const Sym = @import("inode/sym.zig");
const MinimalSuperblock = @import("archive.zig").MinimalSuperblock;
const MetadataReader = @import("util/metadata.zig");
const OffsetFile = @import("util/offset_file.zig");
const Inode = @This(); const Inode = @This();
@@ -43,6 +52,75 @@ pub fn deinit(self: Inode, alloc: std.mem.Allocator) void {
else => {}, else => {},
} }
} }
pub fn copy(self: Inode, alloc: std.mem.Allocator) !Inode {
switch (self.data) {
.dir,
.ext_dir,
.block,
.ext_block,
.char,
.ext_char,
.fifo,
.ext_fifo,
.sock,
.ext_sock,
=> return self,
.file => |f| {
const new_sizes = try alloc.alloc(File.BlockSize, f.block_sizes.len);
@memcpy(new_sizes, f.block_sizes);
return .{
.hdr = self.hdr,
.data = .{ .file = .{
.block_start = f.block_start,
.frag_idx = f.frag_idx,
.block_offset = f.block_offset,
.size = f.size,
.block_sizes = new_sizes,
} },
};
},
.ext_file => |f| {
const new_sizes = try alloc.alloc(File.BlockSize, f.block_sizes.len);
@memcpy(new_sizes, f.block_sizes);
return .{
.hdr = self.hdr,
.data = .{ .ext_file = .{
.block_start = self.block_start,
.size = self.size,
.sparse = self.sparse,
.hard_links = self.hard_links,
.frag_idx = self.frag_idx,
.block_offset = self.block_offset,
.xattr_idx = self.xattr_idx,
.block_sizes = new_sizes,
} },
};
},
.symlink => |s| {
const new_target = try alloc.alloc(u8, s.target.len);
@memcpy(new_target, s.target);
return .{
.hdr = self.hdr,
.data = .{ .symlink = .{
.hard_links = s.hard_links,
.target = new_target,
} },
};
},
.ext_symlink => |s| {
const new_target = try alloc.alloc(u8, s.target.len);
@memcpy(new_target, s.target);
return .{
.hdr = self.hdr,
.data = .{ .ext_symlink = .{
.hard_links = s.hard_links,
.xattr_idx = s.xattr_idx,
.target = new_target,
} },
};
},
}
}
// Types // Types
@@ -94,3 +172,123 @@ pub const Data = union(Type) {
ext_fifo: Misc.ExtIpc, ext_fifo: Misc.ExtIpc,
ext_sock: Misc.ExtIpc, ext_sock: Misc.ExtIpc,
}; };
// Errors
pub const Error = error{
NotDirectory,
NotFound,
NotRegularFile,
};
// Utils functions
/// For directory inodes, tries to find the inode at the given path. Returns both the inode, and it's file name. If the path is empty or "." then a copy of this inode is returned with no name ("").
pub fn findInode(
inode: Inode,
alloc: std.mem.Allocator,
decomp: *const Decompressor,
fil: OffsetFile,
dir_start: u64,
inode_start: u64,
block_size: u32,
filepath: []const u8,
) !struct { inode: Inode, name: []const u8 } {
switch (inode.data) {
.dir => |d| {
const path: []const u8 = std.mem.trim(u8, filepath, "/");
if (path.len == 0 or (path.len == 1 and path[0] == '.'))
return .{ .inode = inode.copy(alloc), .name = "" };
return findInodeRaw(
alloc,
decomp,
fil,
dir_start,
inode_start,
block_size,
path,
d,
);
},
.ext_dir => |d| {
const path: []const u8 = std.mem.trim(u8, filepath, "/");
if (path.len == 0 or (path.len == 1 and path[0] == '.'))
return .{ .inode = inode.copy(alloc), .name = "" };
return findInodeRaw(
alloc,
decomp,
fil,
dir_start,
inode_start,
block_size,
path,
d,
);
},
else => return Error.NotDirectory,
}
}
inline fn findInodeRaw(
inode: Inode,
alloc: std.mem.Allocator,
decomp: *const Decompressor,
fil: OffsetFile,
dir_start: u64,
inode_start: u64,
block_size: u32,
path: []const u8,
dat: anytype,
) !struct { inode: Inode, name: []const u8 } {
const first_element: []const u8 = std.mem.sliceTo(path, '/');
const dirs = try readDirRaw(alloc, decomp, fil, dir_start, dat);
defer {
for (dirs) |dir|
dir.deinit(alloc);
alloc.free(dirs);
}
// Directories are stored ASCIIbetically, so we can use binary search.
var cur_slice = dirs;
var idx: usize = 0;
while (cur_slice.len > 0) {
idx = cur_slice.len / 2;
const mid_name = cur_slice[idx].name;
switch (std.mem.order(u8, first_element, mid_name)) {
.gt => {
cur_slice = cur_slice[idx + 1 ..];
continue;
},
.lt => {
cur_slice = cur_slice[0..idx];
continue;
},
.eq => break,
}
} else return Error.NotFound;
const entry = cur_slice[idx];
var rdr = try fil.readerAt(inode_start + entry.block_start, &[0]u8{});
var meta_rdr: MetadataReader = .init(&rdr.interface, decomp);
try meta_rdr.interface.discardAll(entry.block_offset);
const ret_inode: Inode = try .read(alloc, &meta_rdr.interface, block_size);
if (first_element.len == path.len) {
const name_copy = try alloc.alloc(u8, entry.name.len);
@memcpy(name_copy, entry.name.len);
return .{ .inode = ret_inode, .name = name_copy };
}
return inode.findInode(alloc, decomp, fil, dir_start, inode_start, block_size, path[first_element.len..]);
}
pub fn readDirectory(inode: Inode, alloc: std.mem.Allocator, decomp: *const Decompressor, fil: OffsetFile, dir_start: u64) ![]Directory.Entry {
return switch (inode.data) {
.dir => |d| readDirRaw(alloc, decomp, fil, dir_start, d),
.ext_dir => |d| readDirRaw(alloc, decomp, fil, dir_start, d),
else => Error.NotDirectory,
};
}
inline fn readDirRaw(alloc: std.mem.Allocator, decomp: *const Decompressor, fil: OffsetFile, dir_start: u64, dat: anytype) ![]Directory.Entry {
var rdr = try fil.readerAt(dir_start + dat.block_start, &[0]u8{});
var meta_rdr: MetadataReader = .init(&rdr.interface, decomp);
try meta_rdr.interface.discardAll(dat.block_offset);
return Directory.readDirectory(alloc, meta_rdr, dat.size);
}
View File
+54
View File
@@ -0,0 +1,54 @@
const std = @import("std");
const MinimalSuperblock = @import("../archive.zig").MinimalSuperblock;
const Decompressor = @import("../decomp.zig");
const DirEntry = @import("../directory.zig").Entry;
const File = @import("../file.zig");
const Inode = @import("../inode.zig");
const MetadataReader = @import("metadata.zig");
const OffsetFile = @import("offset_file.zig");
const Utils = @import("utils.zig");
const Iter = @This();
file: OffsetFile,
super: MinimalSuperblock,
decomp: Decompressor,
entries: []DirEntry,
idx: usize = 0,
pub fn deinit(self: Iter) void {
for (self.entries) |ent|
ent.deinit(self.decomp.alloc);
self.decomp.alloc.free(self.entries);
}
pub fn next(self: *Iter) !?File {
if (self.idx >= self.entries.len) return null;
defer self.idx += 1;
const entry = self.entries[self.idx];
const new_name = try self.decomp.alloc.alloc(u8, entry.name.len);
@memcpy(new_name, entry.name);
return .{
.file = self.file,
.super = self.super,
.decomp = self.decomp,
.name = new_name,
.inode = Utils.readInode(
self.decomp.alloc,
&self.decomp,
self.file,
self.super.inode_start,
self.super.block_size,
entry.block_start,
entry.block_offset,
),
};
}
pub fn reset(self: *Iter) void {
self.idx = 0;
}
+4 -3
View File
@@ -1,6 +1,7 @@
const std = @import("std"); const std = @import("std");
const Decompressor = @import("../decomp.zig"); const Decompressor = @import("../decomp.zig");
const DirEntry = @import("../directory.zig").Entry;
const Inode = @import("../inode.zig"); const Inode = @import("../inode.zig");
const MetadataReader = @import("metadata.zig"); const MetadataReader = @import("metadata.zig");
const OffsetFile = @import("offset_file.zig"); const OffsetFile = @import("offset_file.zig");
@@ -16,9 +17,9 @@ pub fn pathIsSelf(path: []const u8) bool {
return std.mem.eql(u8, path, "./"); return std.mem.eql(u8, path, "./");
} }
pub fn refToInode(alloc: std.mem.Allocator, decomp: *const Decompressor, fil: OffsetFile, inode_start: u64, block_size: u32, ref: Inode.Ref) !Inode { pub fn readInode(alloc: std.mem.Allocator, decomp: *const Decompressor, fil: OffsetFile, inode_start: u64, block_size: u32, block_start: u32, block_offset: u16) !Inode {
var rdr = try fil.readerAt(inode_start + ref.block_start, &[0]u8{}); var rdr = try fil.readerAt(inode_start + block_start, &[0]u8{});
var meta: MetadataReader = .init(&rdr.interface, decomp); var meta: MetadataReader = .init(&rdr.interface, decomp);
try meta.interface.discardAll(ref.block_offset); try meta.interface.discardAll(block_offset);
return .read(alloc, &meta.interface, block_size); return .read(alloc, &meta.interface, block_size);
} }