//! Incremental import's durable, source-free roll-forward protocol.
//! The immutable prepared bundle is recovery evidence, never a scratch journal.
//! Only an exact prefix of its tail may already exist. Foreign bytes are never
//! truncated. Callers hold the resolved store writer lock throughout mutation.
const std = @import("std");
const storage = @import("storage.zig");
const Hooks = @import("publication.zig").PublicationHooks;
const Hash = std.crypto.hash.sha2.Sha256;
pub const pending_name = ".zit-import-pending";
pub const staging_name = ".zit-import-staging";
const completed_name = ".zit-import-completed";
const map_name = ".zit-import-map";
const metadata_name = "PREPARED";
const metadata_size = 200;
pub fn requireIdle(io: std.Io, dir: std.Io.Dir) !void {
try requireAbsent(io, dir, pending_name);
// An unsynced retirement rename may roll back after power loss. Keep
// other writers fenced until explicit recovery durably acknowledges it.
try requireAbsent(io, dir, completed_name);
}
fn requireAbsent(io: std.Io, dir: std.Io.Dir, name: []const u8) !void {
_ = dir.statFile(io, name, .{ .follow_symlinks = false }) catch |err| switch (err) {
error.FileNotFound => return,
else => return err,
};
return error.ImportRecoveryRequired;
}
/// An append-only virtual log: existing bytes stay on their retained descriptor,
/// and new bytes go exclusively to the staging tail. No whole-store copy.
pub const Overlay = struct {
base: storage.StorageFile,
tail: storage.StorageFile,
base_len: u64,
pub fn file(self: *Overlay) storage.StorageFile {
return .{ .context = self, .vtable = &vtable };
}
const vtable: storage.StorageFile.VTable = .{
.append = append,
.pread = pread,
.sync = sync,
.size = size,
.truncate = truncate,
.refresh_end = refresh,
};
fn get(ctx: *anyopaque) *Overlay {
return @ptrCast(@alignCast(ctx));
}
fn append(ctx: *anyopaque, bytes: []const u8) storage.StorageError!void {
return get(ctx).tail.append(bytes);
}
fn pread(ctx: *anyopaque, offset: u64, bytes: []u8) storage.StorageError!usize {
const self = get(ctx);
if (offset >= self.base_len) return self.tail.pread(offset - self.base_len, bytes);
const n = try self.base.pread(offset, bytes[0..@min(bytes.len, self.base_len - offset)]);
if (offset + n != self.base_len or n == bytes.len) return n;
return n + try self.tail.pread(0, bytes[n..]);
}
fn sync(ctx: *anyopaque) storage.StorageError!void {
return get(ctx).tail.sync();
}
fn size(ctx: *anyopaque) storage.StorageError!u64 {
const self = get(ctx);
return std.math.add(u64, self.base_len, try self.tail.size()) catch error.Io;
}
fn truncate(ctx: *anyopaque, len: u64) storage.StorageError!void {
const self = get(ctx);
if (len < self.base_len) return error.Io;
return self.tail.truncate(len - self.base_len);
}
fn refresh(_: *anyopaque) storage.StorageError!void {}
};
pub fn digest(io: std.Io, file: std.Io.File, offset: u64, len: u64) ![32]u8 {
var hasher = Hash.init(.{});
var buffer: [64 * 1024]u8 = undefined;
var done: u64 = 0;
while (done < len) {
const chunk = buffer[0..@min(buffer.len, len - done)];
if (try file.readPositionalAll(io, chunk, offset + done) != chunk.len) return error.ShortRead;
hasher.update(chunk);
done += chunk.len;
}
return hasher.finalResult();
}
pub fn digestStorage(file: storage.StorageFile, len: u64) ![32]u8 {
var hasher = Hash.init(.{});
var buffer: [64 * 1024]u8 = undefined;
var done: u64 = 0;
while (done < len) {
const chunk = buffer[0..@min(buffer.len, len - done)];
if (try file.pread(done, chunk) != chunk.len) return error.ShortRead;
hasher.update(chunk);
done += chunk.len;
}
return hasher.finalResult();
}
fn copy(io: std.Io, source: std.Io.File, dest: std.Io.File, source_offset: u64, dest_offset: u64, len: u64, hooks: Hooks) !void {
var buffer: [64 * 1024]u8 = undefined;
var done: u64 = 0;
while (done < len) {
const chunk = buffer[0..@min(buffer.len, len - done)];
if (try source.readPositionalAll(io, chunk, source_offset + done) != chunk.len) return error.ShortRead;
try dest.writePositionalAll(io, chunk, dest_offset + done);
done += chunk.len;
try hooks.check(.into_append_chunk);
}
}
fn copyMember(io: std.Io, source_dir: std.Io.Dir, source_name: []const u8, dest_dir: std.Io.Dir, dest_name: []const u8) !void {
const source = try source_dir.openFile(io, source_name, .{});
defer source.close(io);
const dest = try dest_dir.createFile(io, dest_name, .{ .exclusive = true });
defer dest.close(io);
try copy(io, source, dest, 0, 0, (try source.stat(io)).size, .{});
try dest.sync(io);
}
/// Called under the writer lock and only while no canonical pending bundle exists.
pub fn begin(io: std.Io, dir: std.Io.Dir) !std.Io.Dir {
try requireIdle(io, dir);
try dir.deleteTree(io, staging_name);
try dir.deleteTree(io, completed_name);
try storage.syncDirectory(io, dir);
try dir.createDir(io, staging_name, .default_dir);
return dir.openDir(io, staging_name, .{});
}
/// All members are complete and synced before PREPARED is made canonical.
/// From the rename attempt onward, failure means inspect/recover, never rollback.
pub fn prepare(io: std.Io, dir: std.Io.Dir, stage: std.Io.Dir, base: std.Io.File, hooks: Hooks) !void {
try hooks.check(.into_before_prepare);
try copyMember(io, dir, map_name, stage, "old-map");
var bytes: [metadata_size]u8 = undefined;
@memcpy(bytes[0..8], "ZITIMTX2");
const tail = try stage.openFile(io, "tail", .{});
defer tail.close(io);
const old_map = try stage.openFile(io, "old-map", .{});
defer old_map.close(io);
const new_map = try stage.openFile(io, map_name, .{});
defer new_map.close(io);
const files = [_]std.Io.File{ base, tail, old_map, new_map };
for (files, 0..) |member, i| {
const len = (try member.stat(io)).size;
std.mem.writeInt(u64, bytes[8 + i * 8 ..][0..8], len, .little);
@memcpy(bytes[40 + i * 32 ..][0..32], &(try digest(io, member, 0, len)));
try member.sync(io);
}
Hash.hash(bytes[0..168], bytes[168..200], .{});
const metadata = try stage.createFile(io, metadata_name, .{ .exclusive = true });
defer metadata.close(io);
try metadata.writePositionalAll(io, &bytes, 0);
try metadata.sync(io);
try hooks.check(.into_before_prepare_sync);
try storage.syncDirectory(io, stage);
try dir.rename(staging_name, dir, pending_name, io);
try hooks.check(.into_after_prepare_rename);
try storage.syncDirectory(io, dir);
try hooks.check(.into_after_prepare);
}
const Prepared = struct {
sizes: [4]u64,
hashes: [4][32]u8,
};
fn readPrepared(io: std.Io, bundle: std.Io.Dir) !Prepared {
var bytes: [metadata_size + 1]u8 = undefined;
const raw = try bundle.readFile(io, metadata_name, &bytes);
if (raw.len != metadata_size or !std.mem.eql(u8, raw[0..8], "ZITIMTX2")) return error.DamagedImportJournal;
var checksum: [32]u8 = undefined;
Hash.hash(raw[0..168], &checksum, .{});
if (!std.mem.eql(u8, &checksum, raw[168..200])) return error.DamagedImportJournal;
var prepared: Prepared = undefined;
for (0..4) |i| {
prepared.sizes[i] = std.mem.readInt(u64, raw[8 + i * 8 ..][0..8], .little);
@memcpy(&prepared.hashes[i], raw[40 + i * 32 ..][0..32]);
}
_ = std.math.add(u64, prepared.sizes[0], prepared.sizes[1]) catch return error.DamagedImportJournal;
return prepared;
}
fn validateMember(io: std.Io, member: std.Io.File, size: u64, hash: [32]u8) !void {
if ((try member.stat(io)).size != size) return error.DamagedImportJournal;
if (!std.mem.eql(u8, &(try digest(io, member, 0, size)), &hash)) return error.DamagedImportJournal;
}
/// Returns false on an already complete destination. The caller has acquired
/// writer exclusion, but has NOT opened the log through its tail-repair API.
pub fn recover(io: std.Io, dir: std.Io.Dir, hooks: Hooks) !bool {
var bundle = dir.openDir(io, pending_name, .{}) catch |err| switch (err) {
error.FileNotFound => {
try requireAbsent(io, dir, pending_name);
// A previous completion may have failed after the retirement rename.
// Acknowledge that directory transition before erasing its evidence.
try storage.syncDirectory(io, dir);
try dir.deleteTree(io, completed_name);
try storage.syncDirectory(io, dir);
return false;
},
else => return err,
};
defer bundle.close(io);
// The protocol never leaves both directories; only outside interference
// does. Refuse it permanently rather than invite futile retries.
if (dir.statFile(io, completed_name, .{ .follow_symlinks = false })) |_| {
return error.ConflictingImportState;
} else |err| switch (err) {
error.FileNotFound => {},
else => return err,
}
const prepared = try readPrepared(io, bundle);
const tail = try bundle.openFile(io, "tail", .{});
defer tail.close(io);
const old_map = try bundle.openFile(io, "old-map", .{});
defer old_map.close(io);
const new_map = try bundle.openFile(io, map_name, .{});
defer new_map.close(io);
try validateMember(io, tail, prepared.sizes[1], prepared.hashes[1]);
try validateMember(io, old_map, prepared.sizes[2], prepared.hashes[2]);
try validateMember(io, new_map, prepared.sizes[3], prepared.hashes[3]);
const live = try dir.openFile(io, ".zit", .{ .mode = .read_write });
defer live.close(io);
const live_size = (try live.stat(io)).size;
const base_len = prepared.sizes[0];
const final_len = base_len + prepared.sizes[1];
if (live_size < base_len or live_size > final_len) return error.ForeignImportSuffix;
if (!std.mem.eql(u8, &(try digest(io, live, 0, base_len)), &prepared.hashes[0])) return error.ImportBaseMismatch;
const prefix_len = live_size - base_len;
if (!std.mem.eql(u8, &(try digest(io, live, base_len, prefix_len)), &(try digest(io, tail, 0, prefix_len)))) return error.ForeignImportSuffix;
const current_map = try dir.openFile(io, map_name, .{});
defer current_map.close(io);
const current_size = (try current_map.stat(io)).size;
const current_hash = try digest(io, current_map, 0, current_size);
const is_old = current_size == prepared.sizes[2] and std.mem.eql(u8, ¤t_hash, &prepared.hashes[2]);
const is_new = current_size == prepared.sizes[3] and std.mem.eql(u8, ¤t_hash, &prepared.hashes[3]);
if (!is_old and !is_new) return error.ImportMapMismatch;
if (is_new and live_size != final_len) return error.DamagedImportJournal;
// Preparation may have stopped after rename but before directory sync.
// Recovery must make its canonical fence durable before touching live bytes.
try hooks.check(.into_before_recovery_sync);
try storage.syncDirectory(io, dir);
try hooks.check(.into_after_recovery_sync);
try hooks.check(.into_before_append);
try copy(io, tail, live, prefix_len, live_size, final_len - live_size, hooks);
try hooks.check(.into_before_live_sync);
try live.sync(io);
try hooks.check(.into_after_live_sync);
// The only prepared map must never be renamed: keep immutable evidence
// through ambiguous replace/fsync results and retry from a separate copy.
var publication = try dir.createFileAtomic(io, map_name, .{ .replace = true });
defer publication.deinit(io);
try copy(io, new_map, publication.file, 0, 0, prepared.sizes[3], .{});
try publication.file.sync(io);
try hooks.check(.into_before_map_replace);
try publication.replace(io);
try hooks.check(.into_after_map_replace);
try storage.syncDirectory(io, dir);
try hooks.check(.into_after_map_sync);
// Retire atomically; never erase members while the canonical marker exists.
try dir.rename(pending_name, dir, completed_name, io);
try hooks.check(.into_after_retire);
try storage.syncDirectory(io, dir);
try hooks.check(.into_after_retire_sync);
try dir.deleteTree(io, completed_name);
try storage.syncDirectory(io, dir);
return true;
}
/// Publication capture is read-only. A v2 map's fresh token detects a complete
/// import (including marker ABA) between the two observations. Every changed
/// incremental publication writes v2, so two v1 observations cannot straddle
/// one: a legacy map needs only a fixed-size witness, never a whole-map hash
/// on every read.
/// The map is optional to readers: a damaged or unreadable one yields a fixed
/// sentinel so the repository stays readable and verifiable; only an import or
/// `info --imported-head` depends on the map's contents, and each rejects it.
const unreadable_witness: [32]u8 = "unreadable import map witness...".*;
fn witness(io: std.Io, dir: std.Io.Dir) !?[32]u8 {
const file = dir.openFile(io, map_name, .{}) catch |err| switch (err) {
error.FileNotFound => return null,
else => return unreadable_witness,
};
defer file.close(io);
var header: [42]u8 = undefined;
const n = file.readPositionalAll(io, &header, 0) catch return unreadable_witness;
if (n >= 10 and std.mem.eql(u8, header[0..8], "ZITGIMAP") and std.mem.readInt(u16, header[8..10], .little) == 2) {
if (n != header.len) return unreadable_witness;
return header[10..42].*;
}
var legacy: [32]u8 = @splat(0);
@memcpy(legacy[0..@min(n, legacy.len)], header[0..@min(n, legacy.len)]);
return legacy;
}
pub const Snapshot = struct {
file: std.Io.File,
size: u64,
accepted_stat: std.Io.File.Stat,
publication_witness: ?[32]u8,
pub fn close(self: Snapshot, io: std.Io) void {
self.file.close(io);
}
pub fn stat(self: Snapshot, _: std.Io) error{}!std.Io.File.Stat {
return self.accepted_stat;
}
};
pub fn capture(io: std.Io, dir: std.Io.Dir) !Snapshot {
for (0..4) |_| {
const before = try witness(io, dir);
try requireIdle(io, dir);
const file = try dir.openFile(io, ".zit", .{});
errdefer file.close(io);
const stat = try file.stat(io);
try requireIdle(io, dir);
const after = try witness(io, dir);
const path = try dir.statFile(io, ".zit", .{});
if (std.meta.eql(before, after) and stat.inode == path.inode and stat.size == path.size and stat.kind == .file) return .{ .file = file, .size = stat.size, .accepted_stat = stat, .publication_witness = before };
file.close(io);
}
return error.ImportSnapshotChanged;
}
fn preparedFixture(io: std.Io, dir: std.Io.Dir) !void {
try dir.writeFile(io, .{ .sub_path = ".zit", .data = "base" });
try dir.writeFile(io, .{ .sub_path = map_name, .data = "old map" });
var stage = try begin(io, dir);
defer stage.close(io);
try stage.writeFile(io, .{ .sub_path = "tail", .data = "prepared tail" });
try stage.writeFile(io, .{ .sub_path = map_name, .data = "new map" });
const base = try dir.openFile(io, ".zit", .{});
defer base.close(io);
try prepare(io, dir, stage, base, .{});
}
test "recovery accepts every exact tail prefix including a restored file identity" {
const io = std.testing.io;
for (0.."prepared tail".len + 1) |prefix| {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
try preparedFixture(io, tmp.dir);
const replacement = try tmp.dir.createFile(io, "restored", .{});
try replacement.writePositionalAll(io, "base", 0);
try replacement.writePositionalAll(io, "prepared tail"[0..prefix], 4);
replacement.close(io);
try tmp.dir.rename("restored", tmp.dir, ".zit", io);
try std.testing.expect(try recover(io, tmp.dir, .{}));
var bytes: [64]u8 = undefined;
try std.testing.expectEqualStrings("baseprepared tail", try tmp.dir.readFile(io, ".zit", &bytes));
try std.testing.expectEqualStrings("new map", try tmp.dir.readFile(io, map_name, &bytes));
try std.testing.expect(!try recover(io, tmp.dir, .{}));
try std.testing.expect(!try recover(io, tmp.dir, .{}));
}
}
test "recovery preserves foreign suffixes and refuses damaged evidence or stale maps" {
const io = std.testing.io;
for (0..5) |damage| {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
try preparedFixture(io, tmp.dir);
switch (damage) {
0 => try tmp.dir.writeFile(io, .{ .sub_path = ".zit", .data = "baseforeign" }),
1 => try tmp.dir.writeFile(io, .{ .sub_path = ".zit", .data = "baseprepared tailforeign" }),
2 => try tmp.dir.writeFile(io, .{ .sub_path = pending_name ++ "/tail", .data = "damaged tail" }),
3 => try tmp.dir.writeFile(io, .{ .sub_path = pending_name ++ "/PREPARED", .data = "damaged" }),
4 => try tmp.dir.writeFile(io, .{ .sub_path = map_name, .data = "foreign map" }),
else => unreachable,
}
const original = try tmp.dir.readFileAlloc(io, ".zit", std.testing.allocator, .limited(1024));
defer std.testing.allocator.free(original);
if (recover(io, tmp.dir, .{})) |_| return error.AcceptedDamagedEvidence else |_| {}
var bytes: [64]u8 = undefined;
try std.testing.expectEqualStrings(original, try tmp.dir.readFile(io, ".zit", &bytes));
try std.testing.expectError(error.ImportRecoveryRequired, requireIdle(io, tmp.dir));
}
}
test "publication capture refuses pending state without creating reader files" {
const io = std.testing.io;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
try tmp.dir.writeFile(io, .{ .sub_path = ".zit", .data = "base" });
const snapshot = try capture(io, tmp.dir);
defer snapshot.close(io);
try tmp.dir.createDir(io, pending_name, .default_dir);
try std.testing.expectError(error.ImportRecoveryRequired, capture(io, tmp.dir));
var fs = storage.FileStorage.init(io, snapshot, snapshot.size);
const raw = try tmp.dir.openFile(io, ".zit", .{ .mode = .read_write });
defer raw.close(io);
try raw.writePositionalAll(io, "uncommitted", 4);
try fs.storageFile().refreshEnd();
var bytes: [32]u8 = undefined;
try std.testing.expectEqual(@as(usize, 4), try fs.storageFile().pread(0, &bytes));
}
test "recovery makes an ambiguous preparation durable before appending" {
const io = std.testing.io;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
try tmp.dir.writeFile(io, .{ .sub_path = ".zit", .data = "base" });
try tmp.dir.writeFile(io, .{ .sub_path = map_name, .data = "old map" });
var stage = try begin(io, tmp.dir);
defer stage.close(io);
try stage.writeFile(io, .{ .sub_path = "tail", .data = "tail" });
try stage.writeFile(io, .{ .sub_path = map_name, .data = "new map" });
const base = try tmp.dir.openFile(io, ".zit", .{});
defer base.close(io);
try std.testing.expectError(error.InjectedPublicationFailure, prepare(io, tmp.dir, stage, base, .{ .fail_at = .into_after_prepare_rename }));
try std.testing.expectError(error.InjectedPublicationFailure, recover(io, tmp.dir, .{ .fail_at = .into_before_recovery_sync }));
var bytes: [32]u8 = undefined;
try std.testing.expectEqualStrings("base", try tmp.dir.readFile(io, ".zit", &bytes));
const Order = struct {
synced: bool = false,
appended: bool = false,
invalid: bool = false,
fn observe(ctx: *anyopaque, point: Hooks.FailurePoint) void {
const self: *@This() = @ptrCast(@alignCast(ctx));
if (point == .into_after_recovery_sync) self.synced = true;
if (point == .into_before_append) {
self.appended = true;
if (!self.synced) self.invalid = true;
}
}
};
var order: Order = .{};
try std.testing.expect(try recover(io, tmp.dir, .{ .observer = .{ .context = &order, .notify = Order.observe } }));
try std.testing.expect(order.synced and order.appended and !order.invalid);
}
test "uncertain retirement keeps admission fenced until directory acknowledgement" {
const io = std.testing.io;
for ([_]bool{ false, true }) |rename_rolled_back| {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
try preparedFixture(io, tmp.dir);
try std.testing.expectError(error.InjectedPublicationFailure, recover(io, tmp.dir, .{ .fail_at = .into_after_retire }));
try std.testing.expectError(error.ImportRecoveryRequired, requireIdle(io, tmp.dir));
if (rename_rolled_back) try tmp.dir.rename(completed_name, tmp.dir, pending_name, io);
try std.testing.expectEqual(rename_rolled_back, try recover(io, tmp.dir, .{}));
try requireIdle(io, tmp.dir);
var bytes: [64]u8 = undefined;
try std.testing.expectEqualStrings("baseprepared tail", try tmp.dir.readFile(io, ".zit", &bytes));
try std.testing.expectEqualStrings("new map", try tmp.dir.readFile(io, map_name, &bytes));
}
}
test "legacy witness is fixed-size while every v2 publication token is observed" {
const io = std.testing.io;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const header = "ZITGIMAP" ++ [_]u8{ 1, 0 } ++ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
try tmp.dir.writeFile(io, .{ .sub_path = map_name, .data = header ++ "first legacy body" });
const first = (try witness(io, tmp.dir)).?;
try tmp.dir.writeFile(io, .{ .sub_path = map_name, .data = header ++ "a much longer legacy body that differs entirely from the first one" });
try std.testing.expectEqualSlices(u8, &first, &(try witness(io, tmp.dir)).?);
var v2 = "ZITGIMAP".* ++ [_]u8{ 2, 0 } ++ @as([32]u8, @splat(7));
try tmp.dir.writeFile(io, .{ .sub_path = map_name, .data = &v2 });
const token = (try witness(io, tmp.dir)).?;
try std.testing.expect(!std.mem.eql(u8, &first, &token));
v2[10] = 8;
try tmp.dir.writeFile(io, .{ .sub_path = map_name, .data = &v2 });
try std.testing.expect(!std.mem.eql(u8, &token, &(try witness(io, tmp.dir)).?));
}
test "a damaged optional import map never makes the repository unreadable" {
const io = std.testing.io;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
try tmp.dir.writeFile(io, .{ .sub_path = ".zit", .data = "base" });
try tmp.dir.writeFile(io, .{ .sub_path = map_name, .data = "ZITGIMAP" ++ [_]u8{ 2, 0 } ++ "short" });
const truncated = try capture(io, tmp.dir);
truncated.close(io);
try tmp.dir.deleteFile(io, map_name);
try tmp.dir.createDir(io, map_name, .default_dir);
const directory = try capture(io, tmp.dir);
directory.close(io);
}
test "pending and completed together is a permanent conflict, not a retry" {
const io = std.testing.io;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
try preparedFixture(io, tmp.dir);
try tmp.dir.createDir(io, completed_name, .default_dir);
try std.testing.expectError(error.ConflictingImportState, recover(io, tmp.dir, .{}));
var bytes: [64]u8 = undefined;
try std.testing.expectEqualStrings("base", try tmp.dir.readFile(io, ".zit", &bytes));
}
test "import overlay never mutates its base" {
const io = std.testing.io;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const base = try tmp.dir.createFile(io, "base", .{ .read = true });
defer base.close(io);
try base.writePositionalAll(io, "base", 0);
const tail = try tmp.dir.createFile(io, "tail", .{ .read = true });
defer tail.close(io);
var base_storage = storage.FileStorage.initReadOnly(io, base, 4);
var tail_storage = storage.FileStorage.init(io, tail, 0);
var overlay: Overlay = .{ .base = base_storage.storageFile(), .tail = tail_storage.storageFile(), .base_len = 4 };
const file = overlay.file();
try file.append("tail");
var bytes: [8]u8 = undefined;
try std.testing.expectEqual(@as(usize, 8), try file.pread(0, &bytes));
try std.testing.expectEqualStrings("basetail", &bytes);
try std.testing.expectError(error.Io, file.truncate(3));
try std.testing.expectEqual(@as(u64, 4), (try base.stat(io)).size);
}