//! 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 DataReader = @import("util/data.zig"); const ThreadedDataReader = @import("util/data_threaded.zig"); const MetadataReader = @import("util/metadata.zig"); pub const Ref = packed struct { block_offset: u16, block_start: u32, _: u16, }; pub const InodeType = enum(u16) { dir = 1, file, symlink, block_dev, char_dev, fifo, socket, ext_dir, ext_file, ext_symlink, ext_block_dev, ext_char_dev, ext_fifo, ext_socket, }; pub const InodeData = union(InodeType) { dir: dir.Dir, file: file.File, symlink: misc.Symlink, block_dev: misc.Dev, char_dev: misc.Dev, fifo: misc.IPC, socket: misc.IPC, ext_dir: dir.ExtDir, ext_file: file.ExtFile, ext_symlink: misc.ExtSymlink, ext_block_dev: misc.ExtDev, ext_char_dev: misc.ExtDev, ext_fifo: misc.ExtIPC, ext_socket: misc.ExtIPC, }; pub const Header = packed struct { inode_type: InodeType, permissions: u16, uid_idx: u16, gid_idx: u16, mod_time: u32, num: u32, }; const Inode = @This(); hdr: Header, data: InodeData, pub fn read(alloc: std.mem.Allocator, rdr: *Reader, block_size: u32) !Inode { var hdr: Header = undefined; try rdr.readSliceEndian(Header, @ptrCast(&hdr), .little); 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) }, }, }; } 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 { switch (self.data) { .file => |f| alloc.free(f.block_sizes), .ext_file => |f| alloc.free(f.block_sizes), .symlink => |s| alloc.free(s.target), .ext_symlink => |s| alloc.free(s.target), else => {}, } } /// Get the data reader for a file inode. pub fn dataReader(self: Inode, alloc: std.mem.Allocator, archive: *Archive) !DataReader { return switch (self.hdr.inode_type) { .file => readerFromData(alloc, archive, self.data.file), .ext_file => readerFromData(alloc, archive, self.data.ext_file), else => error.NotRegularFile, }; } fn readerFromData(alloc: std.mem.Allocator, archive: *Archive, 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 archive.frag_table.get(data.frag_idx), data.frag_block_offset); return out; } /// Get a threaded data reader for a file inode. pub fn threadedDataReader(self: Inode, alloc: std.mem.Allocator, archive: *Archive) !ThreadedDataReader { return switch (self.hdr.inode_type) { .file => threadedReaderFromData(alloc, archive, self.data.file), .ext_file => threadedReaderFromData(alloc, archive, self.data.ext_file), else => error.NotRegularFile, }; } fn threadedReaderFromData(alloc: std.mem.Allocator, archive: *Archive, data: anytype) !ThreadedDataReader { var out: ThreadedDataReader = .init(alloc, archive.*, data.block_sizes, data.block_start, data.size); if (data.frag_idx != 0xFFFFFFFF) out.addFragment(try archive.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); } /// Extract the inode to the given path. Single threaded. pub fn extractTo(self: Inode, alloc: std.mem.Allocator, archive: *Archive, path: []const u8, options: ExtractionOptions) !void { if (options.threads > 1) return self.extractToThreaded(alloc, archive, path, options); switch (self.hdr.inode_type) { .dir, .ext_dir => { // Removing any trailing separators since that's the easiest path forward. if (path[path.len - 1] == '/') return self.extractTo(alloc, archive, path[0 .. path.len - 1], options); std.fs.cwd().makeDir(path) catch |err| { if (err != std.fs.Dir.MakeError.PathAlreadyExists) return err; }; const entries = try self.dirEntries(alloc, archive.*); defer { for (entries) |entry| entry.deinit(alloc); alloc.free(entries); } for (entries) |entry| { var new_path = try alloc.alloc(u8, path.len + 1 + entry.name.len); @memcpy(new_path[0..path.len], path); @memcpy(new_path[path.len + 1 ..], entry.name); new_path[path.len] = '/'; defer alloc.free(new_path); var inode: Inode = try readFromEntry(alloc, archive, entry); defer inode.deinit(alloc); try inode.extractTo(alloc, archive, new_path, options); } }, .file, .ext_file => try self.extractRegFile(alloc, archive, path, options), .symlink, .ext_symlink => try self.extractSymlink(path), else => try self.extractDevice(archive, path, options), } } const Parent = struct { alloc: std.mem.Allocator, path: []const u8, uid: u16, gid: u16, perm: u16, mod_time: u32, ignore_permissions: bool, ignore_xattr: bool, wg: WaitGroup = .{}, mut: Mutex = .{}, fn create(alloc: std.mem.Allocator, hdr: Header, archive: *Archive, path: []const u8, options: ExtractionOptions, dir_size: usize) !*Parent { const out = try alloc.create(Parent); errdefer alloc.destroy(out); out.* = .{ .alloc = alloc, .path = path, .uid = try archive.id_table.get(hdr.uid_idx), .gid = try archive.id_table.get(hdr.gid_idx), .perm = hdr.permissions, .mod_time = hdr.mod_time, .ignore_permissions = options.ignore_permissions, .ignore_xattr = options.ignore_xattr, }; out.wg.startMany(dir_size); return out; } fn finish(p: *Parent) !void { p.mut.lock(); { defer p.mut.unlock(); p.wg.finish(); if (!p.wg.isDone()) return; } defer p.alloc.destroy(p); var fil = try std.fs.cwd().openFile(p.path, .{}); defer fil.close(); const time = @as(i128, p.mod_time) * 1000000000; try fil.updateTimes(time, time); if (p.ignore_permissions) { try fil.chmod(p.perm); try fil.chown(p.uid, p.gid); } } }; /// Extract the inode to the given path. Multi-threaded. /// Functions identically to extractTo on all but regular files and directories. /// /// If threads <= 1, then this just calls extractTo. fn extractToThreaded(self: Inode, allocator: std.mem.Allocator, archive: *Archive, path: []const u8, options: ExtractionOptions) !void { switch (self.hdr.inode_type) { .dir, .ext_dir => { // Removing any trailing separators since that's the easiest path forward. if (path[path.len - 1] == '/') return self.extractToThreaded(allocator, archive, path[0 .. path.len - 1], options); // Fixed Allocator // const mem_buf = archive.allocator().alloc(u8, 2 * 1024 * 1024 * 1024); // defer archive.allocator().free(mem_buf); // var fixed_alloc: std.heap.FixedBufferAllocator = .init(mem_buf); // const alloc = fixed_alloc.threadSafeAllocator(); // Arena Allocator var arena_alloc: std.heap.ArenaAllocator = .init(allocator); defer arena_alloc.deinit(); var thread_alloc: std.heap.ThreadSafeAllocator = .{ .child_allocator = arena_alloc.allocator() }; const alloc = thread_alloc.allocator(); var wg: WaitGroup = .{}; // defer if(!options.ignore_permissions) perms.?.deinit(alloc); We don't need to do this due to ArenaAllocator var pool: Pool = undefined; try pool.init(.{ .allocator = allocator, .n_jobs = options.threads - 1 }); defer pool.deinit(); var out_err: ?anyerror = null; wg.start(); self.extractThread(alloc, archive, path, options, &wg, &pool, &out_err, null); pool.waitAndWork(&wg); if (out_err != null) return out_err.?; var fil = try std.fs.cwd().openFile(path, .{}); defer fil.close(); 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 archive.id_table.get(self.hdr.uid_idx), try archive.id_table.get(self.hdr.gid_idx)); } }, .file, .ext_file => { var pool: Pool = undefined; try pool.init(.{ .allocator = allocator, .n_jobs = options.threads - 1 }); defer pool.deinit(); var arena_alloc: std.heap.ArenaAllocator = .init(allocator); defer arena_alloc.deinit(); var thread_alloc: std.heap.ThreadSafeAllocator = .{ .child_allocator = arena_alloc.allocator() }; const alloc = thread_alloc.allocator(); try self.extractRegFileThreaded(alloc, archive, path, options, &pool); var fil = try std.fs.cwd().openFile(path, .{}); defer fil.close(); 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 archive.id_table.get(self.hdr.uid_idx), try archive.id_table.get(self.hdr.gid_idx)); } }, .symlink, .ext_symlink => try self.extractSymlink(path), else => try self.extractDevice(archive, path, options), } } fn extractThreadEntry( entry: DirEntry, alloc: std.mem.Allocator, archive: *Archive, path: []const u8, options: ExtractionOptions, wg: *WaitGroup, pool: *Pool, out_err: *?anyerror, parent: ?*Parent, ) void { var new_path = alloc.alloc(u8, path.len + entry.name.len + 1) catch |err| { wg.finish(); out_err.* = err; return; }; @memcpy(new_path[0..path.len], path); @memcpy(new_path[path.len + 1 ..], entry.name); new_path[path.len] = '/'; var inode = readFromEntry(alloc, archive, entry) catch |err| { out_err.* = err; wg.finish(); return; }; inode.extractThread(alloc, archive, new_path, options, wg, pool, out_err, parent); } /// Extract threadedly the inode to the path. fn extractThread( self: Inode, alloc: std.mem.Allocator, archive: *Archive, path: []const u8, options: ExtractionOptions, wg: *WaitGroup, pool: *Pool, out_err: *?anyerror, parent: ?*Parent, ) void { if (options.verbose) options.verbose_writer.?.print("Extracting inode #{} to {s}\n", .{ self.hdr.num, path }) catch {}; defer { if (parent != null) parent.?.finish() catch |err| { if (options.verbose) options.verbose_writer.?.print("Error setting folder permission to {s}: {}\n", .{ path, err }) catch {}; out_err.* = err; }; wg.finish(); } if (out_err.* != null) return; switch (self.hdr.inode_type) { .dir, .ext_dir => { _ = std.fs.cwd().makePathStatus(path) catch |err| { if (options.verbose) options.verbose_writer.?.print("Error creating {s}: {}\n", .{ path, err }) catch {}; out_err.* = err; return; }; const entries = self.dirEntries(alloc, archive.*) catch |err| { if (options.verbose) options.verbose_writer.?.print("Error getting directory entries for inode #{} (extracting to {s}): {}\n", .{ self.hdr.num, path, err }) catch {}; out_err.* = err; return; }; const p = Parent.create(alloc, self.hdr, archive, path, options, entries.len) catch |err| { out_err.* = err; return; }; wg.startMany(entries.len); // defer files.deinit(alloc); We don't need to do this due to ArenaAllocator for (entries) |entry| { if (entry.inode_type == .dir) { extractThreadEntry(entry, alloc, archive, path, options, wg, pool, out_err, p); continue; } pool.spawn( extractThreadEntry, .{ entry, alloc, archive, path, options, wg, pool, out_err, p, }, ) catch |err| { wg.finish(); if (options.verbose) options.verbose_writer.?.print("Error starting extraction thread: {}\n", .{err}) catch {}; out_err.* = err; continue; }; } }, .file, .ext_file => { self.extractRegFileThreaded(alloc, archive, path, options, pool) catch |err| { if (options.verbose) options.verbose_writer.?.print("Error extracting file inode #{} to {s}: {}\n", .{ self.hdr.num, path, err }) catch {}; out_err.* = err; }; }, .symlink, .ext_symlink => { self.extractSymlink(path) catch |err| { if (options.verbose) options.verbose_writer.?.print("Error extracting symlink inode #{} to {s}: {}\n", .{ self.hdr.num, path, err }) catch {}; out_err.* = err; }; }, else => { self.extractDevice(archive, path, options) catch |err| { if (options.verbose) options.verbose_writer.?.print("Error extracting device/IPC inode #{} to {s}: {}\n", .{ self.hdr.num, path, err }) catch {}; out_err.* = err; }; }, } } /// Creates and writes the inode file contents to the given path. /// Optionally set owner & permissions. /// /// Assumes the inode is a file or ext_file type. fn extractRegFile(self: Inode, alloc: std.mem.Allocator, archive: *Archive, path: []const u8, options: ExtractionOptions) !void { var fil = try std.fs.cwd().createFile(path, .{ .exclusive = true }); defer fil.close(); var wrt = fil.writer(&[0]u8{}); var dat_rdr = try self.dataReader(alloc, archive); defer dat_rdr.deinit(); _ = try dat_rdr.interface.streamRemaining(&wrt.interface); try wrt.interface.flush(); 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 archive.id_table.get(self.hdr.uid_idx), try archive.id_table.get(self.hdr.gid_idx)); } } /// Extract the inode file contents to the given path threadedly. /// pool is used to spawn threads. /// /// Assumes the inode is a file or ext_file type. fn extractRegFileThreaded(self: Inode, alloc: std.mem.Allocator, archive: *Archive, path: []const u8, options: ExtractionOptions, pool: *Pool) !void { var fil = try std.fs.cwd().createFile(path, .{}); defer fil.close(); var data = try self.threadedDataReader(alloc, archive); try data.extractThreaded(fil, pool); 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 archive.id_table.get(self.hdr.uid_idx), try archive.id_table.get(self.hdr.gid_idx)); } } /// Creates the symlink described by the inode. /// /// Assumes the inode is a symlink or ext_symlink type. 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. /// /// Optionally set owner & permissions. /// Assumes the inode is a char_dev, block_dev, fifo, socket, or their extended counterparts. fn extractDevice(self: Inode, archive: *Archive, 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(); 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 archive.id_table.get(self.hdr.uid_idx), try archive.id_table.get(self.hdr.gid_idx)); } if (!options.ignore_xattr) { // TODO } }