/* SPDX-License-Identifier: BSD-2-Clause */
/* Minimal LZ4 decompressor for EROFS FreeBSD */
#include <sys/param.h>
#include <sys/endian.h>
#include <sys/systm.h>

#include "compress.h"

#define EROFS_LZ4_TOKEN_LITERAL_SHIFT 4
#define EROFS_LZ4_TOKEN_MATCH_MASK 0x0f
#define EROFS_LZ4_MAX_RUN 15
#define EROFS_LZ4_EXT_SENTINEL 255
#define EROFS_LZ4_MIN_MATCH 4
#define EROFS_LZ4_OFFSET_BYTES 2

static int
lz4_finish(const uint8_t *ip, const uint8_t *iend, int partial)
{
	if (partial)
		return (0);
	while (ip < iend) {
		if (*ip++ != 0)
			return (EINTEGRITY);
	}
	return (0);
}

int
z_erofs_lz4_decompress(const struct z_erofs_decompress_req *rq)
{
	const uint8_t *ip, *iend;
	uint8_t *op, *oend;
	unsigned int token;
	size_t length, copylen;
	size_t offset;

	ip = rq->in;
	iend = ip + rq->inputsize;
	op = rq->out;
	oend = op + rq->outputsize;
	if (iend < ip || oend < op)
		return (EINTEGRITY);

	while (ip < iend) {
		token = *ip++;
		length = token >> EROFS_LZ4_TOKEN_LITERAL_SHIFT;
		if (length == EROFS_LZ4_MAX_RUN) {
			unsigned int value;
			do {
				if (ip >= iend)
					return (EINTEGRITY);
				value = *ip++;
				if (length > SIZE_MAX - value)
					return (EINTEGRITY);
				length += value;
			} while (value == EROFS_LZ4_EXT_SENTINEL);
		}
		if (length > (size_t)(iend - ip))
			return (EINTEGRITY);
		if (!rq->partial_decoding && length > (size_t)(oend - op))
			return (EINTEGRITY);
		copylen = MIN(length, (size_t)(oend - op));
		memcpy(op, ip, copylen);
		ip += length;
		op += copylen;
		if (op == oend)
			return (lz4_finish(ip, iend, rq->partial_decoding));
		if (ip >= iend)
			break;
		if (ip + EROFS_LZ4_OFFSET_BYTES > iend)
			return (EINTEGRITY);
		offset = le16dec(ip);
		ip += EROFS_LZ4_OFFSET_BYTES;
		if (offset == 0 || offset >
		    (size_t)(op - (uint8_t *)rq->out))
			return (EINTEGRITY);
		length = token & EROFS_LZ4_TOKEN_MATCH_MASK;
		if (length == EROFS_LZ4_MAX_RUN) {
			unsigned int value;
			do {
				if (ip >= iend)
					return (EINTEGRITY);
				value = *ip++;
				if (length > SIZE_MAX - value)
					return (EINTEGRITY);
				length += value;
			} while (value == EROFS_LZ4_EXT_SENTINEL);
		}
		if (length > SIZE_MAX - EROFS_LZ4_MIN_MATCH)
			return (EINTEGRITY);
		length += EROFS_LZ4_MIN_MATCH;
		if (!rq->partial_decoding && length > (size_t)(oend - op))
			return (EINTEGRITY);
		copylen = MIN(length, (size_t)(oend - op));
		while (copylen-- != 0) {
			*op = *(op - offset);
			++op;
		}
		if (op == oend)
			return (lz4_finish(ip, iend, rq->partial_decoding));
	}
	return (op == oend ?
	    lz4_finish(ip, iend, rq->partial_decoding) : EINTEGRITY);
}
