Index: lib/filterStorage.js
===================================================================
--- a/lib/filterStorage.js
+++ b/lib/filterStorage.js
@@ -23,16 +23,17 @@
  */
 
 const {IO} = require("io");
 const {Prefs} = require("prefs");
 const {Filter, ActiveFilter} = require("./filterClasses");
 const {Subscription, SpecialSubscription,
        ExternalSubscription} = require("./subscriptionClasses");
 const {FilterNotifier} = require("./filterNotifier");
+const {INIParser} = require("./iniParser");
 
 /**
  * Version number of the filter storage file format.
  * @type {number}
  */
 let formatVersion = 5;
 
 /**
@@ -675,106 +676,8 @@
 function removeSubscriptionFilters(subscription)
 {
   if (!FilterStorage.knownSubscriptions.has(subscription.url))
     return;
 
   for (let filter of subscription.filters)
     filter.subscriptions.delete(subscription);
 }
-
-/**
- * Listener returned by FilterStorage.importData(), parses filter data.
- * @constructor
- */
-function INIParser()
-{
-  this.fileProperties = this.curObj = {};
-  this.subscriptions = [];
-  this.knownFilters = new Map();
-  this.knownSubscriptions = new Map();
-}
-INIParser.prototype =
-{
-  linesProcessed: 0,
-  subscriptions: null,
-  knownFilters: null,
-  knownSubscriptions: null,
-  wantObj: true,
-  fileProperties: null,
-  curObj: null,
-  curSection: null,
-
-  process(val)
-  {
-    let origKnownFilters = Filter.knownFilters;
-    Filter.knownFilters = this.knownFilters;
-    let origKnownSubscriptions = Subscription.knownSubscriptions;
-    Subscription.knownSubscriptions = this.knownSubscriptions;
-    let match;
-    try
-    {
-      if (this.wantObj === true && (match = /^(\w+)=(.*)$/.exec(val)))
-        this.curObj[match[1]] = match[2];
-      else if (val === null || (match = /^\s*\[(.+)\]\s*$/.exec(val)))
-      {
-        if (this.curObj)
-        {
-          // Process current object before going to next section
-          switch (this.curSection)
-          {
-            case "filter":
-              if ("text" in this.curObj)
-                Filter.fromObject(this.curObj);
-              break;
-            case "subscription": {
-              let subscription = Subscription.fromObject(this.curObj);
-              if (subscription)
-                this.subscriptions.push(subscription);
-              break;
-            }
-            case "subscription filters":
-              if (this.subscriptions.length)
-              {
-                let subscription = this.subscriptions[
-                  this.subscriptions.length - 1
-                ];
-                for (let text of this.curObj)
-                {
-                  let filter = Filter.fromText(text);
-                  subscription.filters.push(filter);
-                  filter.subscriptions.add(subscription);
-                }
-              }
-              break;
-          }
-        }
-
-        if (val === null)
-          return;
-
-        this.curSection = match[1].toLowerCase();
-        switch (this.curSection)
-        {
-          case "filter":
-          case "subscription":
-            this.wantObj = true;
-            this.curObj = {};
-            break;
-          case "subscription filters":
-            this.wantObj = false;
-            this.curObj = [];
-            break;
-          default:
-            this.wantObj = null;
-            this.curObj = null;
-        }
-      }
-      else if (this.wantObj === false && val)
-        this.curObj.push(val.replace(/\\\[/g, "["));
-    }
-    finally
-    {
-      Filter.knownFilters = origKnownFilters;
-      Subscription.knownSubscriptions = origKnownSubscriptions;
-    }
-  }
-};
Index: lib/iniParser.js
===================================================================
new file mode 100644
--- /dev/null
+++ b/lib/iniParser.js
@@ -0,0 +1,153 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-present eyeo GmbH
+ *
+ * Adblock Plus is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * Adblock Plus 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 Adblock Plus.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+"use strict";
+
+/**
+ * @fileOverview INI parsing.
+ */
+
+const {Filter} = require("./filterClasses");
+const {Subscription} = require("./subscriptionClasses");
+
+/**
+ * Parses filter data.
+ */
+class INIParser
+{
+  constructor()
+  {
+    /**
+     * Properties of the filter data.
+     * @type {object}
+     */
+    this.fileProperties = {};
+
+    /**
+     * The list of subscriptions in the filter data.
+     * @type {Array.<Subscription>}
+     */
+    this.subscriptions = [];
+
+    /**
+     * Known filter texts mapped to their corresponding {@link Filter} objects.
+     * @type {Map.<string, Filter>}
+     */
+    this.knownFilters = new Map();
+
+    /**
+     * Known subscription URLs mapped to their corresponding
+     * {@link Subscription} objects.
+     * @type {Map.<string, Subscription>}
+     */
+    this.knownSubscriptions = new Map();
+
+    this._wantObj = true;
+    this._curObj = this.fileProperties;
+    this._curSection = null;
+  }
+
+  /**
+   * Processes a line of filter data.
+   *
+   * @param {string?} line The line of filter data to process. This may be
+   *   <code>null</code>, which indicates the end of the filter data.
+   */
+  process(line)
+  {
+    let origKnownFilters = Filter.knownFilters;
+    Filter.knownFilters = this.knownFilters;
+
+    let origKnownSubscriptions = Subscription.knownSubscriptions;
+    Subscription.knownSubscriptions = this.knownSubscriptions;
+
+    try
+    {
+      let match;
+      if (this._wantObj === true && (match = /^(\w+)=(.*)$/.exec(line)))
+      {
+        this._curObj[match[1]] = match[2];
+      }
+      else if (line === null || (match = /^\s*\[(.+)\]\s*$/.exec(line)))
+      {
+        if (this._curObj)
+        {
+          // Process current object before going to next section
+          switch (this._curSection)
+          {
+            case "filter":
+              if ("text" in this._curObj)
+                Filter.fromObject(this._curObj);
+              break;
+
+            case "subscription":
+              let subscription = Subscription.fromObject(this._curObj);
+              if (subscription)
+                this.subscriptions.push(subscription);
+              break;
+
+            case "subscription filters":
+              if (this.subscriptions.length)
+              {
+                let currentSubscription = this.subscriptions[
+                  this.subscriptions.length - 1
+                ];
+                for (let text of this._curObj)
+                {
+                  let filter = Filter.fromText(text);
+                  currentSubscription.filters.push(filter);
+                  filter.subscriptions.add(currentSubscription);
+                }
+              }
+              break;
+          }
+        }
+
+        if (line === null)
+          return;
+
+        this._curSection = match[1].toLowerCase();
+        switch (this._curSection)
+        {
+          case "filter":
+          case "subscription":
+            this._wantObj = true;
+            this._curObj = {};
+            break;
+          case "subscription filters":
+            this._wantObj = false;
+            this._curObj = [];
+            break;
+          default:
+            this._wantObj = null;
+            this._curObj = null;
+        }
+      }
+      else if (this._wantObj === false && line)
+      {
+        this._curObj.push(line.replace(/\\\[/g, "["));
+      }
+    }
+    finally
+    {
+      Filter.knownFilters = origKnownFilters;
+      Subscription.knownSubscriptions = origKnownSubscriptions;
+    }
+  }
+}
+
+exports.INIParser = INIParser;
