|
1 /** |
|
2 * Handling native paths. |
|
3 * |
|
4 * This module contains a number of functions destined to simplify |
|
5 * working with native paths through a cross-platform API. Functions |
|
6 * of this module will only work with the following assumptions: |
|
7 * |
|
8 * - paths are valid; |
|
9 * - paths are defined with one of the grammars that this module can |
|
10 * parse (see later); |
|
11 * - all path concatenations go through function |join|. |
|
12 * |
|
13 * Limitations of this implementation. |
|
14 * |
|
15 * Windows supports 6 distinct grammars for paths. For the moment, this |
|
16 * implementation supports the following subset: |
|
17 * |
|
18 * - drivename:backslash-separated components |
|
19 * - backslash-separated components |
|
20 * - \\drivename\ followed by backslash-separated components |
|
21 * |
|
22 * Additionally, |normalize| can convert a path containing slash- |
|
23 * separated components to a path containing backslash-separated |
|
24 * components. |
|
25 */ |
|
26 |
|
27 "use strict"; |
|
28 |
|
29 // Boilerplate used to be able to import this module both from the main |
|
30 // thread and from worker threads. |
|
31 if (typeof Components != "undefined") { |
|
32 Components.utils.importGlobalProperties(["URL"]); |
|
33 // Global definition of |exports|, to keep everybody happy. |
|
34 // In non-main thread, |exports| is provided by the module |
|
35 // loader. |
|
36 this.exports = {}; |
|
37 } else if (typeof "module" == "undefined" || typeof "exports" == "undefined") { |
|
38 throw new Error("Please load this module using require()"); |
|
39 } |
|
40 |
|
41 let EXPORTED_SYMBOLS = [ |
|
42 "basename", |
|
43 "dirname", |
|
44 "join", |
|
45 "normalize", |
|
46 "split", |
|
47 "winGetDrive", |
|
48 "winIsAbsolute", |
|
49 "toFileURI", |
|
50 "fromFileURI", |
|
51 ]; |
|
52 |
|
53 /** |
|
54 * Return the final part of the path. |
|
55 * The final part of the path is everything after the last "\\". |
|
56 */ |
|
57 let basename = function(path) { |
|
58 if (path.startsWith("\\\\")) { |
|
59 // UNC-style path |
|
60 let index = path.lastIndexOf("\\"); |
|
61 if (index != 1) { |
|
62 return path.slice(index + 1); |
|
63 } |
|
64 return ""; // Degenerate case |
|
65 } |
|
66 return path.slice(Math.max(path.lastIndexOf("\\"), |
|
67 path.lastIndexOf(":")) + 1); |
|
68 }; |
|
69 exports.basename = basename; |
|
70 |
|
71 /** |
|
72 * Return the directory part of the path. |
|
73 * |
|
74 * If the path contains no directory, return the drive letter, |
|
75 * or "." if the path contains no drive letter or if option |
|
76 * |winNoDrive| is set. |
|
77 * |
|
78 * Otherwise, return everything before the last backslash, |
|
79 * including the drive/server name. |
|
80 * |
|
81 * |
|
82 * @param {string} path The path. |
|
83 * @param {*=} options Platform-specific options controlling the behavior |
|
84 * of this function. This implementation supports the following options: |
|
85 * - |winNoDrive| If |true|, also remove the letter from the path name. |
|
86 */ |
|
87 let dirname = function(path, options) { |
|
88 let noDrive = (options && options.winNoDrive); |
|
89 |
|
90 // Find the last occurrence of "\\" |
|
91 let index = path.lastIndexOf("\\"); |
|
92 if (index == -1) { |
|
93 // If there is no directory component... |
|
94 if (!noDrive) { |
|
95 // Return the drive path if possible, falling back to "." |
|
96 return this.winGetDrive(path) || "."; |
|
97 } else { |
|
98 // Or just "." |
|
99 return "."; |
|
100 } |
|
101 } |
|
102 |
|
103 if (index == 1 && path.charAt(0) == "\\") { |
|
104 // The path is reduced to a UNC drive |
|
105 if (noDrive) { |
|
106 return "."; |
|
107 } else { |
|
108 return path; |
|
109 } |
|
110 } |
|
111 |
|
112 // Ignore any occurrence of "\\: immediately before that one |
|
113 while (index >= 0 && path[index] == "\\") { |
|
114 --index; |
|
115 } |
|
116 |
|
117 // Compute what is left, removing the drive name if necessary |
|
118 let start; |
|
119 if (noDrive) { |
|
120 start = (this.winGetDrive(path) || "").length; |
|
121 } else { |
|
122 start = 0; |
|
123 } |
|
124 return path.slice(start, index + 1); |
|
125 }; |
|
126 exports.dirname = dirname; |
|
127 |
|
128 /** |
|
129 * Join path components. |
|
130 * This is the recommended manner of getting the path of a file/subdirectory |
|
131 * in a directory. |
|
132 * |
|
133 * Example: Obtaining $TMP/foo/bar in an OS-independent manner |
|
134 * var tmpDir = OS.Constants.Path.tmpDir; |
|
135 * var path = OS.Path.join(tmpDir, "foo", "bar"); |
|
136 * |
|
137 * Under Windows, this will return "$TMP\foo\bar". |
|
138 */ |
|
139 let join = function(...path) { |
|
140 let paths = []; |
|
141 let root; |
|
142 let absolute = false; |
|
143 for (let subpath of path) { |
|
144 if (subpath == null) { |
|
145 throw new TypeError("invalid path component"); |
|
146 } |
|
147 let drive = this.winGetDrive(subpath); |
|
148 if (drive) { |
|
149 root = drive; |
|
150 let component = trimBackslashes(subpath.slice(drive.length)); |
|
151 if (component) { |
|
152 paths = [component]; |
|
153 } else { |
|
154 paths = []; |
|
155 } |
|
156 absolute = true; |
|
157 } else if (this.winIsAbsolute(subpath)) { |
|
158 paths = [trimBackslashes(subpath)]; |
|
159 absolute = true; |
|
160 } else { |
|
161 paths.push(trimBackslashes(subpath)); |
|
162 } |
|
163 } |
|
164 let result = ""; |
|
165 if (root) { |
|
166 result += root; |
|
167 } |
|
168 if (absolute) { |
|
169 result += "\\"; |
|
170 } |
|
171 result += paths.join("\\"); |
|
172 return result; |
|
173 }; |
|
174 exports.join = join; |
|
175 |
|
176 /** |
|
177 * Return the drive name of a path, or |null| if the path does |
|
178 * not contain a drive name. |
|
179 * |
|
180 * Drive name appear either as "DriveName:..." (the return drive |
|
181 * name includes the ":") or "\\\\DriveName..." (the returned drive name |
|
182 * includes "\\\\"). |
|
183 */ |
|
184 let winGetDrive = function(path) { |
|
185 if (path == null) { |
|
186 throw new TypeError("path is invalid"); |
|
187 } |
|
188 |
|
189 if (path.startsWith("\\\\")) { |
|
190 // UNC path |
|
191 if (path.length == 2) { |
|
192 return null; |
|
193 } |
|
194 let index = path.indexOf("\\", 2); |
|
195 if (index == -1) { |
|
196 return path; |
|
197 } |
|
198 return path.slice(0, index); |
|
199 } |
|
200 // Non-UNC path |
|
201 let index = path.indexOf(":"); |
|
202 if (index <= 0) return null; |
|
203 return path.slice(0, index + 1); |
|
204 }; |
|
205 exports.winGetDrive = winGetDrive; |
|
206 |
|
207 /** |
|
208 * Return |true| if the path is absolute, |false| otherwise. |
|
209 * |
|
210 * We consider that a path is absolute if it starts with "\\" |
|
211 * or "driveletter:\\". |
|
212 */ |
|
213 let winIsAbsolute = function(path) { |
|
214 let index = path.indexOf(":"); |
|
215 return path.length > index + 1 && path[index + 1] == "\\"; |
|
216 }; |
|
217 exports.winIsAbsolute = winIsAbsolute; |
|
218 |
|
219 /** |
|
220 * Normalize a path by removing any unneeded ".", "..", "\\". |
|
221 * Also convert any "/" to a "\\". |
|
222 */ |
|
223 let normalize = function(path) { |
|
224 let stack = []; |
|
225 |
|
226 if (!path.startsWith("\\\\")) { |
|
227 // Normalize "/" to "\\" |
|
228 path = path.replace(/\//g, "\\"); |
|
229 } |
|
230 |
|
231 // Remove the drive (we will put it back at the end) |
|
232 let root = this.winGetDrive(path); |
|
233 if (root) { |
|
234 path = path.slice(root.length); |
|
235 } |
|
236 |
|
237 // Remember whether we need to restore a leading "\\" or drive name. |
|
238 let absolute = this.winIsAbsolute(path); |
|
239 |
|
240 // And now, fill |stack| from the components, |
|
241 // popping whenever there is a ".." |
|
242 path.split("\\").forEach(function loop(v) { |
|
243 switch (v) { |
|
244 case "": case ".": // Ignore |
|
245 break; |
|
246 case "..": |
|
247 if (stack.length == 0) { |
|
248 if (absolute) { |
|
249 throw new Error("Path is ill-formed: attempting to go past root"); |
|
250 } else { |
|
251 stack.push(".."); |
|
252 } |
|
253 } else { |
|
254 if (stack[stack.length - 1] == "..") { |
|
255 stack.push(".."); |
|
256 } else { |
|
257 stack.pop(); |
|
258 } |
|
259 } |
|
260 break; |
|
261 default: |
|
262 stack.push(v); |
|
263 } |
|
264 }); |
|
265 |
|
266 // Put everything back together |
|
267 let result = stack.join("\\"); |
|
268 if (absolute || root) { |
|
269 result = "\\" + result; |
|
270 } |
|
271 if (root) { |
|
272 result = root + result; |
|
273 } |
|
274 return result; |
|
275 }; |
|
276 exports.normalize = normalize; |
|
277 |
|
278 /** |
|
279 * Return the components of a path. |
|
280 * You should generally apply this function to a normalized path. |
|
281 * |
|
282 * @return {{ |
|
283 * {bool} absolute |true| if the path is absolute, |false| otherwise |
|
284 * {array} components the string components of the path |
|
285 * {string?} winDrive the drive or server for this path |
|
286 * }} |
|
287 * |
|
288 * Other implementations may add additional OS-specific informations. |
|
289 */ |
|
290 let split = function(path) { |
|
291 return { |
|
292 absolute: this.winIsAbsolute(path), |
|
293 winDrive: this.winGetDrive(path), |
|
294 components: path.split("\\") |
|
295 }; |
|
296 }; |
|
297 exports.split = split; |
|
298 |
|
299 /** |
|
300 * Return the file:// URI file path of the given local file path. |
|
301 */ |
|
302 // The case of %3b is designed to match Services.io, but fundamentally doesn't matter. |
|
303 let toFileURIExtraEncodings = {';': '%3b', '?': '%3F', "'": '%27', '#': '%23'}; |
|
304 let toFileURI = function toFileURI(path) { |
|
305 // URI-escape forward slashes and convert backward slashes to forward |
|
306 path = this.normalize(path).replace(/[\\\/]/g, m => (m=='\\')? '/' : '%2F'); |
|
307 let uri = encodeURI(path); |
|
308 |
|
309 // add a prefix, and encodeURI doesn't escape a few characters that we do |
|
310 // want to escape, so fix that up |
|
311 let prefix = "file:///"; |
|
312 uri = prefix + uri.replace(/[;?'#]/g, match => toFileURIExtraEncodings[match]); |
|
313 |
|
314 // turn e.g., file:///C: into file:///C:/ |
|
315 if (uri.charAt(uri.length - 1) === ':') { |
|
316 uri += "/" |
|
317 } |
|
318 |
|
319 return uri; |
|
320 }; |
|
321 exports.toFileURI = toFileURI; |
|
322 |
|
323 /** |
|
324 * Returns the local file path from a given file URI. |
|
325 */ |
|
326 let fromFileURI = function fromFileURI(uri) { |
|
327 let url = new URL(uri); |
|
328 if (url.protocol != 'file:') { |
|
329 throw new Error("fromFileURI expects a file URI"); |
|
330 } |
|
331 |
|
332 // strip leading slash, since Windows paths don't start with one |
|
333 uri = url.pathname.substr(1); |
|
334 |
|
335 let path = decodeURI(uri); |
|
336 // decode a few characters where URL's parsing is overzealous |
|
337 path = path.replace(/%(3b|3f|23)/gi, |
|
338 match => decodeURIComponent(match)); |
|
339 path = this.normalize(path); |
|
340 |
|
341 // this.normalize() does not remove the trailing slash if the path |
|
342 // component is a drive letter. eg. 'C:\'' will not get normalized. |
|
343 if (path.endsWith(":\\")) { |
|
344 path = path.substr(0, path.length - 1); |
|
345 } |
|
346 return this.normalize(path); |
|
347 }; |
|
348 exports.fromFileURI = fromFileURI; |
|
349 |
|
350 /** |
|
351 * Utility function: Remove any leading/trailing backslashes |
|
352 * from a string. |
|
353 */ |
|
354 let trimBackslashes = function trimBackslashes(string) { |
|
355 return string.replace(/^\\+|\\+$/g,''); |
|
356 }; |
|
357 |
|
358 //////////// Boilerplate |
|
359 if (typeof Components != "undefined") { |
|
360 this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS; |
|
361 for (let symbol of EXPORTED_SYMBOLS) { |
|
362 this[symbol] = exports[symbol]; |
|
363 } |
|
364 } |