//go:build linux

// SPDX-License-Identifier: MPL-2.0
/*
 * libpathrs: safe path resolution on Linux
 * Copyright (C) 2019-2025 SUSE LLC
 * Copyright (C) 2026 Aleksa Sarai <cyphar@cyphar.com>
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

// Package libpathrs is an internal thin wrapper around the libpathrs C API.
package libpathrs

import (
	"fmt"
	"syscall"
	"unsafe"
)

/*
// TODO: Figure out if we need to add support for linking against libpathrs
//       statically even if in dynamically linked builds in order to make
//       packaging a bit easier (using "-Wl,-Bstatic -lpathrs -Wl,-Bdynamic" or
//       "-l:pathrs.a").
#cgo pkg-config: pathrs
#include <pathrs.h>

// This is a workaround for unsafe.Pointer() not working for non-void pointers.
char *cast_ptr(void *ptr) { return ptr; }
*/
import "C"

func fetchError(errID C.int) error {
	if errID >= C.__PATHRS_MAX_ERR_VALUE {
		return nil
	}
	cErr := C.pathrs_errorinfo(errID)
	defer C.pathrs_errorinfo_free(cErr)

	var err error
	if cErr != nil {
		err = &Error{
			errno:       syscall.Errno(cErr.saved_errno),
			description: C.GoString(cErr.description),
		}
	}
	return err
}

// OpenRoot wraps pathrs_open_root.
func OpenRoot(path string) (uintptr, error) {
	cPath := C.CString(path)
	defer C.free(unsafe.Pointer(cPath))

	fd := C.pathrs_open_root(cPath)
	return uintptr(fd), fetchError(fd)
}

// Reopen wraps pathrs_reopen.
func Reopen(fd uintptr, flags uint64) (uintptr, error) {
	newFd := C.pathrs_reopen(C.int(fd), C.uint64_t(flags))
	return uintptr(newFd), fetchError(newFd)
}

// InRootResolve wraps pathrs_inroot_resolve.
func InRootResolve(rootFd uintptr, path string) (uintptr, error) {
	cPath := C.CString(path)
	defer C.free(unsafe.Pointer(cPath))

	fd := C.pathrs_inroot_resolve(C.int(rootFd), cPath)
	return uintptr(fd), fetchError(fd)
}

// InRootResolveNoFollow wraps pathrs_inroot_resolve_nofollow.
func InRootResolveNoFollow(rootFd uintptr, path string) (uintptr, error) {
	cPath := C.CString(path)
	defer C.free(unsafe.Pointer(cPath))

	fd := C.pathrs_inroot_resolve_nofollow(C.int(rootFd), cPath)
	return uintptr(fd), fetchError(fd)
}

// InRootOpen wraps pathrs_inroot_open.
func InRootOpen(rootFd uintptr, path string, flags uint64) (uintptr, error) {
	cPath := C.CString(path)
	defer C.free(unsafe.Pointer(cPath))

	fd := C.pathrs_inroot_open(C.int(rootFd), cPath, C.uint64_t(flags))
	return uintptr(fd), fetchError(fd)
}

// InRootReadlink wraps pathrs_inroot_readlink.
func InRootReadlink(rootFd uintptr, path string) (string, error) {
	cPath := C.CString(path)
	defer C.free(unsafe.Pointer(cPath))

	size := 128
	for {
		linkBuf := make([]byte, size)
		n := C.pathrs_inroot_readlink(C.int(rootFd), cPath, C.cast_ptr(unsafe.Pointer(&linkBuf[0])), C.size_t(len(linkBuf)))
		switch {
		case int(n) < C.__PATHRS_MAX_ERR_VALUE:
			return "", fetchError(n)
		case int(n) <= len(linkBuf):
			return string(linkBuf[:int(n)]), nil
		default:
			// The contents were truncated. Unlike readlinkat, pathrs returns
			// the size of the link when it checked. So use the returned size
			// as a basis for the reallocated size (but in order to avoid a DoS
			// where a magic-link is growing by a single byte each iteration,
			// make sure we are a fair bit larger).
			size += int(n)
		}
	}
}

// InRootRmdir wraps pathrs_inroot_rmdir.
func InRootRmdir(rootFd uintptr, path string) error {
	cPath := C.CString(path)
	defer C.free(unsafe.Pointer(cPath))

	err := C.pathrs_inroot_rmdir(C.int(rootFd), cPath)
	return fetchError(err)
}

// InRootUnlink wraps pathrs_inroot_unlink.
func InRootUnlink(rootFd uintptr, path string) error {
	cPath := C.CString(path)
	defer C.free(unsafe.Pointer(cPath))

	err := C.pathrs_inroot_unlink(C.int(rootFd), cPath)
	return fetchError(err)
}

// InRootRemoveAll wraps pathrs_inroot_remove_all.
func InRootRemoveAll(rootFd uintptr, path string) error {
	cPath := C.CString(path)
	defer C.free(unsafe.Pointer(cPath))

	err := C.pathrs_inroot_remove_all(C.int(rootFd), cPath)
	return fetchError(err)
}

// InRootCreat wraps pathrs_inroot_creat.
func InRootCreat(rootFd uintptr, path string, flags uint64, mode uint32) (uintptr, error) {
	cPath := C.CString(path)
	defer C.free(unsafe.Pointer(cPath))

	fd := C.pathrs_inroot_creat(C.int(rootFd), cPath, C.uint64_t(flags), C.uint(mode))
	return uintptr(fd), fetchError(fd)
}

// InRootRename wraps pathrs_inroot_rename.
func InRootRename(oldRootFd uintptr, oldPath string, newRootFd uintptr, newPath string, flags uint64) error {
	cOldPath := C.CString(oldPath)
	defer C.free(unsafe.Pointer(cOldPath))

	cNewPath := C.CString(newPath)
	defer C.free(unsafe.Pointer(cNewPath))

	err := C.pathrs_inroot_rename(C.int(oldRootFd), cOldPath, C.int(newRootFd), cNewPath, C.uint64_t(flags))
	return fetchError(err)
}

// InRootMkdir wraps pathrs_inroot_mkdir.
func InRootMkdir(rootFd uintptr, path string, mode uint32) error {
	cPath := C.CString(path)
	defer C.free(unsafe.Pointer(cPath))

	err := C.pathrs_inroot_mkdir(C.int(rootFd), cPath, C.uint(mode))
	return fetchError(err)
}

// InRootMkdirAll wraps pathrs_inroot_mkdir_all.
func InRootMkdirAll(rootFd uintptr, path string, mode uint32) (uintptr, error) {
	cPath := C.CString(path)
	defer C.free(unsafe.Pointer(cPath))

	fd := C.pathrs_inroot_mkdir_all(C.int(rootFd), cPath, C.uint(mode))
	return uintptr(fd), fetchError(fd)
}

// InRootMknod wraps pathrs_inroot_mknod.
func InRootMknod(rootFd uintptr, path string, mode uint32, dev uint64) error {
	cPath := C.CString(path)
	defer C.free(unsafe.Pointer(cPath))

	err := C.pathrs_inroot_mknod(C.int(rootFd), cPath, C.uint(mode), C.dev_t(dev))
	return fetchError(err)
}

// InRootSymlink wraps pathrs_inroot_symlink.
func InRootSymlink(target string, rootFd uintptr, linkpath string) error {
	cLinkpath := C.CString(linkpath)
	defer C.free(unsafe.Pointer(cLinkpath))

	cTarget := C.CString(target)
	defer C.free(unsafe.Pointer(cTarget))

	err := C.pathrs_inroot_symlink(cTarget, C.int(rootFd), cLinkpath)
	return fetchError(err)
}

// InRootHardlink wraps pathrs_inroot_hardlink.
func InRootHardlink(oldRootFd uintptr, oldPath string, newRootFd uintptr, newPath string, flags uint64) error {
	cNewPath := C.CString(newPath)
	defer C.free(unsafe.Pointer(cNewPath))

	cOldPath := C.CString(oldPath)
	defer C.free(unsafe.Pointer(cOldPath))

	err := C.pathrs_inroot_hardlink(C.int(oldRootFd), cOldPath, C.int(newRootFd), cNewPath, C.uint64_t(flags))
	return fetchError(err)
}

// ProcBase is pathrs_proc_base_t (uint64_t).
type ProcBase C.pathrs_proc_base_t

// FIXME: We need to open-code the constants because CGo unfortunately will
// implicitly convert any non-literal constants (i.e. those resolved using gcc)
// to signed integers. See <https://github.com/golang/go/issues/39136> for some
// more information on the underlying issue (though.
const (
	// ProcRoot is PATHRS_PROC_ROOT.
	ProcRoot ProcBase = 0xFFFF_FFFE_7072_6F63 // C.PATHRS_PROC_ROOT
	// ProcSelf is PATHRS_PROC_SELF.
	ProcSelf ProcBase = 0xFFFF_FFFE_091D_5E1F // C.PATHRS_PROC_SELF
	// ProcThreadSelf is PATHRS_PROC_THREAD_SELF.
	ProcThreadSelf ProcBase = 0xFFFF_FFFE_3EAD_5E1F // C.PATHRS_PROC_THREAD_SELF

	// ProcBaseTypeMask is __PATHRS_PROC_TYPE_MASK.
	ProcBaseTypeMask ProcBase = 0xFFFF_FFFF_0000_0000 // C.__PATHRS_PROC_TYPE_MASK
	// ProcBaseTypePid is __PATHRS_PROC_TYPE_PID.
	ProcBaseTypePid ProcBase = 0x8000_0000_0000_0000 // C.__PATHRS_PROC_TYPE_PID

	// ProcDefaultRootFd is PATHRS_PROC_DEFAULT_ROOTFD.
	ProcDefaultRootFd = -int(syscall.EBADF) // C.PATHRS_PROC_DEFAULT_ROOTFD
)

func assertEqual[T comparable](a, b T, msg string) {
	if a != b {
		panic(fmt.Sprintf("%s ((%T) %#v != (%T) %#v)", msg, a, a, b, b))
	}
}

// Verify that the values above match the actual C values. Unfortunately, Go
// only allows us to forcefully cast int64 to uint64 if you use a temporary
// variable, which means we cannot do it in a const context and thus need to do
// it at runtime (even though it is a check that fundamentally could be done at
// compile-time)...
func init() {
	var (
		actualProcRoot       int64 = C.PATHRS_PROC_ROOT
		actualProcSelf       int64 = C.PATHRS_PROC_SELF
		actualProcThreadSelf int64 = C.PATHRS_PROC_THREAD_SELF
	)

	assertEqual(ProcRoot, ProcBase(actualProcRoot), "PATHRS_PROC_ROOT")
	assertEqual(ProcSelf, ProcBase(actualProcSelf), "PATHRS_PROC_SELF")
	assertEqual(ProcThreadSelf, ProcBase(actualProcThreadSelf), "PATHRS_PROC_THREAD_SELF")

	var (
		actualProcBaseTypeMask uint64 = C.__PATHRS_PROC_TYPE_MASK
		actualProcBaseTypePid  uint64 = C.__PATHRS_PROC_TYPE_PID
	)

	assertEqual(ProcBaseTypeMask, ProcBase(actualProcBaseTypeMask), "__PATHRS_PROC_TYPE_MASK")
	assertEqual(ProcBaseTypePid, ProcBase(actualProcBaseTypePid), "__PATHRS_PROC_TYPE_PID")

	assertEqual(ProcDefaultRootFd, int(C.PATHRS_PROC_DEFAULT_ROOTFD), "PATHRS_PROC_DEFAULT_ROOTFD")
}

// ProcPid reimplements the PROC_PID(x) conversion.
func ProcPid(pid uint32) ProcBase { return ProcBaseTypePid | ProcBase(pid) }

// ProcOpenat wraps pathrs_proc_openat.
func ProcOpenat(procRootFd int, base ProcBase, path string, flags uint64) (uintptr, error) {
	cBase := C.pathrs_proc_base_t(base)

	cPath := C.CString(path)
	defer C.free(unsafe.Pointer(cPath))

	fd := C.pathrs_proc_openat(C.int(procRootFd), cBase, cPath, C.uint64_t(flags))
	return uintptr(fd), fetchError(fd)
}

// ProcReadlinkat wraps pathrs_proc_readlinkat.
func ProcReadlinkat(procRootFd int, base ProcBase, path string) (string, error) {
	// TODO: See if we can unify this code with InRootReadlink.

	cBase := C.pathrs_proc_base_t(base)

	cPath := C.CString(path)
	defer C.free(unsafe.Pointer(cPath))

	size := 128
	for {
		linkBuf := make([]byte, size)
		n := C.pathrs_proc_readlinkat(
			C.int(procRootFd), cBase, cPath,
			C.cast_ptr(unsafe.Pointer(&linkBuf[0])), C.size_t(len(linkBuf)))
		switch {
		case int(n) < C.__PATHRS_MAX_ERR_VALUE:
			return "", fetchError(n)
		case int(n) <= len(linkBuf):
			return string(linkBuf[:int(n)]), nil
		default:
			// The contents were truncated. Unlike readlinkat, pathrs returns
			// the size of the link when it checked. So use the returned size
			// as a basis for the reallocated size (but in order to avoid a DoS
			// where a magic-link is growing by a single byte each iteration,
			// make sure we are a fair bit larger).
			size += int(n)
		}
	}
}

// ProcfsOpenHow is pathrs_procfs_open_how (struct).
type ProcfsOpenHow C.pathrs_procfs_open_how

const (
	// ProcfsNewUnmasked is PATHRS_PROCFS_NEW_UNMASKED.
	ProcfsNewUnmasked = C.PATHRS_PROCFS_NEW_UNMASKED
)

// Flags returns a pointer to the internal flags field to allow other packages
// to modify structure fields that are internal due to Go's visibility model.
func (how *ProcfsOpenHow) Flags() *C.uint64_t { return &how.flags }

// ProcfsOpen is pathrs_procfs_open (sizeof(*how) is passed automatically).
func ProcfsOpen(how *ProcfsOpenHow) (uintptr, error) {
	fd := C.pathrs_procfs_open((*C.pathrs_procfs_open_how)(how), C.size_t(unsafe.Sizeof(*how)))
	return uintptr(fd), fetchError(fd)
}

// VersionInfo is a Go-friendly form of pathrs_version_info_t (struct).
type VersionInfo struct {
	VersionString string
}

// versionInfo is pathrs_version_info_t (struct).
type versionInfo C.pathrs_version_info_t

// Version is pathrs_version_info_t (sizeof(version) is passed automatically).
func Version() (*VersionInfo, error) {
	var rawVersion versionInfo
	size := C.pathrs_version((*C.pathrs_version_info_t)(&rawVersion), C.size_t(unsafe.Sizeof(rawVersion)))
	switch {
	case size < 0:
		return nil, fetchError(size)
	case size > 0:
		// TODO(log): Logging?
		fallthrough
	default:
		// TODO(log): Add a log statement if sizeof(rawVersion) is bigger than
		// the number of fields we store in VersionInfo. Otherwise a rebuild
		// will mask that Go callers cannot see any new fields.
		return &VersionInfo{
			VersionString: C.GoString(rawVersion.version_string),
		}, nil
	}
}
