Files
zig-squashfs/src/file.zig
T
2025-08-08 06:34:31 -05:00

523 lines
20 KiB
Zig

const std = @import("std");
const builtin = @import("builtin");
const dir = @import("directory.zig");
const DirEntry = dir.Entry;
const Inode = @import("inode.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;
pub fn File(comptime T: type) type {
return struct {
pub const FileError = error{
NotRegular,
NotDirectory,
NotFound,
};
const Self = @This();
rdr: *SfsReader(T),
// parent: *File(T),
inode: Inode,
name: []const u8,
/// 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 = name_cpy,
};
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);
},
.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 iterate(self: Self) Iterator {
return .{
.rdr = self.rdr,
.entries = self.entries.?,
};
}
const Iterator = struct {
rdr: *SfsReader(T),
entries: []DirEntry,
idx: u32 = 0,
pub fn next(self: *Iterator) !?File(T) {
if (self.idx >= self.entries.len) return null;
const out = try Self.initFromEntry(self.rdr, self.entries[self.idx]);
self.idx += 1;
return out;
}
pub fn reset(self: *Iterator) void {
self.idx = 0;
}
};
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 exists = true;
var stat: ?std.fs.File.Stat = null;
if (std.fs.cwd().statFile(path)) |s| {
stat = s;
} else |err| {
if (err == std.fs.File.OpenError.FileNotFound) {
exists = false;
} else {
return err;
}
}
switch (self.inode.hdr.type) {
.dir, .ext_dir => {
if (exists and stat.?.kind != .directory) {
return ExtractError.FileExists;
}
},
else => if (exists) return ExtractError.FileExists,
}
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();
self.extractReal(op, path, &errs, &wg, &pol, true);
wg.wait();
if (errs.items.len > 0) return errs.items[0];
}
fn extractReal(
self: *Self,
op: ExtractionOptions,
path: []const u8,
errs: *std.ArrayList(anyerror),
wg: *WaitGroup,
pol: *Pool,
first: bool,
) void {
if (errs.items.len > 0) return;
if (op.verbose) {
std.fmt.format(
op.verbose_logger,
"extracting inode {} \"{s}\" to {s}...\n",
.{ self.inode.hdr.num, self.name, path },
) catch {};
}
return switch (self.inode.hdr.type) {
.dir, .ext_dir => {
var complete = false;
wg.start();
defer if (!complete) wg.finish();
std.fs.cwd().makeDir(path) catch |err| {
if (err != std.fs.Dir.MakeError.PathAlreadyExists) {
errs.append(err) catch {};
return;
}
};
const dir_wg = self.rdr.alloc.create(WaitGroup) catch |err| {
errs.append(err) catch {};
return;
};
dir_wg.* = .{};
for (self.entries.?) |ent| {
const fil = initFromEntry(self.rdr, ent) catch |err| {
std.fmt.format(
op.verbose_logger,
"error extracting inode {} \"{s}\": {}\n",
.{ ent.num, path, err },
) catch {};
continue;
};
const ext_path = blk: {
if (path[path.len - 1] == '/') {
var new = self.rdr.alloc.alloc(u8, path.len + ent.name.len) catch |err| {
break :blk err;
};
@memcpy(new[0..path.len], path);
@memcpy(new[path.len..], ent.name);
break :blk new;
}
var new = self.rdr.alloc.alloc(u8, path.len + ent.name.len + 1) catch |err| {
break :blk err;
};
@memcpy(new[0..path.len], path);
new[path.len] = '/';
@memcpy(new[path.len + 1 ..], ent.name);
break :blk new;
} catch |err| {
std.fmt.format(
op.verbose_logger,
"error extracting inode {} \"{s}\": {}\n",
.{ ent.num, path, err },
) catch {};
continue;
};
var thr = std.Thread.spawn(.{ .allocator = self.rdr.alloc }, extractReal, .{
&fil,
op,
ext_path,
errs,
dir_wg,
pol,
false,
}) catch |err| {
self.rdr.alloc.free(ext_path);
if (op.verbose) {
std.fmt.format(
op.verbose_logger,
"error extracting inode {} \"{s}\": {}\n",
.{ ent.num, path, err },
) catch {};
}
continue;
};
thr.detach();
}
var thr = std.Thread.spawn(
.{ .allocator = self.rdr.alloc },
extractDirWait,
.{
self,
op,
path,
dir_wg,
wg,
first,
},
) catch |err| {
if (op.verbose) {
std.fmt.format(
op.verbose_logger,
"error spawning wait thread for \"{s}\": {}\n",
.{ path, err },
) catch {};
}
self.extractDirWait(op, path, dir_wg, wg, first);
return;
};
thr.detach();
complete = true;
},
.file, .ext_file => {
var complete = false;
wg.start();
defer if (!complete) wg.finish();
var ext_fil = std.fs.cwd().createFile(path, .{}) catch |err| {
if (op.verbose) {
std.fmt.format(
op.verbose_logger,
"error creating file \"{s}\": {}\n",
.{ path, err },
) catch {};
}
errs.append(err) catch {};
return;
};
defer if (!complete) ext_fil.close();
var fil_errs = self.rdr.alloc.create(std.ArrayList(anyerror)) catch |err| {
if (op.verbose) {
std.fmt.format(
op.verbose_logger,
"error allocating memory: {}\n",
.{err},
) catch {};
}
errs.append(err) catch {};
return;
};
defer if (!complete) self.rdr.alloc.destroy(fil_errs);
fil_errs.* = .init(self.rdr.alloc);
defer if (!complete) fil_errs.deinit();
self.data_reader.?.writeToNoBlock(
ext_fil,
extractRegFinish,
.{
self,
op,
path,
fil_errs,
errs,
wg,
ext_fil,
first,
},
) catch |err| {
if (op.verbose) {
std.fmt.format(
op.verbose_logger,
"error extracting file \"{s}\": {}\n",
.{ path, err },
) catch {};
}
errs.append(err) catch {};
return;
};
complete = true;
},
.symlink, .ext_symlink => {},
.block_dev, .ext_block_dev, .char_dev, .ext_char_dev, .fifo, .ext_fifo => {
//TODO: check for all oses that accept unix permissions.
},
else => {
if (op.verbose) {
std.fmt.format(
op.verbose_logger,
"inode {} \"{s}\" is a socket file. Ignoring.\n",
.{ self.inode.hdr.num, path },
) catch {};
}
},
};
}
fn extractDirWait(
self: *Self,
op: ExtractionOptions,
path: []const u8,
dir_wg: *WaitGroup,
wg: *WaitGroup,
first: bool,
) void {
dir_wg.wait();
self.rdr.alloc.destroy(dir_wg);
defer {
wg.finish();
if (!first) {
self.rdr.alloc.free(path);
self.deinit();
}
}
if (op.ignore_permissions) return;
const dir_uid = self.uid() catch |err| {
std.fmt.format(
op.verbose_logger,
"error getting uid for inode {} \"{s}\": {}\n",
.{ self.inode.hdr.num, path, err },
) catch {};
return;
};
const dir_gid = self.gid() catch |err| {
std.fmt.format(
op.verbose_logger,
"error getting gid for inode {} \"{s}\": {}\n",
.{ self.inode.hdr.num, path, err },
) catch {};
return;
};
var ext_dir = std.fs.cwd().openFile(path, .{}) catch |err| {
std.fmt.format(
op.verbose_logger,
"error setting owner & permissions for \"{s}\": {}\n",
.{ path, err },
) catch {};
return;
};
defer ext_dir.close();
ext_dir.chmod(self.inode.hdr.perm) catch |err| {
std.fmt.format(
op.verbose_logger,
"error setting permissions for inode {} \"{s}\": {}\n",
.{ self.inode.hdr.num, path, err },
) catch {};
return;
};
ext_dir.chown(dir_uid, dir_gid) catch |err| {
std.fmt.format(
op.verbose_logger,
"error setting owner for inode {} \"{s}\": {}\n",
.{ self.inode.hdr.num, path, err },
) catch {};
return;
};
}
fn extractRegFinish(
self: *Self,
op: ExtractionOptions,
path: []const u8,
fil_errs: *std.ArrayList(anyerror),
errs: *std.ArrayList(anyerror),
wg: *WaitGroup,
fil: std.fs.File,
first: bool,
) void {
defer {
wg.finish();
fil.close();
self.rdr.alloc.destroy(fil_errs);
if (!first) {
self.deinit();
self.rdr.alloc.free(path);
}
}
if (fil_errs.items.len > 0) {
if (op.verbose) {
std.fmt.format(
op.verbose_logger,
"error extracting inode {} to \"{s}\": {}\n",
.{ self.inode.hdr.num, path, fil_errs.items[0] },
) catch {};
}
errs.append(fil_errs.items[0]) catch {};
return;
}
if (op.ignore_permissions) return;
const fil_uid = self.uid() catch |err| {
std.fmt.format(
op.verbose_logger,
"error getting uid for inode {} \"{s}\": {}\n",
.{ self.inode.hdr.num, path, err },
) catch {};
return;
};
const fil_gid = self.gid() catch |err| {
std.fmt.format(
op.verbose_logger,
"error getting gid for inode {} \"{s}\": {}\n",
.{ self.inode.hdr.num, path, err },
) catch {};
return;
};
fil.chmod(self.inode.hdr.perm) catch |err| {
std.fmt.format(
op.verbose_logger,
"error setting permissions for inode {} \"{s}\": {}\n",
.{ self.inode.hdr.num, path, err },
) catch {};
return;
};
fil.chown(fil_uid, fil_gid) catch |err| {
std.fmt.format(
op.verbose_logger,
"error setting owner for inode {} \"{s}\": {}\n",
.{ self.inode.hdr.num, path, err },
) catch {};
return;
};
}
};
}