// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (C) 2017-2018 HUAWEI, Inc.
 *             https://www.huawei.com/
 * Copyright (C) 2022, Alibaba Cloud
 */

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/dirent.h>
#include <sys/kernel.h>
#include <sys/libkern.h>
#include <sys/limits.h>
#include <sys/malloc.h>
#include <sys/mount.h>
#include <sys/vnode.h>

#include "internal.h"

/* Map EROFS directory entry file type to FreeBSD dirent.d_type. */
static unsigned char
erofs_ftype_to_dtype(uint8_t ftype)
{
	switch (ftype) {
	case EROFS_FT_REG_FILE:
		return (DT_REG);
	case EROFS_FT_DIR:
		return (DT_DIR);
	case EROFS_FT_CHRDEV:
		return (DT_CHR);
	case EROFS_FT_BLKDEV:
		return (DT_BLK);
	case EROFS_FT_FIFO:
		return (DT_FIFO);
	case EROFS_FT_SOCK:
		return (DT_SOCK);
	case EROFS_FT_SYMLINK:
		return (DT_LNK);
	default:
		return (DT_UNKNOWN);
	}
}

static int
erofs_dirname_order(const char *left, size_t leftlen, const char *right,
    size_t rightlen)
{
	size_t common;
	int order;

	common = MIN(leftlen, rightlen);
	order = memcmp(left, right, common);
	if (order != 0)
		return (order);
	if (leftlen == rightlen)
		return (0);
	return (leftlen < rightlen ? -1 : 1);
}

/* Validate one name slot and return its Linux-visible length. */
int
erofs_dirent_namelen(const char *blk, uint32_t nameoff, uint32_t endoff,
    bool trailing, size_t *namelenp)
{
	size_t namelen, span;

	if (endoff <= nameoff)
		return (EINTEGRITY);
	span = endoff - nameoff;
	if (trailing) {
		namelen = strnlen(blk + nameoff, span);
	} else {
		namelen = span;
		if (memchr(blk + nameoff, '\0', span) != NULL)
			return (EINTEGRITY);
	}
	if (namelen == 0 || namelen > EROFS_NAME_LEN)
		return (EINTEGRITY);
	for (size_t i = 0; i < namelen; i++) {
		if (blk[nameoff + i] == '/')
			return (EINTEGRITY);
	}
	*namelenp = namelen;
	return (0);
}

/* Validate the dirent array, name offsets, and names in a block. */
int
erofs_validate_dirblock(const char *blk, uint32_t blksz, uint32_t maxsize,
    uint32_t *ndirentsp)
{
	const struct erofs_dirent *de;
	uint32_t endoff, first_nameoff, idx, nameoff, ndirents, prev_nameoff;
	size_t namelen, prev_namelen;
	int error;

	if (blksz < sizeof(struct erofs_dirent) ||
	    maxsize < sizeof(struct erofs_dirent) ||
	    maxsize > blksz)
		return (EINTEGRITY);
	de = (const struct erofs_dirent *)blk;
	first_nameoff = le16toh(de[0].nameoff);
	if (first_nameoff < sizeof(struct erofs_dirent) ||
	    first_nameoff >= maxsize ||
	    (first_nameoff % sizeof(struct erofs_dirent)) != 0)
		return (EINTEGRITY);
	ndirents = first_nameoff / sizeof(struct erofs_dirent);
	prev_nameoff = 0;
	prev_namelen = 0;
	for (idx = 0; idx < ndirents; idx++) {
		nameoff = le16toh(de[idx].nameoff);
		if ((idx == 0 && nameoff != first_nameoff) ||
		    (idx != 0 && nameoff <= prev_nameoff) ||
		    nameoff < first_nameoff || nameoff >= maxsize)
			return (EINTEGRITY);
		endoff = idx + 1 < ndirents ?
		    le16toh(de[idx + 1].nameoff) : maxsize;
		if (endoff <= nameoff || endoff > maxsize)
			return (EINTEGRITY);
		error = erofs_dirent_namelen(blk, nameoff, endoff,
		    idx + 1 == ndirents, &namelen);
		if (error != 0)
			return (error);
		if (idx != 0 && erofs_dirname_order(blk + prev_nameoff,
		    prev_namelen, blk + nameoff, namelen) >= 0)
			return (EINTEGRITY);
		prev_nameoff = nameoff;
		prev_namelen = namelen;
	}
	*ndirentsp = ndirents;
	return (0);
}

/* Extract name, nid, and type of the idx'th directory entry from a block. */
static int
erofs_dirent_name(const char *blk, uint32_t maxsize,
    uint32_t idx, uint32_t ndirents, char *name, size_t namesz, erofs_nid_t *nid,
    uint8_t *ftype, size_t *namelenp)
{
	const struct erofs_dirent *de;
	uint32_t nameoff, endoff;
	size_t namelen;
	int error;

	de = (const struct erofs_dirent *)blk;
	nameoff = le16toh(de[idx].nameoff);
	if (idx + 1 < ndirents)
		endoff = le16toh(de[idx + 1].nameoff);
	else
		endoff = maxsize;
	error = erofs_dirent_namelen(blk, nameoff, endoff,
	    idx + 1 == ndirents, &namelen);
	if (error != 0)
		return (error);
	if (namelen >= namesz)
		return (EINTEGRITY);
	memcpy(name, blk + nameoff, namelen);
	name[namelen] = '\0';
	*nid = le64toh(de[idx].nid);
	*ftype = de[idx].file_type;
	*namelenp = namelen;
	return (0);
}

static int
erofs_previous_dirname(struct erofs_sb_info *sbi, struct erofs_inode *dir,
    erofs_off_t block_off, char *name, size_t namesz, size_t *namelenp)
{
	erofs_nid_t nid;
	uint32_t maxsize, ndirents;
	uint8_t ftype;
	char *blk;
	int error;

	block_off -= sbi->block_size;
	maxsize = MIN((uint64_t)sbi->block_size, dir->size - block_off);
	error = erofs_read_data(sbi, dir, block_off, maxsize, (void **)&blk);
	if (error != 0)
		return (error);
	error = erofs_validate_dirblock(blk, sbi->block_size, maxsize,
	    &ndirents);
	if (error == 0)
		error = erofs_dirent_name(blk, maxsize, ndirents - 1,
		    ndirents, name, namesz, &nid, &ftype, namelenp);
	erofs_brelse(blk);
	return (error);
}

/* Per-call state for readdir dirent/cookie output. */
struct erofs_uiodir {
	struct dirent *dirent;
	uint64_t *cookies;
	uint64_t last_cookie;
	int ncookies;
	int acookies;
	int eofflag;
};

enum erofs_uiodir_result {
	EROFS_UIODIR_BUFFER_FULL = -1,
	EROFS_UIODIR_OK = 0,
};

/* Push a dirent and its cookie to the caller, modelled after UDF. */
static int
erofs_uiodir(struct erofs_uiodir *uiodir, int de_size, struct uio *uio,
    uint64_t cookie)
{
	int error;

	if (cookie <= uiodir->last_cookie)
		return (EINTEGRITY);
	if (uio->uio_resid < de_size ||
	    (uiodir->cookies != NULL &&
	    uiodir->acookies >= uiodir->ncookies)) {
		return (EROFS_UIODIR_BUFFER_FULL);
	}
	error = uiomove(uiodir->dirent, de_size, uio);
	if (error != 0)
		return (error);
	uiodir->last_cookie = cookie;
	if (uiodir->cookies != NULL)
		uiodir->cookies[uiodir->acookies++] = cookie;
	return (EROFS_UIODIR_OK);
}

/*
 * Process directory entries within a single block and output them to uio.
 * (Linux equivalent: erofs_fill_dentries in Linux's dir.c)
 *
 * Returns 0 on success (all entries consumed), -1 if uio is full, or a
 * positive error code on corruption.
 */
static int
erofs_fill_dentries(struct erofs_sb_info *sbi, struct erofs_uiodir *uiodir,
    struct uio *uio, struct dirent *d, const char *blk, uint32_t maxsize,
    uint32_t start_idx, uint32_t ndirents, erofs_off_t block_off,
    uint64_t *logical_offp)
{
	char name[EROFS_NAME_LEN + 1];
	uint32_t idx;
	uint64_t curpos, nextoff;
	erofs_nid_t nid;
	size_t namelen;
	uint8_t ftype;
	int error;

	for (idx = start_idx; idx < ndirents; idx++) {
		curpos = block_off + idx * sizeof(struct erofs_dirent);
		nextoff = (idx + 1 < ndirents) ?
		    (curpos + sizeof(struct erofs_dirent)) :
		    (block_off + maxsize);
		error = erofs_dirent_name(blk, maxsize, idx, ndirents, name,
		    sizeof(name), &nid, &ftype, &namelen);
		if (error != 0)
			return (error);
		if (!erofs_nid_is_valid(sbi, nid))
			return (EINTEGRITY);
		bzero(d, sizeof(*d));
		d->d_fileno = nid;
		d->d_type = erofs_ftype_to_dtype(ftype);
		d->d_namlen = namelen;
		d->d_reclen = GENERIC_DIRSIZ(d);
		d->d_off = nextoff;
		strlcpy(d->d_name, name, sizeof(d->d_name));
		error = erofs_uiodir(uiodir, d->d_reclen, uio, d->d_off);
		if (error != 0)
			return (error);
		*logical_offp = nextoff;
		uio->uio_offset = *logical_offp;
	}
	return (0);
}

/*
 * Read directory contents and output a FreeBSD dirent stream to uio.
 *
 * Key points:
 * - On-disk entries use their logical file offsets as cookies;
 * - A dot_omitted directory appends a synthetic "." at i_size, matching
 *   Linux, so existing on-disk cookies are not shifted;
 * - The dirent array occupies only the front portion of a block, so after
 *   scanning all entries offset must jump to maxsize (the block end),
 *   otherwise the loop would get stuck on the same block;
 * - Supports a_ncookies / a_cookies for NFS and other callers that need
 *   resumable iteration.
 */
int
erofs_readdir_block(struct vnode *vp, struct uio *uio, int *eofflag,
    int *ncookies, uint64_t **cookies)
{
	struct erofs_inode *dir;
	struct erofs_sb_info *sbi;
	struct erofs_uiodir uiodir;
	struct dirent d;
	uint64_t *cookiebuf;
	size_t cookie_count;
	char first_name[EROFS_NAME_LEN + 1];
	char previous_name[EROFS_NAME_LEN + 1];
	char *blk;
	erofs_off_t block_off;
	uint64_t logical_off;
	uint32_t block_pos, blksz, ndirents, start_idx, maxsize;
	erofs_nid_t edge_nid;
	size_t first_namelen, previous_namelen;
	uint8_t edge_ftype;
	bool have_previous, sequential;
	int error;

	dir = VTOE(vp);
	sbi = MTOE(vp->v_mount);
	blksz = sbi->block_size;
	error = 0;
	cookiebuf = NULL;
	cookie_count = 0;
	have_previous = false;
	uiodir.eofflag = 0;
	uiodir.acookies = 0;
	uiodir.dirent = &d;
	uiodir.cookies = NULL;
	uiodir.ncookies = 0;
	if (cookies != NULL && ncookies != NULL) {
		*cookies = NULL;
		*ncookies = 0;
		if (uio->uio_resid > 0) {
			cookie_count = (size_t)uio->uio_resid /
			    GENERIC_MINDIRSIZ;
			cookie_count = MIN(cookie_count, (size_t)INT_MAX);
			cookie_count = MIN(cookie_count,
			    SIZE_MAX / sizeof(*cookiebuf));
		}
		uiodir.ncookies = (int)cookie_count;
		if (cookie_count != 0)
			cookiebuf = malloc(sizeof(*cookiebuf) * cookie_count,
			    M_TEMP, M_WAITOK);
		uiodir.cookies = cookiebuf;
	}

	if (uio->uio_offset < 0) {
		error = EINVAL;
		goto out;
	}
	if (dir->dot_omitted && dir->size == (uint64_t)OFF_MAX) {
		error = EINTEGRITY;
		goto out;
	}

	logical_off = uio->uio_offset;
	sequential = logical_off == 0;
	uiodir.last_cookie = logical_off;
	uio->uio_offset = logical_off;

	while (logical_off < dir->size) {
		block_off = rounddown2(logical_off, (uint64_t)blksz);
		maxsize = MIN((uint64_t)blksz, dir->size - block_off);
		block_pos = logical_off - block_off;
		if ((block_pos % sizeof(struct erofs_dirent)) != 0) {
			block_pos = roundup(block_pos, sizeof(struct erofs_dirent));
			logical_off = block_off + block_pos;
			uio->uio_offset = logical_off;
		}
		error = erofs_read_data_readahead(sbi, dir, block_off,
		    maxsize, sequential, (void **)&blk);
		if (error != 0)
			goto out;
		error = erofs_validate_dirblock(blk, blksz, maxsize, &ndirents);
		if (error != 0) {
			erofs_brelse(blk);
			goto out;
		}
		if (!have_previous && block_off != 0) {
			error = erofs_previous_dirname(sbi, dir, block_off,
			    previous_name, sizeof(previous_name),
			    &previous_namelen);
			if (error != 0) {
				erofs_brelse(blk);
				goto out;
			}
			have_previous = true;
		}
		error = erofs_dirent_name(blk, maxsize, 0, ndirents,
		    first_name, sizeof(first_name), &edge_nid, &edge_ftype,
		    &first_namelen);
		if (error == 0 && have_previous &&
		    erofs_dirname_order(previous_name, previous_namelen,
		    first_name, first_namelen) >= 0)
			error = EINTEGRITY;
		if (error == 0)
			error = erofs_dirent_name(blk, maxsize, ndirents - 1,
			    ndirents, previous_name, sizeof(previous_name),
			    &edge_nid, &edge_ftype, &previous_namelen);
		if (error != 0) {
			erofs_brelse(blk);
			goto out;
		}
		have_previous = true;
		start_idx = block_pos / sizeof(struct erofs_dirent);
		if (start_idx >= ndirents) {
			logical_off = block_off + maxsize;
			uio->uio_offset = logical_off;
			erofs_brelse(blk);
			continue;
		}
		error = erofs_fill_dentries(sbi, &uiodir, uio, &d, blk, maxsize,
		    start_idx, ndirents, block_off, &logical_off);
		erofs_brelse(blk);
		if (error != 0)
			goto out;
	}
	if (dir->dot_omitted && logical_off == dir->size) {
		bzero(&d, sizeof(d));
		d.d_fileno = dir->nid;
		d.d_type = DT_DIR;
		d.d_namlen = 1;
		d.d_reclen = GENERIC_DIRSIZ(&d);
		d.d_off = dir->size + 1;
		d.d_name[0] = '.';
		d.d_name[1] = '\0';
		error = erofs_uiodir(&uiodir, d.d_reclen, uio, d.d_off);
		if (error != 0)
			goto out;
		logical_off++;
		uio->uio_offset = logical_off;
	}
	uiodir.eofflag = 1;
out:
	if (error == EROFS_UIODIR_BUFFER_FULL)
		error = 0;
	if (eofflag != NULL && error == 0)
		*eofflag = uiodir.eofflag;
	if (cookies != NULL && ncookies != NULL) {
		if (error != 0) {
			if (cookiebuf != NULL)
				free(cookiebuf, M_TEMP);
		} else {
			*ncookies = uiodir.acookies;
			*cookies = cookiebuf;
		}
	}
	return (error);
}
