@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
167 lines • 7.22 kB
JavaScript
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import git from 'isomorphic-git';
import { IOResult } from "../../../../io/types.js";
import { fsStrerror, isEacces, isEisdir, isEnotdir, isMissingPath } from "../../../../errors/fs.js";
import { sha1Hex } from "../../../../utils/hash.js";
import { PathSpec } from "../../../../types.js";
import { readStdinAsync } from "../../../builtin/utils/stream.js";
import { FlagView } from "../../../spec/flag_view.js";
import { OBJECT_TYPES } from "./constants.js";
import { GitError, ObjectWriteReadOnlyError } from "./errors.js";
import { readFile } from "./io.js";
import { repoArgs } from "./repo.js";
import { opened } from "./session.js";
import { checkSwitches, startPoint, requireVfs } from "./util.js";
const ENC = new TextEncoder();
const DEC = new TextDecoder('latin1');
const HEX_ID = /^[0-9a-f]{40}$/;
// fsck's ident: a name, an email in angle brackets, a time and a zone.
const IDENT = /^[^<>\n]*<[^<>\n]*> [0-9]+ [+-][0-9]{4}$/;
/**
* Whether content is the object it claims to be, the checks git's fsck makes
* before hash-object will hash a tree, commit or tag: a tree's entries parse
* and sort, a commit names its tree, parents, author and committer, a tag its
* object, type and name. Mirrors Python's well_formed.
*/
function wellFormed(kind, data) {
const text = DEC.decode(data);
if (kind === 'tree') {
let at = 0;
let prior = null;
while (at < text.length) {
const space = text.indexOf(' ', at);
const nul = text.indexOf('\0', space + 1);
if (space === -1 || nul === -1 || nul + 21 > text.length)
return false;
const mode = text.slice(at, space);
const name = text.slice(space + 1, nul);
if (!/^[0-7]+$/.test(mode) || name === '' || name.includes('/'))
return false;
const key = name + (mode === '40000' || mode === '040000' ? '/' : '');
if (prior !== null && !(prior < key))
return false;
prior = key;
at = nul + 21;
}
return true;
}
const end = text.indexOf('\n\n');
const head = (end === -1 ? text : text.slice(0, end)).split('\n');
if (kind === 'commit') {
const tree = head[0] ?? '';
if (!tree.startsWith('tree ') || !HEX_ID.test(tree.slice(5)))
return false;
let rest = head.slice(1);
while ((rest[0] ?? '').startsWith('parent ')) {
if (!HEX_ID.test((rest[0] ?? '').slice(7)))
return false;
rest = rest.slice(1);
}
const author = rest[0] ?? '';
const committer = rest[1] ?? '';
return (author.startsWith('author ') &&
IDENT.test(author.slice(7)) &&
committer.startsWith('committer ') &&
IDENT.test(committer.slice(10)));
}
if (kind === 'tag') {
const [object = '', type = '', name = ''] = head;
return (object.startsWith('object ') &&
HEX_ID.test(object.slice(7)) &&
type.startsWith('type ') &&
OBJECT_TYPES.some((known) => known === type.slice(5)) &&
name.startsWith('tag ') &&
name.length > 4);
}
return true;
}
/** The id git stores content under: the sha1 of its header and bytes. */
async function objectId(kind, data) {
const header = ENC.encode(`${kind} ${String(data.length)}\0`);
const wrapped = new Uint8Array(header.length + data.length);
wrapped.set(header);
wrapped.set(data, header.length);
return sha1Hex(wrapped);
}
/** A file's bytes, in git's words when it cannot be read. */
async function content(vfs, base, name) {
try {
return await readFile(vfs, PathSpec.fromStrPath(name, undefined, base));
}
catch (err) {
if (isEisdir(err))
throw new GitError(`Unable to hash ${name}`);
if (isMissingPath(err) || isEnotdir(err) || isEacces(err)) {
const reason = fsStrerror(err) ?? 'No such file or directory';
throw new GitError(`could not open '${name}' for reading: ${reason}`);
}
throw err;
}
}
/**
* `git hash-object`: the id content would be stored under, read from each FILE,
* `--stdin` or `--stdin-paths`. Another type than blob is checked first unless
* `--literally` (without git's fsck detail lines), and `-w` writes the object.
* Pinned against git 2.50.1.
*/
export async function hashObject(_accessor, paths, texts, ctx) {
const fl = new FlagView(ctx.flags);
const vfs = requireVfs(ctx);
checkSwitches(ctx, texts);
const asked = fl.asStr('t', 'blob');
const kind = OBJECT_TYPES.find((type) => type === asked);
if (kind === undefined)
throw new GitError(`invalid object type "${asked}"`);
const base = startPoint(fl);
const contents = [];
if (fl.asBool('stdin')) {
contents.push((await readStdinAsync(ctx.stdin)) ?? new Uint8Array());
}
const names = [...texts];
if (fl.asBool('stdin_paths')) {
const listed = new TextDecoder().decode((await readStdinAsync(ctx.stdin)) ?? new Uint8Array());
for (const item of listed.split('\n').filter((line) => line !== ''))
names.push(item);
}
for (const name of names)
contents.push(await content(vfs, base, name));
if (!fl.asBool('literally') && !contents.every((data) => wellFormed(kind, data))) {
throw new GitError('refusing to create malformed object');
}
if (fl.asBool('w') && contents.length > 0) {
const repo = await opened(ctx, fl);
for (const data of contents) {
// hash-object writes any of the four types, which only the general
// writer takes.
// eslint-disable-next-line @typescript-eslint/no-deprecated
await git.writeObject({ ...repoArgs(repo), type: kind, object: data, format: 'content' });
}
}
const ids = [];
for (const data of contents)
ids.push(await objectId(kind, data));
const out = ids.map((id) => `${id}\n`).join('');
return [out === '' ? null : ENC.encode(out), new IOResult()];
}
/**
* hash-object's refusal by a read-only mount: the first object it could not
* add, named as typed, `(null)` for stdin's.
*/
export function hashObjectReadOnly(texts, ctx, _location) {
const fl = new FlagView(ctx.flags);
const first = texts[0];
return new ObjectWriteReadOnlyError(fl.asBool('stdin') || first === undefined ? '(null)' : first);
}
//# sourceMappingURL=hash_object.js.map