#define PERL_NO_GET_CONTEXT
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#include <sys/mman.h>
#include <sys/syscall.h>
#include <linux/memfd.h>

#define die_sys(format) Perl_croak(aTHX_ format, strerror(errno))

static SV* S_io_fdopen(pTHX_ int fd, const char* classname, char type) {
	PerlIO* pio = PerlIO_fdopen(fd, "r");
	GV* gv = newGVgen(classname ? classname : "Linux::FD::Mem");
	SV* ret = newRV_noinc((SV*)gv);
	IO* io = GvIOn(gv);
	IoTYPE(io) = type;
	IoIFP(io) = pio;
	IoOFP(io) = pio;
	if (classname) {
		HV* stash = gv_stashpv(classname, FALSE);
		sv_bless(ret, stash);
	}
	return ret;
}
#define io_fdopen(fd, classname, type) S_io_fdopen(aTHX_ fd, classname, type)

typedef struct { const char* key; size_t length; int value; } map[];

static const map mem_flags = {
	{ STR_WITH_LEN("allow-sealing"), MFD_ALLOW_SEALING },
#ifdef MFD_HUGETLB
	{ STR_WITH_LEN("huge-table"), MFD_HUGETLB },
	{ STR_WITH_LEN("huge-2mb"), MFD_HUGE_2MB },
	{ STR_WITH_LEN("huge-1gb"), MFD_HUGE_1GB },
#endif
};

static UV S_get_mem_flag(pTHX_ SV* flag_name) {
	int i;
	for (i = 0; i < sizeof mem_flags / sizeof *mem_flags; ++i)
		if (strEQ(SvPV_nolen(flag_name), mem_flags[i].key))
			return mem_flags[i].value;
	Perl_croak(aTHX_ "No such flag '%s' known", SvPV_nolen(flag_name));
}
#define get_mem_flag(name) S_get_mem_flag(aTHX_ name)

static const map seal_flags = {
	{ STR_WITH_LEN("seal"), F_SEAL_SEAL },
	{ STR_WITH_LEN("shrink"), F_SEAL_SHRINK },
	{ STR_WITH_LEN("grow"), F_SEAL_GROW },
	{ STR_WITH_LEN("write"), F_SEAL_WRITE },
#ifdef F_SEAL_FUTURE_WRITE
	{ STR_WITH_LEN("future-write"), F_SEAL_FUTURE_WRITE },
#endif
};

static UV S_get_seal_flag(pTHX_ SV* flag_name) {
	int i;
	for (i = 0; i < sizeof seal_flags / sizeof *seal_flags; ++i)
		if (strEQ(SvPV_nolen(flag_name), seal_flags[i].key))
			return seal_flags[i].value;
	Perl_croak(aTHX_ "No such seal '%s' known", SvPV_nolen(flag_name));
}
#define get_seal_flag(name) S_get_seal_flag(aTHX_ name)

#define get_fd(self) PerlIO_fileno(IoOFP(sv_2io(SvRV(self))))

MODULE = Linux::FD::Mem				PACKAGE = Linux::FD::Mem

SV*
new(classname, name, ...)
	const char* classname;
	const char* name;
	PREINIT:
	int memfd;
	int i, flags = MFD_CLOEXEC;
	CODE:
	for (i = 2; i < items; i++)
		flags |= get_mem_flag(ST(i));
	memfd = syscall(__NR_memfd_create, name, flags);
	if (memfd < 0)
		die_sys("Couldn't open memfd: %s");
	RETVAL = io_fdopen(memfd, classname, '+');
	OUTPUT:
		RETVAL

void
seal(file_handle, ...)
	SV* file_handle;
	PREINIT:
		int fd, seals = 0, i, ret;
	CODE:
	fd = get_fd(file_handle);
	for (i = 1; i < items; i++)
		seals |= get_seal_flag(ST(i));
	ret = fcntl(fd, F_ADD_SEALS, seals);
	if (ret < 0)
		die_sys("Couldn't add seal: %s");

void
get_seals(file_handle)
	SV* file_handle;
	PREINIT:
	int seals, fd, i;
	PPCODE:
	fd = get_fd(file_handle);
	seals = fcntl(fd, F_GET_SEALS, 0);
	for (i = 0; i < sizeof seal_flags / sizeof *seal_flags; ++i) {
		if (seal_flags[i].value & seals)
			mXPUSHp(seal_flags[i].key, seal_flags[i].length);
	}