/* * This file is part of Adblock Plus , * Copyright (C) 2006-2017 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 . */ /** * @fileOverview Module containing file I/O helpers. */ let {Services} = Cu.import("resource://gre/modules/Services.jsm", null); let {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", null); let {OS} = Cu.import("resource://gre/modules/osfile.jsm", null); let {Task} = Cu.import("resource://gre/modules/Task.jsm", null); let {Prefs} = require("prefs"); let {Utils} = require("utils"); let firstRead = true; const BUFFER_SIZE = 0x80000; // 512kB let IO = exports.IO = { /** * Retrieves the platform-dependent line break string. */ get lineBreak() { let lineBreak = (Services.appinfo.OS == "WINNT" ? "\r\n" : "\n"); Object.defineProperty(this, "lineBreak", {value: lineBreak}); return lineBreak; }, /** * Tries to interpret a file path as an absolute path or a path relative to * user's profile. Returns a file or null on failure. */ resolveFilePath: function(/**String*/ path) /**nsIFile*/ { if (!path) return null; try { // Assume an absolute path first return new FileUtils.File(path); } catch (e) {} try { // Try relative path now return FileUtils.getFile("ProfD", path.split("/")); } catch (e) {} return null; }, /** * 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. */ readFromFile: function(/**nsIFile*/ file, /**Object*/ listener, /**Function*/ callback) { try { let processing = false; let buffer = ""; let loaded = false; let error = null; let onProgress = function*(data) { let index = (processing ? -1 : Math.max(data.lastIndexOf("\n"), data.lastIndexOf("\r"))); if (index >= 0) { // Protect against reentrance in case the listener processes events. processing = true; try { let oldBuffer = buffer; buffer = data.substr(index + 1); data = data.substr(0, index + 1); let lines = data.split(/[\r\n]+/); lines.pop(); lines[0] = oldBuffer + lines[0]; for (let i = 0; i < lines.length; i++) listener.process(lines[i]); } finally { processing = false; data = buffer; buffer = ""; yield* onProgress(data); if (loaded) { loaded = false; onSuccess(); } if (error) { let param = error; error = null; onError(param); } } } else buffer += data; }; let onSuccess = function() { if (processing) { // Still processing data, delay processing this event. loaded = true; return; } // We are ignoring return value of listener.process() here because // turning this callback into a generator would be complicated, and // delaying isn't really necessary for the last two calls. if (buffer !== "") listener.process(buffer); listener.process(null); callback(null); }; let onError = function(e) { if (processing) { // Still processing data, delay processing this event. error = e; return; } callback(e); }; let decoder = new TextDecoder(); Task.spawn(function*() { if (firstRead && Services.vc.compare(Utils.platformVersion, "23.0a1") <= 0) { // See https://issues.adblockplus.org/ticket/530 - the first file // opened cannot be closed due to Gecko bug 858723. Make sure that // our patterns.ini file doesn't stay locked by opening a dummy file // first. try { let dummyPath = IO.resolveFilePath(Prefs.data_directory + "/dummy").path; let dummy = yield OS.File.open(dummyPath, {write: true, truncate: true}); yield dummy.close(); } catch (e) { // Dummy might be locked already, we don't care } } firstRead = false; let f = yield OS.File.open(file.path, {read: true}); while (true) { let array = yield f.read(BUFFER_SIZE); if (!array.length) break; let data = decoder.decode(array, {stream: true}); yield* onProgress(data); } yield f.close(); }.bind(this)).then(onSuccess, onError); } catch (e) { callback(e); } }, /** * Writes string data to a file in UTF-8 format asynchronously. The callback * will be called when the write operation is done. */ writeToFile: function(/**nsIFile*/ file, /**Iterator*/ data, /**Function*/ callback) { try { let encoder = new TextEncoder(); Task.spawn(function*() { // This mimics OS.File.writeAtomic() but writes in chunks. let tmpPath = file.path + ".tmp"; let f = yield OS.File.open(tmpPath, {write: true, truncate: true}); let buf = []; let bufLen = 0; let lineBreak = this.lineBreak; function writeChunk() { let array = encoder.encode(buf.join(lineBreak) + lineBreak); buf = []; bufLen = 0; return f.write(array); } for (let line of data) { buf.push(line); bufLen += line.length; if (bufLen >= BUFFER_SIZE) yield writeChunk(); } if (bufLen) yield writeChunk(); // OS.File.flush() isn't exposed prior to Gecko 27, see bug 912457. if (typeof f.flush == "function") yield f.flush(); yield f.close(); yield OS.File.move(tmpPath, file.path, {noCopy: true}); }.bind(this)).then(callback.bind(null, null), callback); } catch (e) { callback(e); } }, /** * Copies a file asynchronously. The callback will be called when the copy * operation is done. */ copyFile: function(/**nsIFile*/ fromFile, /**nsIFile*/ toFile, /**Function*/ callback) { try { let promise = OS.File.copy(fromFile.path, toFile.path); promise.then(callback.bind(null, null), callback); } catch (e) { callback(e); } }, /** * Renames a file within the same directory, will call callback when done. */ renameFile: function(/**nsIFile*/ fromFile, /**String*/ newName, /**Function*/ callback) { try { let toFile = fromFile.clone(); toFile.leafName = newName; let promise = OS.File.move(fromFile.path, toFile.path); promise.then(callback.bind(null, null), callback); } catch(e) { callback(e); } }, /** * Removes a file, will call callback when done. */ removeFile: function(/**nsIFile*/ file, /**Function*/ callback) { try { let promise = OS.File.remove(file.path); promise.then(callback.bind(null, null), callback); } catch(e) { callback(e); } }, /** * Gets file information such as whether the file exists. */ statFile: function(/**nsIFile*/ file, /**Function*/ callback) { try { let promise = OS.File.stat(file.path); promise.then(function onSuccess(info) { callback(null, { exists: true, isDirectory: info.isDir, isFile: !info.isDir, lastModified: info.lastModificationDate.getTime() }); }, function onError(e) { if (e.becauseNoSuchFile) { callback(null, { exists: false, isDirectory: false, isFile: false, lastModified: 0 }); } else callback(e); }); } catch(e) { callback(e); } } }