UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

224 lines • 9.94 kB
import git from 'isomorphic-git'; import Hash from 'sha.js'; import { FileType } from "../../../../types.js"; import { IOResult } from "../../../../io/types.js"; import { compareCodePoints } from "../../../../utils/sort.js"; import { sha1Hex } from "../../../../utils/hash.js"; import { isWalkError, fsStrerror } from "../../../../errors/fs.js"; import { toHex } from "../../../../utils/hex.js"; import { FlagView } from "../../../spec/flag_view.js"; import { GITLINK, GITLINK_MODE } from "./constants.js"; import { GitError } from "./errors.js"; import { readIndex } from "./index_file.js"; import { readFile, readNames, readOptional, readRange, statOf } from "./io.js"; import { entryName } from "../../../../utils/remnants.js"; import { loadRefs } from "./refs.js"; import { repoArgs } from "./repo.js"; import { opened } from "./session.js"; const PACK_BLOCK = 1 << 18; /** Hash bounded ranges, retaining only the trailing SHA-1 between reads. */ export async function checkPack(vfs, path, expected) { const digest = new Hash.sha1(); let tail = new Uint8Array(); let offset = 0; try { const size = (await vfs.stat(path)).size; while (size === null || offset < size) { const count = size === null ? PACK_BLOCK : Math.min(PACK_BLOCK, size - offset); const chunk = await readRange(vfs, path, offset, count); if (chunk.length === 0) { if (size !== null && offset < size) throw new GitError(`truncated pack: ${path.virtual}`); break; } offset += chunk.length; const buffered = new Uint8Array(tail.length + chunk.length); buffered.set(tail); buffered.set(chunk, tail.length); if (buffered.length > 20) digest.update(buffered.subarray(0, -20)); tail = buffered.slice(-20); if (offset % (4 * PACK_BLOCK) === 0) await new Promise((resolve) => setTimeout(resolve, 0)); if (size === null && chunk.length < count) break; } } catch (err) { if (!isWalkError(err)) throw err; const detail = fsStrerror(err) ?? (err instanceof Error ? err.message : String(err)); const failure = new GitError(`cannot read pack ${path.virtual}: ${detail}`); failure.cause = err; throw failure; } if (offset < 32) throw new GitError(`truncated pack: ${path.virtual}`); if (digest.digest('hex') !== toHex(tail) || toHex(tail) !== toHex(expected)) throw new GitError(`pack checksum mismatch: ${path.virtual}`); } async function objectIds(repo) { const root = repo.location.commondir.join('objects'); const ids = new Set(); for (const entry of await readNames(repo.vfs, root)) { const fanout = entryName(entry); if (!/^[0-9a-f]{2}$/.test(fanout)) continue; for (const name of await readNames(repo.vfs, root.join(fanout))) { const oid = fanout + entryName(name); if (/^[0-9a-f]{40}$/.test(oid)) ids.add(oid); } } for (const entry of await readNames(repo.vfs, root.join('pack'))) { const name = entryName(entry); if (!name.endsWith('.idx')) continue; const data = await readFile(repo.vfs, root.join(`pack/${name}`)); if (data.length < 1064) throw new GitError(`truncated pack index: ${name}`); if ((await sha1Hex(data.subarray(0, -20))) !== toHex(data.subarray(-20))) throw new GitError(`pack index checksum mismatch: ${name}`); const packName = name.slice(0, -4) + '.pack'; await checkPack(repo.vfs, root.join(`pack/${packName}`), data.subarray(-40, -20)); const view = new DataView(data.buffer, data.byteOffset, data.length); const v2 = view.getUint32(0) === 0xff744f63; if (v2 && view.getUint32(4) !== 2) throw new GitError(`unsupported pack index: ${name}`); const count = view.getUint32((v2 ? 8 : 0) + 255 * 4); for (let index = 0; index < count; index += 1) { const offset = v2 ? 1032 + index * 20 : 1024 + index * 24 + 4; if (offset + 20 > data.length - 40) throw new GitError(`truncated pack index: ${name}`); ids.add(toHex(data.subarray(offset, offset + 20))); } } return ids; } async function logRoots(vfs, path) { const found = new Set(); for (const entry of await readNames(vfs, path)) { const target = path.join(entryName(entry)); if ((await statOf(vfs, target))?.type === FileType.DIRECTORY) { for (const oid of await logRoots(vfs, target)) found.add(oid); } else { const data = await readOptional(vfs, target); for (const line of new TextDecoder().decode(data ?? new Uint8Array()).split('\n')) { for (const oid of line.split(' ').slice(0, 2)) if (/^[0-9a-f]{40}$/.test(oid) && oid !== '0'.repeat(40)) found.add(oid); } } } return found; } /** Walk validated object links from refs, index entries and reflogs. */ function reachable(roots, edges) { const seen = new Set(); const stack = [...roots]; for (let oid = stack.pop(); oid !== undefined; oid = stack.pop()) { if (seen.has(oid)) continue; seen.add(oid); for (const link of edges.get(oid) ?? []) stack.push(link); } return seen; } /** Hash and decode every object, including unreachable objects and packed objects. */ export async function fsck(_accessor, paths, texts, ctx) { const fl = new FlagView(ctx.flags); const repo = await opened(ctx, fl); const args = repoArgs(repo); const refs = await loadRefs(repo.vfs, repo.location.gitdir, repo.location.commondir); const roots = new Set([...refs.values()].filter((oid) => !oid.startsWith('ref: '))); const index = await readIndex(repo, repo.vfs); for (const entry of index.entries.values()) if (entry.mode !== GITLINK) roots.add(entry.oid); for (const conflict of index.conflicts.values()) { for (const entry of [conflict.ancestor, conflict.this, conflict.other]) if (entry !== null && entry.mode !== GITLINK) roots.add(entry.oid); } for (const directory of new Set([repo.location.gitdir, repo.location.commondir])) { for (const oid of await logRoots(repo.vfs, directory.join('logs'))) roots.add(oid); } const referenced = new Set(roots); const edges = new Map(); const objects = new Map(); const errors = []; const ids = await objectIds(repo); for (const oid of roots) ids.add(oid); for (const oid of [...ids].sort(compareCodePoints)) { try { // fsck must inspect unknown object types and their exact encoded content. // eslint-disable-next-line @typescript-eslint/no-deprecated const result = await git.readObject({ ...args, oid, format: 'content' }); const body = result.object; const header = new TextEncoder().encode(`${result.type} ${String(body.length)}\0`); const wrapped = new Uint8Array(header.length + body.length); wrapped.set(header); wrapped.set(body, header.length); if ((await sha1Hex(wrapped)) !== oid) throw new Error('hash mismatch'); const links = []; if (result.type === 'commit') { const { commit } = await git.readCommit({ ...args, oid }); links.push(commit.tree, ...commit.parent); } else if (result.type === 'tree') { const { tree } = await git.readTree({ ...args, oid }); for (const entry of tree) if (entry.mode !== GITLINK_MODE) links.push(entry.oid); } else if (result.type === 'tag') { const { tag } = await git.readTag({ ...args, oid }); links.push(tag.object); } else if (result.type !== 'blob') throw new Error(`invalid object type: ${result.type}`); objects.set(oid, result.type); edges.set(oid, links); for (const link of links) referenced.add(link); } catch (err) { errors.push(`error: object ${oid}: ${err instanceof Error ? err.message : String(err)}\n`); } } for (const oid of [...referenced].sort(compareCodePoints)) if (!objects.has(oid) && !roots.has(oid)) errors.push(`missing object ${oid}\n`); let stderr = errors.join(''); if (roots.size === 0) { const head = await readOptional(repo.vfs, repo.location.gitdir.join('HEAD')); const branch = new TextDecoder() .decode(head ?? new Uint8Array()) .trim() .replace(/^ref: refs\/heads\//, ''); stderr = `notice: HEAD points to an unborn branch (${branch})\nnotice: No default references\n` + stderr; } const unreachable = fl.asBool('unreachable'); const excluded = unreachable ? reachable(roots, edges) : referenced; const label = unreachable ? 'unreachable' : 'dangling'; const stdout = unreachable || !fl.asBool('no_dangling') ? [...objects] .filter(([oid]) => !excluded.has(oid)) .sort(([a], [b]) => compareCodePoints(a, b)) .map(([oid, type]) => `${label} ${type} ${oid}\n`) .join('') : ''; return [ new TextEncoder().encode(stdout), new IOResult({ exitCode: errors.length ? 1 : 0, stderr: new TextEncoder().encode(stderr) }), ]; } //# sourceMappingURL=fsck.js.map