#include "git-compat-util.h"
#include "chunk-format.h"
#include "csum-file.h"
#include "gettext.h"
#include "hash.h"
#include "hex.h"
#include "lockfile.h"
#include "mvcc.h"
#include "strbuf.h"
#include "string-list.h"
#include "strmap.h"
#include "tempfile.h"
#include "wrapper.h"

/*
 * Manifest format constants. See Documentation/technical/mvcc-storage.adoc
 * for the byte-level specification.
 */
#define MVCC_MANIFEST_MAGIC 0x4D564343 /* "MVCC" */
#define MVCC_MANIFEST_VERSION 1
#define MVCC_MANIFEST_HEADER_SIZE 8
#define MVCC_MANIFEST_MAX_PATH_WIDTH 128

#define MVCC_MANIFEST_HASH_SHA1   1
#define MVCC_MANIFEST_HASH_SHA256 2

#define MVCC_CHUNKID_PATH 0x50415448 /* "PATH" */
#define MVCC_CHUNKID_OBJS 0x4F424A53 /* "OBJS" */
#define MVCC_CHUNKID_REFS 0x52454653 /* "REFS" */

static struct strmap storages_by_commondir = STRMAP_INIT;

static void ensure_storage_dirs(const char *base)
{
	const char *dirs[] = {
		"pack",
		"manifests",
		"refs",
	};
	struct strbuf path = STRBUF_INIT;

	if (mkdir(base, 0777) < 0 && errno != EEXIST)
		die_errno(_("mvcc: cannot create directory '%s'"), base);

	for (size_t i = 0; i < ARRAY_SIZE(dirs); i++) {
		strbuf_reset(&path);
		strbuf_addf(&path, "%s/%s", base, dirs[i]);

		if (mkdir(path.buf, 0777) < 0 && errno != EEXIST)
			die_errno(_("mvcc: cannot create directory '%s'"), path.buf);
	}

	strbuf_release(&path);
}

static const char *mvcc_kind_string(enum mvcc_kind kind)
{
	switch (kind) {
	case MVCC_PACKS:
		return "pack";
	case MVCC_REFS:
		return "refs";
	case MVCC_MANIFESTS:
		return "manifests";
	default:
		BUG("unknown mvcc kind received");
	}
}

void mvcc_storage_path(struct mvcc_storage *storage, enum mvcc_kind kind,
		       const char *name, struct strbuf *out)
{
	if (name)
		strbuf_addf(out, "%s/%s/%s", storage->dir,
			    mvcc_kind_string(kind), name);
	else
		strbuf_addf(out, "%s/%s", storage->dir, mvcc_kind_string(kind));
}

/*
 * Compute the location of the manifest pointer file: the external file
 * named by GIT_MVCC_MANIFEST_PATH when set, otherwise <mvcc_dir>/manifest.
 */
static void manifest_pointer_path(struct mvcc_storage *storage,
				  struct strbuf *out)
{
	if (storage->manifest_pointer_path)
		strbuf_addstr(out, storage->manifest_pointer_path);
	else
		strbuf_addf(out, "%s/manifest", storage->dir);
}

/*
 * Invalidate the in-process manifest cache. Used after a CAS failure so
 * that the next call to mvcc_storage_get_manifest re-resolves the pointer
 * from disk rather than returning the stale cached state.
 */
static void invalidate_manifest_cache(struct mvcc_storage *storage)
{
	mvcc_manifest_release(&storage->manifest);
	storage->manifest_initialized = false;
	storage->current_version_set = false;
}

static bool is_valid_manifest_version(const char *s)
{
	if (strlen(s) != GIT_SHA256_HEXSZ)
		return false;
	if (strspn(s, "0123456789abcdef") != GIT_SHA256_HEXSZ)
		return false;
	return true;
}

/*
 * Resolve the SHA-256 of the current manifest. Returns a newly allocated
 * string that the caller must free. Returns NULL on real failures (I/O
 * error, malformed env var, malformed pointer file); errors have already
 * been logged in that case. Returns an empty string when no manifest has
 * been written yet. Otherwise returns a validated lowercase SHA-256 hex
 * string of length GIT_SHA256_HEXSZ.
 *
 * The lookup order is:
 *
 *   1. The GIT_MVCC_MANIFEST environment variable (snapshot pinning).
 *   2. The external pointer file named by GIT_MVCC_MANIFEST_PATH, if set.
 *   3. <mvcc_dir>/manifest.
 */
static char *resolve_manifest_version(struct mvcc_storage *storage)
{
	struct strbuf content = STRBUF_INIT;
	struct strbuf path = STRBUF_INIT;
	const char *pinned;
	char *result = NULL;
	ssize_t ret;

	pinned = getenv("GIT_MVCC_MANIFEST");
	if (pinned) {
		strbuf_addstr(&content, pinned);
	} else {
		manifest_pointer_path(storage, &path);

		ret = strbuf_read_file(&content, path.buf, 0);
		if (ret < 0) {
			if (errno != ENOENT) {
				error_errno(_("mvcc: cannot read '%s'"), path.buf);
				goto out;
			}

			result = xstrdup("");
			goto out;
		}

		strbuf_trim(&content);
	}

	if (!content.len) {
		result = xstrdup("");
		goto out;
	}

	if (!is_valid_manifest_version(content.buf)) {
		error(_("mvcc: manifest pointer is invalid: '%s'"), content.buf);
		goto out;
	}

	result = strbuf_detach(&content, NULL);

out:
	strbuf_release(&content);
	strbuf_release(&path);
	return result;
}

static bool is_valid_manifest_path(const char *path)
{
	if (!*path)
		return false; /* empty */
	if (*path == '/')
		return false; /* absolute */
	if (strstr(path, ".."))
		return false; /* potential path traversal */
	if (strstr(path, "//"))
		return false; /* empty component */
	if (strspn(path, "abcdefghijklmnopqrstuvwxyz0123456789/.") != strlen(path))
		return false; /* invalid characters */
	return true;
}

void mvcc_manifest_release(struct mvcc_manifest *manifest)
{
	free(manifest->paths);
	free(manifest->owned_data);
	memset(manifest, 0, sizeof(*manifest));
}

int mvcc_manifest_read_file(struct mvcc_manifest *manifest, const char *path)
{
	struct strbuf content = STRBUF_INIT;
	struct chunkfile *cf = NULL;
	const unsigned char *path_blob = NULL, *objs_buf = NULL, *refs_buf = NULL;
	size_t path_len = 0, objs_len = 0, refs_len = 0;
	uint8_t version, num_chunks, path_width;
	uint32_t magic;
	int ret;

	mvcc_manifest_release(manifest);

	if (strbuf_read_file(&content, path, 0) < 0) {
		ret = error_errno(_("mvcc: failed reading manifest '%s'"), path);
		goto out;
	}

	if (content.len < MVCC_MANIFEST_HEADER_SIZE) {
		ret = error(_("mvcc: manifest header is too short: '%s'"), path);
		goto out;
	}

	magic = get_be32(content.buf);
	if (magic != MVCC_MANIFEST_MAGIC) {
		ret = error(_("mvcc: manifest has bad magic 0x%08"PRIx32": '%s'"),
			    magic, path);
		goto out;
	}

	version = content.buf[4];
	if (version != MVCC_MANIFEST_VERSION) {
		ret = error(_("mvcc: manifest has unsupported version %u: '%s'"),
			    version, path);
		goto out;
	}

	num_chunks = content.buf[5];
	path_width = content.buf[6];

	switch (content.buf[7]) {
	case MVCC_MANIFEST_HASH_SHA1:
	case MVCC_MANIFEST_HASH_SHA256:
		break;
	default:
		ret = error(_("mvcc: manifest has unknown object hash algorithm %u: '%s'"),
			    content.buf[7], path);
		goto out;
	}

	if (!path_width) {
		ret = error(_("mvcc: manifest has zero PATH record width: %s"), path);
		goto out;
	}

	if (content.len < MVCC_MANIFEST_HEADER_SIZE +
			  (size_t)(num_chunks + 1) * CHUNK_TOC_ENTRY_SIZE +
			  GIT_SHA256_RAWSZ) {
		ret = error(_("mvcc: manifest table of contents is too short: '%s'"), path);
		goto out;
	}

	cf = init_chunkfile(NULL);
	if (read_table_of_contents(cf, (const unsigned char *) content.buf, content.len,
				   MVCC_MANIFEST_HEADER_SIZE, num_chunks, 1)) {
		ret = error(_("mvcc: failed parsing manifest table of contents: '%s'"), path);
		goto out;
	}

	if (pair_chunk(cf, MVCC_CHUNKID_PATH, &path_blob, &path_len)) {
		ret = error(_("mvcc: manifest is missing required PATH chunk: '%s'"), path);
		goto out;
	}

	if (path_len % path_width) {
		ret = error(_("mvcc: PATH chunk size %"PRIuMAX" is not a multiple of record width %u: '%s'"),
			    (uintmax_t)path_len, path_width, path);
		goto out;
	}

	manifest->paths_nr = path_len / path_width;
	if (manifest->paths_nr) {
		ALLOC_ARRAY(manifest->paths, manifest->paths_nr);

		for (size_t i = 0; i < manifest->paths_nr; i++) {
			const char *entry = (const char *)path_blob + i * path_width;

			if (entry[path_width - 1]) {
				ret = error(_("mvcc: manifest contains path that is not NUL-terminated: '%s'"),
					    path);
				goto out;
			}

			if (!is_valid_manifest_path(entry)) {
				ret = error(_("mvcc: manifest contains invalid PATH '%s': '%s'"),
					    entry, path);
				goto out;
			}

			manifest->paths[i] = entry;
		}
	}

	if (!pair_chunk(cf, MVCC_CHUNKID_OBJS, &objs_buf, &objs_len)) {
		uint32_t start, count;

		if (objs_len != 8) {
			ret = error(_("mvcc: OBJS chunk has wrong size (%"PRIuMAX"): '%s'"),
				    (uintmax_t)objs_len, path);
			goto out;
		}

		start = get_be32(objs_buf);
		count = get_be32(objs_buf + 4);

		if ((size_t)start > manifest->paths_nr || (size_t)count > manifest->paths_nr - (size_t)start) {
			ret = error(_("mvcc: OBJS range (%"PRIu32", %"PRIu32") is out of bounds (%"PRIuMAX" paths): '%s'"),
				    start, count, (uintmax_t)manifest->paths_nr, path);
			goto out;
		}

		manifest->objs_start = start;
		manifest->objs_nr = count;
	}

	if (!pair_chunk(cf, MVCC_CHUNKID_REFS, &refs_buf, &refs_len)) {
		if (refs_len % 4) {
			ret = error(_("mvcc: REFS chunk has wrong size (%"PRIuMAX"): '%s'"),
				    (uintmax_t)refs_len, path);
			goto out;
		}

		for (size_t i = 0; i < refs_len / 4; i++) {
			uint32_t idx = get_be32(refs_buf + 4 * i);
			if (idx >= manifest->paths_nr) {
				ret = error(_("mvcc: REFS index %"PRIu32" " "is out of bounds (%"PRIuMAX" paths): '%s'"),
					    idx, (uintmax_t)manifest->paths_nr, path);
				goto out;
			}
		}

		manifest->refs_indices_big_endian = refs_buf;
		manifest->refs_nr = refs_len / 4;
	}

	manifest->owned_data = strbuf_detach(&content, NULL);
	ret = 0;

out:
	strbuf_release(&content);
	free_chunkfile(cf);
	return ret;
}

const char *mvcc_manifest_refs_path(const struct mvcc_manifest *manifest,
				    uint32_t idx)
{
	if (idx >= manifest->refs_nr)
		BUG("mvcc: requested refs index exceeds number of refs");
	return manifest->paths[get_be32(manifest->refs_indices_big_endian + idx * 4)];
}

const char *mvcc_manifest_objs_path(const struct mvcc_manifest *manifest,
				    uint32_t idx)
{
	if (idx >= manifest->objs_nr)
		BUG("mvcc: requested objs index exceeds number of objs");
	return manifest->paths[manifest->objs_start + idx];
}

void mvcc_manifest_into_lists(const struct mvcc_manifest *manifest,
			      struct string_list *obj_paths,
			      struct string_list *ref_paths)
{
	for (size_t i = 0; i < manifest->objs_nr; i++)
		string_list_append(obj_paths, mvcc_manifest_objs_path(manifest, i));
	for (size_t i = 0; i < manifest->refs_nr; i++)
		string_list_append(ref_paths, mvcc_manifest_refs_path(manifest, i));
}

const struct mvcc_manifest *mvcc_storage_get_manifest(struct mvcc_storage *storage)
{
	struct strbuf path = STRBUF_INIT;
	char *version;

	if (storage->manifest_initialized)
		return &storage->manifest;

	version = resolve_manifest_version(storage);
	if (!version)
		die(_("mvcc: failed resolving manifest version"));
	if (!*version) {
		storage->manifest_initialized = true;
		goto out;
	}

	mvcc_storage_path(storage, MVCC_MANIFESTS, version, &path);
	if (access(path.buf, R_OK) < 0)
		die(_("mvcc: manifest body '%s' is not available locally; "
		      "ensure dependencies have been prefetched"), version);

	if (mvcc_manifest_read_file(&storage->manifest, path.buf) < 0)
		die(_("mvcc: failed parsing manifest '%s'"), path.buf);

	storage->manifest_initialized = true;
	memcpy(storage->current_version, version, GIT_SHA256_HEXSZ + 1);
	storage->current_version_set = true;

out:
	free(version);
	strbuf_release(&path);
	return &storage->manifest;
}

struct mvcc_write_ctx {
	const struct string_list *paths;
	size_t path_width;
	size_t objs_start;
	size_t objs_nr;
	uint32_t *refs_indices;
	size_t refs_nr;
};

static int write_path_chunk(struct hashfile *f, void *data)
{
	const struct mvcc_write_ctx *ctx = data;
	unsigned char buf[MVCC_MANIFEST_MAX_PATH_WIDTH];

	for (size_t i = 0; i < ctx->paths->nr; i++) {
		size_t len = strlen(ctx->paths->items[i].string);
		memcpy(buf, ctx->paths->items[i].string, len);
		for (size_t j = len; j < ctx->path_width; j++)
			buf[j] = '\0';
		hashwrite(f, buf, ctx->path_width);
	}

	return 0;
}

static int write_objs_chunk(struct hashfile *f, void *data)
{
	const struct mvcc_write_ctx *ctx = data;
	hashwrite_be32(f, (uint32_t)ctx->objs_start);
	hashwrite_be32(f, (uint32_t)ctx->objs_nr);
	return 0;
}

static int write_refs_chunk(struct hashfile *f, void *data)
{
	const struct mvcc_write_ctx *ctx = data;
	for (size_t i = 0; i < ctx->refs_nr; i++)
		hashwrite_be32(f, ctx->refs_indices[i]);
	return 0;
}

int mvcc_storage_write_manifest(struct mvcc_storage *storage,
				const struct string_list *obj_paths,
				const struct string_list *ref_paths,
				const struct git_hash_algo *object_hash,
				char *version_hex_out)
{
	struct mvcc_write_ctx ctx = { 0 };
	struct string_list sorted_paths = STRING_LIST_INIT_NODUP;
	unsigned char trailing_hash[GIT_MAX_RAWSZ];
	char version_hex[GIT_SHA256_HEXSZ + 1];
	struct strbuf manifests_dir = STRBUF_INIT;
	struct strbuf path = STRBUF_INIT;
	struct tempfile *tempfile = NULL;
	struct chunkfile *cf = NULL;
	struct hashfile *f = NULL;
	size_t max_path_len = 0;
	size_t i;
	int ret;
	int fd;

	if (storage->pinned)
		die(_("mvcc: cannot write while GIT_MVCC_MANIFEST is set"));

	for (i = 0; i < obj_paths->nr; i++)
		string_list_append(&sorted_paths, obj_paths->items[i].string);
	for (i = 0; i < ref_paths->nr; i++)
		string_list_append(&sorted_paths, ref_paths->items[i].string);
	string_list_sort(&sorted_paths);

	for (i = 0; i < sorted_paths.nr; i++) {
		const char *path = sorted_paths.items[i].string;
		size_t path_len = strlen(path);

		if (!is_valid_manifest_path(path)) {
			ret = error(_("mvcc: refusing to emit invalid manifest path '%s'"), path);
			goto out;
		}
		if (i && !strcmp(sorted_paths.items[i - 1].string, path)) {
			ret = error(_("mvcc: duplicate manifest path '%s'"), path);
			goto out;
		}
		if (path_len > max_path_len)
			max_path_len = path_len;
	}

	ctx.paths = &sorted_paths;

	/*
	 * PATH record width must accommodate the longest path plus its
	 * trailing NUL terminator. Round up to a multiple of 4 for tidy
	 * alignment.
	 */
	ctx.path_width = (max_path_len + 1 + 3) & ~(size_t)3;
	if (ctx.path_width > MVCC_MANIFEST_MAX_PATH_WIDTH) {
		ret = error(_("mvcc: manifest path too long (width %"PRIuMAX
			      " exceeds maximum %u)"),
			    (uintmax_t)ctx.path_width, MVCC_MANIFEST_MAX_PATH_WIDTH);
		goto out;
	}

	/* OBJS range: obj paths occupy `[0, sorted_objs.nr)`. */
	ctx.objs_start = 0;
	ctx.objs_nr = obj_paths->nr;

	/*
	 * Resolve REFS indices in stack order by looking up each ref
	 * path's position in the lex-sorted ref half.
	 */
	if (ref_paths->nr) {
		ctx.refs_nr = ref_paths->nr;
		ALLOC_ARRAY(ctx.refs_indices, ctx.refs_nr);

		for (i = 0; i < ref_paths->nr; i++) {
			const char *needle = ref_paths->items[i].string;
			bool exact_match;

			ctx.refs_indices[i] = string_list_find_insert_index(&sorted_paths, needle, &exact_match);
			if (!exact_match)
				BUG("mvcc: ref path '%s' missing from PATH", needle);
		}
	}

	mvcc_storage_path(storage, MVCC_MANIFESTS, NULL, &manifests_dir);
	if (mkdir(manifests_dir.buf, 0777) < 0 && errno != EEXIST) {
		ret = error_errno(_("mvcc: cannot create '%s'"), manifests_dir.buf);
		goto out;
	}

	strbuf_addf(&path, "%s/tmp_manifest_XXXXXX", manifests_dir.buf);
	tempfile = xmks_tempfile(path.buf);
	fd = get_tempfile_fd(tempfile);

	f = hashfd(&hash_algos[GIT_HASH_SHA256], fd, get_tempfile_path(tempfile));
	cf = init_chunkfile(f);

	add_chunk(cf, MVCC_CHUNKID_PATH, sorted_paths.nr * ctx.path_width, write_path_chunk);
	add_chunk(cf, MVCC_CHUNKID_OBJS, sizeof(uint32_t) * 2, write_objs_chunk);
	add_chunk(cf, MVCC_CHUNKID_REFS, sizeof(uint32_t) * ctx.refs_nr, write_refs_chunk);

	hashwrite_be32(f, MVCC_MANIFEST_MAGIC);
	hashwrite_u8(f, MVCC_MANIFEST_VERSION);
	hashwrite_u8(f, (uint8_t)get_num_chunks(cf));
	hashwrite_u8(f, (uint8_t)ctx.path_width);

	switch (hash_algo_by_ptr(object_hash)) {
	case GIT_HASH_SHA1:
		hashwrite_u8(f, MVCC_MANIFEST_HASH_SHA1);
		break;
	case GIT_HASH_SHA256:
		hashwrite_u8(f, MVCC_MANIFEST_HASH_SHA256);
		break;
	default:
		ret = error(_("mvcc: cannot write unknown object hash %s"),
			    object_hash->name);
		goto out;
	}

	if (write_chunkfile(cf, &ctx) < 0) {
		ret = error(_("mvcc: failed writing manifest chunks"));
		goto out;
	}

	if (finalize_hashfile(f, trailing_hash, FSYNC_COMPONENT_NONE,
			      CSUM_HASH_IN_STREAM | CSUM_FSYNC) < 0) {
		ret = error(_("mvcc: failed finalizing manifest"));
		f = NULL;
		goto out;
	}
	f = NULL;

	/*
	 * The trailing SHA-256 hash is the manifest's identity; convert
	 * it to hex and use that as the content-addressed filename.
	 */
	hash_to_hex_algop_r(version_hex, trailing_hash, &hash_algos[GIT_HASH_SHA256]);

	strbuf_reset(&path);
	strbuf_addf(&path, "%s/%s", manifests_dir.buf, version_hex);

	if (rename_tempfile(&tempfile, path.buf) < 0) {
		ret = error_errno(_("mvcc: cannot move manifest tempfile into place"));
		goto out;
	}

	if (version_hex_out)
		memcpy(version_hex_out, version_hex, sizeof(version_hex));

	ret = 0;

out:
	if (f)
		free_hashfile(f);
	string_list_clear(&sorted_paths, 0);
	strbuf_release(&manifests_dir);
	strbuf_release(&path);
	delete_tempfile(&tempfile);
	free(ctx.refs_indices);
	free_chunkfile(cf);
	return ret;
}

int mvcc_storage_activate_manifest(struct mvcc_storage *storage,
				   const struct string_list *obj_paths,
				   const struct string_list *ref_paths,
				   const struct git_hash_algo *object_hash)
{
	struct lock_file lock = LOCK_INIT;
	struct strbuf pointer_path = STRBUF_INIT;
	struct strbuf observed = STRBUF_INIT;
	struct strbuf manifest_path = STRBUF_INIT;
	char version_hex[GIT_SHA256_HEXSZ + 1];
	int fd, ret;

	if (storage->pinned)
		die(_("mvcc: cannot write while GIT_MVCC_MANIFEST is set"));

	/*
	 * Make sure the in-memory manifest cache has been populated so
	 * that the compare-and-swap below has an expected-old value to
	 * compare against. For repositories that have not yet seen any
	 * manifest activation, current_version_set remains false and the
	 * CAS is skipped (first-ever activation).
	 */
	mvcc_storage_get_manifest(storage);

	ret = mvcc_storage_write_manifest(storage, obj_paths, ref_paths,
					  object_hash, version_hex);
	if (ret < 0)
		goto out;

	manifest_pointer_path(storage, &pointer_path);

	fd = hold_lock_file_for_update(&lock, pointer_path.buf, 0);
	if (fd < 0) {
		ret = error_errno(_("mvcc: cannot lock manifest pointer '%s'"),
				  pointer_path.buf);
		goto out;
	}

	/*
	 * TODO: In the current status quo we still have the case that the
	 * manifest may not exist, mostly because the object source does not
	 * yet have a way to initialize its on-disk structures. Once that's
	 * implemented though we should only ever do a CAS.
	 */
	if (storage->current_version_set) {
		if (strbuf_read_file(&observed, pointer_path.buf,
				     GIT_SHA256_HEXSZ + 1) < 0) {
			ret = error_errno(_("mvcc: cannot read manifest pointer '%s' for CAS check"),
					  pointer_path.buf);
			invalidate_manifest_cache(storage);
			goto out;
		}
		strbuf_trim(&observed);
		if (strcmp(observed.buf, storage->current_version)) {
			ret = error(_("mvcc: stale base manifest at '%s'; another writer advanced the pointer"),
				    pointer_path.buf);
			invalidate_manifest_cache(storage);
			goto out;
		}
	} else {
		struct stat st;

		if (!stat(pointer_path.buf, &st) || errno != ENOENT) {
			ret = error(_("mvcc: expected manifest pointer path to not exist"));
			goto out;
		}
	}

	if (write_in_full(fd, version_hex, GIT_SHA256_HEXSZ) < 0 ||
	    write_in_full(fd, "\n", 1) < 0) {
		ret = error_errno(_("mvcc: cannot write manifest pointer"));
		goto out;
	}

	if (commit_lock_file(&lock) < 0) {
		ret = error_errno(_("mvcc: cannot commit manifest pointer"));
		goto out;
	}

	/*
	 * Re-read the just-written body so that subsequent reads in
	 * this process see the new state, and record the new version
	 * as the next CAS's expected-old value.
	 */
	mvcc_storage_path(storage, MVCC_MANIFESTS, version_hex,
				  &manifest_path);
	mvcc_manifest_release(&storage->manifest);
	if (mvcc_manifest_read_file(&storage->manifest, manifest_path.buf) < 0) {
		ret = -1;
		goto out;
	}

	storage->manifest_initialized = true;
	memcpy(storage->current_version, version_hex, GIT_SHA256_HEXSZ + 1);
	storage->current_version_set = true;

	ret = 0;

out:
	strbuf_release(&pointer_path);
	strbuf_release(&observed);
	strbuf_release(&manifest_path);
	rollback_lock_file(&lock);
	return ret;
}

static void require_manifest_pointer_file(const char *path)
{
	struct stat st;

	if (stat(path, &st) < 0) {
		if (errno == ENOENT)
			die(_("mvcc: manifest pointer file '%s' does not exist"), path);
		die_errno(_("mvcc: cannot stat '%s'"), path);
	}

	if (!S_ISREG(st.st_mode))
		die(_("mvcc: manifest pointer '%s' is not a regular file"), path);

	if (st.st_size == 0)
		die(_("mvcc: manifest pointer file '%s' is empty"), path);
}

struct mvcc_storage *mvcc_storage_get(const char *commondir)
{
	struct mvcc_storage *storage;
	const char *manifest_path;
	const char *pinned;

	if (!commondir || !*commondir)
		BUG("mvcc_storage_get requires a common directory");

	/*
	 * Look up by `commondir` so that any number of backends (e.g. the
	 * ODB source and the refs backend) configured against the same
	 * repository share a single handle, and therefore a single
	 * in-memory manifest view.
	 */
	storage = strmap_get(&storages_by_commondir, commondir);
	if (storage) {
		storage->refcount++;
		return storage;
	}

	CALLOC_ARRAY(storage, 1);
	storage->refcount++;
	storage->commondir = xstrdup(commondir);
	storage->dir = xstrfmt("%s/mvcc", commondir);

	pinned = getenv("GIT_MVCC_MANIFEST");
	storage->pinned = pinned && *pinned;

	ensure_storage_dirs(storage->dir);

	manifest_path = getenv("GIT_MVCC_MANIFEST_PATH");
	if (manifest_path && *manifest_path) {
		require_manifest_pointer_file(manifest_path);
		storage->manifest_pointer_path = xstrdup(manifest_path);
	}

	if (strmap_put(&storages_by_commondir, commondir, storage))
		BUG("mvcc storage already cached");

	return storage;
}

void mvcc_storage_release(struct mvcc_storage *storage)
{
	if (!storage)
		return;
	if (storage->refcount-- > 1)
		return;

	strmap_remove(&storages_by_commondir, storage->commondir, 0);
	mvcc_manifest_release(&storage->manifest);
	free(storage->commondir);
	free(storage->dir);
	free(storage->manifest_pointer_path);
	free(storage);
}
