Merge pull request #26663 from poettering/vpick

add new "vpick" concept for automatically picking newest resource from .v/ dir containing versioned files
This commit is contained in:
Lennart Poettering
2024-01-03 22:17:32 +01:00
committed by GitHub
30 changed files with 2401 additions and 128 deletions
+22
View File
@@ -516,3 +516,25 @@ const char* inode_type_to_string(mode_t m) {
return NULL;
}
mode_t inode_type_from_string(const char *s) {
if (!s)
return MODE_INVALID;
if (streq(s, "reg"))
return S_IFREG;
if (streq(s, "dir"))
return S_IFDIR;
if (streq(s, "lnk"))
return S_IFLNK;
if (streq(s, "chr"))
return S_IFCHR;
if (streq(s, "blk"))
return S_IFBLK;
if (streq(s, "fifo"))
return S_IFIFO;
if (streq(s, "sock"))
return S_IFSOCK;
return MODE_INVALID;
}
+1
View File
@@ -114,3 +114,4 @@ int inode_compare_func(const struct stat *a, const struct stat *b);
extern const struct hash_ops inode_hash_ops;
const char* inode_type_to_string(mode_t m);
mode_t inode_type_from_string(const char *s);
+23
View File
@@ -1519,3 +1519,26 @@ ssize_t strlevenshtein(const char *x, const char *y) {
return t1[yl];
}
char *strrstr(const char *haystack, const char *needle) {
const char *f = NULL;
size_t l;
/* Like strstr() but returns the last rather than the first occurence of "needle" in "haystack". */
if (!haystack || !needle)
return NULL;
l = strlen(needle);
/* Special case: for the empty string we return the very last possible occurence, i.e. *after* the
* last char, not before. */
if (l == 0)
return strchr(haystack, 0);
for (const char *p = haystack; *p; p++)
if (strncmp(p, needle, l) == 0)
f = p;
return (char*) f;
}
+2
View File
@@ -322,3 +322,5 @@ static inline int strdup_or_null(const char *s, char **ret) {
*ret = c;
return 1;
}
char *strrstr(const char *haystack, const char *needle);
+10
View File
@@ -905,3 +905,13 @@ int _string_strv_ordered_hashmap_put(OrderedHashmap **h, const char *key, const
}
DEFINE_HASH_OPS_FULL(string_strv_hash_ops, char, string_hash_func, string_compare_func, free, char*, strv_free);
char* strv_endswith(const char *s, char **l) {
STRV_FOREACH(i, l) {
char *e = endswith(s, *i);
if (e)
return (char*) e;
}
return NULL;
}
+2
View File
@@ -252,3 +252,5 @@ int _string_strv_hashmap_put(Hashmap **h, const char *key, const char *value HA
int _string_strv_ordered_hashmap_put(OrderedHashmap **h, const char *key, const char *value HASHMAP_DEBUG_PARAMS);
#define string_strv_hashmap_put(h, k, v) _string_strv_hashmap_put(h, k, v HASHMAP_DEBUG_SRC_ARGS)
#define string_strv_ordered_hashmap_put(h, k, v) _string_strv_ordered_hashmap_put(h, k, v HASHMAP_DEBUG_SRC_ARGS)
char* strv_endswith(const char *s, char **l);
+128 -26
View File
@@ -59,6 +59,7 @@
#include "strv.h"
#include "terminal-util.h"
#include "utmp-wtmp.h"
#include "vpick.h"
#define IDLE_TIMEOUT_USEC (5*USEC_PER_SEC)
#define IDLE_TIMEOUT2_USEC (1*USEC_PER_SEC)
@@ -2859,13 +2860,33 @@ static bool insist_on_sandboxing(
return false;
}
static int setup_ephemeral(const ExecContext *context, ExecRuntime *runtime) {
static int setup_ephemeral(
const ExecContext *context,
ExecRuntime *runtime,
char **root_image, /* both input and output! modified if ephemeral logic enabled */
char **root_directory) { /* ditto */
_cleanup_close_ int fd = -EBADF;
_cleanup_free_ char *new_root = NULL;
int r;
assert(context);
assert(root_image);
assert(root_directory);
if (!*root_image && !*root_directory)
return 0;
if (!runtime || !runtime->ephemeral_copy)
return 0;
assert(runtime->ephemeral_storage_socket[0] >= 0);
assert(runtime->ephemeral_storage_socket[1] >= 0);
new_root = strdup(runtime->ephemeral_copy);
if (!new_root)
return log_oom_debug();
r = posix_lock(runtime->ephemeral_storage_socket[0], LOCK_EX);
if (r < 0)
return log_debug_errno(r, "Failed to lock ephemeral storage socket: %m");
@@ -2876,28 +2897,23 @@ static int setup_ephemeral(const ExecContext *context, ExecRuntime *runtime) {
if (fd >= 0)
/* We got an fd! That means ephemeral has already been set up, so nothing to do here. */
return 0;
if (fd != -EAGAIN)
return log_debug_errno(fd, "Failed to receive file descriptor queued on ephemeral storage socket: %m");
log_debug("Making ephemeral snapshot of %s to %s",
context->root_image ?: context->root_directory, runtime->ephemeral_copy);
if (*root_image) {
log_debug("Making ephemeral copy of %s to %s", *root_image, new_root);
if (context->root_image)
fd = copy_file(context->root_image, runtime->ephemeral_copy, O_EXCL, 0600,
COPY_LOCK_BSD|COPY_REFLINK|COPY_CRTIME);
else
fd = btrfs_subvol_snapshot_at(AT_FDCWD, context->root_directory,
AT_FDCWD, runtime->ephemeral_copy,
BTRFS_SNAPSHOT_FALLBACK_COPY |
BTRFS_SNAPSHOT_FALLBACK_DIRECTORY |
BTRFS_SNAPSHOT_RECURSIVE |
BTRFS_SNAPSHOT_LOCK_BSD);
if (fd < 0)
return log_debug_errno(fd, "Failed to snapshot %s to %s: %m",
context->root_image ?: context->root_directory, runtime->ephemeral_copy);
fd = copy_file(*root_image,
new_root,
O_EXCL,
0600,
COPY_LOCK_BSD|
COPY_REFLINK|
COPY_CRTIME);
if (fd < 0)
return log_debug_errno(fd, "Failed to copy image %s to %s: %m",
*root_image, new_root);
if (context->root_image) {
/* A root image might be subject to lots of random writes so let's try to disable COW on it
* which tends to not perform well in combination with lots of random writes.
*
@@ -2906,13 +2922,35 @@ static int setup_ephemeral(const ExecContext *context, ExecRuntime *runtime) {
*/
r = chattr_fd(fd, FS_NOCOW_FL, FS_NOCOW_FL, NULL);
if (r < 0)
log_debug_errno(fd, "Failed to disable copy-on-write for %s, ignoring: %m", runtime->ephemeral_copy);
log_debug_errno(fd, "Failed to disable copy-on-write for %s, ignoring: %m", new_root);
} else {
assert(*root_directory);
log_debug("Making ephemeral snapshot of %s to %s", *root_directory, new_root);
fd = btrfs_subvol_snapshot_at(
AT_FDCWD, *root_directory,
AT_FDCWD, new_root,
BTRFS_SNAPSHOT_FALLBACK_COPY |
BTRFS_SNAPSHOT_FALLBACK_DIRECTORY |
BTRFS_SNAPSHOT_RECURSIVE |
BTRFS_SNAPSHOT_LOCK_BSD);
if (fd < 0)
return log_debug_errno(fd, "Failed to snapshot directory %s to %s: %m",
*root_directory, new_root);
}
r = send_one_fd(runtime->ephemeral_storage_socket[1], fd, MSG_DONTWAIT);
if (r < 0)
return log_debug_errno(r, "Failed to queue file descriptor on ephemeral storage socket: %m");
if (*root_image)
free_and_replace(*root_image, new_root);
else {
assert(*root_directory);
free_and_replace(*root_directory, new_root);
}
return 1;
}
@@ -2972,6 +3010,63 @@ static int verity_settings_prepare(
return 0;
}
static int pick_versions(
const ExecContext *context,
const ExecParameters *params,
char **ret_root_image,
char **ret_root_directory) {
int r;
assert(context);
assert(params);
assert(ret_root_image);
assert(ret_root_directory);
if (context->root_image) {
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
r = path_pick(/* toplevel_path= */ NULL,
/* toplevel_fd= */ AT_FDCWD,
context->root_image,
&pick_filter_image_raw,
PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE,
&result);
if (r < 0)
return r;
if (!result.path)
return log_exec_debug_errno(context, params, SYNTHETIC_ERRNO(ENOENT), "No matching entry in .v/ directory %s found.", context->root_image);
*ret_root_image = TAKE_PTR(result.path);
*ret_root_directory = NULL;
return r;
}
if (context->root_directory) {
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
r = path_pick(/* toplevel_path= */ NULL,
/* toplevel_fd= */ AT_FDCWD,
context->root_directory,
&pick_filter_image_dir,
PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE,
&result);
if (r < 0)
return r;
if (!result.path)
return log_exec_debug_errno(context, params, SYNTHETIC_ERRNO(ENOENT), "No matching entry in .v/ directory %s found.", context->root_directory);
*ret_root_image = NULL;
*ret_root_directory = TAKE_PTR(result.path);
return r;
}
*ret_root_image = *ret_root_directory = NULL;
return 0;
}
static int apply_mount_namespace(
ExecCommandFlags command_flags,
const ExecContext *context,
@@ -2984,8 +3079,8 @@ static int apply_mount_namespace(
_cleanup_strv_free_ char **empty_directories = NULL, **symlinks = NULL,
**read_write_paths_cleanup = NULL;
_cleanup_free_ char *creds_path = NULL, *incoming_dir = NULL, *propagate_dir = NULL,
*extension_dir = NULL, *host_os_release_stage = NULL;
const char *root_dir = NULL, *root_image = NULL, *tmp_dir = NULL, *var_tmp_dir = NULL;
*extension_dir = NULL, *host_os_release_stage = NULL, *root_image = NULL, *root_dir = NULL;
const char *tmp_dir = NULL, *var_tmp_dir = NULL;
char **read_write_paths;
bool needs_sandboxing, setup_os_release_symlink;
BindMount *bind_mounts = NULL;
@@ -2997,14 +3092,21 @@ static int apply_mount_namespace(
CLEANUP_ARRAY(bind_mounts, n_bind_mounts, bind_mount_free_many);
if (params->flags & EXEC_APPLY_CHROOT) {
r = setup_ephemeral(context, runtime);
r = pick_versions(
context,
params,
&root_image,
&root_dir);
if (r < 0)
return r;
if (context->root_image)
root_image = (runtime ? runtime->ephemeral_copy : NULL) ?: context->root_image;
else
root_dir = (runtime ? runtime->ephemeral_copy : NULL) ?: context->root_directory;
r = setup_ephemeral(
context,
runtime,
&root_image,
&root_dir);
if (r < 0)
return r;
}
r = compile_bind_mounts(context, params, &bind_mounts, &n_bind_mounts, &empty_directories);
+11
View File
@@ -48,6 +48,7 @@
#include "tmpfile-util.h"
#include "uid-alloc-range.h"
#include "user-util.h"
#include "vpick.h"
static enum {
ACTION_DISSECT,
@@ -1817,6 +1818,16 @@ static int run(int argc, char *argv[]) {
if (r <= 0)
return r;
if (arg_image) {
r = path_pick_update_warn(
&arg_image,
&pick_filter_image_raw,
PICK_ARCHITECTURE|PICK_TRIES,
/* ret_result= */ NULL);
if (r < 0)
return r;
}
switch (arg_action) {
case ACTION_UMOUNT:
return action_umount(arg_path);
+66 -3
View File
@@ -112,6 +112,7 @@
#include "umask-util.h"
#include "unit-name.h"
#include "user-util.h"
#include "vpick.h"
/* The notify socket inside the container it can use to talk to nspawn using the sd_notify(3) protocol */
#define NSPAWN_NOTIFY_SOCKET_PATH "/run/host/notify"
@@ -2911,14 +2912,72 @@ static int on_request_stop(sd_bus_message *m, void *userdata, sd_bus_error *erro
return 0;
}
static int pick_paths(void) {
int r;
if (arg_directory) {
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
PickFilter filter = pick_filter_image_dir;
filter.architecture = arg_architecture;
r = path_pick_update_warn(
&arg_directory,
&filter,
PICK_ARCHITECTURE|PICK_TRIES,
&result);
if (r < 0) {
/* Accept ENOENT here so that the --template= logic can work */
if (r != -ENOENT)
return r;
} else
arg_architecture = result.architecture;
}
if (arg_image) {
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
PickFilter filter = pick_filter_image_raw;
filter.architecture = arg_architecture;
r = path_pick_update_warn(
&arg_image,
&filter,
PICK_ARCHITECTURE|PICK_TRIES,
&result);
if (r < 0)
return r;
arg_architecture = result.architecture;
}
if (arg_template) {
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
PickFilter filter = pick_filter_image_dir;
filter.architecture = arg_architecture;
r = path_pick_update_warn(
&arg_template,
&filter,
PICK_ARCHITECTURE,
&result);
if (r < 0)
return r;
arg_architecture = result.architecture;
}
return 0;
}
static int determine_names(void) {
int r;
if (arg_template && !arg_directory && arg_machine) {
/* If --template= was specified then we should not
* search for a machine, but instead create a new one
* in /var/lib/machine. */
/* If --template= was specified then we should not search for a machine, but instead create a
* new one in /var/lib/machine. */
arg_directory = path_join("/var/lib/machines", arg_machine);
if (!arg_directory)
@@ -5406,6 +5465,10 @@ static int run(int argc, char *argv[]) {
if (r < 0)
goto finish;
r = pick_paths();
if (r < 0)
goto finish;
r = determine_names();
if (r < 0)
goto finish;
+272 -57
View File
@@ -45,6 +45,7 @@
#include "strv.h"
#include "time-util.h"
#include "utf8.h"
#include "vpick.h"
#include "xattr-util.h"
static const char* const image_search_path[_IMAGE_CLASS_MAX] = {
@@ -215,40 +216,60 @@ static int image_new(
return 0;
}
static int extract_pretty(
static int extract_image_basename(
const char *path,
const char *class_suffix,
const char *format_suffix,
char **ret) {
const char *class_suffix, /* e.g. ".sysext" (this is an optional suffix) */
char **format_suffixes, /* e.g. ".raw" (one of these will be required) */
char **ret_basename,
char **ret_suffix) {
_cleanup_free_ char *name = NULL;
_cleanup_free_ char *name = NULL, *suffix = NULL;
int r;
assert(path);
assert(ret);
r = path_extract_filename(path, &name);
if (r < 0)
return r;
if (format_suffix) {
char *e = endswith(name, format_suffix);
if (format_suffixes) {
char *e = strv_endswith(name, format_suffixes);
if (!e) /* Format suffix is required */
return -EINVAL;
if (ret_suffix) {
suffix = strdup(e);
if (!suffix)
return -ENOMEM;
}
*e = 0;
}
if (class_suffix) {
char *e = endswith(name, class_suffix);
if (e) /* Class suffix is optional */
if (e) { /* Class suffix is optional */
if (ret_suffix) {
_cleanup_free_ char *j = strjoin(e, suffix);
if (!j)
return -ENOMEM;
free_and_replace(suffix, j);
}
*e = 0;
}
}
if (!image_name_is_valid(name))
return -EINVAL;
*ret = TAKE_PTR(name);
if (ret_suffix)
*ret_suffix = TAKE_PTR(suffix);
if (ret_basename)
*ret_basename = TAKE_PTR(name);
return 0;
}
@@ -303,7 +324,12 @@ static int image_make(
return 0;
if (!pretty) {
r = extract_pretty(filename, image_class_suffix_to_string(c), NULL, &pretty_buffer);
r = extract_image_basename(
filename,
image_class_suffix_to_string(c),
/* format_suffix= */ NULL,
&pretty_buffer,
/* ret_suffix= */ NULL);
if (r < 0)
return r;
@@ -390,7 +416,12 @@ static int image_make(
(void) fd_getcrtime_at(dfd, filename, AT_SYMLINK_FOLLOW, &crtime);
if (!pretty) {
r = extract_pretty(filename, image_class_suffix_to_string(c), ".raw", &pretty_buffer);
r = extract_image_basename(
filename,
image_class_suffix_to_string(c),
STRV_MAKE(".raw"),
&pretty_buffer,
/* ret_suffix= */ NULL);
if (r < 0)
return r;
@@ -424,7 +455,12 @@ static int image_make(
return 0;
if (!pretty) {
r = extract_pretty(filename, NULL, NULL, &pretty_buffer);
r = extract_image_basename(
filename,
/* class_suffix= */ NULL,
/* format_suffix= */ NULL,
&pretty_buffer,
/* ret_suffix= */ NULL);
if (r < 0)
return r;
@@ -488,6 +524,37 @@ static const char *pick_image_search_path(ImageClass class) {
return in_initrd() && image_search_path_initrd[class] ? image_search_path_initrd[class] : image_search_path[class];
}
static char **make_possible_filenames(ImageClass class, const char *image_name) {
_cleanup_strv_free_ char **l = NULL;
assert(image_name);
FOREACH_STRING(v_suffix, "", ".v")
FOREACH_STRING(format_suffix, "", ".raw") {
_cleanup_free_ char *j = NULL;
const char *class_suffix;
class_suffix = image_class_suffix_to_string(class);
if (class_suffix) {
j = strjoin(image_name, class_suffix, format_suffix, v_suffix);
if (!j)
return NULL;
if (strv_consume(&l, TAKE_PTR(j)) < 0)
return NULL;
}
j = strjoin(image_name, format_suffix, v_suffix);
if (!j)
return NULL;
if (strv_consume(&l, TAKE_PTR(j)) < 0)
return NULL;
}
return TAKE_PTR(l);
}
int image_find(ImageClass class,
const char *name,
const char *root,
@@ -503,6 +570,10 @@ int image_find(ImageClass class,
if (!image_name_is_valid(name))
return -ENOENT;
_cleanup_strv_free_ char **names = make_possible_filenames(class, name);
if (!names)
return -ENOMEM;
NULSTR_FOREACH(path, pick_image_search_path(class)) {
_cleanup_free_ char *resolved = NULL;
_cleanup_closedir_ DIR *d = NULL;
@@ -519,43 +590,97 @@ int image_find(ImageClass class,
* to symlink block devices into the search path. (For now, we disable that when operating
* relative to some root directory.) */
flags = root ? AT_SYMLINK_NOFOLLOW : 0;
if (fstatat(dirfd(d), name, &st, flags) < 0) {
_cleanup_free_ char *raw = NULL;
if (errno != ENOENT)
return -errno;
STRV_FOREACH(n, names) {
_cleanup_free_ char *fname_buf = NULL;
const char *fname = *n;
raw = strjoin(name, ".raw");
if (!raw)
return -ENOMEM;
if (fstatat(dirfd(d), fname, &st, flags) < 0) {
if (errno != ENOENT)
return -errno;
if (fstatat(dirfd(d), raw, &st, flags) < 0) {
if (errno == ENOENT)
continue;
return -errno;
continue; /* Vanished while we were looking at it */
}
if (!S_ISREG(st.st_mode))
if (endswith(fname, ".raw")) {
if (!S_ISREG(st.st_mode)) {
log_debug("Ignoring non-regular file '%s' with .raw suffix.", fname);
continue;
}
} else if (endswith(fname, ".v")) {
if (!S_ISDIR(st.st_mode)) {
log_debug("Ignoring non-directory file '%s' with .v suffix.", fname);
continue;
}
_cleanup_free_ char *suffix = NULL;
suffix = strdup(ASSERT_PTR(startswith(fname, name)));
if (!suffix)
return -ENOMEM;
*ASSERT_PTR(endswith(suffix, ".v")) = 0;
_cleanup_free_ char *vp = path_join(resolved, fname);
if (!vp)
return -ENOMEM;
PickFilter filter = {
.type_mask = endswith(suffix, ".raw") ? (UINT32_C(1) << DT_REG) | (UINT32_C(1) << DT_BLK) : (UINT32_C(1) << DT_DIR),
.basename = name,
.architecture = _ARCHITECTURE_INVALID,
.suffix = suffix,
};
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
r = path_pick(root,
/* toplevel_fd= */ AT_FDCWD,
vp,
&filter,
PICK_ARCHITECTURE|PICK_TRIES,
&result);
if (r < 0) {
log_debug_errno(r, "Failed to pick versioned image on '%s', skipping: %m", vp);
continue;
}
if (!result.path) {
log_debug("Found versioned directory '%s', without matching entry, skipping: %m", vp);
continue;
}
/* Refresh the stat data for the discovered target */
st = result.st;
_cleanup_free_ char *bn = NULL;
r = path_extract_filename(result.path, &bn);
if (r < 0) {
log_debug_errno(r, "Failed to extract basename of image path '%s', skipping: %m", result.path);
continue;
}
fname_buf = path_join(fname, bn);
if (!fname_buf)
return log_oom();
fname = fname_buf;
} else if (!S_ISDIR(st.st_mode) && !S_ISBLK(st.st_mode)) {
log_debug("Ignoring non-directory and non-block device file '%s' without suffix.", fname);
continue;
}
r = image_make(class, name, dirfd(d), resolved, raw, &st, ret);
} else {
if (!S_ISDIR(st.st_mode) && !S_ISBLK(st.st_mode))
r = image_make(class, name, dirfd(d), resolved, fname, &st, ret);
if (IN_SET(r, -ENOENT, -EMEDIUMTYPE))
continue;
if (r < 0)
return r;
r = image_make(class, name, dirfd(d), resolved, name, &st, ret);
if (ret)
(*ret)->discoverable = true;
return 1;
}
if (IN_SET(r, -ENOENT, -EMEDIUMTYPE))
continue;
if (r < 0)
return r;
if (ret)
(*ret)->discoverable = true;
return 1;
}
if (class == IMAGE_MACHINE && streq(name, ".host")) {
@@ -566,7 +691,7 @@ int image_find(ImageClass class,
if (ret)
(*ret)->discoverable = true;
return r;
return 1;
}
return -ENOENT;
@@ -613,43 +738,133 @@ int image_discover(
return r;
FOREACH_DIRENT_ALL(de, d, return -errno) {
_cleanup_free_ char *pretty = NULL, *fname_buf = NULL;
_cleanup_(image_unrefp) Image *image = NULL;
_cleanup_free_ char *pretty = NULL;
const char *fname = de->d_name;
struct stat st;
int flags;
if (dot_or_dot_dot(de->d_name))
if (dot_or_dot_dot(fname))
continue;
/* As mentioned above, we follow symlinks on this fstatat(), because we want to
* permit people to symlink block devices into the search path. */
flags = root ? AT_SYMLINK_NOFOLLOW : 0;
if (fstatat(dirfd(d), de->d_name, &st, flags) < 0) {
if (fstatat(dirfd(d), fname, &st, flags) < 0) {
if (errno == ENOENT)
continue;
return -errno;
}
if (S_ISREG(st.st_mode))
r = extract_pretty(de->d_name, image_class_suffix_to_string(class), ".raw", &pretty);
else if (S_ISDIR(st.st_mode))
r = extract_pretty(de->d_name, image_class_suffix_to_string(class), NULL, &pretty);
else if (S_ISBLK(st.st_mode))
r = extract_pretty(de->d_name, NULL, NULL, &pretty);
else {
log_debug("Skipping directory entry '%s', which is neither regular file, directory nor block device.", de->d_name);
continue;
}
if (r < 0) {
log_debug_errno(r, "Skipping directory entry '%s', which doesn't look like an image.", de->d_name);
if (S_ISREG(st.st_mode)) {
r = extract_image_basename(
fname,
image_class_suffix_to_string(class),
STRV_MAKE(".raw"),
&pretty,
/* suffix= */ NULL);
if (r < 0) {
log_debug_errno(r, "Skipping directory entry '%s', which doesn't look like an image.", fname);
continue;
}
} else if (S_ISDIR(st.st_mode)) {
const char *v;
v = endswith(fname, ".v");
if (v) {
_cleanup_free_ char *suffix = NULL, *nov = NULL;
nov = strndup(fname, v - fname); /* Chop off the .v */
if (!nov)
return -ENOMEM;
r = extract_image_basename(
nov,
image_class_suffix_to_string(class),
STRV_MAKE(".raw", ""),
&pretty,
&suffix);
if (r < 0) {
log_debug_errno(r, "Skipping directory entry '%s', which doesn't look like a versioned image.", fname);
continue;
}
_cleanup_free_ char *vp = path_join(resolved, fname);
if (!vp)
return -ENOMEM;
PickFilter filter = {
.type_mask = endswith(suffix, ".raw") ? (UINT32_C(1) << DT_REG) | (UINT32_C(1) << DT_BLK) : (UINT32_C(1) << DT_DIR),
.basename = pretty,
.architecture = _ARCHITECTURE_INVALID,
.suffix = suffix,
};
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
r = path_pick(root,
/* toplevel_fd= */ AT_FDCWD,
vp,
&filter,
PICK_ARCHITECTURE|PICK_TRIES,
&result);
if (r < 0) {
log_debug_errno(r, "Failed to pick versioned image on '%s', skipping: %m", vp);
continue;
}
if (!result.path) {
log_debug("Found versioned directory '%s', without matching entry, skipping: %m", vp);
continue;
}
/* Refresh the stat data for the discovered target */
st = result.st;
_cleanup_free_ char *bn = NULL;
r = path_extract_filename(result.path, &bn);
if (r < 0) {
log_debug_errno(r, "Failed to extract basename of image path '%s', skipping: %m", result.path);
continue;
}
fname_buf = path_join(fname, bn);
if (!fname_buf)
return log_oom();
fname = fname_buf;
} else {
r = extract_image_basename(
fname,
image_class_suffix_to_string(class),
/* format_suffix= */ NULL,
&pretty,
/* ret_suffix= */ NULL);
if (r < 0) {
log_debug_errno(r, "Skipping directory entry '%s', which doesn't look like an image.", fname);
continue;
}
}
} else if (S_ISBLK(st.st_mode)) {
r = extract_image_basename(
fname,
/* class_suffix= */ NULL,
/* format_suffix= */ NULL,
&pretty,
/* ret_v_suffix= */ NULL);
if (r < 0) {
log_debug_errno(r, "Skipping directory entry '%s', which doesn't look like an image.", fname);
continue;
}
} else {
log_debug("Skipping directory entry '%s', which is neither regular file, directory nor block device.", fname);
continue;
}
if (hashmap_contains(h, pretty))
continue;
r = image_make(class, pretty, dirfd(d), resolved, de->d_name, &st, &image);
r = image_make(class, pretty, dirfd(d), resolved, fname, &st, &image);
if (IN_SET(r, -ENOENT, -EMEDIUMTYPE))
continue;
if (r < 0)
+1
View File
@@ -189,6 +189,7 @@ shared_sources = files(
'verbs.c',
'vlan-util.c',
'volatile-util.c',
'vpick.c',
'wall.c',
'watchdog.c',
'web-util.c',
+694
View File
@@ -0,0 +1,694 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <sys/stat.h>
#include "architecture.h"
#include "chase.h"
#include "fd-util.h"
#include "fs-util.h"
#include "parse-util.h"
#include "path-util.h"
#include "recurse-dir.h"
#include "vpick.h"
void pick_result_done(PickResult *p) {
assert(p);
free(p->path);
safe_close(p->fd);
free(p->version);
*p = PICK_RESULT_NULL;
}
static int format_fname(
const PickFilter *filter,
PickFlags flags,
char **ret) {
_cleanup_free_ char *fn = NULL;
int r;
assert(filter);
assert(ret);
if (FLAGS_SET(flags, PICK_TRIES) || !filter->version) /* Underspecified? */
return -ENOEXEC;
/* The format for names we match goes like this:
*
* <basename><suffix>
* or:
* <basename>_<version><suffix>
* or:
* <basename>_<version>_<architecture><suffix>
* or:
* <basename>_<architecture><suffix>
*
* (Note that basename can be empty, in which case the leading "_" is suppressed)
*
* Examples: foo.raw, foo_1.3-7.raw, foo_1.3-7_x86-64.raw, foo_x86-64.raw
*
* Why use "_" as separator here? Primarily because it is not used by Semver 2.0. In RPM it is used
* for "unsortable" versions, i.e. doesn't show up in "sortable" versions, which we matter for this
* usecase here. In Debian the underscore is not allowed (and it uses it itself for separating
* fields).
*
* This is very close to Debian's way to name packages, but allows arbitrary suffixes, and makes the
* architecture field redundant.
*
* Compare with RPM's "NEVRA" concept. Here we have "BVAS" (basename, version, architecture, suffix).
*/
if (filter->basename) {
fn = strdup(filter->basename);
if (!fn)
return -ENOMEM;
}
if (filter->version) {
if (isempty(fn)) {
r = free_and_strdup(&fn, filter->version);
if (r < 0)
return r;
} else if (!strextend(&fn, "_", filter->version))
return -ENOMEM;
}
if (FLAGS_SET(flags, PICK_ARCHITECTURE) && filter->architecture >= 0) {
const char *as = ASSERT_PTR(architecture_to_string(filter->architecture));
if (isempty(fn)) {
r = free_and_strdup(&fn, as);
if (r < 0)
return r;
} else if (!strextend(&fn, "_", as))
return -ENOMEM;
}
if (filter->suffix && !strextend(&fn, filter->suffix))
return -ENOMEM;
if (!filename_is_valid(fn))
return -EINVAL;
*ret = TAKE_PTR(fn);
return 0;
}
static int errno_from_mode(uint32_t type_mask, mode_t found) {
/* Returns the most appropriate error code if we are lookging for an inode of type of those in the
* 'type_mask' but found 'found' instead.
*
* type_mask is a mask of 1U << DT_REG, 1U << DT_DIR, … flags, while found is a S_IFREG, S_IFDIR, …
* mode value. */
if (type_mask == 0) /* type doesn't matter */
return 0;
if (FLAGS_SET(type_mask, UINT32_C(1) << IFTODT(found)))
return 0;
if (type_mask == (UINT32_C(1) << DT_BLK))
return -ENOTBLK;
if (type_mask == (UINT32_C(1) << DT_DIR))
return -ENOTDIR;
if (type_mask == (UINT32_C(1) << DT_SOCK))
return -ENOTSOCK;
if (S_ISLNK(found))
return -ELOOP;
if (S_ISDIR(found))
return -EISDIR;
return -EBADF;
}
static int pin_choice(
const char *toplevel_path,
int toplevel_fd,
const char *inode_path,
int _inode_fd, /* we always take ownership of the fd, even on failure */
unsigned tries_left,
unsigned tries_done,
const PickFilter *filter,
PickFlags flags,
PickResult *ret) {
_cleanup_close_ int inode_fd = TAKE_FD(_inode_fd);
_cleanup_free_ char *resolved_path = NULL;
int r;
assert(toplevel_fd >= 0 || toplevel_fd == AT_FDCWD);
assert(inode_path);
assert(filter);
toplevel_path = strempty(toplevel_path);
if (inode_fd < 0 || FLAGS_SET(flags, PICK_RESOLVE)) {
r = chaseat(toplevel_fd,
inode_path,
CHASE_AT_RESOLVE_IN_ROOT,
FLAGS_SET(flags, PICK_RESOLVE) ? &resolved_path : 0,
inode_fd < 0 ? &inode_fd : NULL);
if (r < 0)
return r;
if (resolved_path)
inode_path = resolved_path;
}
struct stat st;
if (fstat(inode_fd, &st) < 0)
return log_debug_errno(errno, "Failed to stat discovered inode '%s/%s': %m", toplevel_path, inode_path);
if (filter->type_mask != 0 &&
!FLAGS_SET(filter->type_mask, UINT32_C(1) << IFTODT(st.st_mode)))
return log_debug_errno(
SYNTHETIC_ERRNO(errno_from_mode(filter->type_mask, st.st_mode)),
"Inode '%s/%s' has wrong type, found '%s'.",
toplevel_path, inode_path,
inode_type_to_string(st.st_mode));
_cleanup_(pick_result_done) PickResult result = {
.fd = TAKE_FD(inode_fd),
.st = st,
.architecture = filter->architecture,
.tries_left = tries_left,
.tries_done = tries_done,
};
result.path = strdup(inode_path);
if (!result.path)
return log_oom_debug();
if (filter->version) {
result.version = strdup(filter->version);
if (!result.version)
return log_oom_debug();
}
*ret = TAKE_PICK_RESULT(result);
return 1;
}
static int parse_tries(const char *s, unsigned *ret_tries_left, unsigned *ret_tries_done) {
unsigned left, done;
size_t n;
assert(s);
assert(ret_tries_left);
assert(ret_tries_done);
if (s[0] != '+')
goto nomatch;
s++;
n = strspn(s, DIGITS);
if (n == 0)
goto nomatch;
if (s[n] == 0) {
if (safe_atou(s, &left) < 0)
goto nomatch;
done = 0;
} else if (s[n] == '-') {
_cleanup_free_ char *c = NULL;
c = strndup(s, n);
if (!c)
return -ENOMEM;
if (safe_atou(c, &left) < 0)
goto nomatch;
s += n + 1;
if (!in_charset(s, DIGITS))
goto nomatch;
if (safe_atou(s, &done) < 0)
goto nomatch;
} else
goto nomatch;
*ret_tries_left = left;
*ret_tries_done = done;
return 1;
nomatch:
*ret_tries_left = *ret_tries_done = UINT_MAX;
return 0;
}
static int make_choice(
const char *toplevel_path,
int toplevel_fd,
const char *inode_path,
int _inode_fd, /* we always take ownership of the fd, even on failure */
const PickFilter *filter,
PickFlags flags,
PickResult *ret) {
static const Architecture local_architectures[] = {
/* In order of preference */
native_architecture(),
#ifdef ARCHITECTURE_SECONDARY
ARCHITECTURE_SECONDARY,
#endif
_ARCHITECTURE_INVALID, /* accept any arch, as last resort */
};
_cleanup_free_ DirectoryEntries *de = NULL;
_cleanup_free_ char *best_version = NULL, *best_filename = NULL, *p = NULL, *j = NULL;
_cleanup_close_ int dir_fd = -EBADF, object_fd = -EBADF, inode_fd = TAKE_FD(_inode_fd);
const Architecture *architectures;
unsigned best_tries_left = UINT_MAX, best_tries_done = UINT_MAX;
size_t n_architectures, best_architecture_index = SIZE_MAX;
int r;
assert(toplevel_fd >= 0 || toplevel_fd == AT_FDCWD);
assert(inode_path);
assert(filter);
toplevel_path = strempty(toplevel_path);
if (inode_fd < 0) {
r = chaseat(toplevel_fd, inode_path, CHASE_AT_RESOLVE_IN_ROOT, NULL, &inode_fd);
if (r < 0)
return r;
}
/* Maybe the filter is fully specified? Then we can generate the file name directly */
r = format_fname(filter, flags, &j);
if (r >= 0) {
_cleanup_free_ char *object_path = NULL;
/* Yay! This worked! */
p = path_join(inode_path, j);
if (!p)
return log_oom_debug();
r = chaseat(toplevel_fd, p, CHASE_AT_RESOLVE_IN_ROOT, &object_path, &object_fd);
if (r < 0) {
if (r != -ENOENT)
return log_debug_errno(r, "Failed to open '%s/%s': %m", toplevel_path, p);
*ret = PICK_RESULT_NULL;
return 0;
}
return pin_choice(
toplevel_path,
toplevel_fd,
FLAGS_SET(flags, PICK_RESOLVE) ? object_path : p,
TAKE_FD(object_fd), /* unconditionally pass ownership of the fd */
/* tries_left= */ UINT_MAX,
/* tries_done= */ UINT_MAX,
filter,
flags & ~PICK_RESOLVE,
ret);
} else if (r != -ENOEXEC)
return log_debug_errno(r, "Failed to format file name: %m");
/* Underspecified, so we do our enumeration dance */
/* Convert O_PATH to a regular directory fd */
dir_fd = fd_reopen(inode_fd, O_DIRECTORY|O_RDONLY|O_CLOEXEC);
if (dir_fd < 0)
return log_debug_errno(dir_fd, "Failed to reopen '%s/%s' as directory: %m", toplevel_path, inode_path);
r = readdir_all(dir_fd, 0, &de);
if (r < 0)
return log_debug_errno(r, "Failed to read directory '%s/%s': %m", toplevel_path, inode_path);
if (filter->architecture < 0) {
architectures = local_architectures;
n_architectures = ELEMENTSOF(local_architectures);
} else {
architectures = &filter->architecture;
n_architectures = 1;
}
FOREACH_ARRAY(entry, de->entries, de->n_entries) {
unsigned found_tries_done = UINT_MAX, found_tries_left = UINT_MAX;
_cleanup_free_ char *chopped = NULL;
size_t found_architecture_index = SIZE_MAX;
const char *e;
if (!isempty(filter->basename)) {
e = startswith((*entry)->d_name, filter->basename);
if (!e)
continue;
if (e[0] != '_')
continue;
e++;
} else
e = (*entry)->d_name;
if (!isempty(filter->suffix)) {
const char *sfx;
sfx = endswith(e, filter->suffix);
if (!sfx)
continue;
chopped = strndup(e, sfx - e);
if (!chopped)
return log_oom_debug();
e = chopped;
}
if (FLAGS_SET(flags, PICK_TRIES)) {
char *plus = strrchr(e, '+');
if (plus) {
r = parse_tries(plus, &found_tries_left, &found_tries_done);
if (r < 0)
return r;
if (r > 0) /* Found and parsed, now chop off */
*plus = 0;
}
}
if (FLAGS_SET(flags, PICK_ARCHITECTURE)) {
char *underscore = strrchr(e, '_');
Architecture a;
a = underscore ? architecture_from_string(underscore + 1) : _ARCHITECTURE_INVALID;
for (size_t i = 0; i < n_architectures; i++)
if (architectures[i] == a) {
found_architecture_index = i;
break;
}
if (found_architecture_index == SIZE_MAX) { /* No matching arch found */
log_debug("Found entry with architecture '%s' which is not what we are looking for, ignoring entry.", a < 0 ? "any" : architecture_to_string(a));
continue;
}
/* Chop off architecture from string */
if (underscore)
*underscore = 0;
}
if (!version_is_valid(e)) {
log_debug("Version string '%s' of entry '%s' is invalid, ignoring entry.", e, (*entry)->d_name);
continue;
}
if (filter->version && !streq(filter->version, e)) {
log_debug("Found entry with version string '%s', but was looking for '%s', ignoring entry.", e, filter->version);
continue;
}
if (best_filename) { /* Already found one matching entry? Then figure out the better one */
int d = 0;
/* First, prefer entries with tries left over those without */
if (FLAGS_SET(flags, PICK_TRIES))
d = CMP(found_tries_left != 0, best_tries_left != 0);
/* Second, prefer newer versions */
if (d == 0)
d = strverscmp_improved(e, best_version);
/* Third, prefer native architectures over secondary architectures */
if (d == 0 &&
FLAGS_SET(flags, PICK_ARCHITECTURE) &&
found_architecture_index != SIZE_MAX && best_architecture_index != SIZE_MAX)
d = -CMP(found_architecture_index, best_architecture_index);
/* Fourth, prefer entries with more tries left */
if (FLAGS_SET(flags, PICK_TRIES)) {
if (d == 0)
d = CMP(found_tries_left, best_tries_left);
/* Fifth, prefer entries with fewer attempts done so far */
if (d == 0)
d = -CMP(found_tries_done, best_tries_done);
}
/* Last, just compare the filenames as strings */
if (d == 0)
d = strcmp((*entry)->d_name, best_filename);
if (d < 0) {
log_debug("Found entry '%s' but previously found entry '%s' matches better, hence skipping entry.", (*entry)->d_name, best_filename);
continue;
}
}
r = free_and_strdup_warn(&best_version, e);
if (r < 0)
return r;
r = free_and_strdup_warn(&best_filename, (*entry)->d_name);
if (r < 0)
return r;
best_architecture_index = found_architecture_index;
best_tries_left = found_tries_left;
best_tries_done = found_tries_done;
}
if (!best_filename) { /* Everything was good, but we didn't find any suitable entry */
*ret = PICK_RESULT_NULL;
return 0;
}
p = path_join(inode_path, best_filename);
if (!p)
return log_oom_debug();
object_fd = openat(dir_fd, best_filename, O_CLOEXEC|O_PATH);
if (object_fd < 0)
return log_debug_errno(errno, "Failed to open '%s/%s': %m", toplevel_path, p);
return pin_choice(
toplevel_path,
toplevel_fd,
p,
TAKE_FD(object_fd),
best_tries_left,
best_tries_done,
&(const PickFilter) {
.type_mask = filter->type_mask,
.basename = filter->basename,
.version = empty_to_null(best_version),
.architecture = best_architecture_index != SIZE_MAX ? architectures[best_architecture_index] : _ARCHITECTURE_INVALID,
.suffix = filter->suffix,
},
flags,
ret);
}
int path_pick(const char *toplevel_path,
int toplevel_fd,
const char *path,
const PickFilter *filter,
PickFlags flags,
PickResult *ret) {
_cleanup_free_ char *filter_bname = NULL, *dir = NULL, *parent = NULL, *fname = NULL;
const char *filter_suffix, *enumeration_path;
uint32_t filter_type_mask;
int r;
assert(toplevel_fd >= 0 || toplevel_fd == AT_FDCWD);
assert(path);
toplevel_path = strempty(toplevel_path);
/* Given a path, resolve .v/ subdir logic (if used!), and returns the choice made. This supports
* three ways to be called:
*
* • with a path referring a directory of any name, and filter→basename *explicitly* specified, in
* which case we'll use a pattern "<filter→basename>_*<filter→suffix>" on the directory's files.
*
* • with no filter→basename explicitly specified and a path referring to a directory named in format
* "<somestring><filter→suffix>.v" . In this case the filter basename to search for inside the dir
* is derived from the directory name. Example: "/foo/bar/baz.suffix.v" → we'll search for
* "/foo/bar/baz.suffix.v/baz_*.suffix".
*
* • with a path whose penultimate component ends in ".v/". In this case the final component of the
* path refers to the pattern. Example: "/foo/bar/baz.v/waldo__.suffix" → we'll search for
* "/foo/bar/baz.v/waldo_*.suffix".
*/
/* Explicit basename specified, then shortcut things and do .v mode regardless of the path name. */
if (filter->basename)
return make_choice(
toplevel_path,
toplevel_fd,
path,
/* inode_fd= */ -EBADF,
filter,
flags,
ret);
r = path_extract_filename(path, &fname);
if (r < 0) {
if (r != -EADDRNOTAVAIL) /* root dir or "." */
return r;
/* If there's no path element we can derive a pattern off, the don't */
goto bypass;
}
/* Remember if the path ends in a dash suffix */
bool slash_suffix = r == O_DIRECTORY;
const char *e = endswith(fname, ".v");
if (e) {
/* So a path in the form /foo/bar/baz.v is specified. In this case our search basename is
* "baz", possibly with a suffix chopped off if there's one specified. */
filter_bname = strndup(fname, e - fname);
if (!filter_bname)
return -ENOMEM;
if (filter->suffix) {
/* Chop off suffix, if specified */
char *f = endswith(filter_bname, filter->suffix);
if (f)
*f = 0;
}
filter_suffix = filter->suffix;
filter_type_mask = filter->type_mask;
enumeration_path = path;
} else {
/* The path does not end in '.v', hence see if the last element is a pattern. */
char *wildcard = strrstr(fname, "___");
if (!wildcard)
goto bypass; /* Not a pattern, then bypass */
/* We found the '___' wildcard, hence evertyhing after it is our filter suffix, and
* evertyhing before is our filter basename */
*wildcard = 0;
filter_suffix = empty_to_null(wildcard + 3);
filter_bname = TAKE_PTR(fname);
r = path_extract_directory(path, &dir);
if (r < 0) {
if (!IN_SET(r, -EDESTADDRREQ, -EADDRNOTAVAIL)) /* only filename specified (no dir), or root or "." */
return r;
goto bypass; /* No dir extractable, can't check if parent ends in ".v" */
}
r = path_extract_filename(dir, &parent);
if (r < 0) {
if (r != -EADDRNOTAVAIL) /* root dir or "." */
return r;
goto bypass; /* Cannot extract fname from parent dir, can't check if it ends in ".v" */
}
e = endswith(parent, ".v");
if (!e)
goto bypass; /* Doesn't end in ".v", shortcut */
filter_type_mask = filter->type_mask;
if (slash_suffix) {
/* If the pattern is suffixed by a / then we are looking for directories apparently. */
if (filter_type_mask != 0 && !FLAGS_SET(filter_type_mask, UINT32_C(1) << DT_DIR))
return log_debug_errno(SYNTHETIC_ERRNO(errno_from_mode(filter_type_mask, S_IFDIR)),
"Specified pattern ends in '/', but not looking for directories, refusing.");
filter_type_mask = UINT32_C(1) << DT_DIR;
}
enumeration_path = dir;
}
return make_choice(
toplevel_path,
toplevel_fd,
enumeration_path,
/* inode_fd= */ -EBADF,
&(const PickFilter) {
.type_mask = filter_type_mask,
.basename = filter_bname,
.version = filter->version,
.architecture = filter->architecture,
.suffix = filter_suffix,
},
flags,
ret);
bypass:
/* Don't make any choice, but just use the passed path literally */
return pin_choice(
toplevel_path,
toplevel_fd,
path,
/* inode_fd= */ -EBADF,
/* tries_left= */ UINT_MAX,
/* tries_done= */ UINT_MAX,
filter,
flags,
ret);
}
int path_pick_update_warn(
char **path,
const PickFilter *filter,
PickFlags flags,
PickResult *ret_result) {
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
int r;
assert(path);
assert(*path);
/* This updates the first argument if needed! */
r = path_pick(/* toplevel_path= */ NULL,
/* toplevel_fd= */ AT_FDCWD,
*path,
filter,
flags,
&result);
if (r == -ENOENT) {
log_debug("Path '%s' doesn't exist, leaving as is.", *path);
*ret_result = PICK_RESULT_NULL;
return 0;
}
if (r < 0)
return log_error_errno(r, "Failed to pick version on path '%s': %m", *path);
if (r == 0)
return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No matching entries in versioned directory '%s' found.", *path);
log_debug("Resolved versioned directory pattern '%s' to file '%s' as version '%s'.", result.path, *path, strna(result.version));
if (ret_result) {
r = free_and_strdup_warn(path, result.path);
if (r < 0)
return r;
*ret_result = TAKE_PICK_RESULT(result);
} else
free_and_replace(*path, result.path);
return 1;
}
const PickFilter pick_filter_image_raw = {
.type_mask = (UINT32_C(1) << DT_REG) | (UINT32_C(1) << DT_BLK),
.architecture = _ARCHITECTURE_INVALID,
.suffix = ".raw",
};
const PickFilter pick_filter_image_dir = {
.type_mask = UINT32_C(1) << DT_DIR,
.architecture = _ARCHITECTURE_INVALID,
};
+59
View File
@@ -0,0 +1,59 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include <sys/types.h>
#include "architecture.h"
typedef enum PickFlags {
PICK_ARCHITECTURE = 1 << 0, /* Look for an architecture suffix */
PICK_TRIES = 1 << 1, /* Look for tries left/tries done counters */
PICK_RESOLVE = 1 << 2, /* Return the fully resolved (chased) path, rather than the path to the entry */
} PickFlags;
typedef struct PickFilter {
uint32_t type_mask; /* A mask of 1U << DT_REG, 1U << DT_DIR, … */
const char *basename; /* Can be overriden by search pattern */
const char *version;
Architecture architecture;
const char *suffix; /* Can be overriden by search pattern */
} PickFilter;
typedef struct PickResult {
char *path;
int fd; /* O_PATH */
struct stat st;
char *version;
Architecture architecture;
unsigned tries_left;
unsigned tries_done;
} PickResult;
#define PICK_RESULT_NULL \
(const PickResult) { \
.fd = -EBADF, \
.st.st_mode = MODE_INVALID, \
.architecture = _ARCHITECTURE_INVALID, \
.tries_left = UINT_MAX, \
.tries_done = UINT_MAX, \
}
#define TAKE_PICK_RESULT(ptr) TAKE_GENERIC(ptr, PickResult, PICK_RESULT_NULL)
void pick_result_done(PickResult *p);
int path_pick(const char *toplevel_path,
int toplevel_fd,
const char *path,
const PickFilter *filter,
PickFlags flags,
PickResult *ret);
int path_pick_update_warn(
char **path,
const PickFilter *filter,
PickFlags flags,
PickResult *ret);
extern const PickFilter pick_filter_image_raw;
extern const PickFilter pick_filter_image_dir;
+1
View File
@@ -178,6 +178,7 @@ simple_tests += files(
'test-user-util.c',
'test-utf8.c',
'test-verbs.c',
'test-vpick.c',
'test-web-util.c',
'test-xattr-util.c',
'test-xml.c',
+15
View File
@@ -180,6 +180,21 @@ TEST(dir_is_empty) {
assert_se(dir_is_empty_at(AT_FDCWD, empty_dir, /* ignore_hidden_or_backup= */ false) > 0);
}
TEST(inode_type_from_string) {
static const mode_t types[] = {
S_IFREG,
S_IFDIR,
S_IFLNK,
S_IFCHR,
S_IFBLK,
S_IFIFO,
S_IFSOCK,
};
FOREACH_ARRAY(m, types, ELEMENTSOF(types))
assert_se(inode_type_from_string(inode_type_to_string(*m)) == *m);
}
static int intro(void) {
log_show_color(true);
return EXIT_SUCCESS;
+23
View File
@@ -1324,4 +1324,27 @@ TEST(strlevenshtein) {
assert_se(strlevenshtein("sunday", "saturday") == 3);
}
TEST(strrstr) {
assert_se(!strrstr(NULL, NULL));
assert_se(!strrstr("foo", NULL));
assert_se(!strrstr(NULL, "foo"));
const char *p = "foo";
assert_se(strrstr(p, "foo") == p);
assert_se(strrstr(p, "fo") == p);
assert_se(strrstr(p, "f") == p);
assert_se(strrstr(p, "oo") == p + 1);
assert_se(strrstr(p, "o") == p + 2);
assert_se(strrstr(p, "") == p + strlen(p));
assert_se(!strrstr(p, "bar"));
p = "xoxoxox";
assert_se(strrstr(p, "") == p + strlen(p));
assert_se(strrstr(p, "x") == p + 6);
assert_se(strrstr(p, "ox") == p + 5);
assert_se(strrstr(p, "xo") == p + 4);
assert_se(strrstr(p, "xox") == p + 4);
assert_se(!strrstr(p, "xx"));
}
DEFINE_TEST_MAIN(LOG_DEBUG);
+8
View File
@@ -1006,4 +1006,12 @@ TEST(strv_find_first_field) {
assert_se(streq_ptr(strv_find_first_field(STRV_MAKE("i", "k", "l", "m", "d", "c", "a", "b"), haystack), "j"));
}
TEST(strv_endswith) {
assert_se(streq_ptr(strv_endswith("waldo", STRV_MAKE("xxx", "yyy", "ldo", "zzz")), "ldo"));
assert_se(streq_ptr(strv_endswith("waldo", STRV_MAKE("xxx", "yyy", "zzz")), NULL));
assert_se(streq_ptr(strv_endswith("waldo", STRV_MAKE("waldo")), "waldo"));
assert_se(streq_ptr(strv_endswith("waldo", STRV_MAKE("w", "o", "ldo")), "o"));
assert_se(streq_ptr(strv_endswith("waldo", STRV_MAKE("knurz", "", "waldo")), ""));
}
DEFINE_TEST_MAIN(LOG_INFO);
+171
View File
@@ -0,0 +1,171 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "fd-util.h"
#include "fileio.h"
#include "fs-util.h"
#include "mkdir.h"
#include "path-util.h"
#include "rm-rf.h"
#include "tests.h"
#include "tmpfile-util.h"
#include "vpick.h"
TEST(path_pick) {
_cleanup_(rm_rf_physical_and_freep) char *p = NULL;
_cleanup_close_ int dfd = -EBADF, sub_dfd = -EBADF;
dfd = mkdtemp_open(NULL, O_DIRECTORY|O_CLOEXEC, &p);
assert(dfd >= 0);
sub_dfd = open_mkdir_at(dfd, "foo.v", O_CLOEXEC, 0777);
assert(sub_dfd >= 0);
assert_se(write_string_file_at(sub_dfd, "foo_5.5.raw", "5.5", WRITE_STRING_FILE_CREATE) >= 0);
assert_se(write_string_file_at(sub_dfd, "foo_55.raw", "55", WRITE_STRING_FILE_CREATE) >= 0);
assert_se(write_string_file_at(sub_dfd, "foo_5.raw", "5", WRITE_STRING_FILE_CREATE) >= 0);
assert_se(write_string_file_at(sub_dfd, "foo_5_ia64.raw", "5", WRITE_STRING_FILE_CREATE) >= 0);
assert_se(write_string_file_at(sub_dfd, "foo_7.raw", "7", WRITE_STRING_FILE_CREATE) >= 0);
assert_se(write_string_file_at(sub_dfd, "foo_7_x86-64.raw", "7 64bit", WRITE_STRING_FILE_CREATE) >= 0);
assert_se(write_string_file_at(sub_dfd, "foo_55_x86-64.raw", "55 64bit", WRITE_STRING_FILE_CREATE) >= 0);
assert_se(write_string_file_at(sub_dfd, "foo_55_x86.raw", "55 32bit", WRITE_STRING_FILE_CREATE) >= 0);
assert_se(write_string_file_at(sub_dfd, "foo_99_x86.raw", "99 32bit", WRITE_STRING_FILE_CREATE) >= 0);
/* Let's add an entry for sparc (which is a valid arch, but almost certainly not what we test
* on). This entry should hence always be ignored */
if (native_architecture() != ARCHITECTURE_SPARC)
assert_se(write_string_file_at(sub_dfd, "foo_100_sparc.raw", "100 sparc", WRITE_STRING_FILE_CREATE) >= 0);
assert_se(write_string_file_at(sub_dfd, "quux_1_s390.raw", "waldo1", WRITE_STRING_FILE_CREATE) >= 0);
assert_se(write_string_file_at(sub_dfd, "quux_2_s390+4-6.raw", "waldo2", WRITE_STRING_FILE_CREATE) >= 0);
assert_se(write_string_file_at(sub_dfd, "quux_3_s390+0-10.raw", "waldo3", WRITE_STRING_FILE_CREATE) >= 0);
_cleanup_free_ char *pp = NULL;
pp = path_join(p, "foo.v");
assert_se(pp);
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
PickFilter filter = {
.architecture = _ARCHITECTURE_INVALID,
.suffix = ".raw",
};
if (IN_SET(native_architecture(), ARCHITECTURE_X86, ARCHITECTURE_X86_64)) {
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
assert_se(S_ISREG(result.st.st_mode));
assert_se(streq_ptr(result.version, "99"));
assert_se(result.architecture == ARCHITECTURE_X86);
assert_se(endswith(result.path, "/foo_99_x86.raw"));
pick_result_done(&result);
}
filter.architecture = ARCHITECTURE_X86_64;
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
assert_se(S_ISREG(result.st.st_mode));
assert_se(streq_ptr(result.version, "55"));
assert_se(result.architecture == ARCHITECTURE_X86_64);
assert_se(endswith(result.path, "/foo_55_x86-64.raw"));
pick_result_done(&result);
filter.architecture = ARCHITECTURE_IA64;
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
assert_se(S_ISREG(result.st.st_mode));
assert_se(streq_ptr(result.version, "5"));
assert_se(result.architecture == ARCHITECTURE_IA64);
assert_se(endswith(result.path, "/foo_5_ia64.raw"));
pick_result_done(&result);
filter.architecture = _ARCHITECTURE_INVALID;
filter.version = "5";
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
assert_se(S_ISREG(result.st.st_mode));
assert_se(streq_ptr(result.version, "5"));
if (native_architecture() != ARCHITECTURE_IA64) {
assert_se(result.architecture == _ARCHITECTURE_INVALID);
assert_se(endswith(result.path, "/foo_5.raw"));
}
pick_result_done(&result);
filter.architecture = ARCHITECTURE_IA64;
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
assert_se(S_ISREG(result.st.st_mode));
assert_se(streq_ptr(result.version, "5"));
assert_se(result.architecture == ARCHITECTURE_IA64);
assert_se(endswith(result.path, "/foo_5_ia64.raw"));
pick_result_done(&result);
filter.architecture = ARCHITECTURE_CRIS;
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) == 0);
assert_se(result.st.st_mode == MODE_INVALID);
assert_se(!result.version);
assert_se(result.architecture < 0);
assert_se(!result.path);
assert_se(unlinkat(sub_dfd, "foo_99_x86.raw", 0) >= 0);
filter.architecture = _ARCHITECTURE_INVALID;
filter.version = NULL;
if (IN_SET(native_architecture(), ARCHITECTURE_X86_64, ARCHITECTURE_X86)) {
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
assert_se(S_ISREG(result.st.st_mode));
assert_se(streq_ptr(result.version, "55"));
if (native_architecture() == ARCHITECTURE_X86_64) {
assert_se(result.architecture == ARCHITECTURE_X86_64);
assert_se(endswith(result.path, "/foo_55_x86-64.raw"));
} else {
assert_se(result.architecture == ARCHITECTURE_X86);
assert_se(endswith(result.path, "/foo_55_x86.raw"));
}
pick_result_done(&result);
}
/* Test explicit patterns in last component of path not being .v */
free(pp);
pp = path_join(p, "foo.v/foo___.raw");
assert_se(pp);
if (IN_SET(native_architecture(), ARCHITECTURE_X86, ARCHITECTURE_X86_64)) {
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
assert_se(S_ISREG(result.st.st_mode));
assert_se(streq_ptr(result.version, "55"));
assert_se(result.architecture == native_architecture());
assert_se(endswith(result.path, ".raw"));
assert_se(strrstr(result.path, "/foo_55_x86"));
pick_result_done(&result);
}
/* Specify an explicit path */
free(pp);
pp = path_join(p, "foo.v/foo_5.raw");
assert_se(pp);
filter.type_mask = UINT32_C(1) << DT_DIR;
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) == -ENOTDIR);
filter.type_mask = UINT32_C(1) << DT_REG;
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
assert_se(S_ISREG(result.st.st_mode));
assert_se(!result.version);
assert_se(result.architecture == _ARCHITECTURE_INVALID);
assert_se(path_equal(result.path, pp));
pick_result_done(&result);
free(pp);
pp = path_join(p, "foo.v");
assert_se(pp);
filter.architecture = ARCHITECTURE_S390;
filter.basename = "quux";
assert_se(path_pick(NULL, AT_FDCWD, pp, &filter, PICK_ARCHITECTURE|PICK_TRIES, &result) > 0);
assert_se(S_ISREG(result.st.st_mode));
assert_se(streq_ptr(result.version, "2"));
assert_se(result.tries_left == 4);
assert_se(result.tries_done == 6);
assert_se(endswith(result.path, "quux_2_s390+4-6.raw"));
assert_se(result.architecture == ARCHITECTURE_S390);
}
DEFINE_TEST_MAIN(LOG_DEBUG);
+9
View File
@@ -0,0 +1,9 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
executables += [
executable_template + {
'name' : 'systemd-vpick',
'public' : true,
'sources' : files('vpick-tool.c'),
},
]
+350
View File
@@ -0,0 +1,350 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <getopt.h>
#include "architecture.h"
#include "build.h"
#include "format-table.h"
#include "fs-util.h"
#include "main-func.h"
#include "path-util.h"
#include "parse-util.h"
#include "pretty-print.h"
#include "stat-util.h"
#include "string-util.h"
#include "strv.h"
#include "terminal-util.h"
#include "vpick.h"
static char *arg_filter_basename = NULL;
static char *arg_filter_version = NULL;
static Architecture arg_filter_architecture = _ARCHITECTURE_INVALID;
static char *arg_filter_suffix = NULL;
static uint32_t arg_filter_type_mask = 0;
static enum {
PRINT_PATH,
PRINT_FILENAME,
PRINT_VERSION,
PRINT_TYPE,
PRINT_ARCHITECTURE,
PRINT_TRIES,
PRINT_ALL,
_PRINT_INVALID = -EINVAL,
} arg_print = _PRINT_INVALID;
static PickFlags arg_flags = PICK_ARCHITECTURE|PICK_TRIES;
STATIC_DESTRUCTOR_REGISTER(arg_filter_basename, freep);
STATIC_DESTRUCTOR_REGISTER(arg_filter_version, freep);
STATIC_DESTRUCTOR_REGISTER(arg_filter_suffix, freep);
static int help(void) {
_cleanup_free_ char *link = NULL;
int r;
r = terminal_urlify_man("systemd-vpick", "1", &link);
if (r < 0)
return log_oom();
printf("%1$s [OPTIONS...] PATH...\n"
"\n%5$sPick entry from versioned directory.%6$s\n\n"
" -h --help Show this help\n"
" --version Show package version\n"
"\n%3$sLookup Keys:%4$s\n"
" -B --basename=BASENAME\n"
" Look for specified basename\n"
" -V VERSION Look for specified version\n"
" -A ARCH Look for specified architecture\n"
" -S --suffix=SUFFIX Look for specified suffix\n"
" -t --type=TYPE Look for specified inode type\n"
"\n%3$sOutput:%4$s\n"
" -p --print=filename Print selected filename rather than path\n"
" -p --print=version Print selected version rather than path\n"
" -p --print=type Print selected inode type rather than path\n"
" -p --print=arch Print selected architecture rather than path\n"
" -p --print=tries Print selected tries left/tries done rather than path\n"
" -p --print=all Print all of the above\n"
" --resolve=yes Canonicalize the result path\n"
"\nSee the %2$s for details.\n",
program_invocation_short_name,
link,
ansi_underline(), ansi_normal(),
ansi_highlight(), ansi_normal());
return 0;
}
static int parse_argv(int argc, char *argv[]) {
enum {
ARG_VERSION = 0x100,
ARG_RESOLVE,
};
static const struct option options[] = {
{ "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, ARG_VERSION },
{ "basename", required_argument, NULL, 'B' },
{ "suffix", required_argument, NULL, 'S' },
{ "type", required_argument, NULL, 't' },
{ "print", required_argument, NULL, 'p' },
{ "resolve", required_argument, NULL, ARG_RESOLVE },
{}
};
int c, r;
assert(argc >= 0);
assert(argv);
while ((c = getopt_long(argc, argv, "hB:V:A:S:t:p:", options, NULL)) >= 0) {
switch (c) {
case 'h':
return help();
case ARG_VERSION:
return version();
case 'B':
if (!filename_part_is_valid(optarg))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid basename string: %s", optarg);
r = free_and_strdup_warn(&arg_filter_basename, optarg);
if (r < 0)
return r;
break;
case 'V':
if (!version_is_valid(optarg))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid version string: %s", optarg);
r = free_and_strdup_warn(&arg_filter_version, optarg);
if (r < 0)
return r;
break;
case 'A':
if (streq(optarg, "native"))
arg_filter_architecture = native_architecture();
else if (streq(optarg, "secondary")) {
#ifdef ARCHITECTURE_SECONDARY
arg_filter_architecture = ARCHITECTURE_SECONDARY;
#else
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Local architecture has no secondary architecture.");
#endif
} else if (streq(optarg, "uname"))
arg_filter_architecture = uname_architecture();
else if (streq(optarg, "auto"))
arg_filter_architecture = _ARCHITECTURE_INVALID;
else {
arg_filter_architecture = architecture_from_string(optarg);
if (arg_filter_architecture < 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown architecture: %s", optarg);
}
break;
case 'S':
if (!filename_part_is_valid(optarg))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid suffix string: %s", optarg);
r = free_and_strdup_warn(&arg_filter_suffix, optarg);
if (r < 0)
return r;
break;
case 't':
if (isempty(optarg))
arg_filter_type_mask = 0;
else {
mode_t m;
m = inode_type_from_string(optarg);
if (m == MODE_INVALID)
return log_error_errno(m, "Unknown inode type: %s", optarg);
arg_filter_type_mask |= UINT32_C(1) << IFTODT(m);
}
break;
case 'p':
if (streq(optarg, "path"))
arg_print = PRINT_PATH;
else if (streq(optarg, "filename"))
arg_print = PRINT_FILENAME;
else if (streq(optarg, "version"))
arg_print = PRINT_VERSION;
else if (streq(optarg, "type"))
arg_print = PRINT_TYPE;
else if (STR_IN_SET(optarg, "arch", "architecture"))
arg_print = PRINT_ARCHITECTURE;
else if (streq(optarg, "tries"))
arg_print = PRINT_TRIES;
else if (streq(optarg, "all"))
arg_print = PRINT_ALL;
else
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown --print= argument: %s", optarg);
break;
case ARG_RESOLVE:
r = parse_boolean(optarg);
if (r < 0)
return log_error_errno(r, "Failed to parse --resolve= value: %m");
SET_FLAG(arg_flags, PICK_RESOLVE, r);
break;
case '?':
return -EINVAL;
default:
assert_not_reached();
}
}
if (arg_print < 0)
arg_print = PRINT_PATH;
return 1;
}
static int run(int argc, char *argv[]) {
int r;
log_show_color(true);
log_parse_environment();
log_open();
r = parse_argv(argc, argv);
if (r <= 0)
return r;
if (optind >= argc)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Path to resolve must be specified.");
for (int i = optind; i < argc; i++) {
_cleanup_free_ char *p = NULL;
r = path_make_absolute_cwd(argv[i], &p);
if (r < 0)
return log_error_errno(r, "Failed to make path '%s' absolute: %m", argv[i]);
_cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL;
r = path_pick(/* toplevel_path= */ NULL,
/* toplevel_fd= */ AT_FDCWD,
p,
&(PickFilter) {
.basename = arg_filter_basename,
.version = arg_filter_version,
.architecture = arg_filter_architecture,
.suffix = arg_filter_suffix,
.type_mask = arg_filter_type_mask,
},
arg_flags,
&result);
if (r < 0)
return log_error_errno(r, "Failed to pick version for '%s': %m", p);
if (r == 0)
return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No matching version for '%s' found.", p);
switch (arg_print) {
case PRINT_PATH:
fputs(result.path, stdout);
if (result.st.st_mode != MODE_INVALID && S_ISDIR(result.st.st_mode) && !endswith(result.path, "/"))
fputc('/', stdout);
fputc('\n', stdout);
break;
case PRINT_FILENAME: {
_cleanup_free_ char *fname = NULL;
r = path_extract_filename(result.path, &fname);
if (r < 0)
return log_error_errno(r, "Failed to extract filename from path '%s': %m", result.path);
puts(fname);
break;
}
case PRINT_VERSION:
if (!result.version)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No version information discovered.");
puts(result.version);
break;
case PRINT_TYPE:
if (result.st.st_mode == MODE_INVALID)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No inode type information discovered.");
puts(inode_type_to_string(result.st.st_mode));
break;
case PRINT_ARCHITECTURE:
if (result.architecture < 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No architecture information discovered.");
puts(architecture_to_string(result.architecture));
break;
case PRINT_TRIES:
if (result.tries_left == UINT_MAX)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No tries left/tries done information discovered.");
printf("+%u-%u", result.tries_left, result.tries_done);
break;
case PRINT_ALL: {
_cleanup_(table_unrefp) Table *t = NULL;
t = table_new_vertical();
if (!t)
return log_oom();
table_set_ersatz_string(t, TABLE_ERSATZ_NA);
r = table_add_many(
t,
TABLE_FIELD, "Path",
TABLE_PATH, result.path,
TABLE_FIELD, "Version",
TABLE_STRING, result.version,
TABLE_FIELD, "Type",
TABLE_STRING, result.st.st_mode == MODE_INVALID ? NULL : inode_type_to_string(result.st.st_mode),
TABLE_FIELD, "Architecture",
TABLE_STRING, result.architecture < 0 ? NULL : architecture_to_string(result.architecture));
if (r < 0)
return table_log_add_error(r);
if (result.tries_left != UINT_MAX) {
r = table_add_many(
t,
TABLE_FIELD, "Tries left",
TABLE_UINT, result.tries_left,
TABLE_FIELD, "Tries done",
TABLE_UINT, result.tries_done);
if (r < 0)
return table_log_add_error(r);
}
r = table_print(t, stdout);
if (r < 0)
return table_log_print_error(r);
break;
}
default:
assert_not_reached();
}
}
return 0;
}
DEFINE_MAIN_FUNCTION(run);