/* SPDX-License-Identifier: BSD-2-Clause */

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/dirent.h>
#include <sys/extattr.h>
#include <sys/fnv_hash.h>
#include <sys/kernel.h>
#include <sys/libkern.h>
#include <sys/limits.h>
#include <sys/malloc.h>
#include <sys/namei.h>
#include <sys/proc.h>
#include <sys/stat.h>
#include <sys/unistd.h>
#include <sys/vnode.h>
#include <sys/acl.h>

#include <vm/vnode_pager.h>

#include "internal.h"
#include "xattr.h"

static vop_inactive_t erofs_inactive;
static vop_reclaim_t erofs_reclaim;

static vop_readdir_t erofs_readdir;
static vop_readlink_t erofs_readlink;

static vop_open_t erofs_open;
static vop_read_t erofs_read;
static vop_bmap_t erofs_bmap;

static vop_getattr_t erofs_getattr;
static vop_setattr_t erofs_setattr;
static vop_access_t erofs_access;
static vop_pathconf_t erofs_pathconf;

static vop_getextattr_t erofs_getextattr;
static vop_listextattr_t erofs_listextattr;
static vop_deleteextattr_t erofs_deleteextattr;
static vop_setextattr_t erofs_setextattr;
static vop_getacl_t erofs_vop_getacl;
static vop_aclcheck_t erofs_aclcheck;
static vop_setacl_t erofs_setacl;

/* vop_fhtovp removed in FreeBSD 15.0 */
static vop_vptofh_t erofs_vptofh;

/* Access check: data nodes are read-only, but device/FIFO nodes are not denied
 * writes. */
static int
erofs_access(struct vop_access_args *ap)
{
	struct vnode *vp;
	struct erofs_inode *vi;
	struct acl *acl;
	accmode_t accmode;
	int error;

	vp = ap->a_vp;
	vi = VTOE(vp);
	accmode = ap->a_accmode;
	if ((accmode & VMODIFY_PERMS) != 0) {
		switch (vp->v_type) {
		case VDIR:
		case VLNK:
		case VREG:
			return (EROFS);
		default:
			break;
		}
	}
	error = vfs_unixify_accmode(&accmode);
	if (error != 0)
		return (error);
	if ((vp->v_mount->mnt_flag & MNT_ACLS) == 0)
		return (vaccess(vp->v_type, vi->mode & ALLPERMS, vi->uid,
		    vi->gid, accmode, ap->a_cred));

	acl = acl_alloc(M_WAITOK);
	error = erofs_get_acl(vp, ACL_TYPE_ACCESS, acl);
	if (error == 0)
		error = vaccess_acl_posix1e(vp->v_type, vi->uid, vi->gid, acl,
		    accmode, ap->a_cred);
	acl_free(acl);
	return (error);
}

/*
 * Tell the generic pager that EROFS does not provide block-level bmap.
 *
 * Returning EOPNOTSUPP prevents the pager from assuming a bufobj/strategy is
 * available, avoiding “No strategy for buffer” errors.  VM will correctly
 * fall back to the VOP_READ-based page-in path.
 */
static int
erofs_bmap(struct vop_bmap_args *ap)
{
	(void)ap;
	return (EOPNOTSUPP);
}

/* No dirty writeback on last ref release, so inactive is a no-op. */
static int
erofs_inactive(struct vop_inactive_args *ap)
{
	(void)ap;
	return (0);
}

/*
 * Create VM object when opening a regular vnode.
 *
 * The FreeBSD local vnode pager services synchronous and asynchronous faults
 * through VOP_READ without requiring a block strategy method.
 */
static int
erofs_open(struct vop_open_args *ap)
{
	struct vnode *vp;
	struct erofs_inode *vi;

	vp = ap->a_vp;
	vi = VTOE(vp);
	if (VN_ISDEV(vp))
		return (EOPNOTSUPP);
	if (vp->v_type == VREG)
		vnode_create_vobject(vp, vi->size, ap->a_td);
	return (0);
}

static int
erofs_getattr(struct vop_getattr_args *ap)
{
	struct vnode *vp;
	struct erofs_inode *vi;
	struct erofs_sb_info *sbi;
	struct vattr *vap;

	vp = ap->a_vp;
	vi = VTOE(vp);
	sbi = MTOE(vp->v_mount);
	vap = ap->a_vap;
	VATTR_NULL(vap);
	vap->va_type = vp->v_type;
	vap->va_mode = vi->mode & ALLPERMS;
	vap->va_nlink = vi->nlink;
	vap->va_uid = vi->uid;
	vap->va_gid = vi->gid;
	vap->va_fsid = vp->v_mount->mnt_stat.f_fsid.val[0];
	vap->va_fileid = vi->nid;
	vap->va_size = vi->size;
	vap->va_blocksize = sbi->block_size;
	vap->va_atime.tv_sec = vi->mtime;
	vap->va_mtime.tv_sec = vi->mtime;
	vap->va_ctime.tv_sec = vi->mtime;
	vap->va_atime.tv_nsec = vi->mtime_nsec;
	vap->va_mtime.tv_nsec = vi->mtime_nsec;
	vap->va_ctime.tv_nsec = vi->mtime_nsec;
	vap->va_gen = vi->generation;
	vap->va_flags = 0;
	vap->va_rdev = VN_ISDEV(vp) ? vi->rdev : NODEV;
	if (vi->data_blocks > (UINT64_MAX >> sbi->blkszbits))
		return (EINTEGRITY);
	vap->va_bytes = vi->data_blocks << sbi->blkszbits;
	vap->va_filerev = 0;
	return (0);
}

/*
 * Read-only xattr get entry point.
 *
 * Delegates to erofs_getxattr() for two namespaces:
 * - EXTATTR_NAMESPACE_USER
 * - EXTATTR_NAMESPACE_SYSTEM (trusted.* / security.*)
 */
static int
erofs_getextattr(struct vop_getextattr_args *ap)
{
	int error;

	if (VN_ISDEV(ap->a_vp))
		return (EOPNOTSUPP);
	error = extattr_check_cred(ap->a_vp, ap->a_attrnamespace, ap->a_cred,
	    ap->a_td, VREAD);
	if (error != 0)
		return (error);
	if (ap->a_name == NULL || ap->a_name[0] == '\0')
		return (EINVAL);
	if (strlen(ap->a_name) > EXTATTR_MAXNAMELEN)
		return (EINVAL);

	switch (ap->a_attrnamespace) {
	case EXTATTR_NAMESPACE_USER:
	case EXTATTR_NAMESPACE_SYSTEM:
		break;
	default:
		return (EOPNOTSUPP);
	}

	return (erofs_getxattr(ap->a_vp, ap->a_attrnamespace, ap->a_name,
	    ap->a_uio, ap->a_size));
}

/*
 * Read-only xattr list entry point.
 *
 * Delegates to erofs_listxattr() for two namespaces:
 * - EXTATTR_NAMESPACE_USER
 * - EXTATTR_NAMESPACE_SYSTEM (trusted.* / security.*)
 */
static int
erofs_listextattr(struct vop_listextattr_args *ap)
{
	int error;

	if (VN_ISDEV(ap->a_vp))
		return (EOPNOTSUPP);
	error = extattr_check_cred(ap->a_vp, ap->a_attrnamespace, ap->a_cred,
	    ap->a_td, VREAD);
	if (error != 0)
		return (error);

	switch (ap->a_attrnamespace) {
	case EXTATTR_NAMESPACE_USER:
	case EXTATTR_NAMESPACE_SYSTEM:
		break;
	default:
		return (EOPNOTSUPP);
	}

	return (erofs_listxattr(ap->a_vp, ap->a_attrnamespace, ap->a_uio,
	    ap->a_size));
}

static int
erofs_deleteextattr(struct vop_deleteextattr_args *ap)
{
	if (VN_ISDEV(ap->a_vp))
		return (EOPNOTSUPP);
	return (EROFS);
}

static int
erofs_setextattr(struct vop_setextattr_args *ap)
{
	if (VN_ISDEV(ap->a_vp))
		return (EOPNOTSUPP);
	return (EROFS);
}

/* EROFS is read-only; mutations on regular files/dirs/symlinks are denied, size
 * changes on special vnodes are treated as no-ops per read-only convention. */
static int
erofs_setattr(struct vop_setattr_args *ap)
{
	struct vnode *vp;
	struct vattr *vap;

	vp = ap->a_vp;
	vap = ap->a_vap;
	if (vap->va_mode != (mode_t)VNOVAL || vap->va_uid != (uid_t)VNOVAL ||
	    vap->va_gid != (gid_t)VNOVAL || vap->va_atime.tv_sec != VNOVAL ||
	    vap->va_atime.tv_nsec != VNOVAL || vap->va_mtime.tv_sec != VNOVAL ||
	    vap->va_mtime.tv_nsec != VNOVAL || vap->va_flags != VNOVAL)
		return (EROFS);
	if (vap->va_size != VNOVAL) {
		switch (vp->v_type) {
		case VDIR:
			return (EISDIR);
		case VLNK:
		case VREG:
			return (EROFS);
		case VCHR:
		case VBLK:
		case VSOCK:
		case VFIFO:
		case VNON:
		case VBAD:
		case VMARKER:
			return (0);
		}
	}
	return (0);
}

static int
erofs_read(struct vop_read_args *ap)
{
	switch (ap->a_vp->v_type) {
	case VREG:
		return (erofs_read_file(ap->a_vp, ap->a_uio, ap->a_ioflag));
	case VDIR:
		return (EISDIR);
	default:
		return (EINVAL);
	}
}

static int
erofs_readdir(struct vop_readdir_args *ap)
{
	if (ap->a_vp->v_type != VDIR)
		return (ENOTDIR);
	return (erofs_readdir_block(ap->a_vp, ap->a_uio, ap->a_eofflag,
	    ap->a_ncookies, ap->a_cookies));
}

static int
erofs_readlink(struct vop_readlink_args *ap)
{
	if (ap->a_vp->v_type != VLNK)
		return (EINVAL);
	return (erofs_readlink_target(ap->a_vp, ap->a_uio));
}

static int
erofs_pathconf(struct vop_pathconf_args *ap)
{
	switch (ap->a_name) {
	case _PC_NAME_MAX:
		*ap->a_retval = EROFS_NAME_LEN;
		return (0);
	case _PC_PATH_MAX:
		*ap->a_retval = PATH_MAX;
		return (0);
	case _PC_FILESIZEBITS:
		*ap->a_retval = 64;
		return (0);
	case _PC_LINK_MAX:
		*ap->a_retval = INT_MAX;
		return (0);
	case _PC_CHOWN_RESTRICTED:
	case _PC_NO_TRUNC:
		*ap->a_retval = 1;
		return (0);
	case _PC_ACL_EXTENDED:
		*ap->a_retval =
		    ((ap->a_vp->v_mount->mnt_flag & MNT_ACLS) != 0) ? 1 : 0;
		return (0);
	case _PC_ACL_PATH_MAX:
		*ap->a_retval =
		    ((ap->a_vp->v_mount->mnt_flag & MNT_ACLS) != 0) ?
		    ACL_MAX_ENTRIES : 3;
		return (0);
	case _PC_ACL_NFS4:
		*ap->a_retval = 0;
		return (0);
	default:
		return (vop_stdpathconf(ap));
	}
}

static int
erofs_vop_getacl(struct vop_getacl_args *ap)
{
	if (VN_ISDEV(ap->a_vp))
		return (EOPNOTSUPP);
	if ((ap->a_vp->v_mount->mnt_flag & MNT_ACLS) == 0)
		return (EOPNOTSUPP);
	return (erofs_get_acl(ap->a_vp, ap->a_type, ap->a_aclp));
}

static int
erofs_aclcheck(struct vop_aclcheck_args *ap)
{
	if (VN_ISDEV(ap->a_vp))
		return (EOPNOTSUPP);
	if ((ap->a_vp->v_mount->mnt_flag & MNT_ACLS) == 0)
		return (EOPNOTSUPP);
	if (ap->a_aclp == NULL)
		return (EINVAL);
	switch (ap->a_type) {
	case ACL_TYPE_ACCESS:
		break;
	case ACL_TYPE_DEFAULT:
		if (ap->a_vp->v_type != VDIR)
			return (EINVAL);
		break;
	default:
		return (EINVAL);
	}
	return (acl_posix1e_check(ap->a_aclp));
}

static int
erofs_setacl(struct vop_setacl_args *ap)
{
	if (VN_ISDEV(ap->a_vp))
		return (EOPNOTSUPP);
	return (EROFS);
}

static u_int
erofs_vfs_hash(erofs_nid_t nid)
{

	return (fnv_32_buf(&nid, sizeof(nid), FNV1_32_INIT));
}

static int
erofs_vfs_hash_cmp(struct vnode *vp, void *pnid)
{
	struct erofs_inode *vi;

	vi = VTOE(vp);
	return (vi == NULL || vi->nid != *(erofs_nid_t *)pnid);
}

static void
erofs_fill_vnode(struct erofs_sb_info *sbi, struct vnode *vp,
    const struct erofs_inode *vi)
{
	vp->v_type = vi->vtype;
	if (vp->v_type == VFIFO)
		vp->v_op = &erofs_fifoops;
	if (vi->nid == sbi->root_nid)
		vp->v_vflag |= VV_ROOT;
}

/*
 * Get vnode by raw on-disk nid.  The raw nid is also the FreeBSD fileid and
 * hash identity, so the metabox selector bit remains collision-free.
 * Uses the standard FreeBSD vfs_hash API.
 * (Linux equivalent: erofs_iget in Linux's inode.c)
 */
int
erofs_vget(struct mount *mp, ino_t ino, int flags, struct vnode **vpp)
{
	struct erofs_sb_info *sbi;
	struct erofs_inode *vi;
	struct thread *td;
	struct vnode *vp;
	erofs_nid_t nid;
	u_int hash;
	bool shared;
	int error;

	td = curthread;
	nid = (uint64_t)ino;
	shared = (flags & LK_TYPE_MASK) == LK_SHARED;
	hash = erofs_vfs_hash(nid);
	error = vfs_hash_get(mp, hash, flags, td, vpp, erofs_vfs_hash_cmp,
	    &nid);
	if (error != 0 || *vpp != NULL)
		return (error);

	sbi = MTOE(mp);
	vi = malloc(sizeof(*vi), M_EROFS, M_WAITOK | M_ZERO);
	error = getnewvnode("erofs", mp, &erofs_vnodeops, &vp);
	if (error != 0) {
		free(vi, M_EROFS);
		*vpp = NULL;
		return (error);
	}
	vp->v_data = vi;
	vi->nid = nid;
	lockmgr(vp->v_vnlock, LK_EXCLUSIVE, NULL);
	error = insmntque(vp, mp);
	if (error != 0) {
		free(vi, M_EROFS);
		*vpp = NULL;
		return (error);
	}
	error = vfs_hash_insert(vp, hash, flags, td, vpp, erofs_vfs_hash_cmp,
	    &nid);
	if (error != 0 || *vpp != NULL)
		return (error);

	error = erofs_read_inode(sbi, nid, vi);
	if (error != 0) {
		*vpp = NULL;
		vgone(vp);
		vput(vp);
		return (error);
	}
	erofs_xattr_cache_init(vi);
	erofs_fill_vnode(sbi, vp, vi);
	vn_set_state(vp, VSTATE_CONSTRUCTED);
	if (shared)
		VOP_LOCK(vp, LK_DOWNGRADE);
	*vpp = vp;
	return (0);
}

static int
erofs_reclaim(struct vop_reclaim_args *ap)
{
	struct vnode *vp;
	struct erofs_inode *vi;

	vp = ap->a_vp;
	vi = VTOE(vp);
	if (vi != NULL) {
		vfs_hash_remove(vp);
		erofs_xattr_cache_fini(MTOE(vp->v_mount), vi);
		free(vi, M_EROFS);
		vp->v_data = NULL;
	}
	return (0);
}

/* Vnode pointer to persistent EROFS file handle. */
static int
erofs_vptofh(struct vop_vptofh_args *ap)
{
	struct erofs_fid efid;
	struct erofs_inode *vi;

	vi = VTOE(ap->a_vp);
	bzero(&efid, sizeof(efid));
	efid.len = sizeof(efid);
	efid.nid_hi = vi->nid >> 32;
	efid.nid_lo = vi->nid;
	efid.gen = vi->generation;
	memcpy(ap->a_fhp, &efid, sizeof(efid));
	return (0);
}

struct vop_vector erofs_vnodeops = {
	.vop_default = &default_vnodeops,
	.vop_inactive = erofs_inactive,
	.vop_reclaim = erofs_reclaim,

	.vop_lookup = vfs_cache_lookup,
	.vop_cachedlookup = erofs_lookup,
	.vop_readdir = erofs_readdir,
	.vop_readlink = erofs_readlink,

	.vop_open = erofs_open,
	.vop_read = erofs_read,
	.vop_bmap = erofs_bmap,
	.vop_getpages = vnode_pager_local_getpages,
	.vop_getpages_async = vnode_pager_local_getpages_async,

	.vop_getattr = erofs_getattr,
	.vop_setattr = erofs_setattr,
	.vop_access = erofs_access,
	.vop_pathconf = erofs_pathconf,

	.vop_getextattr = erofs_getextattr,
	.vop_listextattr = erofs_listextattr,
	.vop_deleteextattr = erofs_deleteextattr,
	.vop_setextattr = erofs_setextattr,
	.vop_getacl = erofs_vop_getacl,
	.vop_aclcheck = erofs_aclcheck,
	.vop_setacl = erofs_setacl,

	.vop_vptofh = erofs_vptofh,
};
VFS_VOP_VECTOR_REGISTER(erofs_vnodeops);

struct vop_vector erofs_fifoops = {
	.vop_default = &fifo_specops,
	.vop_access = erofs_access,
	.vop_aclcheck = erofs_aclcheck,
	.vop_deleteextattr = erofs_deleteextattr,
	.vop_getacl = erofs_vop_getacl,
	.vop_getextattr = erofs_getextattr,
	.vop_getattr = erofs_getattr,
	.vop_listextattr = erofs_listextattr,
	.vop_pathconf = erofs_pathconf,
	.vop_reclaim = erofs_reclaim,
	.vop_setacl = erofs_setacl,
	.vop_setattr = erofs_setattr,
	.vop_setextattr = erofs_setextattr,
	.vop_vptofh = erofs_vptofh,
};
VFS_VOP_VECTOR_REGISTER(erofs_fifoops);
