Moved mobile_provider API into this repo.

Moved the mobile_provider API out of flimflam and into the
mobile-broadband-provider-info repo, so that it can be shared by
flimflam and shill. The unit tests and the browsedb tool have been
moved as well. The serviceproviders.bfd file is now created here
and installed alongside serviceproviders.xml in
/usr/share/mobile-broadband-provider-info.

BUG=chromium-os:19783
TEST=Built mobile-providers, with and without FEATURES=test, and
with and without USE=tools. Unit tests passed, and browsedb tool
was built and installed (when USE=tools). Checked that
serviceproviders.bfd was created and installed.  Also emerged and
tested flimflam.

Change-Id: I5b76dc6de447a426f0dc88d1da42236427cc5055
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;
+}