diff --git a/toolkit/components/osfile/modules/ospath_unix.jsm b/toolkit/components/osfile/modules/ospath_unix.jsm index 35d19b103611..a040459549ed 100644 --- a/toolkit/components/osfile/modules/ospath_unix.jsm +++ b/toolkit/components/osfile/modules/ospath_unix.jsm @@ -20,6 +20,7 @@ // Boilerplate used to be able to import this module both from the main // thread and from worker threads. if (typeof Components != "undefined") { + Components.utils.importGlobalProperties(["URL"]); // Global definition of |exports|, to keep everybody happy. // In non-main thread, |exports| is provided by the module // loader. @@ -33,7 +34,9 @@ let EXPORTED_SYMBOLS = [ "dirname", "join", "normalize", - "split" + "split", + "toFileURI", + "fromFileURI", ]; /** @@ -148,6 +151,37 @@ let split = function(path) { }; exports.split = split; +/** + * Returns the file:// URI file path of the given local file path. + */ +// The case of %3b is designed to match Services.io, but fundamentally doesn't matter. +let toFileURIExtraEncodings = {';': '%3b', '?': '%3F', "'": '%27', '#': '%23'}; +let toFileURI = function toFileURI(path) { + let uri = encodeURI(this.normalize(path)); + + // add a prefix, and encodeURI doesn't escape a few characters that we do + // want to escape, so fix that up + let prefix = "file://"; + uri = prefix + uri.replace(/[;?'#]/g, match => toFileURIExtraEncodings[match]); + + return uri; +}; +exports.toFileURI = toFileURI; + +/** + * Returns the local file path from a given file URI. + */ +let fromFileURI = function fromFileURI(uri) { + let url = new URL(uri); + if (url.protocol != 'file:') { + throw new Error("fromFileURI expects a file URI"); + } + let path = this.normalize(decodeURIComponent(url.pathname)); + return path; +}; +exports.fromFileURI = fromFileURI; + + //////////// Boilerplate if (typeof Components != "undefined") { this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS; diff --git a/toolkit/components/osfile/modules/ospath_win.jsm b/toolkit/components/osfile/modules/ospath_win.jsm index 45cc7bd507a2..bf3079638442 100644 --- a/toolkit/components/osfile/modules/ospath_win.jsm +++ b/toolkit/components/osfile/modules/ospath_win.jsm @@ -29,6 +29,7 @@ // Boilerplate used to be able to import this module both from the main // thread and from worker threads. if (typeof Components != "undefined") { + Components.utils.importGlobalProperties(["URL"]); // Global definition of |exports|, to keep everybody happy. // In non-main thread, |exports| is provided by the module // loader. @@ -44,7 +45,9 @@ let EXPORTED_SYMBOLS = [ "normalize", "split", "winGetDrive", - "winIsAbsolute" + "winIsAbsolute", + "toFileURI", + "fromFileURI", ]; /** @@ -287,6 +290,57 @@ let split = function(path) { }; exports.split = split; +/** + * Return the file:// URI file path of the given local file path. + */ +// The case of %3b is designed to match Services.io, but fundamentally doesn't matter. +let toFileURIExtraEncodings = {';': '%3b', '?': '%3F', "'": '%27', '#': '%23'}; +let toFileURI = function toFileURI(path) { + // URI-escape forward slashes and convert backward slashes to forward + path = this.normalize(path).replace(/[\\\/]/g, m => (m=='\\')? '/' : '%2F'); + let uri = encodeURI(path); + + // add a prefix, and encodeURI doesn't escape a few characters that we do + // want to escape, so fix that up + let prefix = "file:///"; + uri = prefix + uri.replace(/[;?'#]/g, match => toFileURIExtraEncodings[match]); + + // turn e.g., file:///C: into file:///C:/ + if (uri.charAt(uri.length - 1) === ':') { + uri += "/" + } + + return uri; +}; +exports.toFileURI = toFileURI; + +/** + * Returns the local file path from a given file URI. + */ +let fromFileURI = function fromFileURI(uri) { + let url = new URL(uri); + if (url.protocol != 'file:') { + throw new Error("fromFileURI expects a file URI"); + } + + // strip leading slash, since Windows paths don't start with one + uri = url.pathname.substr(1); + + let path = decodeURI(uri); + // decode a few characters where URL's parsing is overzealous + path = path.replace(/%(3b|3f|23)/gi, + match => decodeURIComponent(match)); + path = this.normalize(path); + + // this.normalize() does not remove the trailing slash if the path + // component is a drive letter. eg. 'C:\'' will not get normalized. + if (path.endsWith(":\\")) { + path = path.substr(0, path.length - 1); + } + return this.normalize(path); +}; +exports.fromFileURI = fromFileURI; + /** * Utility function: Remove any leading/trailing backslashes * from a string. diff --git a/toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js b/toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js new file mode 100644 index 000000000000..9802a2a9d9ea --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js @@ -0,0 +1,111 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() { + Components.utils.import("resource://gre/modules/Services.jsm"); + Components.utils.import("resource://gre/modules/osfile.jsm"); + Components.utils.import("resource://gre/modules/FileUtils.jsm"); + + let isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes); + + // Test cases for filePathToURI + let paths = isWindows ? [ + 'C:\\', + 'C:\\test', + 'C:\\test\\', + 'C:\\test%2f', + 'C:\\test\\test\\test', + 'C:\\test;+%', + 'C:\\test?action=index\\', + 'C:\\test\ test', + '\\\\C:\\a\\b\\c', + '\\\\Server\\a\\b\\c', + + // note that per http://support.microsoft.com/kb/177506 (under more info), + // the following characters are allowed on Windows: + 'C:\\char^', + 'C:\\char&', + 'C:\\char\'', + 'C:\\char@', + 'C:\\char{', + 'C:\\char}', + 'C:\\char[', + 'C:\\char]', + 'C:\\char,', + 'C:\\char$', + 'C:\\char=', + 'C:\\char!', + 'C:\\char-', + 'C:\\char#', + 'C:\\char(', + 'C:\\char)', + 'C:\\char%', + 'C:\\char.', + 'C:\\char+', + 'C:\\char~', + 'C:\\char_' + ] : [ + '/', + '/test', + '/test/', + '/test%2f', + '/test/test/test', + '/test;+%', + '/test?action=index/', + '/test\ test', + '/punctuation/;,/?:@&=+$-_.!~*\'()"#', + '/CasePreserving' + ]; + + // some additional URIs to test, beyond those generated from paths + let uris = isWindows ? [ + 'file:///C:/test/', + 'file://localhost/C:/test', + 'file:///c:/test/test.txt', + //'file:///C:/foo%2f', // trailing, encoded slash + 'file:///C:/%3f%3F', + 'file:///C:/%3b%3B', + 'file:///C:/%3c%3C', // not one of the special-cased ? or ; + 'file:///C:/%78', // 'x', not usually uri encoded + 'file:///C:/test#frag', // a fragment identifier + 'file:///C:/test?action=index' // an actual query component + ] : [ + 'file:///test/', + 'file://localhost/test', + 'file:///test/test.txt', + 'file:///foo%2f', // trailing, encoded slash + 'file:///%3f%3F', + 'file:///%3b%3B', + 'file:///%3c%3C', // not one of the special-cased ? or ; + 'file:///%78', // 'x', not usually uri encoded + 'file:///test#frag', // a fragment identifier + 'file:///test?action=index' // an actual query component + ]; + + for (let path of paths) { + // convert that to a uri using FileUtils and Services, which toFileURI is trying to model + let file = FileUtils.File(path); + let uri = Services.io.newFileURI(file).spec; + do_check_eq(uri, OS.Path.toFileURI(path)); + + // keep the resulting URI to try the reverse + uris.push(uri) + } + + for (let uri of uris) { + // convert URIs to paths with nsIFileURI, which fromFileURI is trying to model + let path = Services.io.newURI(uri, null, null).QueryInterface(Components.interfaces.nsIFileURL).file.path; + do_check_eq(path, OS.Path.fromFileURI(uri)); + } + + // check that non-file URLs aren't allowed + let thrown = false; + try { + OS.Path.fromFileURI('http://test.com') + } catch (e) { + do_check_eq(e.message, "fromFileURI expects a file URI"); + thrown = true; + } + do_check_true(thrown); +} diff --git a/toolkit/components/osfile/tests/xpcshell/xpcshell.ini b/toolkit/components/osfile/tests/xpcshell/xpcshell.ini index 19646c017c28..29bc2b02e049 100644 --- a/toolkit/components/osfile/tests/xpcshell/xpcshell.ini +++ b/toolkit/components/osfile/tests/xpcshell/xpcshell.ini @@ -13,6 +13,7 @@ tail = [test_removeEmptyDir.js] [test_makeDir.js] [test_profiledir.js] +[test_file_URL_conversion.js] [test_logging.js] [test_creationDate.js] [test_exception.js]