Merge "Moved mobile_provider API into this repo."
diff --git a/Makefile.am b/Makefile.am
index 646d47c..fc27571 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1,8 +1,12 @@
+# 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.
+
 ## Process this file with automake to produce Makefile.in
 
-SP_XML_DB = serviceproviders.xml
+SUBDIRS = src
 
-dist_pkgdata_DATA = $(SP_XML_DB) serviceproviders.2.dtd
+dist_pkgdata_DATA = @PROVIDERDB_XML@ @PROVIDERDB_BFD@ serviceproviders.2.dtd
 
 EXTRA_DIST = mobile-broadband-provider-info.pc.in
 
@@ -10,5 +14,7 @@
 pkgconfig_DATA = mobile-broadband-provider-info.pc
 
 check-local:
-	xmllint --valid $(top_srcdir)/$(SP_XML_DB) 2>&1 > /dev/null || exit 1;
+	xmllint --valid --noout @PROVIDERDB_XML@ || exit 1;
 
+@PROVIDERDB_BFD@: @PROVIDERDB_XML@
+	xsltproc --nonet --output $@ @PROVIDERDB_CONVERTER@ $<
diff --git a/PRESUBMIT.cfg b/PRESUBMIT.cfg
new file mode 100644
index 0000000..a2140cd
--- /dev/null
+++ b/PRESUBMIT.cfg
@@ -0,0 +1,5 @@
+# This config file disables some of the ChromiumOS source style checks.
+# Comment out the disable-flags for any checks you want to leave enabled.
+
+[Hook Overrides]
+tab_check: false
diff --git a/configure.ac b/configure.ac
old mode 100755
new mode 100644
index 75df22b..7f630c6
--- a/configure.ac
+++ b/configure.ac
@@ -1,11 +1,32 @@
 AC_PREREQ(2.52)
 
-AC_INIT(mobile-broadband-provider-info, 20110511, dcbw@redhat.com, mobile-broadband-provider-info)
+AC_INIT([mobile-broadband-provider-info], [20110511],
+        [dcbw@redhat.com], [mobile-broadband-provider-info])
 AM_INIT_AUTOMAKE([-Wall -Werror foreign no-dist-gzip dist-bzip2])
 AM_MAINTAINER_MODE
 
+AC_PROG_CC
+AC_PROG_LIBTOOL
+
+AM_CONFIG_HEADER([config.h])
 AC_CONFIG_FILES([
 	Makefile
+        src/Makefile
 	mobile-broadband-provider-info.pc
 ])
+
+PKG_CHECK_MODULES([glib], [glib-2.0])
+AC_SUBST(glib_CFLAGS)
+AC_SUBST(glib_LIBS)
+
+PROVIDERDB_XML="serviceproviders.xml"
+PROVIDERDB_BFD="serviceproviders.bfd"
+PROVIDERDB_CONVERTER="convert-serviceproviders.xsl"
+AC_SUBST(PROVIDERDB_XML)
+AC_SUBST(PROVIDERDB_BFD)
+AC_SUBST(PROVIDERDB_CONVERTER)
+
+AC_ARG_ENABLE(tools, AC_HELP_STRING([--enable-tools], [enable testing tools]))
+AM_CONDITIONAL(TOOLS, test "${enable_tools}" = "yes")
+
 AC_OUTPUT
diff --git a/convert-serviceproviders.xsl b/convert-serviceproviders.xsl
new file mode 100644
index 0000000..0364ca6
--- /dev/null
+++ b/convert-serviceproviders.xsl
@@ -0,0 +1,105 @@
+<!--
+Connection Manager
+
+Copyright (C) 2011  The Chromium OS Authors. All rights reserved.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License version 2 as
+published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+-->
+
+<!--
+Generate an easy-to-parse table of APN's from
+the mobile providers database
+-->
+
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+<xsl:strip-space elements="country"/>
+
+<xsl:template match="provider">
+<xsl:if test="count(gsm[*])!=0">
+provider:<xsl:value-of select="count(name)"/>,<xsl:value-of select="count(gsm/apn)"/>,<xsl:choose>
+  <xsl:when test="@primary='true'">1</xsl:when><xsl:otherwise>0</xsl:otherwise>
+</xsl:choose>
+<xsl:apply-templates select="name"/><xsl:apply-templates select="gsm"/>
+</xsl:if>
+</xsl:template>
+
+<xsl:template match="name">
+name:<xsl:value-of select="@xml:lang"/>,<xsl:value-of select="."/>
+</xsl:template>
+
+<xsl:template match="gsm">
+networks:<xsl:apply-templates select="network-id"/>
+<xsl:apply-templates select="apn"/></xsl:template>
+
+<xsl:template match="network-id"><xsl:value-of select="@mcc"/><xsl:value-of select="@mnc"/><xsl:if test="position()!=last()">,</xsl:if></xsl:template>
+
+<xsl:template match="apn">
+apn:<xsl:value-of select="count(name)"/>,<xsl:value-of select="@value"/>,<xsl:value-of select="username"/>,<xsl:value-of select="password"/>
+<xsl:apply-templates select="name"/>
+</xsl:template>
+
+<xsl:output method="text" indent="no" encoding="utf-8"
+  omit-xml-declaration="yes"/>
+
+<xsl:template match="/serviceproviders"># Generated file --  Do not edit
+#
+# This file starts with the line
+#
+#    serviceproviders:<xsl:value-of select="@format"/>
+#
+# The rest of the file is divided into country blocks, each of which
+# is of the form
+#
+#    country:xx
+#    &lt;provider block 1&gt;
+#    &lt;provider block 2&gt;
+#     . . .
+#    &lt;provider block N&gt;
+#
+# where xx is the two character ISO country code.
+#
+# Each provider block is of the form
+#
+#   provider:&lt;# of names&gt;,&lt;# of APNs&gt;,&lt;primary&gt;
+#   name:&lt;lang&gt;,&lt;name 1&gt;
+#   name:&lt;lang&gt;,&lt;name 2&gt;
+#   . . .
+#   networks:NNNNNN,NNNNNN,...
+#   apn:&lt;# of descriptive names&gt;,&lt;value&gt;,&lt;username&gt;,&lt;password&gt;
+#   name:&lt;lang&gt;,&lt;desciptive name 1&gt;
+#   name:&lt;lang&gt;,&lt;desciptive name 2&gt;
+#   . . .
+#   apn:&lt;# of descriptive names&gt;,&lt;value&gt;,&lt;username&gt;,&lt;password&gt;
+#   . . .
+#
+# where each NNNNNN is the five or six digit string constructed by
+# concatenating the three-digit MCC (Mobile Country Code) and two-
+# or three-digit MNC (Mobile Network Code), and where the &lt;username&gt;
+# and &lt;password&gt; fields may be empty. Each 'name' entry for both
+# providers and APNs, has an optional language attribute that can
+# be used to choose a localized name to display to the user, based
+# on the current locale. The language tags are in the format defined
+# by RFC 5646.
+#
+serviceproviders:<xsl:value-of select="@format"/> 
+<xsl:text>
+</xsl:text>
+<xsl:for-each select="./country">country:<xsl:value-of select="@code"/>
+<xsl:apply-templates/><xsl:text>
+</xsl:text>
+</xsl:for-each>
+</xsl:template>
+</xsl:stylesheet>
+
+<!-- vim:set sw=2 sts=2 et: -->
diff --git a/src/Makefile.am b/src/Makefile.am
new file mode 100644
index 0000000..1c7b35f
--- /dev/null
+++ b/src/Makefile.am
@@ -0,0 +1,23 @@
+# 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.
+
+AM_CPPFLAGS = @glib_CFLAGS@ -I.
+
+include_HEADERS = mobile_provider.h
+COMMON_LIBS = libmobile-provider.la @glib_LIBS@
+
+lib_LTLIBRARIES = libmobile-provider.la
+libmobile_provider_la_SOURCES = mobile_provider.c
+
+if TOOLS
+bin_PROGRAMS = browsedb
+else
+noinst_PROGRAMS = browsedb
+endif
+browsedb_SOURCES = browsedb.c
+browsedb_LDADD = $(COMMON_LIBS)
+
+check_PROGRAMS = mobile_provider_unittest
+mobile_provider_unittest_SOURCES = mobile_provider_unittest.c
+mobile_provider_unittest_LDADD = $(COMMON_LIBS) -lgtest
diff --git a/src/browsedb.c b/src/browsedb.c
new file mode 100644
index 0000000..15ee05b
--- /dev/null
+++ b/src/browsedb.c
@@ -0,0 +1,147 @@
+/*
+ * 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 "mobile_provider.h"
+
+void print_provider(struct mobile_provider *provider)
+{
+  struct mobile_apn *apn;
+  gchar *networks;
+  int i, j;
+  int num_networks;
+  int namelen, maxnamelen;
+  int userlen, maxuserlen;
+  int passlen, maxpasslen;
+
+  num_networks = g_strv_length(provider->networks);
+  printf("\"\033[1;33m%s\033[m\" in country \"\033[1;33m%s\033[m\" has "
+           "%d network ID%s and %d APN%s%s\n",
+         provider->names[0]->name,
+         provider->country,
+         num_networks,
+         num_networks == 1 ? "" : "s",
+         provider->num_apns,
+         provider->num_apns == 1 ? "" : "s",
+         provider->primary ? " [\033[1;31mprimary\033[m]" : "");
+  for (i = 1; i < provider->num_names; i++)
+    if (provider->names[i]->lang == NULL)
+      printf("  name: %s\n", provider->names[i]->name);
+    else
+      printf("  name: %s lang: %s\n",
+             provider->names[i]->name,
+             provider->names[i]->lang);
+  networks = g_strjoinv(",", provider->networks);
+  printf("  networks: %s\n", networks);
+  g_free(networks);
+  maxnamelen = -1;
+  maxuserlen = maxpasslen = strlen("<none>");
+  for (i = 0; i < provider->num_apns; i++) {
+    apn = provider->apns[i];
+    namelen = strlen(apn->value);
+    if (namelen > maxnamelen)
+      maxnamelen = namelen;
+    if (apn->username && (userlen = strlen(apn->username)) > maxuserlen)
+      maxuserlen = userlen;
+    if (apn->password && (passlen = strlen(apn->password)) > maxpasslen)
+      passlen = passlen;
+  }
+  if (maxuserlen == -1)
+    maxuserlen = strlen("<none>");
+  if (maxpasslen == -1)
+    maxpasslen = strlen("<none>");
+  for (i = 0; i < provider->num_apns; i++) {
+    apn = provider->apns[i];
+    printf("  APN %d: \033[1;32m%-*s\033[m  username %-*s  password %-*s\n",
+           i+1,
+           maxnamelen, apn->value,
+           maxuserlen, apn->username ? apn->username : "<none>",
+           maxpasslen, apn->password ? apn->password : "<none>");
+    for (j = 0; j < apn->num_names; j++)
+      if (apn->names[j]->lang == NULL)
+        printf("    name: %s\n", apn->names[j]->name);
+      else
+        printf("    name: %s lang; %s\n",
+               apn->names[j]->name,
+               apn->names[j]->lang);
+  }
+}
+
+void print_provider_list(struct mobile_provider *provider)
+{
+  while (provider != NULL) {
+    print_provider(provider);
+    provider = provider->next;
+  }
+  printf("---------------------------------------------------------------\n");
+}
+
+void print_network(gchar *network, struct mobile_provider *provider)
+{
+  printf("[%s]: ", network);
+  print_provider(provider);
+  printf("---------------------------------------------------------------\n");
+}
+
+int main(int argc, const char *argv[])
+{
+  char linebuf[BUFSIZ];
+  char *line;
+  struct mobile_provider *provider;
+  struct mobile_provider_db *provider_db;
+
+  if (argc < 2) {
+    fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
+    exit(1);
+  }
+  provider_db = mobile_provider_open_db(argv[1]);
+  if (provider_db == NULL) {
+    perror("Cannot open provider database");
+    exit(1);
+  }
+  while (1) {
+    int linelen;
+    printf("Type a network ID or provider name: ");
+    line = fgets(linebuf, sizeof(linebuf), stdin);
+    if (line == NULL || line[0] == '\n')
+      break;
+    linelen = strlen(line);
+    if (line[linelen-1] == '\n')
+      line[--linelen] = '\0';
+    if ((linelen == 5 || linelen == 6) &&
+        isdigit(line[0]) && isdigit(line[1]) && isdigit(line[linelen-1])) {
+      provider = mobile_provider_lookup_by_network(provider_db,
+                                                   line);
+      if (provider != NULL)
+        print_provider(provider);
+    } else if (line[0] == '*') {
+      if (line[1] == 'p')
+        mobile_provider_foreach_provider(provider_db,
+                                         print_provider_list);
+      else if (line[1] == 'n')
+        mobile_provider_foreach_network(provider_db,
+                                        print_network);
+      else
+        printf("error: Unknown command \"%s\"\n", line);
+      continue;
+    } else {
+      provider = mobile_provider_lookup_by_name(provider_db,
+                                                line);
+      if (provider != NULL)
+        print_provider_list(provider);
+    }
+    if (provider == NULL)
+      printf("No provider found for %s\n", line);
+  }
+  mobile_provider_close_db(provider_db);
+  return 0;
+}
diff --git a/src/mobile_provider.c b/src/mobile_provider.c
new file mode 100644
index 0000000..79e9bf1
--- /dev/null
+++ b/src/mobile_provider.c
@@ -0,0 +1,581 @@
+/*
+ * 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;
+  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) != 3) {
+    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);
+  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);
+  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;
+        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);
+  }
+  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);
+  }
+}
+
+
+static struct mobile_provider *network_find_provider(
+    const struct mobile_provider_db *db,
+    const gchar *network_id,
+    gboolean primary)
+{
+  GSList *list;
+
+  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 (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->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);
+}
diff --git a/src/mobile_provider.h b/src/mobile_provider.h
new file mode 100644
index 0000000..ce81942
--- /dev/null
+++ b/src/mobile_provider.h
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+
+#ifndef __MOBILE_PROVIDER_H
+#define __MOBILE_PROVIDER_H
+
+#include <glib.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct localized_name {
+  gchar *name;
+  gchar *lang;
+};
+
+struct mobile_apn {
+  gchar *value;
+  int num_names;
+  struct localized_name **names;
+  gchar *username;
+  gchar *password;
+};
+
+struct mobile_provider
+{
+  /*
+   * the "next" field links providers with
+   * same name in different countries
+   */
+  struct mobile_provider *next;
+  gchar country[3];
+  gchar **networks;
+  int num_names;
+  struct localized_name **names;
+  int num_apns;
+  struct mobile_apn **apns;
+  int refcnt;
+  gboolean primary;
+};
+
+struct mobile_provider_db;
+
+typedef void (*ProviderIterFunc)(struct mobile_provider *);
+typedef void (*NetworkIterFunc)(gchar *, struct mobile_provider *);
+
+struct mobile_provider_db *mobile_provider_open_db(const gchar *pathname);
+void mobile_provider_close_db(struct mobile_provider_db *db);
+struct mobile_provider *mobile_provider_lookup_by_name(
+    const struct mobile_provider_db *db,
+    const gchar *provider_name);
+struct mobile_provider *mobile_provider_lookup_by_network(
+    const struct mobile_provider_db *db,
+    const gchar *network_id);
+struct mobile_provider *mobile_provider_lookup_best_match(
+    const struct mobile_provider_db *db,
+    const gchar *provider_name,
+    const gchar *network_id);
+const gchar *mobile_provider_get_name(struct mobile_provider *provider);
+void mobile_provider_foreach_provider(
+    const struct mobile_provider_db *db,
+    ProviderIterFunc func);
+void mobile_provider_foreach_network(
+    const struct mobile_provider_db *db,
+    NetworkIterFunc func);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __MOBILE_PROVIDER_H */
diff --git a/src/mobile_provider_unittest.c b/src/mobile_provider_unittest.c
new file mode 100644
index 0000000..7af7d37
--- /dev/null
+++ b/src/mobile_provider_unittest.c
@@ -0,0 +1,458 @@
+/*
+ * 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 <glib.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "mobile_provider.h"
+
+const char *testfile = "/tmp/providers_test_data";
+
+typedef struct {
+  const char *filename;
+  struct mobile_provider_db *provider_db;
+} TestFixture;
+
+static char *test_db =
+"serviceproviders:2.0\n"
+"country:at\n"
+"provider:1,2,0\n"
+"name:,A1/Telekom Austria\n"
+"networks:23201\n"
+"apn:1,a1.net,ppp@a1plus.at,ppp\n"
+"name:,A1 Breitband\n"
+"apn:1,aon.data,,ppp\n"
+"name:,aon (Flex, Breitband-Duo, BusinessFlex)\n"
+"provider:1,3,0\n"
+"name:,T-Mobile\n"
+"networks:23203\n"
+"apn:1,gprswap,t-mobile,tm\n"
+"name:,WAP\n"
+"apn:1,gprsinternet,t-mobile,tm\n"
+"name:,Internet\n"
+"apn:1,business.gprsinternet,t-mobile,tm\n"
+"name:,Business Internet\n"
+"country:ca\n"
+"provider:1,1,0\n"
+"name:,Vidéotron\n"
+"networks:302500,302510\n"
+"apn:1,ihvm.videotron,,\n"
+"name:,IHVM\n"
+"country:gb\n"
+"provider:1,1,0\n"
+"name:,T-Mobile (Great Britain)\n"
+"networks:23430\n"
+"apn:0,general.t-mobile.uk,User,mms\n"
+"provider:1,1,0\n"
+"name:,3\n"
+"networks:23499\n"
+"apn:0,three.uk,,\n"
+"country:ie\n"
+"provider:2,1,0\n"
+"name:,Cubic\n"
+"name:,CUBIC\n"
+"networks:\n"
+"apn:0,mtoom.com,mtoom,mtoom\n"
+"country:ir\n"
+"provider:1,1,0\n"
+"name:,همراه اول\n"
+"networks:43211\n"
+"apn:0,mcinet,,\n"
+"provider:1,1,0\n"
+"name:,ایرانسل\n"
+"networks:43235\n"
+"apn:0,mtnirancell,,\n"
+"country:py\n"
+"provider:1,2,0\n"
+"name:,Tigo\n"
+"networks:74404\n"
+"apn:1,internet.tigo.py,,\n"
+"name:,Internet\n"
+"apn:2,broadband.tigo.py,tigo,tigo\n"
+"name:,Broadband\n"
+"name:es,Banda Ancha Móvil\n"
+"country:ru\n"
+"provider:2,1,0\n"
+"name:,BaikalWestCom\n"
+"name:ru,БайкалВестКом\n"
+"networks:25012\n"
+"apn:0,inet.bwc.ru,bwc,bwc\n"
+"country:us\n"
+"provider:1,1,0\n"
+"name:,3\n"
+"networks:31099\n"
+"apn:0,three.com,,\n"
+"provider:1,4,0\n"
+"name:,AT&T\n"
+"networks:310038,310090,310150,310410,310560,310680\n"
+"apn:0,Broadband,,\n"
+"apn:1,wap.cingular,,\n"
+"name:,MEdia Net\n"
+"apn:1,isp.cingular,,\n"
+"name:,Data Connect\n"
+"apn:1,ISP.CINGULAR,ISPDA@CINGULARGPRS.COM,CINGULAR1\n"
+"name:,Data Connect (Accelerated)\n"
+"provider:1,4,1\n"
+"name:,T-Mobile\n"
+"networks:310160,310200,310210,310220,310230,310240,310250,310260,310270,"
+         "310310,310490,31058,310660,310800\n"
+"apn:1,epc.tmobile.com,,\n"
+"name:,Internet/WebConnect\n"
+"apn:1,wap.voicestream.com,,\n"
+"name:,Web2Go/t-zones\n"
+"apn:1,internet2.voicestream.com,,\n"
+"name:,Internet (old)\n"
+"apn:1,internet3.voicestream.com,,\n"
+"name:,Internet with VPN (old)\n"
+"provider:1,1,0\n"
+"name:,T-Mobile MVNO\n"
+"networks:310160,310200,310210,310220,310230,310240,310250,310260,310270,"
+         "310310,310490,31058,310660,310800\n"
+"apn:1,mvno.tmobile.com,,\n"
+"name:,MVNO Internet/WebConnect\n"
+;
+
+static char *test_db_ambiguous =
+"serviceproviders:2.0\n"
+"country:gb\n"
+"provider:1,1,0\n"
+"name:,T-Mobile (Great Britain)\n"
+"networks:23430\n"
+"apn:0,general.t-mobile.uk,User,mms\n"
+"provider:1,1,0\n"
+"name:,3\n"
+"networks:23499\n"
+"apn:0,three.uk,,\n"
+"country:ie\n"
+"provider:2,1,0\n"
+"name:,Cubic\n"
+"name:,CUBIC\n"
+"networks:\n"
+"apn:0,mtoom.com,mtoom,mtoom\n"
+"country:ir\n"
+"provider:1,1,0\n"
+"name:,همراه اول\n"
+"networks:43211\n"
+"apn:0,mcinet,,\n"
+"provider:1,1,0\n"
+"name:,ایرانسل\n"
+"networks:43235\n"
+"apn:0,mtnirancell,,\n"
+"country:py\n"
+"provider:2,1,0\n"
+"name:,Cubic\n"
+"name:,CUBIC\n"
+"networks:\n"
+"apn:0,moot.com,moot,moot\n"
+"provider:1,2,0\n"
+"name:,Tigo\n"
+"networks:74404\n"
+"apn:1,internet.tigo.py,,\n"
+"name:,Internet\n"
+"apn:2,broadband.tigo.py,tigo,tigo\n"
+"name:,Broadband\n"
+"name:es,Banda Ancha Móvil\n"
+"country:ru\n"
+"provider:2,1,0\n"
+"name:,BaikalWestCom\n"
+"name:ru,БайкалВестКом\n"
+"networks:25012\n"
+"apn:0,inet.bwc.ru,bwc,bwc\n"
+;
+
+void setup_data(const char *file, const char *test_data)
+{
+  gboolean success = g_file_set_contents(file, test_data, -1, NULL);
+  g_assert(success == TRUE);
+}
+
+void test_fixture_setup(TestFixture *fixture, gconstpointer test_data)
+{
+  g_assert(fixture->provider_db == NULL);
+  if (fixture->filename == NULL) {
+    setup_data(testfile, test_data);
+    fixture->filename = testfile;
+  }
+  fixture->provider_db = mobile_provider_open_db(testfile);
+  g_assert(fixture->provider_db != NULL);
+}
+
+void test_fixture_teardown(TestFixture *fixture, gconstpointer test_data)
+{
+  g_assert(fixture->provider_db != NULL);
+  mobile_provider_close_db(fixture->provider_db);
+  fixture->provider_db = NULL;
+}
+
+void test_lookup_by_name(TestFixture *fixture, gconstpointer test_data)
+{
+  struct mobile_provider *provider;
+
+  provider = mobile_provider_lookup_by_name(fixture->provider_db, "AT&T");
+  g_assert(provider != NULL);
+  g_assert_cmpint(provider->num_names, ==, 1);
+  g_assert_cmpstr(provider->names[0]->name, ==, "AT&T");
+  g_assert_cmpstr(provider->country, ==, "us");
+  g_assert_cmpint(provider->num_apns, ==, 4);
+  g_assert_cmpint(g_strv_length(provider->networks), ==, 6);
+  g_assert_cmpstr(provider->networks[2], ==, "310150");
+  g_assert_cmpstr(provider->apns[3]->value, ==, "ISP.CINGULAR");
+  g_assert_cmpstr(provider->apns[3]->username, ==, "ISPDA@CINGULARGPRS.COM");
+  g_assert_cmpstr(provider->apns[3]->password, ==, "CINGULAR1");
+}
+
+void test_lookup_by_mvno(TestFixture *fixture, gconstpointer test_data)
+{
+  struct mobile_provider *provider;
+
+  provider = mobile_provider_lookup_by_name(fixture->provider_db,
+                                            "T-Mobile MVNO");
+  g_assert(provider != NULL);
+  g_assert_cmpint(provider->primary, ==, 0);
+  g_assert_cmpint(provider->num_names, ==, 1);
+  g_assert_cmpstr(provider->names[0]->name, ==, "T-Mobile MVNO");
+  g_assert_cmpstr(provider->country, ==, "us");
+  g_assert_cmpint(provider->num_apns, ==, 1);
+  g_assert_cmpint(g_strv_length(provider->networks), ==, 14);
+  g_assert_cmpstr(provider->apns[0]->value, ==, "mvno.tmobile.com");
+  g_assert(provider->apns[0]->username == NULL);
+  g_assert(provider->apns[0]->password == NULL);
+}
+
+void test_lookup_by_network(TestFixture *fixture, gconstpointer test_data)
+{
+  struct mobile_provider *provider;
+
+  provider = mobile_provider_lookup_by_network(fixture->provider_db, "31026");
+  g_assert(provider != NULL);
+  g_assert_cmpint(provider->primary, ==, 1);
+  g_assert_cmpint(provider->num_names, ==, 1);
+  g_assert_cmpstr(provider->names[0]->name, ==, "T-Mobile");
+  g_assert_cmpstr(provider->country, ==, "us");
+  g_assert_cmpint(provider->num_apns, ==, 4);
+  g_assert_cmpint(g_strv_length(provider->networks), ==, 14);
+  g_assert_cmpstr(provider->apns[0]->value, ==, "epc.tmobile.com");
+  g_assert_cmpstr(provider->apns[1]->value, ==, "wap.voicestream.com");
+  g_assert(provider->apns[0]->username == NULL);
+  g_assert(provider->apns[1]->password == NULL);
+}
+
+void test_other_network_id(TestFixture *fixture, gconstpointer test_data)
+{
+  struct mobile_provider *provider;
+  struct mobile_provider *provider1;
+
+  provider = mobile_provider_lookup_by_network(fixture->provider_db, "310260");
+  g_assert(provider != NULL);
+  provider1 = mobile_provider_lookup_by_network(fixture->provider_db, "310210");
+  g_assert(provider1 == provider);
+}
+
+void test_normalize_network_id(TestFixture *fixture, gconstpointer test_data)
+{
+  struct mobile_provider *provider;
+  struct mobile_provider *provider1;
+
+  provider = mobile_provider_lookup_by_network(fixture->provider_db, "31058");
+  g_assert(provider != NULL);
+  provider1 = mobile_provider_lookup_by_network(fixture->provider_db, "310580");
+  g_assert(provider1 == provider);
+}
+
+void test_no_network_id(TestFixture *fixture, gconstpointer test_data)
+{
+  struct mobile_provider *provider;
+
+  /* In the following, the network ID argument will be ignored */
+  provider = mobile_provider_lookup_best_match(fixture->provider_db,
+                                               "Cubic", "11111");
+  g_assert(provider != NULL);
+  g_assert(provider->next == NULL);
+  g_assert_cmpstr(provider->country, ==, "ie");
+  g_assert_cmpint(provider->num_names, ==, 2);
+  g_assert_cmpint(provider->num_apns, ==, 1);
+}
+
+void test_no_network_id_ambiguous(TestFixture *fixture,
+                                  gconstpointer test_data)
+{
+  struct mobile_provider *provider;
+
+  /* In the following, the network ID argument will be ignored */
+  provider = mobile_provider_lookup_best_match(fixture->provider_db,
+                                               "Cubic", "11111");
+  g_assert(provider == NULL);
+}
+
+void test_multi_country(TestFixture *fixture, gconstpointer test_data)
+{
+  struct mobile_provider *provider;
+
+  provider = mobile_provider_lookup_by_name(fixture->provider_db, "T-Mobile");
+  g_assert(provider != NULL);
+  g_assert_cmpstr(provider->country, ==, "at");
+  g_assert(provider->next != NULL);
+  g_assert_cmpstr(provider->next->country, ==, "gb");
+  g_assert(provider->next->next != NULL);
+  g_assert_cmpstr(provider->next->next->country, ==, "us");
+}
+
+void test_non_ascii_names(TestFixture *fixture, gconstpointer test_data)
+{
+  struct mobile_provider *provider;
+
+  provider = mobile_provider_lookup_by_name(fixture->provider_db, "Vidéotron");
+  g_assert(provider != NULL);
+  g_assert_cmpint(provider->num_names, ==, 1);
+  g_assert_cmpstr(provider->names[0]->name, ==, "Vidéotron");
+  g_assert_cmpstr(provider->country, ==, "ca");
+
+  provider = mobile_provider_lookup_by_name(fixture->provider_db,
+                                            "همراه اول");
+  g_assert(provider != NULL);
+  g_assert_cmpint(provider->num_names, ==, 1);
+  g_assert_cmpstr(provider->names[0]->name, ==, "همراه اول");
+  g_assert_cmpstr(provider->country, ==, "ir");
+}
+
+void test_multi_provider_names(TestFixture *fixture, gconstpointer test_data)
+{
+  struct mobile_provider *provider;
+
+  provider = mobile_provider_lookup_by_name(fixture->provider_db,
+                                            "BaikalWestCom");
+  g_assert(provider != NULL);
+  g_assert_cmpstr(provider->country, ==, "ru");
+  g_assert_cmpint(provider->num_names, ==, 2);
+  g_assert_cmpstr(provider->names[0]->name, ==, "BaikalWestCom");
+  g_assert(provider->names[0]->lang == NULL);
+  g_assert_cmpstr(provider->names[1]->name, ==, "БайкалВестКом");
+  g_assert(provider->names[1]->lang != NULL);
+  g_assert_cmpstr(provider->names[1]->lang, ==, "ru");
+
+  provider = mobile_provider_lookup_by_name(fixture->provider_db,
+                                            "БайкалВестКом");
+  g_assert(provider != NULL);
+  g_assert_cmpstr(provider->country, ==, "ru");
+  g_assert_cmpint(provider->num_names, ==, 2);
+  g_assert_cmpstr(provider->names[0]->name, ==, "BaikalWestCom");
+}
+
+void test_multi_apn_names(TestFixture *fixture, gconstpointer test_data)
+{
+  struct mobile_provider *provider;
+
+  provider = mobile_provider_lookup_by_name(fixture->provider_db, "Tigo");
+  g_assert(provider != NULL);
+  g_assert_cmpstr(provider->country, ==, "py");
+  g_assert_cmpint(provider->num_names, ==, 1);
+  g_assert_cmpstr(provider->names[0]->name, ==, "Tigo");
+  g_assert(provider->names[0]->lang == NULL);
+  g_assert_cmpint(provider->num_apns, ==, 2);
+
+  g_assert_cmpint(provider->apns[0]->num_names, ==, 1);
+  g_assert_cmpstr(provider->apns[0]->value, ==, "internet.tigo.py");
+  g_assert_cmpstr(provider->apns[0]->names[0]->name, ==, "Internet");
+  g_assert(provider->apns[0]->names[0]->lang == NULL);
+
+  g_assert_cmpint(provider->apns[1]->num_names, ==, 2);
+  g_assert_cmpstr(provider->apns[1]->value, ==, "broadband.tigo.py");
+  g_assert_cmpstr(provider->apns[1]->names[0]->name, ==, "Broadband");
+  g_assert(provider->apns[1]->names[0]->lang == NULL);
+  g_assert_cmpstr(provider->apns[1]->names[1]->name, ==, "Banda Ancha Móvil");
+  g_assert(provider->apns[1]->names[1]->lang != NULL);
+  g_assert_cmpstr(provider->apns[1]->names[1]->lang, ==, "es");
+}
+
+void test_name_with_commas(TestFixture *fixture, gconstpointer test_data)
+{
+  struct mobile_provider *provider;
+
+  provider = mobile_provider_lookup_by_network(fixture->provider_db, "23201");
+  g_assert(provider != NULL);
+  g_assert_cmpint(provider->num_names, ==, 1);
+  g_assert_cmpstr(provider->names[0]->name, ==, "A1/Telekom Austria");
+  g_assert_cmpint(provider->num_apns, ==, 2);
+  g_assert_cmpint(provider->apns[1]->num_names, ==, 1);
+  g_assert_cmpstr(provider->apns[1]->names[0]->name, ==,
+                  "aon (Flex, Breitband-Duo, BusinessFlex)");
+}
+
+void test_bad_provider_name(TestFixture *fixture, gconstpointer test_data)
+{
+  struct mobile_provider *provider;
+
+  provider = mobile_provider_lookup_by_name(fixture->provider_db,
+                                            "NOSUCHPROVIDER");
+  g_assert(provider == NULL);
+}
+
+void test_bad_network_id(TestFixture *fixture, gconstpointer test_data)
+{
+  struct mobile_provider *provider;
+
+  provider = mobile_provider_lookup_by_network(fixture->provider_db,
+                                               "000000");
+  g_assert(provider == NULL);
+}
+
+void test_lookup_best_match(TestFixture *fixture, gconstpointer test_data)
+{
+  struct mobile_provider *provider;
+
+  /* Make sure we find the one in the US */
+  provider = mobile_provider_lookup_best_match(fixture->provider_db, "3",
+                                               "31099");
+  g_assert_cmpint(provider->num_apns, ==, 1);
+  g_assert_cmpstr(provider->apns[0]->value, ==, "three.com");
+
+  /* Make sure we find the one in the UK */
+  provider = mobile_provider_lookup_best_match(fixture->provider_db, "3",
+                                               "23499");
+  g_assert_cmpint(provider->num_apns, ==, 1);
+  g_assert_cmpstr(provider->apns[0]->value, ==, "three.uk");
+
+  /* Make sure we find the one in the UK even with a different MNC */
+  provider = mobile_provider_lookup_best_match(fixture->provider_db, "3",
+                                               "234878");
+  g_assert_cmpint(provider->num_apns, ==, 1);
+  g_assert_cmpstr(provider->apns[0]->value, ==, "three.uk");
+}
+
+int main(int argc, char *argv[])
+{
+  int result;
+
+  g_test_init(&argc, &argv, NULL);
+
+#define ADD_TEST(name, data) \
+  g_test_add("/flimflam/providerdb/" #name, TestFixture, data, \
+             test_fixture_setup, test_##name, test_fixture_teardown)
+
+  ADD_TEST(lookup_by_name, test_db);
+  ADD_TEST(lookup_by_network, test_db);
+  ADD_TEST(lookup_by_mvno, test_db);
+  ADD_TEST(other_network_id, test_db);
+  ADD_TEST(normalize_network_id, test_db);
+  ADD_TEST(no_network_id, test_db);
+  ADD_TEST(no_network_id_ambiguous, test_db_ambiguous);
+  ADD_TEST(multi_country, test_db);
+  ADD_TEST(non_ascii_names, test_db);
+  ADD_TEST(multi_provider_names, test_db);
+  ADD_TEST(multi_apn_names, test_db);
+  ADD_TEST(name_with_commas, test_db);
+  ADD_TEST(bad_provider_name, test_db);
+  ADD_TEST(bad_network_id, test_db);
+  ADD_TEST(lookup_best_match, test_db);
+
+  result = g_test_run();
+
+  unlink(testfile);
+  return result;
+}