/* boot-managed-by-snapd.c - APT JSON hook to tell users nicely that boot is managed by snapd
 *
 * Copyright (C) Canonical Ltd
 *
 * SPDX-License-Identifier: GPL-3.0
 */

#include <json-c/json_object.h>
#include <json-c/json_tokener.h>

#include <assert.h>
#include <libintl.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Exit codes for main()
enum {
    E_OK,         // No error
    E_NOSOCKET,   // The socket is missing
    E_WRITE,      // Failed to write to the JSON-RPC socket
    E_INVALIDMSG, // The message has invalid format
};

// Our response to the hello call from apt, we just statically dump it in there and
// ignore APT's version negotation mechanism since we only support 0.1 anyhow.
static const char HELLO_MSG[] =
    "{\"jsonrpc\":\"2.0\",\"id\":0,\"result\":{\"version\":\"0.1\"}}\n\n";

// All the packages we conflict against
static const char *CONFLICTS[] = {
    "grub-pc",
    "grub-efi-amd64-signed",
    "grub-efi-amd64",
    "grub-efi-arm64-signed",
    "grub-efi-arm64",
    "secureboot-db",
    "shim",
    "shim-signed",
    "linux-image",
    "linux-firmware",
    "nullboot",
    NULL,
};

// Helper to check if we conflict with a given package name
static bool conflicts(const char *name) {
    for (const char **conflict = CONFLICTS; *conflict; conflict++)
        if (strcmp(*conflict, name) == 0)
            return true;
    return false;
}

// Prints a more informative, translated error message.
static void log_fail(struct json_object *name) {
    fprintf(stderr, "\n");
    fprintf(stderr,
            gettext(
                "Cannot install %s on system as boot is managed by snapd.\n"),
            json_object_get_string(name));
}

// We receive a signal from APT that it failed, and want to act on it. The format
// can be looked up in the apt source package, `doc/json-hooks-protocol.md`, but
// for our purpose the interesting structure of the signal is:
//
// {"params": {"packages": [{"name": "string", mode: "string"}]}}
//
// By checking if mode == "install" and conflicts(name) we can check if we are
// installing a package that we conflict against and then log an error message.
static int handle_fail(struct json_object *object) {
    struct json_object *params;
    if (!json_object_object_get_ex(object, "params", &params))
        return fprintf(stderr, "Missing params array\n"), E_INVALIDMSG;
    struct json_object *packages;
    if (!json_object_object_get_ex(params, "packages", &packages))
        return fprintf(stderr, "Missing packages array\n"), E_INVALIDMSG;

    for (size_t i = 0, len = json_object_array_length(packages); i < len; i++) {
        struct json_object *package = json_object_array_get_idx(packages, i);
        struct json_object *name;
        if (!json_object_object_get_ex(package, "name", &name))
            return fprintf(stderr, "Package has no name\n"), E_INVALIDMSG;
        struct json_object *mode;
        if (!json_object_object_get_ex(package, "mode", &mode))
            return fprintf(stderr, "Package has no mode\n"), E_INVALIDMSG;
        if (strcmp(json_object_get_string(mode), "install") != 0)
            continue;

        // We will only print the message for the first name, and then exit
        // successfully, otherwise APT would add more errors about the hook
        // failing that would distract the user rather than provide input.
        //
        // One issue is that we do not know which of the packages was referenced
        // in the command-line, we may be complaining about a package down the
        // dependency chain somewhere when the user tries to e.g. apt install
        // shim-signed.
        if (conflicts(json_object_get_string(name)))
            return log_fail(name), 0;
    }

    return 0;
}

int main(int argc, char *argv[]) {
    if (argc > 1 && strcmp(argv[1], "--print") == 0) {
        for (const char **conflict = CONFLICTS; *conflict; conflict++)
            printf("%s\n", *conflict);
        return 0;
    }

    textdomain("boot-managed-by-snapd");

    // The socket fd is located in an APT_HOOK_SOCKET variable, parse it
    // and open read and write ends
    char *socket_env = getenv("APT_HOOK_SOCKET");
    if (socket_env == NULL)
        return perror("No socket"), E_NOSOCKET;
    int socket_fd = atoi(socket_env);
    if (socket_fd < 1)
        return perror("Invalid socket number"), E_NOSOCKET;

    FILE *fsocket = fdopen(socket_fd, "r");
    FILE *fsocketw = fdopen(socket_fd, "w");
    char *command = NULL;
    size_t command_len = 0;

    // The JSON-RPC protocol of APT is line based. Each message is on its own line and
    // separated by an empty line.
    struct json_tokener *tokener = json_tokener_new();
    while (getline(&command, &command_len, fsocket) != -1) {

        // Separator line. We do not strictly validate here.
        if (strcmp(command, "\n") == 0)
            continue;

        // Parse and validate the message
        json_tokener_reset(tokener);
        struct json_object *object = json_tokener_parse(command);
        struct json_object *method;

        if (object == NULL)
            return fprintf(stderr, "E: Invalid message '%s'", command), E_INVALIDMSG;
        if (!json_object_object_get_ex(object, "method", &method))
            return fprintf(stderr, "E: Invalid message '%s'", command), E_INVALIDMSG;

        // Signal/method dispatcher. This should be table-based dispatch if more methods
        // end up being implemented.
        if (strcmp(json_object_get_string(method), "org.debian.apt.hooks.hello") ==
            0) {
            if (fwrite(HELLO_MSG, sizeof(HELLO_MSG) - 1, 1, fsocketw) != 1)
                return perror("Failed to write"), E_WRITE;
            if (fflush(fsocketw))
                return perror("Failed to write"), E_WRITE;
        } else if (strcmp(json_object_get_string(method),
                          "org.debian.apt.hooks.install.fail") == 0) {
            int ret = handle_fail(object);
            if (ret)
                return ret;
        } else if (strcmp(json_object_get_string(method),
                          "org.debian.apt.hooks.bye") == 0) {
            break;
        }
    }
    return 0;
}
