--- /dev/null
+/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
+
+#include <stdio.h>
+#include <getopt.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <assert.h>
+#include <sys/statfs.h>
+#include <sys/stat.h>
+#include <errno.h>
+#include <string.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <ctype.h>
+#include <limits.h>
+#include <blkid.h>
+
+/* TODO:
+ *
+ * - Implement --remove
+ * - Maybe write EFI variables as right-away?
+ * - Make backups of foreign files we overwrite
+ * - Generate loader.conf from /etc/os-release?
+ */
+
+static enum {
+ ACTION_STATUS,
+ ACTION_INSTALL,
+ ACTION_UPDATE,
+ ACTION_REMOVE
+} arg_action = ACTION_STATUS;
+
+static const char *arg_path = NULL;
+
+static int help(void) {
+
+ printf("%s [OPTIONS...]\n\n"
+ "Install, update or remove the Gummiboot EFI boot loader.\n\n"
+ " -h --help Show this help\n"
+ " --path=PATH Path to the EFI System Partition (ESP)\n"
+ " --install Install Gummiboot to the ESPs\n"
+ " --update Update Gummiboot in the ESP\n"
+ " --remove Remove Gummiboot from the ESP\n",
+ program_invocation_short_name);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+
+ enum {
+ ARG_INSTALL = 0x100,
+ ARG_UPDATE,
+ ARG_REMOVE,
+ ARG_PATH,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "install", no_argument, NULL, ARG_INSTALL },
+ { "update", no_argument, NULL, ARG_UPDATE },
+ { "remove", no_argument, NULL, ARG_REMOVE },
+ { "path", required_argument, NULL, ARG_PATH },
+ { NULL, 0, NULL, 0 }
+ };
+
+ int c;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) {
+
+ switch (c) {
+
+ case 'h':
+ help();
+ return 0;
+
+ case ARG_INSTALL:
+ arg_action = ACTION_INSTALL;
+ break;
+
+ case ARG_UPDATE:
+ arg_action = ACTION_UPDATE;
+ break;
+
+ case ARG_REMOVE:
+ arg_action = ACTION_REMOVE;
+ break;
+
+ case ARG_PATH:
+ arg_path = optarg;
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ fprintf(stderr, "Unknown option code '%c'.\n", c);
+ return -EINVAL;
+ }
+ }
+
+ if (argc > optind) {
+ fprintf(stderr, "Excess arguments.\n");
+ return -EINVAL;
+ }
+
+ return 1;
+}
+
+static int verify_esp(void) {
+ const char *p;
+ struct statfs sfs;
+ struct stat st, st2;
+ char *t;
+ blkid_probe b = NULL;
+ int r;
+ const char *v;
+
+ p = arg_path ? arg_path : "/boot";
+
+ if (statfs(p, &sfs) < 0) {
+ fprintf(stderr, "Failed to check file system type of %s: %m\n", p);
+ return -errno;
+ }
+
+ if (sfs.f_type != 0x4d44) {
+ fprintf(stderr, "%s is not a FAT/ESP file system.\n", p);
+ return -ENODEV;
+ }
+
+ if (stat(p, &st) < 0) {
+ fprintf(stderr, "Failed to determine block device node of %s: %m\n", p);
+ return -errno;
+ }
+
+ if (major(st.st_dev) == 0) {
+ fprintf(stderr, "Block device node of %p is invalid.\n", p);
+ return -ENODEV;
+ }
+
+ r = asprintf(&t, "%s/..", p);
+ if (r < 0) {
+ fprintf(stderr, "Out of memory.\n");
+ return -ENOMEM;
+ }
+
+ r = stat(t, &st2);
+ free(t);
+ if (r < 0) {
+ fprintf(stderr, "Failed to determine block device node of parent of %s: %m\n", p);
+ return -errno;
+ }
+
+ if (st.st_dev == st2.st_dev) {
+ fprintf(stderr, "%s is not the root of the ESP file system.\n", p);
+ return -ENODEV;
+ }
+
+ r = asprintf(&t, "/dev/block/%u:%u", major(st.st_dev), minor(st.st_dev));
+ if (r < 0) {
+ fprintf(stderr, "Out of memory.\n");
+ return -ENOMEM;
+ }
+
+ errno = 0;
+ b = blkid_new_probe_from_filename(t);
+ free(t);
+ if (!b) {
+ if (errno != 0) {
+ fprintf(stderr, "Failed to open file system %s: %s\n", p, strerror(errno));
+ return -errno;
+ }
+
+ fprintf(stderr, "Out of memory.\n");
+ return -ENOMEM;
+ }
+
+ blkid_probe_enable_superblocks(b, 1);
+ blkid_probe_set_superblocks_flags(b, BLKID_SUBLKS_TYPE);
+ blkid_probe_enable_partitions(b, 1);
+ blkid_probe_set_partitions_flags(b, BLKID_PARTS_ENTRY_DETAILS);
+
+ errno = 0;
+ r = blkid_do_safeprobe(b);
+ if (r == -2) {
+ fprintf(stderr, "%s file system is ambigious.\n", p);
+ r = -ENODEV;
+ goto fail;
+ } else if (r == 1) {
+ fprintf(stderr, "%s does not contain a file system label.\n", p);
+ r = -ENODEV;
+ goto fail;
+ } else if (r != 0) {
+ fprintf(stderr, "Failed to probe file system %s: %s\n", p, strerror(errno ? errno : EIO));
+ r = errno ? -errno : -EIO;
+ goto fail;
+ }
+
+ errno = 0;
+ r = blkid_probe_lookup_value(b, "TYPE", &v, NULL);
+ if (r != 0) {
+ fprintf(stderr, "Failed to probe file system type %s: %s\n", p, strerror(errno ? errno : EIO));
+ r = errno ? -errno : -EIO;
+ goto fail;
+ }
+
+ if (strcmp(v, "vfat") != 0) {
+ fprintf(stderr, "%s is not a FAT/ESP file system after all.\n", p);
+ r = -ENODEV;
+ goto fail;
+ }
+
+ errno = 0;
+ r = blkid_probe_lookup_value(b, "PART_ENTRY_SCHEME", &v, NULL);
+ if (r != 0) {
+ fprintf(stderr, "Failed to probe partition scheme %s: %s\n", p, strerror(errno ? errno : EIO));
+ r = errno ? -errno : -EIO;
+ goto fail;
+ }
+
+ if (strcmp(v, "gpt") != 0) {
+ fprintf(stderr, "%s is not on a GPT partition table.\n", p);
+ r = -ENODEV;
+ goto fail;
+ }
+
+ errno = 0;
+ r = blkid_probe_lookup_value(b, "PART_ENTRY_TYPE", &v, NULL);
+ if (r != 0) {
+ fprintf(stderr, "Failed to probe partition type UUID %s: %s\n", p, strerror(errno ? errno : EIO));
+ r = errno ? -errno : -EIO;
+ goto fail;
+ }
+
+ if (strcmp(v, "c12a7328-f81f-11d2-ba4b-00a0c93ec93b") != 0) {
+ fprintf(stderr, "%s is not an EFI System Partition (ESP).\n", p);
+ r = -ENODEV;
+ goto fail;
+ }
+
+ blkid_free_probe(b);
+ arg_path = p;
+ return 0;
+
+fail:
+ if (b)
+ blkid_free_probe(b);
+
+ return r;
+}
+
+static int get_file_version(FILE *f, char **v) {
+ size_t k;
+ char s[LINE_MAX], *e, *x;
+
+ assert(f);
+ assert(v);
+
+ for (;;) {
+ k = fread(s, 1, 17, f);
+ if (ferror(f)) {
+ fprintf(stderr, "Failed to read file: %m\n");
+ return -errno;
+ }
+
+ if (k < 17) {
+ *v = NULL;
+ return 0;
+ }
+
+ if (strncmp(s, "#### LoaderInfo: ", 17) == 0)
+ break;
+
+ if (fseek(f, -16, SEEK_CUR) < 0) {
+ fprintf(stderr, "Failed to seek backwards in: %m\n");
+ return -errno;
+ }
+ }
+
+ k = fread(s, 1, sizeof(s)-1, f);
+ if (ferror(f)) {
+ fprintf(stderr, "Failed to read file: %m\n");
+ return -errno;
+ }
+ s[k] = 0;
+
+ e = strstr(s, " ####");
+ if (!e) {
+ fprintf(stderr, "Malformed version string.\n");
+ return -EINVAL;
+ }
+
+ x = strndup(s, e - s);
+ if (!x) {
+ fprintf(stderr, "Out of memory.\n");
+ return -ENOMEM;
+ }
+
+ *v = x;
+ return 1;
+}
+
+static int enumerate_binaries(const char *path, const char *prefix) {
+ struct dirent *de;
+ char *p = NULL, *q = NULL;
+ DIR *d = NULL;
+ int r = 0, c = 0;
+
+ if (asprintf(&p, "%s/%s", arg_path, path) < 0) {
+ fprintf(stderr, "Out of memory.\n");
+ r = -ENOMEM;
+ goto finish;
+ }
+
+ d = opendir(p);
+ if (!d) {
+ if (errno == ENOENT) {
+ r = 0;
+ goto finish;
+ }
+
+ fprintf(stderr, "Failed to read %s: %m\n", p);
+ r = -errno;
+ goto finish;
+ }
+
+ while ((de = readdir(d))) {
+ char *v;
+ size_t n;
+ FILE *f;
+
+ if (de->d_name[0] == '.')
+ continue;
+
+ n = strlen(de->d_name);
+ if (n < 4 || strcasecmp(de->d_name + n - 4, ".efi") != 0)
+ continue;
+
+ if (prefix && strncasecmp(de->d_name, prefix, strlen(prefix)) != 0)
+ continue;
+
+ free(q);
+ q = NULL;
+ if (asprintf(&q, "%s/%s/%s", arg_path, path, de->d_name) < 0) {
+ fprintf(stderr, "Out of memory.\n");
+ r = -ENOMEM;
+ goto finish;
+ }
+
+ f = fopen(q, "re");
+ if (!f) {
+ fprintf(stderr, "Failed to open %s for reading: %m\n", q);
+ r = -errno;
+ goto finish;
+ }
+
+ r = get_file_version(f, &v);
+ fclose(f);
+
+ if (r < 0)
+ goto finish;
+
+ if (r == 0)
+ printf("%s (Unknown product and version)\n", q);
+ else
+ printf("%s (%s)\n", q, v);
+
+ c++;
+
+ free(v);
+ }
+
+ r = c;
+
+finish:
+ if (d)
+ closedir(d);
+
+ free(p);
+ free(q);
+
+ return r;
+}
+
+static int status_binaries(void) {
+ int r;
+
+ r = enumerate_binaries("EFI/gummiboot", NULL);
+ if (r == 0)
+ fprintf(stderr, "Gummiboot not installed to ESP.\n");
+ else if (r < 0)
+ return r;
+
+ r = enumerate_binaries("EFI/BOOT", "BOOT");
+ if (r == 0)
+ fprintf(stderr, "No fallback for removable devices installed to ESP.\n");
+ else if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int compare_product(const char *a, const char *b) {
+ size_t x, y;
+
+ assert(a);
+ assert(b);
+
+ x = strcspn(a, " ");
+ y = strcspn(b, " ");
+ if (x != y)
+ return x < y ? -1 : x > y ? 1 : 0;
+
+ return strncmp(a, b, x);
+}
+
+static int compare_version(const char *a, const char *b) {
+ assert(a);
+ assert(b);
+
+ a += strcspn(a, " ");
+ a += strspn(a, " ");
+ b += strcspn(b, " ");
+ b += strspn(b, " ");
+
+ return strverscmp(a, b);
+}
+
+static int version_check(FILE *f, const char *from, const char *to) {
+ FILE *g = NULL;
+ char *a = NULL, *b = NULL;
+ int r;
+
+ assert(f);
+ assert(from);
+ assert(to);
+
+ r = get_file_version(f, &a);
+ if (r < 0)
+ goto finish;
+ if (r == 0) {
+ r = -EINVAL;
+ fprintf(stderr, "Source file %s does not carry version information!\n", from);
+ goto finish;
+ }
+
+ g = fopen(to, "re");
+ if (!g) {
+ if (errno == ENOENT) {
+ r = 0;
+ goto finish;
+ }
+
+ r = -errno;
+ fprintf(stderr, "Failed to open %s for reading: %m\n", to);
+ goto finish;
+ }
+
+ r = get_file_version(g, &b);
+ if (r < 0)
+ goto finish;
+ if (r == 0 || compare_product(a, b) != 0) {
+ r = -EEXIST;
+ fprintf(stderr, "Not overwriting %s, since it's owned by another boot loader.\n", to);
+ goto finish;
+ }
+
+ if (compare_version(a, b) < 0) {
+ r = -EEXIST;
+ fprintf(stderr, "Not overwriting %s, since it's a newer boot loader version already.\n", to);
+ goto finish;
+ }
+
+ r = 0;
+
+finish:
+ free(a);
+ free(b);
+
+ if (g)
+ fclose(g);
+
+ return r;
+}
+
+static int copy_file(const char *from, const char *to) {
+ FILE *f = NULL, *g = NULL;
+ char *p = NULL;
+ int r;
+ struct timespec t[2];
+ struct stat st;
+
+ assert(from);
+ assert(to);
+
+ f = fopen(from, "re");
+ if (!f) {
+ fprintf(stderr, "Failed to open %s for reading: %m\n", from);
+ return -errno;
+ }
+
+ if (arg_action == ACTION_UPDATE) {
+ /* If this is an update, then let's compare versions first */
+
+ r = version_check(f, from, to);
+ if (r < 0)
+ goto finish;
+ }
+
+ if (asprintf(&p, "%s.tmp", to) < 0) {
+ fprintf(stderr, "Out of memory.\n");
+ r = -ENOMEM;
+ goto finish;
+ }
+
+ g = fopen(p, "wxe");
+ if (!g) {
+ fprintf(stderr, "Failed to open %s for writing: %m\n", to);
+ r = -errno;
+ goto finish;
+ }
+
+ rewind(f);
+ do {
+ size_t k;
+ uint8_t buf[32*1024];
+
+ k = fread(buf, 1, sizeof(buf), f);
+ if (ferror(f)) {
+ fprintf(stderr, "Failed to read %s: %m\n", from);
+ r = -errno;
+ goto finish;
+ }
+ if (k == 0)
+ break;
+
+ fwrite(buf, 1, k, g);
+ if (ferror(g)) {
+ fprintf(stderr, "Failed to write %s: %m\n", to);
+ r = -errno;
+ goto finish;
+ }
+ } while (!feof(f));
+
+ r = fstat(fileno(f), &st);
+ if (r < 0) {
+ fprintf(stderr, "Failed to get file timestamps of %s: %m", from);
+ r = -errno;
+ goto finish;
+ }
+
+ t[0] = st.st_atim;
+ t[1] = st.st_mtim;
+
+ r = futimens(fileno(g), t);
+ if (r < 0) {
+ fprintf(stderr, "Failed to change file timestamps for %s: %m", p);
+ r = -errno;
+ goto finish;
+ }
+
+ if (rename(p, to) < 0) {
+ fprintf(stderr, "Failed to rename %s to %s: %m\n", p, to);
+ r = -errno;
+ goto finish;
+ }
+
+ fprintf(stderr, "Copied %s to %s.\n", from, to);
+
+ free(p);
+ p = NULL;
+ r = 0;
+
+finish:
+ if (f)
+ fclose(f);
+ if (g)
+ fclose(g);
+
+ if (p) {
+ unlink(p);
+ free(p);
+ }
+
+ return r;
+}
+
+static char* strupper(char *s) {
+ char *p;
+
+ for (p = s; *p; p++)
+ *p = toupper(*p);
+
+ return s;
+}
+
+static int mkdir_one(const char *prefix, const char *suffix) {
+ char *p;
+
+ if (asprintf(&p, "%s/%s", prefix, suffix) < 0) {
+ fprintf(stderr, "Out of memory.\n");
+ return -ENOMEM;
+ }
+
+ if (mkdir(p, 0700) < 0) {
+ if (errno != EEXIST) {
+ fprintf(stderr, "Failed to create %s: %m\n", p);
+ free(p);
+ return -errno;
+ }
+ } else
+ fprintf(stderr, "Created %s.\n", p);
+
+ free(p);
+ return 0;
+}
+
+static int create_dirs(void) {
+ int r;
+
+ r = mkdir_one(arg_path, "EFI");
+ if (r < 0)
+ return r;
+
+ r = mkdir_one(arg_path, "EFI/gummiboot");
+ if (r < 0)
+ return r;
+
+ r = mkdir_one(arg_path, "EFI/BOOT");
+ if (r < 0)
+ return r;
+
+ r = mkdir_one(arg_path, "loader");
+ if (r < 0)
+ return r;
+
+ r = mkdir_one(arg_path, "loader/entries");
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int copy_one_file(const char *name) {
+ char *p = NULL, *q = NULL, *v = NULL;
+ int r;
+
+ if (asprintf(&p, "/usr/lib/gummiboot/%s", name) < 0) {
+ fprintf(stderr, "Out of memory.\n");
+ r = -ENOMEM;
+ goto finish;
+ }
+
+ if (asprintf(&q, "%s/EFI/gummiboot/%s", arg_path, name) < 0) {
+ fprintf(stderr, "Out of memory.\n");
+ r = -ENOMEM;
+ goto finish;
+ }
+
+ r = copy_file(p, q);
+
+ if (strncmp(name, "gummiboot", 9) == 0) {
+ int k;
+ /* Create the fallback names for removable devices */
+
+ if (asprintf(&v, "%s/EFI/BOOT/%s", arg_path, name + 5) < 0) {
+ fprintf(stderr, "Out of memory.\n");
+ r = -ENOMEM;
+ goto finish;
+ }
+ strupper(strrchr(v, '/') + 1);
+
+ k = copy_file(p, v);
+ if (k < 0 && r == 0) {
+ r = k;
+ goto finish;
+ }
+ }
+
+finish:
+ free(p);
+ free(q);
+ free(v);
+
+ return r;
+}
+
+static int install_binaries(void) {
+ struct dirent *de;
+ DIR *d;
+ int r = 0;
+
+ if (arg_action == ACTION_INSTALL) {
+ /* Don't create any of these directories when we are
+ * just updating. When we update we'll drop-in our
+ * files (unless there are newer ones already), but we
+ * won't create the directories for them in the first
+ * place. */
+
+ r = create_dirs();
+ if (r < 0)
+ return r;
+ }
+
+ d = opendir("/usr/lib/gummiboot");
+ if (!d) {
+ fprintf(stderr, "Failed to open /usr/lib/gummiboot: %s\n", strerror(errno));
+ return -errno;
+ }
+
+ while ((de = readdir(d))) {
+ size_t n;
+ int k;
+
+ if (de->d_name[0] == '.')
+ continue;
+
+ n = strlen(de->d_name);
+ if (n < 4 || strcmp(de->d_name + n - 4, ".efi") != 0)
+ continue;
+
+ k = copy_one_file(de->d_name);
+ if (k < 0 && r == 0)
+ r = k;
+ }
+
+ closedir(d);
+ return r;
+}
+
+static int remove_binaries(void) {
+
+ /* Remove /EFI/gummiboot/ entirely */
+
+ /* Remove /EFI/BOOT/BOOT*.EFI, but only if there's our version string inside */
+
+ return -ENOTSUP;
+}
+
+int main(int argc, char*argv[]) {
+ int r;
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ goto finish;
+
+ if (geteuid() != 0) {
+ fprintf(stderr, "Need to be root.\n");
+ r = -EPERM;
+ goto finish;
+ }
+
+ r = verify_esp();
+ if (r < 0)
+ goto finish;
+
+ fprintf(stderr, "Using EFI System Partition on %s.\n", arg_path);
+
+ switch (arg_action) {
+
+ case ACTION_STATUS:
+ r = status_binaries();
+ break;
+
+ case ACTION_INSTALL:
+ case ACTION_UPDATE:
+ umask(0002);
+
+ r = install_binaries();
+ break;
+
+ case ACTION_REMOVE:
+ r = remove_binaries();
+ break;
+ }
+
+finish:
+ return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
+}