//! Planning and execution owned by `spec/PRODUCT.md` sections 10 and 11 //! (`TJ-PLAN-01` through `TJ-PLAN-10`, `TJ-EXEC-01` through `TJ-EXEC-05`). //! Selection becomes a deterministic, immutable plan: ordered actions with //! reviewed manifests, descriptor-relative revalidation immediately before //! every `unlinkat`, and no path that bypasses the reviewed plan. //! //! Spine increment: direct filesystem actions for regular files, symlinks, //! and empty directories. A selected non-empty directory builds a complete //! reviewed manifest and stays reviewable; its action is unavailable until a //! later increment implements descendant manifests. const std = @import("std"); const linux = std.os.linux; const spec_data = @import("spec_data"); const scan_mod = @import("scan.zig"); const runlog_mod = @import("runlog.zig"); pub const actions_max: u32 = @intCast(spec_data.limit_value.plan_actions); /// One reviewed leaf: the exact identity that revalidation compares before /// unlinking (`TJ-PLAN-06`, `TJ-EXEC-01`). pub const ManifestEntry = struct { ino: u64, dev_major: u32, dev_minor: u32, mnt_id: u64, kind: scan_mod.Kind, nlink: u64, size: u64, mtime_sec: i64, mtime_nsec: u32, }; pub const Action = struct { finding: u32, leaf: ManifestEntry = .{ .ino = 0, .dev_major = 0, .dev_minor = 0, .mnt_id = 0, .kind = .file, .nlink = 0, .size = 0, .mtime_sec = 0, .mtime_nsec = 0, }, /// False when the reviewed manifest could not be completed; an /// incomplete manifest stays reviewable but unconfirmable /// (`TJ-PLAN-06`). complete: bool = true, unavailable_reason: []const u8 = "", estimate: u64 = 0, estimate_known: bool = false, /// Selected scan root the finding was retained under; execution reopens /// this root and revalidates ancestry below it (`TJ-EXEC-02`). root_index: u32 = 0, /// Reviewed descendant manifest slice for a directory action /// (`TJ-PLAN-06`); empty for file and symlink actions. manifest_offset: u32 = 0, manifest_len: u32 = 0, }; /// One immutable plan owned by one scan generation (`TJ-PLAN-01`). pub const Plan = struct { actions: [actions_max]Action = undefined, actions_len: u32 = 0, pub fn confirmable(self: *const Plan) bool { if (self.actions_len == 0) return false; var index: u32 = 0; while (index < self.actions_len) : (index += 1) { if (!self.actions[index].complete) return false; } return true; } pub fn knownEstimate(self: *const Plan) ?u64 { var total: u64 = 0; var index: u32 = 0; while (index < self.actions_len) : (index += 1) { if (!self.actions[index].estimate_known) return null; total += self.actions[index].estimate; } return total; } }; /// Collects the reviewed descendant manifest for one individually selected /// directory: every retained descendant of the immutable scan generation, /// ordered deepest-first with raw name bytes ascending so children always /// precede their parents at execution (`TJ-EXEC-04`). Capacity exhaustion or /// an unsupported entry kind leaves the action reviewable but unconfirmable. fn buildManifest(scan: *scan_mod.Scan, directory: u32, action: *Action) void { if (manifest_pool_used >= manifest_pool.len) { action.complete = false; action.unavailable_reason = "manifest capacity exhausted"; return; } var member: [scan_mod.retained_findings_max / 8 + 1]u8 = undefined; @memset(&member, 0); // Membership by upward parent walk: every finding whose chain reaches // the selected directory descends from the same immutable generation. var index: u32 = 0; while (index < scan.findings_len) : (index += 1) { if (index == directory) continue; var cursor = scan.findings[index].parent; var depth_guard: u32 = 0; while (cursor != scan_mod.no_parent and depth_guard < scan_mod.traversal_depth_max) : (depth_guard += 1) { if (cursor == directory) { member[index / 8] |= @as(u8, 1) << @intCast(index % 8); break; } cursor = scan.findings[cursor].parent; } } var total: u32 = 1; index = 0; while (index < scan.findings_len) : (index += 1) { const bit = member[index / 8] & (@as(u8, 1) << @intCast(index % 8)); if (bit == 0) continue; if (total >= manifest_pool.len) { action.complete = false; action.unavailable_reason = "manifest capacity exhausted"; return; } manifest_pool[total] = .{ .finding = index, .parent = index }; total += 1; } const entries = manifest_pool[0..total]; entries[0] = .{ .finding = directory, .parent = 0 }; const Context = struct { scan: *scan_mod.Scan, directory: u32, fn lessThan(ctx: @This(), a: ManifestRef, b: ManifestRef) bool { // The selected directory anchors index zero; every other entry // follows deepest-first with raw name bytes ascending, so a // forward walk mutates children before their parents. const a_root = a.finding == ctx.directory; const b_root = b.finding == ctx.directory; if (a_root != b_root) return a_root; const first = ctx.scan.findings[a.finding]; const second = ctx.scan.findings[b.finding]; if (first.depth != second.depth) return first.depth > second.depth; return std.mem.order(u8, first.name, second.name) == .lt; } }; std.mem.sort(ManifestRef, entries, Context{ .scan = scan, .directory = directory }, Context.lessThan); for (entries, 0..) |entry, position| manifest_index_of[entry.finding] = @intCast(position); for (entries) |*entry| { if (entry.finding == directory) { entry.parent = 0; } else { entry.parent = manifest_index_of[scan.findings[entry.finding].parent]; } } for (entries) |entry| { if (scan.findings[entry.finding].kind == .other) { action.complete = false; action.unavailable_reason = "unsupported entry type in directory manifest"; return; } } action.manifest_offset = manifest_pool_used; action.manifest_len = total; manifest_pool_used += total; } fn leafEntry(finding: *const scan_mod.Finding) ManifestEntry { return .{ .ino = finding.ino, .dev_major = finding.dev_major, .dev_minor = finding.dev_minor, .mnt_id = finding.mnt_id, .kind = finding.kind, .nlink = finding.nlink, .size = finding.apparent_size, .mtime_sec = finding.mtime_sec, .mtime_nsec = finding.mtime_nsec, }; } /// One reviewed descendant entry of a directory action, referencing the /// immutable scan generation (`TJ-PLAN-06`): names, identity, and parent /// structure are owned by the finding and never duplicated. The parent /// field is the manifest-local index of the parent entry; the selected /// directory itself is entry zero and points at itself. pub const ManifestRef = struct { finding: u32, parent: u32, }; pub const manifest_entries_per_action: u32 = @intCast(spec_data.limit_value.manifest_entries_per_action); /// Shared reviewed-manifest pool for one plan build. Actions reference /// disjoint slices; exhaustion leaves a later directory action reviewable /// but unconfirmable (`TJ-PLAN-06`). var manifest_pool: [manifest_entries_per_action]ManifestRef = undefined; var manifest_pool_used: u32 = 0; /// Manifest-local entry index per finding for the current build. var manifest_index_of: [scan_mod.retained_findings_max]u32 = undefined; /// Builds the plan from the scan's selected findings. Canonical order is /// deepest-first, then raw name bytes ascending (`TJ-PLAN-02`). pub fn build(scan: *scan_mod.Scan) Plan { manifest_pool_used = 0; effect_parents_len = 0; effect_dir_parents_len = 0; var plan: Plan = .{}; var order: [actions_max]u32 = undefined; var selected_count: u32 = 0; var index: u32 = 0; while (index < scan.findings_len) : (index += 1) { const finding = scan.findings[index]; if (!finding.selected) continue; if (finding.kind != .file and finding.kind != .symlink and finding.kind != .directory) continue; if (selected_count >= actions_max) break; order[selected_count] = index; selected_count += 1; } const Context = struct { scan: *scan_mod.Scan, fn lessThan(ctx: @This(), a: u32, b: u32) bool { if (ctx.scan.findings[a].depth != ctx.scan.findings[b].depth) { return ctx.scan.findings[a].depth > ctx.scan.findings[b].depth; } return std.mem.order(u8, ctx.scan.findings[a].name, ctx.scan.findings[b].name) == .lt; } }; std.mem.sort(u32, order[0..selected_count], Context{ .scan = scan }, Context.lessThan); for (order[0..selected_count]) |finding_index| { const finding = scan.findings[finding_index]; var action: Action = .{ .finding = finding_index, .leaf = leafEntry(&finding), .estimate = finding.allocated_bytes, .estimate_known = finding.blocks_known and finding.allocated_bytes != scan_mod.unknown_size, .root_index = finding.root_index, }; if (finding.kind == .directory) { buildManifest(scan, finding_index, &action); } plan.actions[plan.actions_len] = action; plan.actions_len += 1; } return plan; } pub const Confirm = struct { /// Bounded phrase input: seven phrase bytes, an eighth printable byte /// clears it; Enter accepts only the exact phrase; Backspace edits /// (`TJ-PLAN-10`). bytes: [7]u8 = undefined, len: u32 = 0, pub fn text(self: *const Confirm) []const u8 { return self.bytes[0..self.len]; } pub const Feed = enum { accepted, cleared, more }; pub fn feed(self: *Confirm, byte: u8) Feed { if (byte == '\r' or byte == '\n') { if (self.len == 7 and std.mem.eql(u8, self.bytes[0..7], "CONFIRM")) { self.len = 0; return .accepted; } self.len = 0; return .cleared; } if (byte == 0x7F or byte == 0x08) { if (self.len > 0) self.len -= 1; return .more; } if (byte < 0x20 or byte > 0x7E) return .more; if (self.len >= 7) { self.len = 0; return .cleared; } self.bytes[self.len] = byte; self.len += 1; return .more; } }; pub const Outcome = enum { success, failure }; /// Executes one action with the complete immediate revalidation sequence of /// `TJ-EXEC-02`: every ancestor reopened descriptor-relative with no-follow /// semantics and validated before descending, the leaf compared against the /// reviewed manifest, then one adjacent `unlinkat`. Any mismatch fails the /// action without mutating. The intent frame is appended and flushed before /// admission; the result frame after (`TJ-LOG-05`). /// Frames are limit-sized (`log_payload_bytes`) and must never live on the /// stack: a single frame exceeds the process stack budget. Execution is /// single-threaded and strictly sequential, so module storage is safe and /// statically allocated (`CODING_STYLE.md`). var intent_frame: runlog_mod.Frame = .{}; var result_frame: runlog_mod.Frame = .{}; pub fn executeAction( paths: [][]const u8, roots: []const []const u8, scan: *const scan_mod.Scan, action: *const Action, log: *runlog_mod.RunLog, ) Outcome { const path = paths[action.finding]; if (action.root_index >= roots.len) return .failure; intent_frame = .{}; if (!appendEscaped(&intent_frame, "intent", path, log)) return .failure; const ok = mutate(action, roots[action.root_index], path, scan); result_frame = .{}; // The payload is formatted into separate scratch: `renderFrame` copies // the payload into the frame, so the two must never alias. var payload_buffer: [64]u8 = undefined; const payload = std.fmt.bufPrint( &payload_buffer, "{{\"t\":\"result\",\"s\":\"{s}\"}}", .{if (ok) "success" else "failure"}, ) catch return .failure; if (!runlog_mod.renderFrame(payload, &result_frame) or !log.append(&result_frame)) { // A post-mutation logging failure marks the audit uncertain and // disables further mutation (`TJ-LOG-06`); the caller observes the // log state. return .failure; } return if (ok) .success else .failure; } fn appendEscaped(frame: *runlog_mod.Frame, kind: []const u8, path: []const u8, log: *runlog_mod.RunLog) bool { var escaped: [runlog_mod.escaped_value_bytes_max]u8 = undefined; const escaped_path = runlog_mod.escapeInto(path, &escaped) orelse return false; // The payload is fixed keys plus exactly one escaped value, so a // right-sized stack scratch bound by `escaped_value_bytes_max` always // suffices; `bufPrint` fails closed on overflow. var payload_buffer: [runlog_mod.escaped_value_bytes_max + 32]u8 = undefined; const payload = std.fmt.bufPrint( &payload_buffer, "{{\"t\":\"{s}\",\"p\":\"{s}\"}}", .{ kind, escaped_path }, ) catch return false; if (!runlog_mod.renderFrame(payload, frame)) return false; return log.append(frame); } fn identityMatches(stat: *const linux.Statx, leaf: *const ManifestEntry) bool { return stat.ino == leaf.ino and stat.dev_major == leaf.dev_major and stat.dev_minor == leaf.dev_minor and stat.mnt_id == leaf.mnt_id and (scan_mod.kindOf(stat) orelse .other) == leaf.kind and stat.nlink == leaf.nlink and stat.size == leaf.size and stat.mtime.sec == leaf.mtime_sec and stat.mtime.nsec == leaf.mtime_nsec; } /// Full identity compare of one live entry against its reviewed finding /// (`TJ-EXEC-01`): type, inode, device, mount ID, and link count are strict; /// size and modification timestamp relax only under an exactly recorded /// earlier-action effect, including effects of the directory action in /// progress. fn identityMatchesFinding( stat: *const linux.Statx, record: *const scan_mod.Finding, finding: u32, ) bool { if ((scan_mod.kindOf(stat) orelse .other) != record.kind) return false; if (stat.ino != record.ino) return false; if (stat.dev_major != record.dev_major) return false; if (stat.dev_minor != record.dev_minor) return false; if (stat.mnt_id != record.mnt_id) return false; // Link count carries exactly the recorded removal of reviewed // subdirectories; every other difference is drift (`TJ-EXEC-01`). const removed_subdirs = removedSubdirectories(finding); if (stat.nlink + removed_subdirs != record.nlink) return false; if (!effectTouches(finding)) { if (stat.size != record.apparent_size) return false; if (stat.mtime.sec != record.mtime_sec) return false; if (stat.mtime.nsec != record.mtime_nsec) return false; } return true; } /// Exactly recorded earlier-action effects for the current plan: the parent /// of every removed entry, and separately the parent of every removed /// directory, whose link count changed (`TJ-EXEC-05`). Recording is capped; /// overflow leaves later checks strict, which fails closed. var effect_parents: [manifest_entries_per_action]u32 = undefined; var effect_parents_len: u32 = 0; var effect_dir_parents: [manifest_entries_per_action]u32 = undefined; var effect_dir_parents_len: u32 = 0; fn recordEffect(parent: u32, removed_directory: bool) void { if (effect_parents_len < effect_parents.len) { effect_parents[effect_parents_len] = parent; effect_parents_len += 1; } if (removed_directory and effect_dir_parents_len < effect_dir_parents.len) { effect_dir_parents[effect_dir_parents_len] = parent; effect_dir_parents_len += 1; } } fn effectTouches(finding: u32) bool { var index: u32 = 0; while (index < effect_parents_len) : (index += 1) { if (effect_parents[index] == finding) return true; } return false; } fn removedSubdirectories(finding: u32) u32 { var total: u32 = 0; var index: u32 = 0; while (index < effect_dir_parents_len) : (index += 1) { if (effect_dir_parents[index] == finding) total += 1; } return total; } fn splitComponents(path: []const u8, components: [][]const u8) ?usize { var count: usize = 0; var rest = path; while (rest.len > 0) { const slash = std.mem.indexOfScalar(u8, rest, '/') orelse rest.len; const component = rest[0..slash]; rest = if (slash < rest.len) rest[slash + 1 ..] else ""; if (component.len == 0) continue; if (count >= components.len) return null; components[count] = component; count += 1; } return count; } /// Reopens the selected root, revalidates it and every reviewed ancestor of /// one manifest entry descriptor-relative with no-follow semantics, and /// returns the parent descriptor of that entry (`TJ-EXEC-02` steps 1-3). fn openEntryParent( root_path: []const u8, scan: *const scan_mod.Scan, root_finding: u32, entry_finding: u32, ) ?std.posix.fd_t { var components: [scan_mod.traversal_depth_max][]const u8 = undefined; var path_buffer: [scan_mod.raw_path_bytes_max]u8 = undefined; const path = scan.pathOf(entry_finding, &path_buffer); const count = splitComponents(path, &components) orelse return null; var root_components: [scan_mod.traversal_depth_max][]const u8 = undefined; const root_count = splitComponents(root_path, &root_components) orelse return null; if (root_count == 0 or count <= root_count) return null; for (root_components[0..root_count], components[0..root_count]) |r, p| { if (!std.mem.eql(u8, r, p)) return null; } var dir_fd = std.posix.openat( std.posix.AT.FDCWD, root_path, .{ .ACCMODE = .RDONLY, .DIRECTORY = true, .CLOEXEC = true, .NOFOLLOW = true }, 0, ) catch return null; { const root_stat = scan_mod.statxFd(dir_fd) catch { _ = linux.close(dir_fd); return null; }; if (!identityMatchesFinding(&root_stat, &scan.findings[root_finding], root_finding)) { _ = linux.close(dir_fd); return null; } } const ancestor_total = count - root_count - 1; var chain: [scan_mod.traversal_depth_max]u32 = undefined; { var cursor = scan.findings[entry_finding].parent; var collected: usize = 0; while (collected < ancestor_total) { if (cursor == scan_mod.no_parent or collected >= chain.len) { _ = linux.close(dir_fd); return null; } chain[collected] = cursor; collected += 1; cursor = scan.findings[cursor].parent; } } var level: usize = root_count; while (level + 1 < count) : (level += 1) { @memcpy(scan_mod.name_nul[0..components[level].len], components[level]); scan_mod.name_nul[components[level].len] = 0; const stat = scan_mod.statxAt(dir_fd, &scan_mod.name_nul) catch { _ = linux.close(dir_fd); return null; }; const reviewed = chain[ancestor_total - 1 - (level - root_count)]; if (!identityMatchesFinding(&stat, &scan.findings[reviewed], reviewed)) { _ = linux.close(dir_fd); return null; } const next_fd = std.posix.openat( dir_fd, scan_mod.name_nul[0..components[level].len :0], .{ .ACCMODE = .RDONLY, .DIRECTORY = true, .CLOEXEC = true, .NOFOLLOW = true }, 0, ) catch { _ = linux.close(dir_fd); return null; }; _ = linux.close(dir_fd); dir_fd = next_fd; } return dir_fd; } /// Checks the complete live child set of every manifest directory before /// any mutation (`TJ-EXEC-03`, `TJ-EXEC-04`): each live child must match one /// reviewed entry by raw name and full identity, no reviewed child may be /// missing, and no unreviewed child may exist. Descriptor-relative, /// no-follow, bounded by the traversal depth. fn validateSubtree( directory_fd: std.posix.fd_t, entries: []const ManifestRef, scan: *const scan_mod.Scan, ) bool { const io = std.Io.Threaded.global_single_threaded.io(); const Frame = struct { fd: std.posix.fd_t, entry: u32 }; var stack: [scan_mod.traversal_depth_max]Frame = undefined; var matched: [manifest_entries_per_action / 8 + 1]u8 = undefined; @memset(&matched, 0); stack[0] = .{ .fd = directory_fd, .entry = 0 }; var top: usize = 1; while (top > 0) { top -= 1; const frame = stack[top]; defer _ = linux.close(frame.fd); var expected_children: u32 = 0; for (entries, 0..) |entry, index| { if (entry.parent != frame.entry) continue; if (frame.entry == 0 and index == 0) continue; expected_children += 1; } var matched_children: u32 = 0; var iterator = std.Io.Dir.iterate(std.Io.Dir{ .handle = frame.fd }); while (iterator.next(io) catch return false) |live| { if (live.name.len > scan_mod.raw_component_bytes_max) return false; var found: ?u32 = null; for (entries, 0..) |entry, index| { if (entry.parent != frame.entry) continue; if (frame.entry == 0 and index == 0) continue; if (std.mem.eql(u8, scan.findings[entry.finding].name, live.name)) { found = @intCast(index); break; } } const child = found orelse { return false; }; if (matched[child / 8] & (@as(u8, 1) << @intCast(child % 8)) != 0) return false; var name_buffer: [scan_mod.raw_component_bytes_max + 1]u8 = undefined; @memcpy(name_buffer[0..live.name.len], live.name); name_buffer[live.name.len] = 0; const child_stat = scan_mod.statxAt(frame.fd, @ptrCast(&name_buffer)) catch return false; const child_finding = entries[child].finding; const relax_parent = scan.findings[child_finding].parent; if (!identityMatchesFinding(&child_stat, &scan.findings[child_finding], relax_parent)) { return false; } matched[child / 8] |= @as(u8, 1) << @intCast(child % 8); matched_children += 1; if (scan.findings[child_finding].kind == .directory) { const child_fd = std.posix.openat( frame.fd, name_buffer[0..live.name.len :0], .{ .ACCMODE = .RDONLY, .DIRECTORY = true, .CLOEXEC = true, .NOFOLLOW = true }, 0, ) catch return false; if (top >= stack.len) { _ = linux.close(child_fd); return false; } stack[top] = .{ .fd = child_fd, .entry = child }; top += 1; } } if (matched_children != expected_children) { return false; } } return true; } fn basename(path: []const u8) []const u8 { const slash = std.mem.lastIndexOfScalar(u8, path, '/') orelse return path; return path[slash + 1 ..]; } /// Mutates the reviewed manifest deepest-first, each entry preceded by its /// own reopened ancestry and adjacent final check, the selected directory /// last (`TJ-EXEC-04`). Every removal records its parent for the expected /// earlier-action relaxation of later checks. fn executeManifest( action: *const Action, root_path: []const u8, entries: []const ManifestRef, scan: *const scan_mod.Scan, ) bool { const root_finding = scan.root_findings[action.root_index]; var position: u32 = 1; while (position < entries.len) : (position += 1) { const entry = entries[position]; if (!mutateManifestEntry(root_path, scan, root_finding, entry.finding)) return false; } return mutateManifestEntry(root_path, scan, root_finding, entries[0].finding); } fn mutateManifestEntry( root_path: []const u8, scan: *const scan_mod.Scan, root_finding: u32, entry_finding: u32, ) bool { const parent_fd = openEntryParent(root_path, scan, root_finding, entry_finding) orelse { return false; }; defer _ = linux.close(parent_fd); var path_buffer: [scan_mod.raw_path_bytes_max]u8 = undefined; const path = scan.pathOf(entry_finding, &path_buffer); const name = basename(path); @memcpy(scan_mod.name_nul[0..name.len], name); scan_mod.name_nul[name.len] = 0; const stat = scan_mod.statxAt(parent_fd, &scan_mod.name_nul) catch return false; if (!identityMatchesFinding(&stat, &scan.findings[entry_finding], entry_finding)) { return false; } const flags: u32 = if (scan.findings[entry_finding].kind == .directory) linux.AT.REMOVEDIR else 0; const rc = linux.unlinkat(parent_fd, &scan_mod.name_nul, flags); if (linux.errno(rc) != .SUCCESS) return false; recordEffect( scan.findings[entry_finding].parent, scan.findings[entry_finding].kind == .directory, ); return true; } fn mutate( action: *const Action, root_path: []const u8, path: []const u8, scan: *const scan_mod.Scan, ) bool { if (!action.complete) return false; var components: [scan_mod.traversal_depth_max][]const u8 = undefined; const count = splitComponents(path, &components) orelse return false; var root_components: [scan_mod.traversal_depth_max][]const u8 = undefined; const root_count = splitComponents(root_path, &root_components) orelse return false; if (root_count == 0 or count <= root_count) return false; // never mutate a root itself for (root_components[0..root_count], components[0..root_count]) |r, p| { if (!std.mem.eql(u8, r, p)) return false; } // Reopen the selected root by path, then every reviewed ancestor below // it descriptor-relative with no-follow semantics, validating each // before descending (`TJ-EXEC-02` steps 1-3). Android application // domains cannot open `/`; the configured root is the traversal anchor // and is always openable because the scan opened it. var dir_fd = std.posix.openat( std.posix.AT.FDCWD, root_path, .{ .ACCMODE = .RDONLY, .DIRECTORY = true, .CLOEXEC = true, .NOFOLLOW = true }, 0, ) catch return false; defer _ = std.os.linux.close(dir_fd); // Reopen and validate the selected root itself against its reviewed // identity (`TJ-EXEC-02` step 1). A root whose metadata changed since the // review fails closed unless the change is exactly a recorded earlier // action effect. if (action.root_index >= scan.root_findings_len) return false; const root_finding = scan.root_findings[action.root_index]; { const root_stat = scan_mod.statxFd(dir_fd) catch return false; if (!identityMatchesFinding(&root_stat, &scan.findings[root_finding], root_finding)) return false; } // Collect the reviewed ancestor chain, leaf up to root, bounded by the // registered traversal depth (`TJ-EXEC-01`). const ancestor_total = count - root_count - 1; var chain: [scan_mod.traversal_depth_max]u32 = undefined; { var cursor = scan.findings[action.finding].parent; var collected: usize = 0; while (collected < ancestor_total) { if (cursor == scan_mod.no_parent or collected >= chain.len) return false; chain[collected] = cursor; collected += 1; cursor = scan.findings[cursor].parent; } } var level: usize = root_count; while (level + 1 < count) : (level += 1) { @memcpy(scan_mod.name_nul[0..components[level].len], components[level]); scan_mod.name_nul[components[level].len] = 0; const stat = scan_mod.statxAt(dir_fd, &scan_mod.name_nul) catch return false; const reviewed_index = chain[ancestor_total - 1 - (level - root_count)]; if (!identityMatchesFinding(&stat, &scan.findings[reviewed_index], reviewed_index)) return false; const next_fd = std.posix.openat( dir_fd, scan_mod.name_nul[0..components[level].len :0], .{ .ACCMODE = .RDONLY, .DIRECTORY = true, .CLOEXEC = true, .NOFOLLOW = true }, 0, ) catch return false; _ = std.os.linux.close(dir_fd); dir_fd = next_fd; } const leaf = components[count - 1]; @memcpy(scan_mod.name_nul[0..leaf.len], leaf); scan_mod.name_nul[leaf.len] = 0; const stat = scan_mod.statxAt(dir_fd, &scan_mod.name_nul) catch return false; if (!identityMatchesFinding(&stat, &scan.findings[action.finding], action.finding)) return false; if (action.leaf.kind != .directory) { const rc = linux.unlinkat(dir_fd, &scan_mod.name_nul, 0); if (linux.errno(rc) != .SUCCESS) return false; recordEffect(scan.findings[action.finding].parent, false); return true; } // Directory action (`TJ-EXEC-03`): the complete child set is checked // before any mutation, then manifest entries mutate deepest-first with // their own adjacent checks, and the selected directory goes last. if (action.manifest_len == 0) { const rc = linux.unlinkat(dir_fd, &scan_mod.name_nul, linux.AT.REMOVEDIR); if (linux.errno(rc) != .SUCCESS) return false; recordEffect(scan.findings[action.finding].parent, true); return true; } const entries = manifest_pool[action.manifest_offset..][0..action.manifest_len]; const directory_fd = std.posix.openat( dir_fd, scan_mod.name_nul[0..leaf.len :0], .{ .ACCMODE = .RDONLY, .DIRECTORY = true, .CLOEXEC = true, .NOFOLLOW = true }, 0, ) catch return false; defer _ = linux.close(directory_fd); if (!validateSubtree(directory_fd, entries, scan)) return false; const removed = executeManifest(action, root_path, entries, scan); if (removed) recordEffect(scan.findings[action.finding].parent, true); return removed; } comptime { std.debug.assert(actions_max > 0); var confirm: Confirm = .{}; _ = confirm.feed('C'); _ = confirm.feed('O'); _ = confirm.feed('N'); _ = confirm.feed('F'); _ = confirm.feed('I'); _ = confirm.feed('R'); _ = confirm.feed('M'); std.debug.assert(confirm.feed('\r') == .accepted); std.debug.assert(confirm.len == 0); }