/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
 * Copyright (C) 2009 Novell, Inc.
 * Author: Tambet Ingo (tambet@gmail.com).
 * Copyright (C) 2009 - 2019 Red Hat, Inc.
 * Copyright (C) 2012 Lanedo GmbH
 */

#include "src/core/nm-default-daemon.h"

#include "nm-service-providers.h"

typedef enum {
    PARSER_TOPLEVEL = 0,
    PARSER_COUNTRY,
    PARSER_PROVIDER,
    PARSER_METHOD_GSM,
    PARSER_METHOD_GSM_APN,
    PARSER_METHOD_CDMA,
    PARSER_DONE,
    PARSER_ERROR
} ParseContextState;

typedef struct {
    char                            *mccmnc;
    NMServiceProvidersGsmApnCallback callback;
    gpointer                         user_data;
    GCancellable                    *cancellable;
    GMarkupParseContext             *ctx;
    char                             buffer[4096];

    char             *text_buffer;
    ParseContextState state;

    gboolean mccmnc_matched;
    gboolean found_internet_apn;
    char    *apn;
    char    *username;
    char    *password;
    char    *gateway;
    char    *auth_method;
    GSList  *dns;
} ParseContext;

/*****************************************************************************/

static void
parser_toplevel_start(ParseContext *parse_context,
                      const char   *name,
                      const char  **attribute_names,
                      const char  **attribute_values)
{
    int i;

    if (strcmp(name, "serviceproviders") == 0) {
        for (i = 0; attribute_names && attribute_names[i]; i++) {
            if (strcmp(attribute_names[i], "format") == 0) {
                if (strcmp(attribute_values[i], "2.0")) {
                    g_warning("%s: mobile broadband provider database format '%s'"
                              " not supported.",
                              __func__,
                              attribute_values[i]);
                    parse_context->state = PARSER_ERROR;
                    break;
                }
            }
        }
    } else if (strcmp(name, "country") == 0) {
        parse_context->state = PARSER_COUNTRY;
    }
}

static void
parser_country_start(ParseContext *parse_context,
                     const char   *name,
                     const char  **attribute_names,
                     const char  **attribute_values)
{
    if (strcmp(name, "provider") == 0)
        parse_context->state = PARSER_PROVIDER;
}

static void
parser_provider_start(ParseContext *parse_context,
                      const char   *name,
                      const char  **attribute_names,
                      const char  **attribute_values)
{
    parse_context->mccmnc_matched = FALSE;
    if (strcmp(name, "gsm") == 0)
        parse_context->state = PARSER_METHOD_GSM;
    else if (strcmp(name, "cdma") == 0)
        parse_context->state = PARSER_METHOD_CDMA;
}

static void
parser_gsm_start(ParseContext *parse_context,
                 const char   *name,
                 const char  **attribute_names,
                 const char  **attribute_values)
{
    int i;

    if (strcmp(name, "network-id") == 0) {
        const char *mcc = NULL, *mnc = NULL;

        for (i = 0; attribute_names && attribute_names[i]; i++) {
            if (strcmp(attribute_names[i], "mcc") == 0)
                mcc = attribute_values[i];
            else if (strcmp(attribute_names[i], "mnc") == 0)
                mnc = attribute_values[i];
            if (mcc && strlen(mcc) && mnc && strlen(mnc)) {
                char *mccmnc = g_strdup_printf("%s%s", mcc, mnc);

                if (strcmp(mccmnc, parse_context->mccmnc) == 0)
                    parse_context->mccmnc_matched = TRUE;
                g_free(mccmnc);
                break;
            }
        }
    } else if (strcmp(name, "apn") == 0) {
        parse_context->found_internet_apn = FALSE;
        nm_clear_g_free(&parse_context->apn);
        nm_clear_g_free(&parse_context->username);
        nm_clear_g_free(&parse_context->password);
        nm_clear_g_free(&parse_context->gateway);
        nm_clear_g_free(&parse_context->auth_method);
        g_slist_free_full(parse_context->dns, g_free);
        parse_context->dns = NULL;

        for (i = 0; attribute_names && attribute_names[i]; i++) {
            if (strcmp(attribute_names[i], "value") == 0) {
                parse_context->state = PARSER_METHOD_GSM_APN;
                parse_context->apn   = g_strstrip(g_strdup(attribute_values[i]));
                break;
            }
        }
    }
}

static void
parser_gsm_apn_start(ParseContext *parse_context,
                     const char   *name,
                     const char  **attribute_names,
                     const char  **attribute_values)
{
    int i;

    if (strcmp(name, "usage") == 0) {
        for (i = 0; attribute_names && attribute_names[i]; i++) {
            if ((strcmp(attribute_names[i], "type") == 0)
                && (strcmp(attribute_values[i], "internet") == 0)) {
                parse_context->found_internet_apn = TRUE;
                break;
            }
        }
    } else if (strcmp(name, "authentication") == 0) {
        for (i = 0; attribute_names && attribute_names[i]; i++) {
            if (strcmp(attribute_names[i], "method") == 0) {
                nm_clear_g_free(&parse_context->auth_method);
                parse_context->auth_method = g_strstrip(g_strdup(attribute_values[i]));
                break;
            }
        }
    }
}

static void
parser_start_element(GMarkupParseContext *context,
                     const char          *element_name,
                     const char         **attribute_names,
                     const char         **attribute_values,
                     gpointer             user_data,
                     GError             **error)
{
    ParseContext *parse_context = user_data;

    nm_clear_g_free(&parse_context->text_buffer);

    switch (parse_context->state) {
    case PARSER_TOPLEVEL:
        parser_toplevel_start(parse_context, element_name, attribute_names, attribute_values);
        break;
    case PARSER_COUNTRY:
        parser_country_start(parse_context, element_name, attribute_names, attribute_values);
        break;
    case PARSER_PROVIDER:
        parser_provider_start(parse_context, element_name, attribute_names, attribute_values);
        break;
    case PARSER_METHOD_GSM:
        parser_gsm_start(parse_context, element_name, attribute_names, attribute_values);
        break;
    case PARSER_METHOD_GSM_APN:
        parser_gsm_apn_start(parse_context, element_name, attribute_names, attribute_values);
        break;
    case PARSER_METHOD_CDMA:
        break;
    case PARSER_ERROR:
        break;
    case PARSER_DONE:
        break;
    }
}

static void
parser_country_end(ParseContext *parse_context, const char *name)
{
    if (strcmp(name, "country") == 0) {
        nm_clear_g_free(&parse_context->text_buffer);
        parse_context->state = PARSER_TOPLEVEL;
    }
}

static void
parser_provider_end(ParseContext *parse_context, const char *name)
{
    if (strcmp(name, "provider") == 0) {
        nm_clear_g_free(&parse_context->text_buffer);
        parse_context->state = PARSER_COUNTRY;
    }
}

static void
parser_gsm_end(ParseContext *parse_context, const char *name)
{
    if (strcmp(name, "gsm") == 0) {
        nm_clear_g_free(&parse_context->text_buffer);
        parse_context->state = PARSER_PROVIDER;
    }
}

static void
parser_gsm_apn_end(ParseContext *parse_context, const char *name)
{
    if (strcmp(name, "username") == 0) {
        nm_clear_g_free(&parse_context->username);
        parse_context->username = g_steal_pointer(&parse_context->text_buffer);
    } else if (strcmp(name, "password") == 0) {
        nm_clear_g_free(&parse_context->password);
        parse_context->password = g_steal_pointer(&parse_context->text_buffer);
    } else if (strcmp(name, "dns") == 0) {
        parse_context->dns =
            g_slist_prepend(parse_context->dns, g_steal_pointer(&parse_context->text_buffer));
    } else if (strcmp(name, "gateway") == 0) {
        nm_clear_g_free(&parse_context->gateway);
        parse_context->gateway = g_steal_pointer(&parse_context->text_buffer);
    } else if (strcmp(name, "apn") == 0) {
        nm_clear_g_free(&parse_context->text_buffer);

        if (parse_context->mccmnc_matched && parse_context->found_internet_apn)
            parse_context->state = PARSER_DONE;
        else
            parse_context->state = PARSER_METHOD_GSM;
    }
}

static void
parser_cdma_end(ParseContext *parse_context, const char *name)
{
    if (strcmp(name, "cdma") == 0) {
        nm_clear_g_free(&parse_context->text_buffer);
        parse_context->state = PARSER_PROVIDER;
    }
}

static void
parser_end_element(GMarkupParseContext *context,
                   const char          *element_name,
                   gpointer             user_data,
                   GError             **error)
{
    ParseContext *parse_context = user_data;

    switch (parse_context->state) {
    case PARSER_TOPLEVEL:
        break;
    case PARSER_COUNTRY:
        parser_country_end(parse_context, element_name);
        break;
    case PARSER_PROVIDER:
        parser_provider_end(parse_context, element_name);
        break;
    case PARSER_METHOD_GSM:
        parser_gsm_end(parse_context, element_name);
        break;
    case PARSER_METHOD_GSM_APN:
        parser_gsm_apn_end(parse_context, element_name);
        break;
    case PARSER_METHOD_CDMA:
        parser_cdma_end(parse_context, element_name);
        break;
    case PARSER_ERROR:
        break;
    case PARSER_DONE:
        break;
    }
}

static void
parser_text(GMarkupParseContext *context,
            const char          *text,
            gsize                text_len,
            gpointer             user_data,
            GError             **error)
{
    ParseContext *parse_context = user_data;

    g_free(parse_context->text_buffer);
    parse_context->text_buffer = g_strdup(text);
}

static const GMarkupParser parser = {
    .start_element = parser_start_element,
    .end_element   = parser_end_element,
    .text          = parser_text,
    .passthrough   = NULL,
    .error         = NULL,
};

/*****************************************************************************/

static void
finish_parse_context(ParseContext *parse_context, GError *error)
{
    if (parse_context->callback) {
        if (error) {
            parse_context
                ->callback(NULL, NULL, NULL, NULL, NULL, NULL, error, parse_context->user_data);
        } else {
            parse_context->callback(parse_context->apn,
                                    parse_context->username,
                                    parse_context->password,
                                    parse_context->gateway,
                                    parse_context->auth_method,
                                    parse_context->dns,
                                    error,
                                    parse_context->user_data);
        }
    }

    g_free(parse_context->mccmnc);
    g_markup_parse_context_free(parse_context->ctx);

    g_free(parse_context->text_buffer);
    g_free(parse_context->apn);
    g_free(parse_context->username);
    g_free(parse_context->password);
    g_free(parse_context->gateway);
    g_free(parse_context->auth_method);
    g_slist_free_full(parse_context->dns, g_free);

    g_slice_free(ParseContext, parse_context);
}

static void read_next_chunk(GInputStream *stream, ParseContext *parse_context);

static void
stream_read_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
    GInputStream *stream        = G_INPUT_STREAM(source_object);
    ParseContext *parse_context = user_data;
    gssize        len;
    GError       *error = NULL;

    len = g_input_stream_read_finish(stream, res, &error);
    if (len == -1) {
        g_prefix_error(&error, "Error reading service provider database: ");
        finish_parse_context(parse_context, error);
        g_clear_error(&error);
        return;
    }

    if (len == 0) {
        g_set_error(&error,
                    NM_UTILS_ERROR,
                    NM_UTILS_ERROR_UNKNOWN,
                    "Operator ID '%s' not found in service provider database",
                    parse_context->mccmnc);
        finish_parse_context(parse_context, error);
        g_clear_error(&error);
        return;
    }

    if (!g_markup_parse_context_parse(parse_context->ctx, parse_context->buffer, len, &error)) {
        g_prefix_error(&error, "Error parsing service provider database: ");
        finish_parse_context(parse_context, error);
        g_clear_error(&error);
        return;
    }

    if (parse_context->state == PARSER_DONE) {
        finish_parse_context(parse_context, NULL);
        return;
    }

    read_next_chunk(stream, parse_context);
}

static void
read_next_chunk(GInputStream *stream, ParseContext *parse_context)
{
    g_input_stream_read_async(stream,
                              parse_context->buffer,
                              sizeof(parse_context->buffer),
                              G_PRIORITY_DEFAULT,
                              parse_context->cancellable,
                              stream_read_cb,
                              parse_context);
}

static void
file_read_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
    GFile                *file          = G_FILE(source_object);
    ParseContext         *parse_context = user_data;
    GFileInputStream     *stream;
    gs_free_error GError *error = NULL;

    stream = g_file_read_finish(file, res, &error);
    if (!stream) {
        g_prefix_error(&error, "Error opening service provider database: ");
        finish_parse_context(parse_context, error);
        return;
    }

    read_next_chunk(G_INPUT_STREAM(stream), parse_context);

    g_object_unref(stream);
}

/*****************************************************************************/

void
nm_service_providers_find_gsm_apn(const char                      *service_providers,
                                  const char                      *mccmnc,
                                  GCancellable                    *cancellable,
                                  NMServiceProvidersGsmApnCallback callback,
                                  gpointer                         user_data)
{
    GFile        *file;
    ParseContext *parse_context;

    parse_context              = g_slice_new0(ParseContext);
    parse_context->mccmnc      = g_strdup(mccmnc);
    parse_context->cancellable = cancellable;
    parse_context->callback    = callback;
    parse_context->user_data   = user_data;
    parse_context->ctx         = g_markup_parse_context_new(&parser, 0, parse_context, NULL);

    file = g_file_new_for_path(service_providers);

    g_file_read_async(file, G_PRIORITY_DEFAULT, cancellable, file_read_cb, parse_context);

    g_object_unref(file);
}
