diff --git a/build.zig b/build.zig index 65cd0ad..6c2d769 100644 --- a/build.zig +++ b/build.zig @@ -108,6 +108,6 @@ pub fn build(b: *std.Build) !void { .root_module = exe_mod, }); const check = b.step("check", "Check if unsquashfs compiles"); - check.dependOn(&lib_check.step); check.dependOn(&exe_check.step); + check.dependOn(&lib_check.step); } diff --git a/src/archive.zig b/src/archive.zig index 7287768..4344589 100644 --- a/src/archive.zig +++ b/src/archive.zig @@ -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 { 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, &self.stateless_decomp, self.file, self.super.inode_start, self.super.block_size, - self.super.root_ref, + self.super.root_ref.block_start, + self.super.root_ref.block_offset, ), .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 { 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 diff --git a/src/decomp.zig b/src/decomp.zig index 29c994a..5396164 100644 --- a/src/decomp.zig +++ b/src/decomp.zig @@ -17,6 +17,11 @@ vtable: *const struct { 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 { return self.vtable.decompress(self, in, out); } diff --git a/src/directory.zig b/src/directory.zig index cb714e0..2856e76 100644 --- a/src/directory.zig +++ b/src/directory.zig @@ -42,6 +42,10 @@ pub const Entry = struct { num: u32, inode_type: InodeType, 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). diff --git a/src/file.zig b/src/file.zig index b37bd45..781607c 100644 --- a/src/file.zig +++ b/src/file.zig @@ -3,8 +3,12 @@ const std = @import("std"); const Archive = @import("archive.zig"); const Decompressor = @import("decomp.zig"); const Directory = @import("directory.zig"); +const ExtractionOptions = @import("options.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 OffsetFile = @import("util/offset_file.zig"); const Utils = @import("util/utils.zig"); pub const Error = error{ @@ -14,9 +18,8 @@ pub const Error = error{ const File = @This(); -alloc: std.mem.Allocator, - -superblock: Archive.MinimalSuperblock, +file: OffsetFile, +super: Archive.MinimalSuperblock, decomp: Decompressor, 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); errdefer alloc.free(new_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 .{ - .alloc = alloc, - .superblock = archive.super, + .file = archive.file, + .super = archive.super, .decomp = .{ .alloc = alloc, .vtable = &.{ .stateless = archive.stateless_decomp.vtable.stateless }, }, .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 { - 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. -pub fn open(self: File, alloc: std.mem.Allocator, path: []const u8) !File { +// Directory functions + +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) { - .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, } - if (Utils.pathIsSelf(path)) { - const new_name = try alloc.alloc(u8, self.name.len); - @memcpy(new_name, self.name); - return .{ - .alloc = alloc, - .superblock = self.superblock, - .decomp = .{ - .alloc = alloc, - .vtable = &.{ .stateless = self.decomp.vtable.stateless }, - }, - .name = new_name, - .inode = self.inode, - }; - } +} +pub fn iter(self: File, alloc: std.mem.Allocator) !FileIter { + return .{ + .alloc = alloc, + .entries = try self.inode.readDirectory(alloc, &self.decomp, self.file, self.super.dir_start), + }; +} +// 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; } diff --git a/src/inode.zig b/src/inode.zig index 7fc5d55..23abaac 100644 --- a/src/inode.zig +++ b/src/inode.zig @@ -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 Reader = std.Io.Reader; +const Decompressor = @import("decomp.zig"); +const Directory = @import("directory.zig"); const Dir = @import("inode/dir.zig"); const File = @import("inode/file.zig"); const Misc = @import("inode/misc.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(); @@ -43,6 +52,75 @@ pub fn deinit(self: Inode, alloc: std.mem.Allocator) void { 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 @@ -94,3 +172,123 @@ pub const Data = union(Type) { ext_fifo: 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); +} diff --git a/src/util/data_reader.zig b/src/util/data_reader.zig new file mode 100644 index 0000000..e69de29 diff --git a/src/util/iter.zig b/src/util/iter.zig new file mode 100644 index 0000000..86176f3 --- /dev/null +++ b/src/util/iter.zig @@ -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; +} diff --git a/src/util/utils.zig b/src/util/utils.zig index 2d47a37..88c7d6a 100644 --- a/src/util/utils.zig +++ b/src/util/utils.zig @@ -1,6 +1,7 @@ const std = @import("std"); const Decompressor = @import("../decomp.zig"); +const DirEntry = @import("../directory.zig").Entry; const Inode = @import("../inode.zig"); const MetadataReader = @import("metadata.zig"); const OffsetFile = @import("offset_file.zig"); @@ -16,9 +17,9 @@ pub fn pathIsSelf(path: []const u8) bool { 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 { - var rdr = try fil.readerAt(inode_start + ref.block_start, &[0]u8{}); +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 + block_start, &[0]u8{}); 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); }