@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
224 lines • 9.94 kB
JavaScript
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