blob: bf1b0a9ecbeaed698ac0f7e8f6cdcb5894ab3c61 [file] [log] [blame]
/*
* Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include <ctype.h>
#include <errno.h>
#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include "mobile_provider.h"
struct mobile_provider_db {
GHashTable *name2provider;
GHashTable *network2provider;
};
struct parser_state {
const gchar *filename;
int linenum;
GHashTable *name_table;
GHashTable *network_table;
gchar *country;
struct mobile_provider *provider;
struct mobile_apn *apn;
int nameindex;
int apnindex;
};
#define WARN(fmt, arg...) \
syslog(LOG_WARNING, "%s line %d: " fmt, state->filename, \
state->linenum, ## arg)
#ifdef DEBUG
#define DEBUG(fmt, arg...) printf(fmt, ## arg)
#else
#define DEBUG(fmt, arg...)
#endif
#define NORMAL_NETWORK_ID_LEN (6)
/************ Utility functions ************/
/*
* Normalize a network ID by appending a '0' to the end if
* the network ID consists of 5 digits. |output| is assumed
* to refer to a buffer of at least 7 bytes.
*/
static void normalize(const gchar *input, gchar *output, size_t size)
{
int len = strlen(input);
if (len > size - 1)
len = size - 1;
strncpy(output, input, len);
if (len == NORMAL_NETWORK_ID_LEN-1) {
output[len] = '0';
output[len+1] = '\0';
} else {
output[len] = '\0';
}
}
/*
* Normalize a list of network IDs. Each id that is changed
* replaces the original string in the list.
*/
static void normalize_list(gchar **netids)
{
int listlen = g_strv_length(netids);
int i;
for (i = 0; i < listlen; i++) {
gchar *id = netids[i];
if (strlen(netids[i])) {
netids[i] = g_malloc0(NORMAL_NETWORK_ID_LEN+1);
normalize(id, netids[i], NORMAL_NETWORK_ID_LEN+1);
g_free(id);
}
}
}
/*
* Extract a provider name from a string that may be of the form
* "<provider-name> (<extra-text>)". Modifies its argument.
*/
static char *extract_name(gchar *text)
{
char *paren = strchr(text, '(');
if (paren != NULL) {
*paren = '\0';
g_strchomp(text);
}
return g_strdup(text);
}
static void free_network_providers(gpointer key, gpointer value,
gpointer user_data)
{
GSList *list = (GSList *)value;
g_slist_free(list);
}
static void free_provider(gpointer key, gpointer value, gpointer user_data)
{
struct mobile_provider *provider = (struct mobile_provider *)value;
struct mobile_provider *next = provider->next;
int i, j;
/*
* A provider may be in the hash table under multiple names.
* Free the structure only when the last name is removed.
*/
while (provider != NULL) {
DEBUG("free_provider: %s / %s (%s)\n",
key == NULL ? "***" : (char *)key,
provider->names[0]->name, provider->country);
next = provider->next;
if (--provider->refcnt != 0) {
DEBUG("Not freeing %s / %s, refcnt = %d\n",
key == NULL ? "***" : (char *)key,
provider->names[0]->name,
provider->refcnt);
break;
}
for (i = 0; i < provider->num_apns; i++) {
for (j = 0; j < provider->apns[i]->num_names; j++) {
g_free(provider->apns[i]->names[j]->name);
g_free(provider->apns[i]->names[j]);
}
g_free(provider->apns[i]->names);
g_free(provider->apns[i]->value);
g_free(provider->apns[i]->username);
g_free(provider->apns[i]->password);
g_free(provider->apns[i]);
}
g_free(provider->apns);
for (i = 0; i < provider->num_names; i++) {
g_free(provider->names[i]->name);
g_free(provider->names[i]->lang);
g_free(provider->names[i]);
}
g_free(provider->names);
g_strfreev(provider->networks);
g_free(provider);
/*
* key is invalid after provider is freed
*/
key = NULL;
provider = next;
}
}
static void provider_iter_shim(gpointer key, gpointer value, gpointer user_data)
{
struct mobile_provider *provider = (struct mobile_provider *)value;
ProviderIterFunc func = (ProviderIterFunc)user_data;
func(provider);
}
static void network_iter_shim(gpointer key, gpointer value, gpointer user_data)
{
gchar *network = (gchar *)key;
struct mobile_provider *provider = (struct mobile_provider *)value;
NetworkIterFunc func = (NetworkIterFunc)user_data;
func(network, provider);
}
static void handle_provider(struct parser_state *state, gchar *text)
{
gchar **pfields;
int num_names;
int num_apns;
int primary;
int roaming;
struct mobile_provider *provider;
state->provider = NULL;
state->apn = NULL;
state->apnindex = 0;
state->nameindex = 0;
pfields = g_strsplit(text, ",", 0);
if (g_strv_length(pfields) != 4) {
WARN("Badly formed \"providers\" entry: \"%s\"", text);
g_strfreev(pfields);
return;
}
errno = 0;
num_names = strtol(pfields[0], NULL, 0);
num_apns = strtol(pfields[1], NULL, 0);
primary = strtol(pfields[2], NULL, 0);
roaming = strtol(pfields[3], NULL, 0);
if (errno != 0) {
WARN("Error parsing \"providers\" entry \"%s\"", text);
g_strfreev(pfields);
return;
}
provider = g_new0(struct mobile_provider, 1);
strncpy(provider->country, state->country, sizeof(provider->country)-1);
provider->num_names = num_names;
if (num_names != 0)
provider->names = g_new0(struct localized_name *, num_names);
provider->num_apns = num_apns;
provider->primary = primary != 0;
if (num_apns != 0)
provider->apns = g_new0(struct mobile_apn *, num_apns);
provider->requires_roaming = roaming;
state->provider = provider;
g_strfreev(pfields);
}
static void network_add_provider(GHashTable *network_table,
gchar *network_id,
struct mobile_provider *provider)
{
GSList *list;
list = g_hash_table_lookup(network_table, network_id);
list = g_slist_prepend(list, provider);
g_hash_table_insert(network_table, network_id, list);
}
static void handle_networks(struct parser_state *state, gchar *text)
{
int listlen;
int i;
struct mobile_provider *provider;
if (state->provider == NULL)
return;
provider = state->provider;
provider->networks = g_strsplit(text, ",", 0);
if (provider->networks != NULL) {
normalize_list(provider->networks);
listlen = g_strv_length(provider->networks);
for (i = 0; i < listlen; i++) {
network_add_provider(state->network_table,
provider->networks[i],
provider);
}
}
}
static void handle_apn(struct parser_state *state, gchar *text)
{
gchar **apnfields;
struct mobile_apn *apn;
struct mobile_provider *provider;
int num_names;
if (state->provider == NULL)
return;
state->apn = NULL;
provider = state->provider;
state->nameindex = 0;
apnfields = g_strsplit(text, ",", 0);
if (g_strv_length(apnfields) != 4) {
WARN("Badly formed \"apn\" entry: \"%s\"", text);
g_strfreev(apnfields);
return;
}
errno = 0;
num_names = strtol(apnfields[0], NULL, 0);
if (errno != 0) {
WARN("Error parsing \"apn\" entry \"%s\"", text);
g_strfreev(apnfields);
return;
}
apn = g_new0(struct mobile_apn, 1);
apn->num_names = num_names;
apn->value = g_strdup(apnfields[1]);
if (strlen(apnfields[2]) != 0)
apn->username = g_strdup(apnfields[2]);
if (strlen(apnfields[3]) != 0)
apn->password = g_strdup(apnfields[3]);
provider->apns[state->apnindex++] = apn;
state->apn = apn;
g_strfreev(apnfields);
}
static void handle_name(struct parser_state *state, gchar *text)
{
gchar **namefields;
struct localized_name *name = NULL;
struct mobile_provider *provider;
struct mobile_provider *other_provider;
struct mobile_apn *apn;
if (state->provider == NULL && state->apn == NULL)
return;
provider = state->provider;
apn = state->apn;
namefields = g_strsplit(text, ",", 2);
if (g_strv_length(namefields) != 2) {
WARN("Badly formed \"name\" entry: \"%s\"", text);
g_strfreev(namefields);
return;
}
name = g_new0(struct localized_name, 1);
if (strlen(namefields[0]) != 0)
name->lang = g_strdup(namefields[0]);
if (apn != NULL) {
if (apn->names == NULL)
apn->names = g_new0(struct localized_name *,
apn->num_names);
name->name = g_strdup(namefields[1]);
apn->names[state->nameindex++] = name;
} else if (provider != NULL) {
/*
* If this is the first name encountered for the provider,
* put the provider in the hash table under this name.
*/
if (state->nameindex == 0) {
name->name = extract_name(namefields[1]);
/*
* If a provider already exists under this name, but
* for a different country, then chain the new provider
* onto the end of a linked list.
*/
other_provider = g_hash_table_lookup(state->name_table,
name->name);
if (other_provider == NULL) {
DEBUG("Add new %s (%s)\n", name->name, provider->country);
g_hash_table_insert(state->name_table,
name->name, provider);
} else {
DEBUG("Chain %s (%s) to (%s)\n", name->name,
provider->country,
other_provider->country);
while (other_provider->next != NULL)
other_provider = other_provider->next;
other_provider->next = provider;
}
++provider->refcnt;
} else {
name->name = g_strdup(namefields[1]);
DEBUG("Additional name %s (%s) for %s\n",
name->name, provider->country,
provider->names[0]->name);
if (g_hash_table_lookup(state->name_table,
name->name) == NULL) {
g_hash_table_insert(state->name_table,
name->name, provider);
++provider->refcnt;
}
}
provider->names[state->nameindex++] = name;
}
g_strfreev(namefields);
}
/*********** Exported functions ***********/
struct mobile_provider_db *mobile_provider_open_db(const gchar *pathname)
{
char linebuf[BUFSIZ];
char *line;
size_t linelen;
gchar **fields;
FILE *dbfile;
struct mobile_provider_db *db = NULL;
int firstline = 1;
const gchar *fname;
struct parser_state *state;
dbfile = fopen(pathname, "r");
if (dbfile == NULL)
return NULL;
fname = strrchr(pathname, '/');
if (fname == NULL)
fname = pathname;
else
++fname;
state = g_new0(struct parser_state, 1);
state->filename = fname;
while ((line = fgets(linebuf, sizeof(linebuf), dbfile)) != NULL) {
++state->linenum;
linelen = strlen(line);
if (linelen <= 1 || line[0] == '#')
continue;
if (line[linelen-1] == '\n')
line[linelen-1] = '\0';
fields = g_strsplit(line, ":", 2);
if (g_strv_length(fields) != 2) {
WARN("Badly formed line: \"%s\"", line);
g_strfreev(fields);
continue;
}
/* Do a basic check to see whether we have a valid file */
if (firstline) {
char *errmsg = NULL;
if (strcmp(fields[0], "serviceproviders") != 0)
errmsg = "File does not begin with \"serviceproviders\" entry";
else if (strcmp(fields[1], "2.0") != 0)
errmsg = "Unrecognized serviceproviders format";
if (errmsg != NULL) {
WARN("%s", errmsg);
g_strfreev(fields);
g_free(state);
errno = ENOSYS;
fclose(dbfile);
return NULL;
}
firstline = 0;
state->name_table = g_hash_table_new(g_str_hash, g_str_equal);
state->network_table = g_hash_table_new(g_str_hash, g_str_equal);
} else if (strcmp(fields[0], "country") == 0) {
g_free(state->country);
state->country = g_strdup(fields[1]);
} else if (strcmp(fields[0], "provider") == 0) {
handle_provider(state, fields[1]);
} else if (strcmp(fields[0], "networks") == 0) {
handle_networks(state, fields[1]);
} else if (strcmp(fields[0], "apn") == 0) {
handle_apn(state, fields[1]);
} else if (strcmp(fields[0], "name") == 0) {
handle_name(state, fields[1]);
}
g_strfreev(fields);
}
fclose(dbfile);
if (state->country == NULL && state->provider == NULL) {
g_hash_table_destroy(state->name_table);
g_hash_table_destroy(state->network_table);
errno = ENOMSG;
} else {
g_free(state->country);
db = g_new0(struct mobile_provider_db, 1);
db->name2provider = state->name_table;
db->network2provider = state->network_table;
}
g_free(state);
return db;
}
void mobile_provider_close_db(struct mobile_provider_db *db)
{
if (db != NULL) {
g_hash_table_foreach(db->name2provider, free_provider, NULL);
g_hash_table_destroy(db->name2provider);
g_hash_table_foreach(db->network2provider, free_network_providers, NULL);
g_hash_table_destroy(db->network2provider);
g_free(db);
}
}
struct mobile_provider *network_find_provider(
const struct mobile_provider_db *db,
const gchar *network_id,
gboolean primary)
{
GSList *list;
if (db == NULL || network_id == NULL)
return NULL;
list = g_hash_table_lookup(db->network2provider, network_id);
for( ; list != NULL; list = list->next) {
struct mobile_provider *provider = list->data;
if (provider->primary == primary)
return provider;
}
return NULL;
}
struct mobile_provider *mobile_provider_lookup_by_network(
const struct mobile_provider_db *db,
const gchar *network_id)
{
char netid[NORMAL_NETWORK_ID_LEN+1];
struct mobile_provider *provider;
int primary;
if (db == NULL || network_id == NULL)
return NULL;
/*
* First try to find a match of a primary provider, if that
* fails, return the first matching non-primary provider.
*/
for (primary = 1; primary >= 0; primary--) {
/*
* Try the lookup of the MCC/MNC pair with 6 digits
* and then 5 digits, to account for the vagaries of
* whether the MNC is 2 or 3 digits, which is
* difficult to determine.
*/
normalize(network_id, netid, sizeof(netid));
provider = network_find_provider(db, netid, primary);
if (provider != NULL)
return provider;
normalize(network_id, netid, sizeof(netid) - 1);
provider = network_find_provider(db, netid, primary);
if (provider != NULL)
return provider;
}
return NULL;
}
struct mobile_provider *mobile_provider_lookup_by_name(
const struct mobile_provider_db *db,
const gchar *provider_name)
{
if (db != NULL && provider_name != NULL && strlen(provider_name) != 0)
return g_hash_table_lookup(db->name2provider, provider_name);
return NULL;
}
struct mobile_provider *mobile_provider_lookup_best_match(
const struct mobile_provider_db *db,
const gchar *provider_name,
const gchar *network_id)
{
struct mobile_provider *provider = NULL;
if (db == NULL)
return NULL;
if (provider_name != NULL)
provider = g_hash_table_lookup(db->name2provider,
provider_name);
/*
* We have a linked list of providers with the given name, but
* they may not be in the right country. Pick one with a
* matching MCC. If there are no networks listed for the provider,
* and there are no other providers with the same name, then return
* the result without doing any further checks.
*/
if (provider != NULL) {
/*
* If the provider has no networks listed, then return it
* if it is unambiguous, i.e., there's only one entry for
* that provider name.
*/
if (provider->networks == NULL || provider->networks[0] == NULL) {
if (provider->next == NULL)
return provider;
else {
syslog(LOG_WARNING,
"%s: Provider \"%s\" in country \"%s\" has no networks, "
"but has multiple entries.",
__func__, provider_name, provider->country);
return NULL;
}
} else {
for ( ; provider != NULL ; provider = provider->next) {
gchar **networks;
for (networks = provider->networks;
*networks != NULL;
networks++) {
if (strncmp(network_id, *networks, 3) == 0)
break;
}
if (*networks != NULL)
break;
}
}
}
if (provider ==NULL)
provider = mobile_provider_lookup_by_network(db, network_id);
return provider;
}
const gchar *mobile_provider_get_name(struct mobile_provider *provider)
{
if (provider != NULL && provider->num_names != 0)
return provider->names[0]->name;
return NULL;
}
void mobile_provider_foreach_provider(const struct mobile_provider_db *db,
ProviderIterFunc func)
{
g_hash_table_foreach(db->name2provider, provider_iter_shim, func);
}
void mobile_provider_foreach_network(const struct mobile_provider_db *db,
NetworkIterFunc func)
{
g_hash_table_foreach(db->network2provider, network_iter_shim, func);
}