| Index: lib/io.js | 
| =================================================================== | 
| --- a/lib/io.js | 
| +++ b/lib/io.js | 
| @@ -50,125 +50,183 @@ function callWebExt(method, ...args) | 
| { | 
| let id = ++messageID; | 
| messageCallbacks.set(id, {resolve, reject}); | 
| port.postMessage({id, method, args}); | 
| }); | 
| }); | 
| } | 
|  | 
| -function attachCallback(promise, callback, fallback) | 
| +function callLegacy(method, ...args) | 
| { | 
| -  promise.then(result => | 
| +  return new Promise((resolve, reject) => | 
| { | 
| -    callback(null, result); | 
| -  }).catch(error => | 
| -  { | 
| -    if (fallback && error == "NoSuchFile") | 
| -      fallback(); | 
| -    else | 
| -      callback(error); | 
| +    LegacyIO[method](...args, (error, result) => | 
| +    { | 
| +      if (error) | 
| +        reject(error); | 
| +      else | 
| +        resolve(result); | 
| +    }); | 
| }); | 
| } | 
|  | 
| +function legacyFile(fileName) | 
| +{ | 
| +  let file = LegacyIO.resolveFilePath("adblockplus"); | 
| +  file.append(fileName); | 
| +  return file; | 
| +} | 
| + | 
| exports.IO = | 
| { | 
| -  resolveFilePath: LegacyIO.resolveFilePath, | 
| +  /** | 
| +   * @callback TextSink | 
| +   * @param {string} line | 
| +   */ | 
|  | 
| /** | 
| -   * Reads strings from a file asynchronously, calls listener.process() with | 
| -   * each line read and with a null parameter once the read operation is done. | 
| -   * The callback will be called when the operation is done. | 
| +   * Reads text lines from a file. | 
| +   * @param {string} fileName | 
| +   *    Name of the file to be read | 
| +   * @param {TextSink} listener | 
| +   *    Function that will be called for each line in the file | 
| +   * @return {Promise} | 
| +   *    Promise to be resolved or rejected once the operation is completed | 
| */ | 
| -  readFromFile(/**nsIFile*/ file, /**Object*/ listener, /**Function*/ callback) | 
| +  readFromFile(fileName, listener) | 
| { | 
| -    attachCallback( | 
| -      callWebExt("readFromFile", file.leafName).then(contents => | 
| +    return callWebExt("readFromFile", fileName).then(contents => | 
| +    { | 
| +      return new Promise((resolve, reject) => | 
| { | 
| -        return new Promise((resolve, reject) => | 
| +        let lineIndex = 0; | 
| + | 
| +        function processBatch() | 
| { | 
| -          let lineIndex = 0; | 
| - | 
| -          function processBatch() | 
| +          while (lineIndex < contents.length) | 
| { | 
| -            while (lineIndex < contents.length) | 
| +            listener(contents[lineIndex++]); | 
| +            if (lineIndex % 1000 == 0) | 
| { | 
| -              listener.process(contents[lineIndex++]); | 
| -              if (lineIndex % 1000 == 0) | 
| -              { | 
| -                Utils.runAsync(processBatch); | 
| -                return; | 
| -              } | 
| +              Utils.runAsync(processBatch); | 
| +              return; | 
| } | 
| +          } | 
| +          resolve(); | 
| +        } | 
|  | 
| -            listener.process(null); | 
| -            resolve(); | 
| +        processBatch(); | 
| +      }); | 
| +    }).catch(error => | 
| +    { | 
| +      if (error == "NoSuchFile") | 
| +      { | 
| +        let wrapper = { | 
| +          process(line) | 
| +          { | 
| +            if (line !== null) | 
| +              listener(line); | 
| } | 
| - | 
| -          processBatch(); | 
| -        }); | 
| -      }), | 
| -      callback, | 
| -      () => LegacyIO.readFromFile(file, listener, callback) | 
| -    ); | 
| +        }; | 
| +        return callLegacy("readFromFile", legacyFile(fileName), wrapper); | 
| +      } | 
| +      throw error; | 
| +    }); | 
| }, | 
|  | 
| /** | 
| -   * Writes string data to a file in UTF-8 format asynchronously. The callback | 
| -   * will be called when the write operation is done. | 
| +   * Writes text lines to a file. | 
| +   * @param {string} fileName | 
| +   *    Name of the file to be written | 
| +   * @param {Iterable.<string>} data | 
| +   *    An array-like or iterable object containing the lines (without line | 
| +   *    endings) | 
| +   * @return {Promise} | 
| +   *    Promise to be resolved or rejected once the operation is completed | 
| */ | 
| -  writeToFile(/**nsIFile*/ file, /**Iterator*/ data, /**Function*/ callback) | 
| +  writeToFile(fileName, data) | 
| { | 
| -    attachCallback( | 
| -      callWebExt("writeToFile", file.leafName, Array.from(data)), | 
| -      callback | 
| -    ); | 
| -  }, | 
| - | 
| -  /** | 
| -   * Copies a file asynchronously. The callback will be called when the copy | 
| -   * operation is done. | 
| -   */ | 
| -  copyFile(/**nsIFile*/ fromFile, /**nsIFile*/ toFile, /**Function*/ callback) | 
| -  { | 
| -    attachCallback( | 
| -      callWebExt("copyFile", fromFile.leafName, toFile.leafName), | 
| -      callback, | 
| -      () => LegacyIO.copyFile(fromFile, toFile, callback) | 
| -    ); | 
| +    return callWebExt("writeToFile", fileName, Array.from(data)); | 
| }, | 
|  | 
| /** | 
| -   * Renames a file within the same directory, will call callback when done. | 
| +   * Copies a file. | 
| +   * @param {string} fromFile | 
| +   *    Name of the file to be copied | 
| +   * @param {string} toFile | 
| +   *    Name of the file to be written, will be overwritten if exists | 
| +   * @return {Promise} | 
| +   *    Promise to be resolved or rejected once the operation is completed | 
| */ | 
| -  renameFile(/**nsIFile*/ fromFile, /**String*/ newName, /**Function*/ callback) | 
| +  copyFile(fromFile, toFile) | 
| { | 
| -    attachCallback( | 
| -      callWebExt("renameFile", fromFile.leafName, newName), | 
| -      callback, | 
| -      () => LegacyIO.renameFile(fromFile, newName, callback) | 
| -    ); | 
| +    return callWebExt("copyFile", fromFile, toFile).catch(error => | 
| +    { | 
| +      if (error == "NoSuchFile") | 
| +        return callLegacy("copyFile", legacyFile(fromFile), legacyFile(toFile)); | 
| +      throw error; | 
| +    }); | 
| }, | 
|  | 
| /** | 
| -   * Removes a file, will call callback when done. | 
| +   * Renames a file. | 
| +   * @param {string} fromFile | 
| +   *    Name of the file to be renamed | 
| +   * @param {string} newName | 
| +   *    New file name, will be overwritten if exists | 
| +   * @return {Promise} | 
| +   *    Promise to be resolved or rejected once the operation is completed | 
| */ | 
| -  removeFile(/**nsIFile*/ file, /**Function*/ callback) | 
| +  renameFile(fromFile, newName) | 
| { | 
| -    attachCallback( | 
| -      callWebExt("removeFile", file.leafName), | 
| -      callback, | 
| -      () => LegacyIO.removeFile(file, callback) | 
| -    ); | 
| +    return callWebExt("renameFile", fromFile, newName).catch(error => | 
| +    { | 
| +      if (error == "NoSuchFile") | 
| +        return callLegacy("renameFile", legacyFile(fromFile), newName); | 
| +      throw error; | 
| +    }); | 
| }, | 
|  | 
| /** | 
| -   * Gets file information such as whether the file exists. | 
| +   * Removes a file. | 
| +   * @param {string} fileName | 
| +   *    Name of the file to be removed | 
| +   * @return {Promise} | 
| +   *    Promise to be resolved or rejected once the operation is completed | 
| */ | 
| -  statFile(/**nsIFile*/ file, /**Function*/ callback) | 
| +  removeFile(fileName) | 
| { | 
| -    attachCallback( | 
| -      callWebExt("statFile", file.leafName), | 
| -      callback, | 
| -      () => LegacyIO.statFile(file, callback) | 
| -    ); | 
| +    return callWebExt("removeFile", fileName).catch(error => | 
| +    { | 
| +      if (error == "NoSuchFile") | 
| +        return callLegacy("removeFile", legacyFile(fileName)); | 
| +      throw error; | 
| +    }); | 
| +  }, | 
| + | 
| +  /** | 
| +   * @typedef StatData | 
| +   * @type {object} | 
| +   * @property {boolean} exists | 
| +   *    true if the file exists | 
| +   * @property {number} lastModified | 
| +   *    file modification time in milliseconds | 
| +   */ | 
| + | 
| +  /** | 
| +   * Retrieves file metadata. | 
| +   * @param {string} fileName | 
| +   *    Name of the file to be looked up | 
| +   * @return {Promise.<StatData>} | 
| +   *    Promise to be resolved with file metadata once the operation is | 
| +   *    completed | 
| +   */ | 
| +  statFile(fileName) | 
| +  { | 
| +    return callWebExt("statFile", fileName).catch(error => | 
| +    { | 
| +      if (error == "NoSuchFile") | 
| +        return callLegacy("statFile", legacyFile(fileName)); | 
| +      throw error; | 
| +    }); | 
| } | 
| }; | 
|  |