summaryrefslogtreecommitdiffstats
path: root/toolkit/components/osfile/modules/ospath_win.jsm
blob: 31a87b11575c5649f1c2305ed2acafd62dfd2c98 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
/**
 * Handling native paths.
 *
 * This module contains a number of functions destined to simplify
 * working with native paths through a cross-platform API. Functions
 * of this module will only work with the following assumptions:
 *
 * - paths are valid;
 * - paths are defined with one of the grammars that this module can
 *   parse (see later);
 * - all path concatenations go through function |join|.
 *
 * Limitations of this implementation.
 *
 * Windows supports 6 distinct grammars for paths. For the moment, this
 * implementation supports the following subset:
 *
 * - drivename:backslash-separated components
 * - backslash-separated components
 * - \\drivename\ followed by backslash-separated components
 *
 * Additionally, |normalize| can convert a path containing slash-
 * separated components to a path containing backslash-separated
 * components.
 */

"use strict";

// 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.
  this.exports = {};
} else if (typeof module == "undefined" || typeof exports == "undefined") {
  throw new Error("Please load this module using require()");
}

var EXPORTED_SYMBOLS = [
  "basename",
  "dirname",
  "join",
  "normalize",
  "split",
  "winGetDrive",
  "winIsAbsolute",
  "toFileURI",
  "fromFileURI",
];

/**
 * Return the final part of the path.
 * The final part of the path is everything after the last "\\".
 */
var basename = function(path) {
  if (path.startsWith("\\\\")) {
    // UNC-style path
    let index = path.lastIndexOf("\\");
    if (index != 1) {
      return path.slice(index + 1);
    }
    return ""; // Degenerate case
  }
  return path.slice(Math.max(path.lastIndexOf("\\"),
                             path.lastIndexOf(":")) + 1);
};
exports.basename = basename;

/**
 * Return the directory part of the path.
 *
 * If the path contains no directory, return the drive letter,
 * or "." if the path contains no drive letter or if option
 * |winNoDrive| is set.
 *
 * Otherwise, return everything before the last backslash,
 * including the drive/server name.
 *
 *
 * @param {string} path The path.
 * @param {*=} options Platform-specific options controlling the behavior
 * of this function. This implementation supports the following options:
 *  - |winNoDrive| If |true|, also remove the letter from the path name.
 */
var dirname = function(path, options) {
  let noDrive = (options && options.winNoDrive);

  // Find the last occurrence of "\\"
  let index = path.lastIndexOf("\\");
  if (index == -1) {
    // If there is no directory component...
    if (!noDrive) {
      // Return the drive path if possible, falling back to "."
      return this.winGetDrive(path) || ".";
    } else {
      // Or just "."
      return ".";
    }
  }

  if (index == 1 && path.charAt(0) == "\\") {
    // The path is reduced to a UNC drive
    if (noDrive) {
      return ".";
    } else {
      return path;
    }
  }

  // Ignore any occurrence of "\\: immediately before that one
  while (index >= 0 && path[index] == "\\") {
    --index;
  }

  // Compute what is left, removing the drive name if necessary
  let start;
  if (noDrive) {
    start = (this.winGetDrive(path) || "").length;
  } else {
    start = 0;
  }
  return path.slice(start, index + 1);
};
exports.dirname = dirname;

/**
 * Join path components.
 * This is the recommended manner of getting the path of a file/subdirectory
 * in a directory.
 *
 * Example: Obtaining $TMP/foo/bar in an OS-independent manner
 *  var tmpDir = OS.Constants.Path.tmpDir;
 *  var path = OS.Path.join(tmpDir, "foo", "bar");
 *
 * Under Windows, this will return "$TMP\foo\bar".
 *
 * Empty components are ignored, i.e. `OS.Path.join("foo", "", "bar)` is the
 * same as `OS.Path.join("foo", "bar")`.
 */
var join = function(...path) {
  let paths = [];
  let root;
  let absolute = false;
  for (let subpath of path) {
    if (subpath == null) {
      throw new TypeError("invalid path component");
    }
    if (subpath == "") {
      continue;
    }
    let drive = this.winGetDrive(subpath);
    if (drive) {
      root = drive;
      let component = trimBackslashes(subpath.slice(drive.length));
      if (component) {
        paths = [component];
      } else {
        paths = [];
      }
      absolute = true;
    } else if (this.winIsAbsolute(subpath)) {
      paths = [trimBackslashes(subpath)];
      absolute = true;
    } else {
      paths.push(trimBackslashes(subpath));
    }
  }
  let result = "";
  if (root) {
    result += root;
  }
  if (absolute) {
    result += "\\";
  }
  result += paths.join("\\");
  return result;
};
exports.join = join;

/**
 * Return the drive name of a path, or |null| if the path does
 * not contain a drive name.
 *
 * Drive name appear either as "DriveName:..." (the return drive
 * name includes the ":") or "\\\\DriveName..." (the returned drive name
 * includes "\\\\").
 */
var winGetDrive = function(path) {
  if (path == null) {
    throw new TypeError("path is invalid");
  }

  if (path.startsWith("\\\\")) {
    // UNC path
    if (path.length == 2) {
      return null;
    }
    let index = path.indexOf("\\", 2);
    if (index == -1) {
      return path;
    }
    return path.slice(0, index);
  }
  // Non-UNC path
  let index = path.indexOf(":");
  if (index <= 0) return null;
  return path.slice(0, index + 1);
};
exports.winGetDrive = winGetDrive;

/**
 * Return |true| if the path is absolute, |false| otherwise.
 *
 * We consider that a path is absolute if it starts with "\\"
 * or "driveletter:\\".
 */
var winIsAbsolute = function(path) {
  let index = path.indexOf(":");
  return path.length > index + 1 && path[index + 1] == "\\";
};
exports.winIsAbsolute = winIsAbsolute;

/**
 * Normalize a path by removing any unneeded ".", "..", "\\".
 * Also convert any "/" to a "\\".
 */
var normalize = function(path) {
  let stack = [];

  if (!path.startsWith("\\\\")) {
    // Normalize "/" to "\\"
    path = path.replace(/\//g, "\\");
  }

  // Remove the drive (we will put it back at the end)
  let root = this.winGetDrive(path);
  if (root) {
    path = path.slice(root.length);
  }

  // Remember whether we need to restore a leading "\\" or drive name.
  let absolute = this.winIsAbsolute(path);

  // And now, fill |stack| from the components,
  // popping whenever there is a ".."
  path.split("\\").forEach(function loop(v) {
    switch (v) {
    case "":  case ".": // Ignore
      break;
    case "..":
      if (stack.length == 0) {
        if (absolute) {
          throw new Error("Path is ill-formed: attempting to go past root");
        } else {
         stack.push("..");
        }
      } else {
        if (stack[stack.length - 1] == "..") {
          stack.push("..");
        } else {
          stack.pop();
        }
      }
      break;
    default:
      stack.push(v);
    }
  });

  // Put everything back together
  let result = stack.join("\\");
  if (absolute || root) {
    result = "\\" + result;
  }
  if (root) {
    result = root + result;
  }
  return result;
};
exports.normalize = normalize;

/**
 * Return the components of a path.
 * You should generally apply this function to a normalized path.
 *
 * @return {{
 *   {bool} absolute |true| if the path is absolute, |false| otherwise
 *   {array} components the string components of the path
 *   {string?} winDrive the drive or server for this path
 * }}
 *
 * Other implementations may add additional OS-specific informations.
 */
var split = function(path) {
  return {
    absolute: this.winIsAbsolute(path),
    winDrive: this.winGetDrive(path),
    components: path.split("\\")
  };
};
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.
var toFileURIExtraEncodings = {';': '%3b', '?': '%3F', '#': '%23'};
var toFileURI = function toFileURI(path) {
  // URI-escape forward slashes and convert backward slashes to forward
  path = this.normalize(path).replace(/[\\\/]/g, m => (m=='\\')? '/' : '%2F');
  // Per https://url.spec.whatwg.org we should not encode [] in the path
  let dontNeedEscaping = {'%5B': '[', '%5D': ']'};
  let uri = encodeURI(path).replace(/%(5B|5D)/gi,
    match => dontNeedEscaping[match]);

  // 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.
 */
var 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.
*/
var trimBackslashes = function trimBackslashes(string) {
  return string.replace(/^\\+|\\+$/g,'');
};

//////////// Boilerplate
if (typeof Components != "undefined") {
  this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS;
  for (let symbol of EXPORTED_SYMBOLS) {
    this[symbol] = exports[symbol];
  }
}