--
-- SPDX-License-Identifier: BSD-2-Clause
--
-- Copyright (c) 2021-2024 SRI International
-- Copyright (c) 2024 Tyler Baxter <agge@FreeBSD.org>
-- Copyright (c) 2023 Warner Losh <imp@bsdimp.com>
-- Copyright (c) 2019 Kyle Evans <kevans@FreeBSD.org>
--

--
-- Code to read in the config file that drives this. Since we inherit from the
-- FreeBSD makesyscall.sh legacy, all config is done through a config file that
-- sets a number of variables (as noted below); it used to be a .sh file that
-- was sourced in. This dodges the need to write a command line parser.
--

local util = require("tools.util")

--
-- Global config map.
-- Default configuration is native. Any of these may get replaced by an
-- optionally specified configuration file.
--
local config = {
	sysnames = "syscalls.c",
	syshdr = "../sys/syscall.h",
	syshdr_extra = nil;
	sysmk = "/dev/null",
	syssw = "init_sysent.c",
	systrace = "systrace_args.c",
	sysproto = "../sys/sysproto.h",
	libsysmap = "/dev/null",
	libsys_h = "/dev/null",
	sysproto_h = "_SYS_SYSPROTO_H_",
	syscallprefix = "SYS_",
	switchname = "sysent",
	namesname = "syscallnames",
	abi_flags = {},
	abi_func_prefix = "",
	abi_type_suffix = "",
	abi_long = "long",
	abi_u_long = "u_long",
	abi_semid_t = "semid_t",
	abi_size_t = "size_t",
	abi_ptr_array_t = "",
	abi_headers = "",
	abi_intptr_t = "intptr_t",
	ptr_intptr_t_cast = "intptr_t",
	obsol = {},
	unimpl = {},
	compat_set = "native",
	mincompat = 0,
	-- System calls that require ABI-specific handling.
	syscall_abi_change = {},
	-- System calls that appear to require handling, but don't.
	syscall_no_abi_change = {},
	-- Keep track of modifications if there are.
	modifications = {},
	-- Stores compat_sets from syscalls.conf; config.mergeCompat()
	-- instantiates.
	compat_options = {},
}

--
-- For each entry, the ABI flag is the key. One may also optionally provide an
-- expr, which are contained in an array associated with each key; expr gets
-- applied to each argument type to indicate whether this argument is subject to
-- ABI change given the configured flags.
--
config.known_abi_flags = {
	long_size = {
		"_Contains[a-z_]*_long_",
		"^long [a-z0-9_]+$",
		"long [*]",
		"size_t [*]",
		-- semid_t is not included because it is only used
		-- as an argument or written out individually and
		-- said writes are handled by the ksem framework.
		-- Technically a sign-extension issue exists for
		-- arguments, but because semid_t is actually a file
		-- descriptor negative 32-bit values are invalid
		-- regardless of sign-extension.
	},
	time_t_size = {
		"_Contains[a-z_]*_timet_",
	},
	pointer_args = {
		-- no expr
	},
	pointer_size = {
		"_Contains[a-z_]*_ptr_",
		"[*][*]",
	},
	pair_64bit = {
		"^dev_t[ ]*$",
		"^id_t[ ]*$",
		"^off_t[ ]*$",
	},
}

-- All compat option entries should have five entries:
--	definition: The preprocessor macro that will be set for this.
--	compatlevel: The level this compatibility should be included at. This
--	    generally represents the version of FreeBSD that it is compatible 
--	    with, but ultimately it's just the level of mincompat in which it's
--	    included.
--	flag: The name of the flag in syscalls.master.
--	prefix: The prefix to use for _args and syscall prototype.  This will be
--	    used as-is, without "_" or any other character appended.
--	descr: The description of this compat option in init_sysent.c comments.
-- The special "stdcompat" entry will cause the other five to be autogenerated.
local compat_option_sets = {
	native = {
		{
			definition = "COMPAT_43",
			compatlevel = 3,
			flag = "COMPAT",
			prefix = "o",
			descr = "old",
		},
		{ stdcompat = "FREEBSD4" },
		{ stdcompat = "FREEBSD6" },
		{ stdcompat = "FREEBSD7" },
		{ stdcompat = "FREEBSD10" },
		{ stdcompat = "FREEBSD11" },
		{ stdcompat = "FREEBSD12" },
		{ stdcompat = "FREEBSD13" },
		{ stdcompat = "FREEBSD14" },
	},
}

--
-- config looks like a shell script; in fact, the previous makesyscalls.sh
-- script actually sourced it in.  It had a pretty common format, so we should
-- be fine to make various assumptions.
--
-- This function processes config to be merged into our global config map with
-- config.merge(). It aborts if there's malformed lines and returns NIL and a
-- message if no file was provided.
--
function config.process(file)
	local cfg = {}
	local comment_line_expr = "^%s*#.*"
	-- We capture any whitespace padding here so we can easily advance to
	-- the end of the line as needed to check for any trailing bogus bits.
	-- Alternatively, we could drop the whitespace and instead try to
	-- use a pattern to strip out the meaty part of the line, but then we
	-- would need to sanitize the line for potentially special characters.
	local line_expr = "^([%w%p]+%s*)=(%s*[`\"]?[^\"`]*[`\"]?)"

	if not file then
		return nil, "No file given"
	end

	local fh = assert(io.open(file))

	for nextline in fh:lines() do
		-- Strip any whole-line comments.
		nextline = nextline:gsub(comment_line_expr, "")
		-- Parse it into key, value pairs.
		local key, value = nextline:match(line_expr)
		if key ~= nil and value ~= nil then
			local kvp = key .. "=" .. value
			key = util.trim(key)
			value = util.trim(value)
			local delim = value:sub(1,1)
			if delim == '"' then
				local trailing_context

				-- Strip off the key/value part.
				trailing_context = nextline:sub(kvp:len() + 1)
				-- Strip off any trailing comment.
				trailing_context = trailing_context:gsub("#.*$",
				    "")
				-- Strip off leading/trailing whitespace.
				trailing_context = util.trim(trailing_context)
				if trailing_context ~= "" then
					print(trailing_context)
					util.abort(1,
					    "Malformed line: " .. nextline)
				end

				value = util.trim(value, delim)
			else
				-- Strip off potential comments.
				value = value:gsub("#.*$", "")
				-- Strip off any padding whitespace.
				value = util.trim(value)
				if value:match("%s") then
					util.abort(1,
					    "Malformed config line: " ..
					    nextline)
				end
			end
			cfg[key] = value
		elseif not nextline:match("^%s*$") then
			-- Make sure format violations don't get overlooked
			-- here, but ignore blank lines.  Comments are already
			-- stripped above.
			util.abort(1, "Malformed config line: " .. nextline)
		end
	end

	assert(fh:close())
	return cfg
end

-- Merges processed configuration file into the global config map (see above),
-- or returns NIL and a message if no file was provided.
function config.merge(fh)
	if not fh then
		return nil, "No file given"
	end

	local res = assert(config.process(fh))

	for k, v in pairs(res) do
		if v ~= config[k] then
			-- Handling of string lists:
			if k:find("abi_flags") then
				-- Match for pipe, that's how abi_flags
				-- is formatted.
				config[k] = util.setFromString(v, "[^|]+")
			elseif k:find("syscall_abi_change") or
			    k:find("syscall_no_abi_change") or
			    k:find("obsol") or
			    k:find("unimpl") then
				-- Match for space, that's how these
				-- are formatted.
				config[k] = util.setFromString(v, "[^ ]+")
			else
				config[k] = v
			end
			-- Construct config modified table as config
			-- is processed.
			config.modifications[k] = true
		else
			-- config wasn't modified.
			config.modifications[k] = false
		end
	end
end

-- Returns TRUE if there are ABI changes from native for the provided ABI flag.
function config.abiChanges(name)
	if config.known_abi_flags[name] == nil then
		util.abort(1, "abi_changes: unknown flag: " .. name)
	end
	return config.abi_flags[name] ~= nil
end

-- Instantiates config.compat_options.
function config.mergeCompat()
	if config.compat_set ~= "" then
		if not compat_option_sets[config.compat_set] then
			util.abort(1, "Undefined compat set: " ..
			    config.compat_set)
		end

		config.compat_options = compat_option_sets[config.compat_set]
	end
end

return config
