UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

167 lines • 7.22 kB
// ========= 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