Lindenii Project Forge
Login

hare-git

Git library for Hare

Warning: Due to various recent migrations, viewing non-HEAD refs may be broken.

/git/loose.ha (raw)

use bytes;
use compress::zlib;
use crypto::sha256;
use encoding::utf8;
use errors;
use fmt;
use fs;
use hash;
use io;
use memio;
use path;
use strconv;
use strings;

// Find the path to a loose object with the given ID,
// relative to the repository root.
fn loose_path_resolve(id: oid) (str | nomem) = {
	const hex = oid_stringify(id)?;
	defer free(hex);

	const dir = strings::bytesub(hex, 0z, 2z)!;
	const file = strings::bytesub(hex, 2z, strings::end)!;

	return fmt::asprintf("objects/{}/{}", dir, file);
};

// Reads a loose object from the repository by its ID.
export fn loose_read(
	r: repo,
	id: oid,
) (object | fs::error | io::error | errors::invalid | strconv::invalid | strconv::overflow | nomem | utf8::invalid) = {
	const rel = loose_path_resolve(id)?;
	defer free(rel);

	const fh = fs::open(r.root, rel)?;
	defer io::close(fh)!;

	let zr = zlib::decompress(fh)?;
	defer io::close(&zr.vtable)!;

	let buf = io::drain(&zr.vtable)?;
	defer free(buf);

	let mnul = bytes::index(buf, 0u8);
	if (mnul is void) {
		return errors::invalid;
	};
	let nul = mnul: size;

	const header = buf[..nul];
	const body = buf[nul + 1z ..];

	let msp = bytes::index(header, ' ');
	if (msp is void) {
		return errors::invalid;
	};
	let sp = msp: size;

	const ty = strings::fromutf8(header[..sp])?;
	const szs = strings::fromutf8(header[sp + 1z ..])?;
	const expect = strconv::stoz(szs)?;

	if (expect != len(body)) {
		return errors::invalid;
	};

	if (!object_verify_oid(buf, id)) {
		return errors::invalid;
	};

	if (ty == "blob") {
		const b = blob_parse(body)?;
		return (b: object);
	} else if (ty == "tree") {
		const t = tree_parse(body)?;
		return (t: object);
	} else if (ty == "commit") {
		const c = commit_parse(body)?;
		return (c: object);
	} else if (ty == "tag") {
		const g = tag_parse(body)?;
		return (g: object);
	};

	return errors::invalid;
};

// Reads a loose object from the repository by its ID,
// returning its type and raw data.
export fn loose_read_typed(
	r: repo,
	id: oid,
) ((objtype, []u8) | fs::error | io::error | errors::invalid | errors::noentry | strconv::invalid | strconv::overflow | nomem) = {
	const rel = loose_path_resolve(id)?;
	defer free(rel);

	let fh = fs::open(r.root, rel)?;
	defer io::close(fh)!;

	let zr = zlib::decompress(fh)?;
	defer io::close(&zr.vtable)!;

	let buf = io::drain(&zr.vtable)?;
	defer free(buf);

	let mnul = bytes::index(buf, 0u8);
	if (mnul is void) {
		return errors::invalid;
	};
	let nul = mnul: size;

	const header = buf[..nul];
	const body = buf[nul + 1z ..];

	let msp = bytes::index(header, ' ');
	if (msp is void) {
		return errors::invalid;
	};
	let sp = msp: size;

	const ty = strings::fromutf8(header[..sp])?;
	const szs = strings::fromutf8(header[sp + 1z ..])?;
	const expect = strconv::stoz(szs)?;
	if (expect != len(body)) {
		return errors::invalid;
	};

	let code: objtype = objtype::OBJ_INVALID;
	if (ty == "blob") {
		code = objtype::OBJ_BLOB;
	} else if (ty == "tree") {
		code = objtype::OBJ_TREE;
	} else if (ty == "commit") {
		code = objtype::OBJ_COMMIT;
	} else if (ty == "tag") {
		code = objtype::OBJ_TAG;
	} else {
		return errors::invalid;
	};

	let out = alloc(body...)?;
	return (code, out);
};

fn loose_body_slice(raw: []u8) ([]u8 | errors::invalid) = {
	let mnul = bytes::index(raw, 0u8);
	if (mnul is void) {
		return errors::invalid;
	};
	let nul = mnul: size;
	if (nul + 1z > len(raw)) {
		return errors::invalid;
	};
	return raw[nul + 1z ..];
};

// Writes a loose object given its type tag and raw body, returning the new ID.
export fn loose_write_typed(
	r: repo,
	ty: objtype,
	body: []u8,
) (oid | fs::error | io::error | errors::invalid | nomem) = {
	let tyname: const str = switch (ty) {
	case objtype::OBJ_BLOB => yield "blob";
	case objtype::OBJ_TREE => yield "tree";
	case objtype::OBJ_COMMIT => yield "commit";
	case objtype::OBJ_TAG => yield "tag";
	case =>
		return errors::invalid;
	};
	const tynameb = strings::toutf8(tyname);
	const size_str = strconv::ztos(len(body));
	const sizeb = strings::toutf8(size_str);

	let rawlen = len(tynameb) + 1z + len(sizeb) + 1z + len(body);
	let raw = alloc([0u8...], rawlen)?;
	defer free(raw);

	let pos = 0z;
	raw[pos .. pos + len(tynameb)] = tynameb;
	pos += len(tynameb);
	raw[pos] = ' ';
	pos += 1z;
	raw[pos .. pos + len(sizeb)] = sizeb;
	pos += len(sizeb);
	raw[pos] = 0u8;
	pos += 1z;
	raw[pos .. pos + len(body)] = body;

	let st = sha256::sha256();
	defer hash::close(&st);
	hash::write(&st, raw);

	let id: oid = [0...];
	hash::sum(&st, id);

	let src = memio::fixed(raw);
	let zw = zlib::compress(&src);
	let compressed: []u8 = [];
	match (io::drain(&zw.vtable)) {
	case let buf: []u8 =>
		compressed = buf;
	case let err: io::error => {
		match (io::close(&zw.vtable)) {
		case void =>
			return err;
		case let cerr: io::error =>
			return cerr;
		};
	};
	};
	defer free(compressed);

	match (io::close(&zw.vtable)) {
	case void => void;
	case let err: io::error =>
		return err;
	};

	let rel = loose_path_resolve(id)?;
	defer free(rel);

	const dir = path::dirname(rel);
	match (fs::mkdirs(r.root, dir, (0o755: fs::mode))) {
	case void => void;
	case let err: fs::error =>
		return err;
	};

	let file = match (fs::create(r.root, rel, (0o644: fs::mode))) {
	case let fh: io::handle =>
		yield fh;
	case let err: fs::error =>
		return err;
	};

	let write_res = io::writeall(file, compressed);
	let close_res = io::close(file);

	match (write_res) {
	case size => void;
	case let err: io::error =>
		return err;
	};

	match (close_res) {
	case void => void;
	case let err: io::error =>
		return err;
	};

	return id;
};

// Writes a loose object, returning the new ID.
export fn loose_write(
	r: repo,
	o: object,
) (oid | fs::error | io::error | errors::invalid | nomem) = {
	match (o) {
	case let b: blob =>
		return loose_write_typed(r, objtype::OBJ_BLOB, b.data);
	case let t: tree => {
		let serialized = tree_serialize(t)?;
		defer free(serialized);
		let body = loose_body_slice(serialized)?;
		return loose_write_typed(r, objtype::OBJ_TREE, body);
	};
	case let c: commit => {
		let serialized = commit_serialize(c)?;
		defer free(serialized);
		let body = loose_body_slice(serialized)?;
		return loose_write_typed(r, objtype::OBJ_COMMIT, body);
	};
	case let g: tag => {
		let serialized = tag_serialize(g)?;
		defer free(serialized);
		let body = loose_body_slice(serialized)?;
		return loose_write_typed(r, objtype::OBJ_TAG, body);
	};
	case =>
		abort("Unknown object variant written to loose storage");
	};
};