/*
 * Sai - ./src/common/c-pool.c
 *
 * Copyright (C) 2019 - 2026 Andy Green <andy@warmcat.com>
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation:
 *  version 2.1 of the License.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 *  MA  02110-1301  USA
 *
 * Pool sync helpers shared by sai-builder and sai-server, see
 * READMEs/README-pool.md.
 *
 * Everything a pool record names ends up as a path component on the builder,
 * and in the server's db, so both sides check names with these before using
 * them, whoever sent them.
 */

#include <libwebsockets.h>
#include <string.h>

#include "include/private.h"

/*
 * A pool name, from .sai.json: short, lowercase alnum plus - and _
 */

int
sai_pool_name_ok(const char *name)
{
	size_t n = 0;

	if (!name || !*name)
		return 0;

	while (name[n]) {
		char c = name[n];

		if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
		      c == '-' || c == '_'))
			return 0;
		if (++n > 32)
			return 0;
	}

	return 1;
}

static int
sai_pool_safe_char(char c)
{
	return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
	       (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.';
}

/*
 * The subdir part of an entry name, eg, "corpus-h2".  It's a single path
 * component that can't be hidden, ".." or anything else special.
 */

int
sai_pool_sub_ok(const char *sub, size_t len)
{
	size_t n;

	if (!len || len > 32 || sub[0] == '.')
		return 0;

	for (n = 0; n < len; n++)
		if (!sai_pool_safe_char(sub[n]))
			return 0;

	return 1;
}

/*
 * A whole entry name, "<sub>/<name>".  In the content addressed namespaces the
 * name is the 40 char lowercase hex sha1 of the content; findings can be named
 * anything safe, that isn't hidden.
 */

int
sai_pool_entry_name_ok(int ns, const char *name, size_t len)
{
	const char *sl = memchr(name, '/', len);
	size_t sub_len, nl, n;
	const char *nm;

	if (!sl)
		return 0;

	sub_len = lws_ptr_diff_size_t(sl, name);
	if (!sai_pool_sub_ok(name, sub_len))
		return 0;

	nm = sl + 1;
	nl = len - sub_len - 1;

	if (ns == SAI_POOL_NS_FINDINGS) {
		if (!nl || nl > 64 || nm[0] == '.')
			return 0;
		for (n = 0; n < nl; n++)
			if (!sai_pool_safe_char(nm[n]))
				return 0;

		return 1;
	}

	if (nl != 40)
		return 0;

	for (n = 0; n < nl; n++)
		if (!((nm[n] >= '0' && nm[n] <= '9') ||
		      (nm[n] >= 'a' && nm[n] <= 'f')))
			return 0;

	return 1;
}

/* is the content really what its content addressed name says? */

int
sai_pool_content_matches(const uint8_t *data, size_t len, const char *sha1hex)
{
	uint8_t md[20];
	char hex[41];
	int n;

	lws_SHA1(data, len, md);
	for (n = 0; n < 20; n++)
		lws_snprintf(hex + (n * 2), 3, "%02x", md[n]);

	return !memcmp(hex, sha1hex, 40);
}

void
sai_pool_rec_hdr_write(uint8_t *p, int type, int ns, size_t name_len,
		       size_t len)
{
	p[0] = (uint8_t)type;
	p[1] = (uint8_t)ns;
	p[2] = (uint8_t)(name_len >> 8);
	p[3] = (uint8_t)name_len;
	p[4] = (uint8_t)(len >> 24);
	p[5] = (uint8_t)(len >> 16);
	p[6] = (uint8_t)(len >> 8);
	p[7] = (uint8_t)len;
}

void
sai_pool_rec_hdr_read(const uint8_t *p, sai_pool_rec_hdr_t *h)
{
	h->type		= p[0];
	h->ns		= p[1];
	h->name_len	= (uint16_t)((p[2] << 8) | p[3]);
	h->len		= ((uint32_t)p[4] << 24) | ((uint32_t)p[5] << 16) |
			  ((uint32_t)p[6] << 8) | (uint32_t)p[7];
}

/* the most data a record of this type and namespace may carry */

size_t
sai_pool_rec_max(int ns, int type)
{
	switch (type) {
	case SAI_POOL_REC_PULL:
	case SAI_POOL_REC_PULL_END:
		return 8;
	case SAI_POOL_REC_OFFER:
	case SAI_POOL_REC_WANT:
		return SAI_POOL_OFFER_MAX;
	case SAI_POOL_REC_REPLACE:
		return SAI_POOL_LIST_MAX;
	case SAI_POOL_REC_PUT:
	case SAI_POOL_REC_ENTRY:
		return ns == SAI_POOL_NS_FINDINGS ? SAI_POOL_FINDING_MAX :
						    SAI_POOL_ENTRY_MAX;
	}

	return 0;
}

void
sai_pool_u64_write(uint8_t *p, uint64_t v)
{
	int n;

	for (n = 7; n >= 0; n--) {
		p[n] = (uint8_t)v;
		v >>= 8;
	}
}

uint64_t
sai_pool_u64_read(const uint8_t *p)
{
	uint64_t v = 0;
	int n;

	for (n = 0; n < 8; n++)
		v = (v << 8) | p[n];

	return v;
}

/*
 * Where sai-server keeps a repo's pool, which sai-web reads too.  The repo name
 * was checked at hook intake, but it's going in a path.
 */

void
sai_pool_db_path(char *buf, size_t len, const char *lhs, const char *repo,
		 const char *pool)
{
	char saf[128], *p;

	lws_snprintf(saf, sizeof(saf), "%s-%s", repo, pool);
	lws_filename_purify_inplace(saf);
	p = saf;
	while ((p = strchr(p, '/')))
		*p++ = '_';

	lws_snprintf(buf, len, "%s-pool-%s.sqlite3", lhs, saf);
}