Verified Commit b93311ab authored by Karel Koci's avatar Karel Koci 🤘

Add syscnf module and drop some config variables

Incompatible changes:
* --model and --board options were dropped for both pkgupdate and
  pkgtransaction

Following variables were dropped from updater configuration scripts:
* board_name, model: this should be replaced by distribution root script
* serial: same case like board_name and model
* architectures: replaced by LEDE_ARCH of os_release

New variables were introduced as a partial replacement:
* os_release: target system etc/os-release content
* host_os_release: host system /etc/os-release content
parent 8fd3e47a
......@@ -9,7 +9,7 @@ RUN \
busybox ca-certificates curl git \
make pkg-config gcc \
check cppcheck lua-check valgrind \
libcurl4-openssl-dev libevent-dev libssl-dev \
libcurl4-openssl-dev libevent-dev libssl-dev uthash-dev \
lua5.1 liblua5.1-0-dev \
asciidoc lcov markdown libcommon-sense-perl \
wget procps && \
......
......@@ -11,7 +11,7 @@ __pycache__
# The desired results
*.html
*.so
/src/opkg-trans/opkg-trans
/src/pkgtransaction/pkgtransaction
/src/pkgupdate/pkgupdate
/src/migrator/pkgmigrate
# Some stuff for debugging
......
UPDATER_VERSION := $(shell (git describe --match 'v*' --dirty || echo 'unknown') | sed -e 's/^v//')
LUA_NAME := $(shell for lua in lua5.1 lua-5.1 lua51 lua ; do if pkg-config $$lua ; then echo $$lua ; break ; fi ; done)
VALGRIND:=IN_VALGRIND=1 valgrind --leak-check=full --show-leak-kinds=all --track-fds=yes --trace-children=no --child-silent-after-fork=yes --error-exitcode=1 --track-origins=yes
VALGRIND:=IN_VALGRIND=1 valgrind --leak-check=full --show-leak-kinds=definite,indirect,possible --track-fds=yes --trace-children=no --child-silent-after-fork=yes --error-exitcode=1 --track-origins=yes
# For picosat, it otherwise needs some headers not available on musl for a feature we don't need. And we need trace enabled.
EXTRA_DEFINES := NGETRUSAGE TRACE UPDATER_VERSION='"$(UPDATER_VERSION)"'
ifdef BUSYBOX_EXEC
......
......@@ -598,34 +598,6 @@ root_dir
Root directory specified from command line or `/` if no such option
was specified. Use this if you are accessing some files.
serial
~~~~~~
The variable contains the serial number of the device. It may be `nil`
in case it is not supported on the given device.
architectures
~~~~~~~~~~~~~
Allowed package architectures (in a table).
model
~~~~~
Content of `/tmp/sysinfo/model`. On non-OpenWRT systems it has to be supplied by
`--model` argument.
board_name
~~~~~~~~~~
Content of `/tmp/sysinfo/board_name`. On non-OpenWRT systems it has to be supplied
by `--board` argument.
turris_version
~~~~~~~~~~~~~~
Content of `/etc/turris-version`. Might be nil on non-Turris systems.
self_version
~~~~~~~~~~~~
......@@ -683,6 +655,32 @@ The top-level table is instantiated (not generated through
meta-tables), therefore it is possible to get the list of installed
packages.
os_release
~~~~~~~~~~
This is table with parsed content of os-release file. Path to this file is
`etc/os-release` but relative to target root. This means that if you are running
updater on root file system that is not current root then values in this table are
for target not for host system.
This is normal table and you can iterate trough it using `pairs` or you can
directly access specific value by indexing it. List of standard options can be
found https://www.freedesktop.org/software/systemd/man/os-release.html[here].
The most interesting value is `os_release.VERSION` as this contains current system
release version.
This table can be empty if there was no `os-release` file.
host_os_release
~~~~~~~~~~~~~~~
This is table with parsed content of os-release file for host system. Source file
for this is always `/etc/os-release`. See variable os_release for example usage
and expected content.
Table can be empty if there was no `/etc/os-release`.
Export variables to Script
--------------------------
......
......@@ -30,6 +30,7 @@ libupdater_MODULES := \
locks \
picosat \
util \
syscnf \
logging
ifdef COV
libupdater_MODULES += lcoverage.embed
......
......@@ -77,10 +77,6 @@ static const char *opt_help[COT_LAST] = {
"--exclude=<name> Exclude this from output.\n",
[COT_USIGN] =
"--usign=<path> Path to usign tool used to verify packages signature. In default /usr/bin/usign.\n",
[COT_MODEL] =
"--model=<model> Set/override target system model (e.g. Turris Omnia)\n",
[COT_BOARD] =
"--board=<board> Set/override target system board (e.g. rtrom01)\n",
[COT_NO_REPLAN] =
"--no-replan Don't replan. Install everyting at once. Use this if updater you are running isn't from packages it installs.\n",
[COT_NO_IMMEDIATE_REBOOT] =
......@@ -102,8 +98,6 @@ enum option_val {
OPT_TASK_LOG_VAL,
OPT_EXCLUDE,
OPT_USIGN,
OPT_MODEL,
OPT_BOARD,
OPT_NO_REPLAN,
OPT_NO_IMMEDIATE_REBOOT,
OPT_OUT_OF_ROOT,
......@@ -127,8 +121,6 @@ static const struct option opt_long[] = {
{ .name = "task-log", .has_arg = required_argument, .val = OPT_TASK_LOG_VAL },
{ .name = "exclude", .has_arg = required_argument, .val = OPT_EXCLUDE },
{ .name = "usign", .has_arg = required_argument, .val = OPT_USIGN },
{ .name = "model", .has_arg = required_argument, .val = OPT_MODEL },
{ .name = "board", .has_arg = required_argument, .val = OPT_BOARD },
{ .name = "no-replan", .has_arg = no_argument, .val = OPT_NO_REPLAN },
{ .name = "no-immediate-reboot", .has_arg = no_argument, .val = OPT_NO_IMMEDIATE_REBOOT },
{ .name = "out-of-root", .has_arg = no_argument, .val = OPT_OUT_OF_ROOT },
......@@ -154,8 +146,6 @@ static const struct simple_opt {
[OPT_TASK_LOG_VAL] = { COT_TASK_LOG, true, true },
[OPT_EXCLUDE] = { COT_EXCLUDE, true, true },
[OPT_USIGN] = { COT_USIGN, true, true },
[OPT_MODEL] = { COT_MODEL, true, true },
[OPT_BOARD] = { COT_BOARD, true, true },
[OPT_NO_REPLAN] = { COT_NO_REPLAN, false, true },
[OPT_NO_IMMEDIATE_REBOOT] = { COT_NO_IMMEDIATE_REBOOT, false, true },
[OPT_OUT_OF_ROOT] = { COT_OUT_OF_ROOT, false, false },
......@@ -276,8 +266,6 @@ struct cmd_op *cmd_args_parse(int argc, char *argv[], const enum cmd_op_type acc
case COT_APPROVE:
case COT_EXCLUDE:
case COT_USIGN:
case COT_MODEL:
case COT_BOARD:
case COT_NO_REPLAN:
case COT_TASK_LOG: {
struct cmd_op tmp = result[i];
......
......@@ -68,10 +68,6 @@ enum cmd_op_type {
COT_EXCLUDE,
// Path to usign tool
COT_USIGN,
// Target model specification
COT_MODEL,
// Target board specification
COT_BOARD,
// Don't replan (do whole install at once)
COT_NO_REPLAN,
// Don't immediatelly reboot system
......
--[[
Copyright 2018, CZ.NIC z.s.p.o. (http://www.nic.cz/)
This file is part of the turris updater.
Updater is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Updater is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Updater. If not, see <http://www.gnu.org/licenses/>.
]]--
local os = os
local utils = require "utils"
local getcwd = getcwd
local DIE = DIE
module "syscnf"
-- Variables accessed from outside of this module
-- luacheck: globals root_dir status_file info_dir pkg_download_dir pkg_unpacked_dir dir_opkg_collided target_model target_board
-- Functions that we want to access from outside of this module
-- luacheck: globals set_root_dir set_target
local status_file_suffix = "usr/lib/opkg/status"
local info_dir_suffix = "usr/lib/opkg/info/"
local pkg_unpacked_dir_suffix = "usr/share/updater/unpacked/"
local pkg_download_dir_suffix = "usr/share/updater/download/"
local dir_opkg_collided_suffix = "usr/share/updater/collided/"
--[[
Canonizes path to absolute path. It does no change in case path is already an
absolute but it if not then it prepends current working directory. There is also
special handling in case path starts with tilde (~) in that case that character is
replaced with content of HOME environment variable.
]]
local function path2abspath(path)
if path:match("^/") then
return path
elseif path:match("^~/") then
return os.getenv('HOME') .. "/" .. path
else
return getcwd() .. "/" .. path
end
end
--[[
Set all the configurable directories to be inside the provided dir
Effectively sets that the whole system is mounted under some
prefix.
]]
function set_root_dir(dir)
if dir then
dir = (path2abspath(dir) .. "/"):gsub("/+", "/")
else
dir = "/"
end
-- A root directory
root_dir = dir
-- The file with status of installed packages
status_file = dir .. status_file_suffix
-- The directory where unpacked control files of the packages live
info_dir = dir .. info_dir_suffix
-- A directory to which we download packages
pkg_download_dir = dir .. pkg_download_dir_suffix
-- A directory where unpacked packages live
pkg_unpacked_dir = dir .. pkg_unpacked_dir_suffix
-- Directory where we move files and directories that weren't part of any package.
dir_opkg_collided = dir .. dir_opkg_collided_suffix
end
--[[
Set variables taget_model and target_board.
You can explicitly specify model or board or both. If not specified then detection
is performed. That is files from /tmp/sysinfo directory are used.
If no model or board is specified (passed as nil) and detection failed than this
function causes error and execution termination.
]]
function set_target(model, board)
-- Name of the target model (ex: Turris Omnia)
target_model = model or utils.strip(utils.read_file('/tmp/sysinfo/model'))
-- Name of the target board (ex: rtrom01)
target_board = board or utils.strip(utils.read_file('/tmp/sysinfo/board_name'))
if not target_model or not target_board then
DIE("Auto detection of target model or board failed.You can specify them " ..
"explicitly using --model and --board arguments.")
end
end
......@@ -763,13 +763,13 @@ function steal_configs(current_status, installed_confs, configs)
end
--[[
Move anything on given path to dir_opkg_collided. This backups and removes original files.
Move anything on given path to opkg_collided_dir. This backups and removes original files.
When keep is set to true, file is copied instead of moved.
]]
function user_path_move(path, keep)
-- At first create same parent directory relative to dir_opkg_collided
-- At first create same parent directory relative to opkg_collided_dir
local fpath = ""
for dir in (syscnf.dir_opkg_collided .. path):gsub("[^/]*/?$", ""):gmatch("[^/]+") do
for dir in (syscnf.opkg_collided_dir .. path):gsub("[^/]*/?$", ""):gmatch("[^/]+") do
local randex = ""
while not utils.dir_ensure(fpath .. "/" .. dir .. randex) do
-- If there is file with same name, then append some random extension
......
......@@ -33,8 +33,6 @@ local tostring = tostring
local error = error
local WARN = WARN
local ERROR = ERROR
local run_command = run_command
local events_wait = events_wait
local get_updater_version = get_updater_version
local utils = require "utils"
local backend = require "backend"
......@@ -120,12 +118,11 @@ function load_state_vars()
]]
state_vars = {
root_dir = syscnf.root_dir,
model = syscnf.target_model,
board_name = syscnf.target_board,
turris_version = utils.strip(utils.read_file('/etc/turris-version')),
self_version = get_updater_version(),
language_version = 1,
features = updater_features,
os_release = syscnf.os_release(),
host_os_release = syscnf.host_os_release(),
--[[
In case we fail to read that file (it is not there), we match against
an empty string, which produces nil ‒ the element won't be in there.
......@@ -147,11 +144,6 @@ function load_state_vars()
end
end)
}
events_wait(run_command(function (ecode, _, stdout, _)
if ecode == 0 then
state_vars.serial = utils.strip(stdout)
end
end, nil, nil, -1, -1, '/usr/bin/atsha204cmd', 'serial-number'))
end
......
......@@ -25,6 +25,7 @@
#include "journal.h"
#include "locks.h"
#include "arguments.h"
#include "syscnf.h"
#include "picosat.h"
#include <lua.h>
......@@ -1020,6 +1021,7 @@ struct interpreter *interpreter_create(struct events *events) {
// Some binary embedded modules
journal_mod_init(L);
locks_mod_init(L);
syscnf_mod_init(L);
picosat_mod_init(L);
#ifdef COVERAGE
interpreter_load_coverage(result);
......
/*
* Copyright 2019, CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This file is part of the Turris Updater.
*
* Updater is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Updater is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Updater. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "syscnf.h"
#include "util.h"
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include <regex.h>
#include <uthash.h>
#include <lauxlib.h>
#include <lualib.h>
#include "logging.h"
#include "inject.h"
enum e_paths {
P_ROOT_DIR,
P_FILE_STATUS,
P_DIR_INFO,
P_DIR_PKG_UNPACKED,
P_DIR_PKG_DOWNLOAD,
P_DIR_OPKG_COLLIDED,
P_LAST
};
static const char* const default_paths[] = {
[P_ROOT_DIR] = "/",
[P_FILE_STATUS] = "/usr/lib/opkg/status",
[P_DIR_INFO] = "/usr/lib/opkg/info/",
[P_DIR_PKG_UNPACKED] = "/usr/share/updater/unpacked/",
[P_DIR_PKG_DOWNLOAD] = "/usr/share/updater/download/",
[P_DIR_OPKG_COLLIDED] = "/usr/share/updater/collided/",
};
static char* paths[] = {
[P_ROOT_DIR] = NULL,
[P_FILE_STATUS] = NULL,
[P_DIR_INFO] = NULL,
[P_DIR_PKG_UNPACKED] = NULL,
[P_DIR_PKG_DOWNLOAD] = NULL,
[P_DIR_OPKG_COLLIDED] = NULL,
};
struct os_release_data {
char *field;
char *content;
UT_hash_handle hh;
};
static struct os_release_data *osr = NULL;
static struct os_release_data *osr_host = NULL;
void set_path(enum e_paths tp, const char *value) {
if (paths[tp])
free(paths[tp]);
if (value)
asprintf(&paths[tp], "%s%s", value, default_paths[tp]);
else
paths[tp] = NULL;
}
void set_root_dir(const char *root) {
char *pth = NULL;
if (root) {
if (root[0] == '/')
pth = aprintf("%s", root);
else if (root[0] == '~' && root[1] == '/') {
struct passwd *pw = getpwuid(getuid());
pth = aprintf("%s%s", pw->pw_dir, root + 1);
} else {
char *cwd = getcwd(NULL, 0);
pth = aprintf("%s/%s", cwd, root);
free(cwd);
}
size_t last = strlen(pth) - 1;
while (last > 0 && pth[last] == '/')
pth[last--] = '\0';
}
set_path(P_ROOT_DIR, pth);
set_path(P_FILE_STATUS, pth);
set_path(P_DIR_INFO, pth);
set_path(P_DIR_PKG_UNPACKED, pth);
set_path(P_DIR_PKG_DOWNLOAD, pth);
set_path(P_DIR_OPKG_COLLIDED, pth);
TRACE("Target root directory set to: %s", root_dir());
}
static struct os_release_data *read_os_release(const char *path) {
FILE *f = fopen(path, "r");
if (!f) {
ERROR("Unable to open os-release (%s): %s", path, strerror(errno));
return NULL;
}
TRACE("Parsing os-release: %s", path);
struct os_release_data *osr_dt = NULL;
regex_t rgex;
ASSERT(!regcomp(&rgex, "^([^=]*)=(\"?)(.*)\\2$", REG_NEWLINE | REG_EXTENDED));
regmatch_t match[4];
char *line = NULL;
size_t linel = 0;
while (getline(&line, &linel, f) != -1) {
if (regexec(&rgex, line, 4, match, 0) == REG_NOMATCH) {
ERROR("Unable to parse os-release (%s) line: %.*s", path, (int)strlen(line) - 1, line);
} else {
struct os_release_data *n = malloc(sizeof *n);
n->field = strndup(&line[match[1].rm_so], match[1].rm_eo - match[1].rm_so);
n->content = strndup(&line[match[3].rm_so], match[3].rm_eo - match[3].rm_so);
HASH_ADD_KEYPTR(hh, osr_dt, n->field, strlen(n->field), n);
TRACE("Parsed os-release (%s): %s=\"%s\"", path, n->field, n->content);
}
}
free(line);
regfree(&rgex);
fclose(f);
return osr_dt;
}
static void os_release_free(struct os_release_data *dt) {
struct os_release_data *w, *tmp;
HASH_ITER(hh, dt, w, tmp) {
HASH_DEL(dt, w);
free(w->field);
free(w->content);
free(w);
}
}
void system_detect() {
if (osr == osr_host)
osr = NULL;
os_release_free(osr_host);
os_release_free(osr);
osr_host = NULL;
osr = NULL;
osr_host = read_os_release("/etc/os-release");
if (root_dir_is_root()) {
TRACE("Detecting system: native run");
osr = osr_host;
} else {
TRACE("Detecting system: out of root run");
osr = read_os_release(aprintf("%setc/os-release", root_dir()));
}
}
static const char *os_release_get(struct os_release_data *dt, const char *option) {
struct os_release_data *w = NULL;
HASH_FIND_STR(dt, option, w);
if (!w)
return NULL;
return w->content;
}
const char *os_release(const char *option) {
return os_release_get(osr, option);
}
const char *host_os_release(const char *option) {
return os_release_get(osr_host, option);
}
static const char *get_path(enum e_paths tp) {
if (paths[tp])
return paths[tp];
return default_paths[tp];
}
const char *root_dir() {
return get_path(P_ROOT_DIR);
}
const char *status_file() {
return get_path(P_FILE_STATUS);
}
const char *info_dir() {
return get_path(P_DIR_INFO);
}
const char *pkg_unpacked_dir() {
return get_path(P_DIR_PKG_UNPACKED);
}
const char *pkg_download_dir() {
return get_path(P_DIR_PKG_DOWNLOAD);
}
const char *opkg_collided_dir() {
return get_path(P_DIR_OPKG_COLLIDED);
}
bool root_dir_is_root() {
return !strcmp("/", root_dir());
}
static int lua_set_root_dir(lua_State *L) {
if (lua_isnoneornil(L, 1))
set_root_dir(NULL);
else
set_root_dir(luaL_checkstring(L, 1));
return 0;
}
static int lua_system_detect(lua_State *L __attribute__((unused))) {
system_detect();
return 0;
}
static int lua_os_release_gen(lua_State *L, struct os_release_data *dt) {
lua_newtable(L);
struct os_release_data *w, *tmp;
HASH_ITER(hh, dt, w, tmp) {
lua_pushstring(L, w->field);
lua_pushstring(L, w->content);
lua_settable(L, -3);
}
return 1;
}
static int lua_os_release(lua_State *L) {
return lua_os_release_gen(L, osr);
}
static int lua_host_os_release(lua_State *L) {
return lua_os_release_gen(L, osr_host);
}
static int lua_syscnf_index(lua_State *L) {
const char *idx = luaL_checkstring(L, 2);
if (!strcmp("root_dir", idx))
lua_pushstring(L, root_dir());
else if (!strcmp("status_file", idx))
lua_pushstring(L, status_file());
else if (!strcmp("info_dir", idx))
lua_pushstring(L, info_dir());
else if (!strcmp("pkg_unpacked_dir", idx))
lua_pushstring(L, pkg_unpacked_dir());
else if (!strcmp("pkg_download_dir", idx))
lua_pushstring(L, pkg_download_dir());
else if (!strcmp("opkg_collided_dir", idx))
lua_pushstring(L, opkg_collided_dir());
else if (luaL_getmetafield(L, 1, idx) == 0)
lua_pushnil(L);
return 1;
}
static const struct inject_func funcs[] = {
{ lua_set_root_dir, "set_root_dir" },
{ lua_system_detect, "system_detect" },
{ lua_os_release, "os_release" },
{ lua_host_os_release, "host_os_release" },
{ lua_syscnf_index, "__index" },
};
void syscnf_mod_init(lua_State *L) {
TRACE("Syscnf module init");
lua_newtable(L);
inject_func_n(L, "syscnf", funcs, sizeof funcs / sizeof *funcs);
lua_pushvalue(L, -1);
lua_setmetatable(L, -2);
inject_module(L, "syscnf");
}
/*
* Copyright 2019, CZ.NIC z.s.p.o. (http://www.nic.cz/)
*
* This file is part of the Turris Updater.
*
* Updater is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Updater is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Updater. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef UPDATER_SYSCNF_H
#define UPDATER_SYSCNF_H
#include <lua.h>
#include <stdbool.h>
//// Setting calls ////
// Note: for correct approach you should first set root_dir and then detect system
// Modify root directory
void set_root_dir(const char*);
// Parses different system files and fills internal variables
// Note: detection considers root_dir so you should set it before calling this.
void system_detect();
//// Getting calls ////
// System os-release values
#define OS_RELEASE_NAME "NAME"
#define OS_RELEASE_VERSION "VERSION"
#define OS_RELEASE_ID "ID"
#define OS_RELEASE_PRETTY_NAME "PRETTY_NAME"
// This returns field as read from etc/os-release relative to root_dir
const char *os_release(const char *option) __attribute__((nonnull));
// This returns field as read from /etc/os-release
const char *host_os_release(const char *option) __attribute__((nonnull));
// Root directory of update system
// This never returns NULL and always contains trailing slash if it is a directory
const char *root_dir();
// Updater specific paths
const char *status_file();
const char *info_dir();
const char *pkg_unpacked_dir();
const char *pkg_download_dir();
const char *opkg_collided_dir();
// Returns true if root_dir() is "/", otherwise false.
bool root_dir_is_root();
// Create syscnf module and inject it into the lua state
void syscnf_mod_init(lua_State *L) __attribute__((nonnull));
#endif
......@@ -29,6 +29,7 @@
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <signal.h>
#include <poll.h>
......@@ -58,6 +59,15 @@ char *readfile(const char *file) {
return ret;
}
bool statfile(const char *file, int mode) {
struct stat st;
if (stat(file, &st))
return false;
if (!S_ISREG(st.st_mode))
return false;
return !access(file, mode);
}
static int exec_dir_filter(const struct dirent *de) {
// ignore system paths and accept only files
return strcmp(de->d_name, ".") && strcmp(de->d_name, "..") && de->d_type == DT_REG;
......
......@@ -26,6 +26,7 @@
#include <stdio.h>
#include <stdbool.h>
#include <alloca.h>
#include <unistd.h>
// Writes given text to file. Be aware that no information about failure is given.
bool dump2file (const char *file, const char *text) __attribute__((nonnull,nonnull));
......@@ -34,6 +35,10 @@ bool dump2file (const char *file, const char *text) __attribute__((nonnull,nonnu
// Returned memory has to be freed by used.
char *readfile(const char *file) __attribute__((nonnull));
// Returns true if file exists and is accessible in given mode
// Mode is bitwise OR of one or more of R_OK, W_OK, and X_OK.
bool statfile(const char *file, int mode);