From b212be08f62a48656c5befd8be0a82d691ea66e4 Mon Sep 17 00:00:00 2001 From: Brian White Date: Fri, 5 Feb 2016 22:57:35 -0500 Subject: [PATCH] path: performance improvements on all platforms This commit significantly improves performance of all path functions. Optimization strategies include: * Replacing regexps with manual parsers * Avoiding unnecessary array creation (including split() + join()) * Returning earlier where possible to avoid unnecessary work * Minimize unnecessary string creation and concatenations * Combining string iterations PR-URL: https://github.com/nodejs/node/pull/5123 Reviewed-By: Roman Reiss Reviewed-By: James M Snell --- lib/path.js | 1929 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 1466 insertions(+), 463 deletions(-) diff --git a/lib/path.js b/lib/path.js index a2162cae7c..064c4102cc 100644 --- a/lib/path.js +++ b/lib/path.js @@ -1,602 +1,1605 @@ 'use strict'; -const util = require('util'); -const isWindows = process.platform === 'win32'; +const inspect = require('util').inspect; function assertPath(path) { if (typeof path !== 'string') { - throw new TypeError('Path must be a string. Received ' + - util.inspect(path)); + throw new TypeError('Path must be a string. Received ' + inspect(path)); } } -// resolves . and .. elements in a path array with directory names there -// must be no slashes or device names (c:\) in the array -// (so also no leading and trailing slashes - it does not distinguish -// relative and absolute paths) -function normalizeArray(parts, allowAboveRoot) { - var res = []; - for (var i = 0; i < parts.length; i++) { - var p = parts[i]; - - // ignore empty parts - if (!p || p === '.') - continue; - - if (p === '..') { - if (res.length && res[res.length - 1] !== '..') { - res.pop(); - } else if (allowAboveRoot) { - res.push('..'); +// Resolves . and .. elements in a path with directory names +function normalizeStringWin32(path, allowAboveRoot) { + var res = ''; + var lastSlash = -1; + var dots = 0; + var code; + for (var i = 0; i <= path.length; ++i) { + if (i < path.length) + code = path.charCodeAt(i); + else if (code === 47/*/*/ || code === 92/*\*/) + break; + else + code = 47/*/*/; + if (code === 47/*/*/ || code === 92/*\*/) { + if (lastSlash === i - 1 || dots === 1) { + // NOOP + } else if (lastSlash !== i - 1 && dots === 2) { + if (res.length < 2 || + res.charCodeAt(res.length - 1) !== 46/*.*/ || + res.charCodeAt(res.length - 2) !== 46/*.*/) { + if (res.length > 2) { + const start = res.length - 1; + var j = start; + for (; j >= 0; --j) { + if (res.charCodeAt(j) === 92/*\*/) + break; + } + if (j !== start) { + if (j === -1) + res = ''; + else + res = res.slice(0, j); + lastSlash = i; + dots = 0; + continue; + } + } else if (res.length === 2) { + res = ''; + lastSlash = i; + dots = 0; + continue; + } + } + if (allowAboveRoot) { + if (res.length > 0) + res += '\\..'; + else + res = '..'; + } + } else { + if (res.length > 0) + res += '\\' + path.slice(lastSlash + 1, i); + else + res = path.slice(lastSlash + 1, i); } + lastSlash = i; + dots = 0; + } else if (code === 46/*.*/ && dots !== -1) { + ++dots; } else { - res.push(p); + dots = -1; } } - return res; } -// Returns an array with empty elements removed from either end of the input -// array or the original array if no elements need to be removed -function trimArray(arr) { - var lastIndex = arr.length - 1; - var start = 0; - for (; start <= lastIndex; start++) { - if (arr[start]) +// Resolves . and .. elements in a path with directory names +function normalizeStringPosix(path, allowAboveRoot) { + var res = ''; + var lastSlash = -1; + var dots = 0; + var code; + for (var i = 0; i <= path.length; ++i) { + if (i < path.length) + code = path.charCodeAt(i); + else if (code === 47/*/*/) break; + else + code = 47/*/*/; + if (code === 47/*/*/) { + if (lastSlash === i - 1 || dots === 1) { + // NOOP + } else if (lastSlash !== i - 1 && dots === 2) { + if (res.length < 2 || + res.charCodeAt(res.length - 1) !== 46/*.*/ || + res.charCodeAt(res.length - 2) !== 46/*.*/) { + if (res.length > 2) { + const start = res.length - 1; + var j = start; + for (; j >= 0; --j) { + if (res.charCodeAt(j) === 47/*/*/) + break; + } + if (j !== start) { + if (j === -1) + res = ''; + else + res = res.slice(0, j); + lastSlash = i; + dots = 0; + continue; + } + } else if (res.length === 2) { + res = ''; + lastSlash = i; + dots = 0; + continue; + } + } + if (allowAboveRoot) { + if (res.length > 0) + res += '/..'; + else + res = '..'; + } + } else { + if (res.length > 0) + res += '/' + path.slice(lastSlash + 1, i); + else + res = path.slice(lastSlash + 1, i); + } + lastSlash = i; + dots = 0; + } else if (code === 46/*.*/ && dots !== -1) { + ++dots; + } else { + dots = -1; + } } + return res; +} - var end = lastIndex; - for (; end >= 0; end--) { - if (arr[end]) - break; - } - if (start === 0 && end === lastIndex) - return arr; - if (start > end) - return []; - return arr.slice(start, end + 1); -} +const win32 = { + // path.resolve([from ...], to) + resolve: function resolve() { + var resolvedDevice = ''; + var resolvedTail = ''; + var resolvedAbsolute = false; -// Regex to split a windows path into three parts: [*, device, slash, -// tail] windows-only -const splitDeviceRe = - /^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/]+)?([\\\/])?([\s\S]*?)$/; - -// Regex to split the tail part of the above into [*, dir, basename, ext] -const splitTailRe = - /^([\s\S]*?)((?:\.{1,2}|[^\\\/]+?|)(\.[^.\/\\]*|))(?:[\\\/]*)$/; - -var win32 = {}; - -// Function to split a filename into [root, dir, basename, ext] -function win32SplitPath(filename) { - // Separate device+slash from tail - const result = splitDeviceRe.exec(filename); - const device = (result[1] || '') + (result[2] || ''); - const tail = result[3]; - // Split the tail into dir, basename and extension - const result2 = splitTailRe.exec(tail); - const dir = result2[1]; - const basename = result2[2]; - const ext = result2[3]; - return [device, dir, basename, ext]; -} + for (var i = arguments.length - 1; i >= -1; i--) { + var path; + if (i >= 0) { + path = arguments[i]; + } else if (!resolvedDevice) { + path = process.cwd(); + } else { + // Windows has the concept of drive-specific current working + // directories. If we've resolved a drive letter but not yet an + // absolute path, get cwd for that drive. We're sure the device is not + // a UNC path at this points, because UNC paths are always absolute. + path = process.env['=' + resolvedDevice]; + // Verify that a drive-local cwd was found and that it actually points + // to our drive. If not, default to the drive's root. + if (path === undefined || + path.slice(0, 3).toLowerCase() !== + resolvedDevice.toLowerCase() + '\\') { + path = resolvedDevice + '\\'; + } + } -function win32StatPath(path) { - const result = splitDeviceRe.exec(path); - const device = result[1] || ''; - const isUnc = !!device && device[1] !== ':'; - return { - device, - isUnc, - isAbsolute: isUnc || !!result[2], // UNC paths are always absolute - tail: result[3] - }; -} + assertPath(path); -function normalizeUNCRoot(device) { - return '\\\\' + device.replace(/^[\\\/]+/, '').replace(/[\\\/]+/g, '\\'); -} + // Skip empty entries + if (path.length === 0) { + continue; + } -// path.resolve([from ...], to) -win32.resolve = function() { - var resolvedDevice = ''; - var resolvedTail = ''; - var resolvedAbsolute = false; - - for (var i = arguments.length - 1; i >= -1; i--) { - var path; - if (i >= 0) { - path = arguments[i]; - } else if (!resolvedDevice) { - path = process.cwd(); - } else { - // Windows has the concept of drive-specific current working - // directories. If we've resolved a drive letter but not yet an - // absolute path, get cwd for that drive. We're sure the device is not - // an unc path at this points, because unc paths are always absolute. - path = process.env['=' + resolvedDevice]; - // Verify that a drive-local cwd was found and that it actually points - // to our drive. If not, default to the drive's root. - if (!path || path.substr(0, 3).toLowerCase() !== - resolvedDevice.toLowerCase() + '\\') { - path = resolvedDevice + '\\'; + var len = path.length; + var rootEnd = 0; + var code = path.charCodeAt(0); + var device = ''; + var isAbsolute = false; + + // Try to match a root + if (len > 1) { + if (code === 47/*/*/ || code === 92/*\*/) { + // Possible UNC root + + // If we started with a separator, we know we at least have an + // absolute path of some kind (UNC or otherwise) + isAbsolute = true; + + code = path.charCodeAt(1); + if (code === 47/*/*/ || code === 92/*\*/) { + // Matched double path separator at beginning + var j = 2; + var last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + code = path.charCodeAt(j); + if (code === 47/*/*/ || code === 92/*\*/) + break; + } + if (j < len && j !== last) { + const firstPart = path.slice(last, j); + // Matched! + last = j; + // Match 1 or more path separators + for (; j < len; ++j) { + code = path.charCodeAt(j); + if (code !== 47/*/*/ && code !== 92/*\*/) + break; + } + if (j < len && j !== last) { + // Matched! + last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + code = path.charCodeAt(j); + if (code === 47/*/*/ || code === 92/*\*/) + break; + } + if (j === len) { + // We matched a UNC root only + + device = '\\\\' + firstPart + '\\' + path.slice(last); + rootEnd = j; + } else if (j !== last) { + // We matched a UNC root with leftovers + + device = '\\\\' + firstPart + '\\' + path.slice(last, j); + rootEnd = j; + } + } + } + } else { + rootEnd = 1; + } + } else if ((code >= 65/*A*/ && code <= 90/*Z*/) || + (code >= 97/*a*/ && code <= 122/*z*/)) { + // Possible device root + + code = path.charCodeAt(1); + if (path.charCodeAt(1) === 58/*:*/) { + device = path.slice(0, 2); + rootEnd = 2; + if (len > 2) { + code = path.charCodeAt(2); + if (code === 47/*/*/ || code === 92/*\*/) { + // Treat separator following drive name as an absolute path + // indicator + isAbsolute = true; + rootEnd = 3; + } + } + } + } + } else if (code === 47/*/*/ || code === 92/*\*/) { + // `path` contains just a path separator + rootEnd = 1; + isAbsolute = true; } - } - assertPath(path); + if (device.length > 0 && + resolvedDevice.length > 0 && + device.toLowerCase() !== resolvedDevice.toLowerCase()) { + // This path points to another device so it is not applicable + continue; + } + + if (resolvedDevice.length === 0 && device.length > 0) { + resolvedDevice = device; + } + if (!resolvedAbsolute) { + resolvedTail = path.slice(rootEnd) + '\\' + resolvedTail; + resolvedAbsolute = isAbsolute; + } - // Skip empty entries - if (path === '') { - continue; + if (resolvedDevice.length > 0 && resolvedAbsolute) { + break; + } } - const result = win32StatPath(path); - const device = result.device; - var isUnc = result.isUnc; - const isAbsolute = result.isAbsolute; - const tail = result.tail; + // At this point the path should be resolved to a full absolute path, + // but handle relative paths to be safe (might happen when process.cwd() + // fails) - if (device && - resolvedDevice && - device.toLowerCase() !== resolvedDevice.toLowerCase()) { - // This path points to another device so it is not applicable - continue; - } + // Normalize the tail path + resolvedTail = normalizeStringWin32(resolvedTail, !resolvedAbsolute); - if (!resolvedDevice) { - resolvedDevice = device; - } - if (!resolvedAbsolute) { - resolvedTail = tail + '\\' + resolvedTail; - resolvedAbsolute = isAbsolute; + return (resolvedDevice + (resolvedAbsolute ? '\\' : '') + resolvedTail) || + '.'; + }, + + normalize: function normalize(path) { + assertPath(path); + const len = path.length; + if (len === 0) + return '.'; + var rootEnd = 0; + var code = path.charCodeAt(0); + var device; + var isAbsolute = false; + + // Try to match a root + if (len > 1) { + if (code === 47/*/*/ || code === 92/*\*/) { + // Possible UNC root + + // If we started with a separator, we know we at least have an absolute + // path of some kind (UNC or otherwise) + isAbsolute = true; + + code = path.charCodeAt(1); + if (code === 47/*/*/ || code === 92/*\*/) { + // Matched double path separator at beginning + var j = 2; + var last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + code = path.charCodeAt(j); + if (code === 47/*/*/ || code === 92/*\*/) + break; + } + if (j < len && j !== last) { + const firstPart = path.slice(last, j); + // Matched! + last = j; + // Match 1 or more path separators + for (; j < len; ++j) { + code = path.charCodeAt(j); + if (code !== 47/*/*/ && code !== 92/*\*/) + break; + } + if (j < len && j !== last) { + // Matched! + last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + code = path.charCodeAt(j); + if (code === 47/*/*/ || code === 92/*\*/) + break; + } + if (j === len) { + // We matched a UNC root only + // Return the normalized version of the UNC root since there + // is nothing left to process + + return '\\\\' + firstPart + '\\' + path.slice(last) + '\\'; + } else if (j !== last) { + // We matched a UNC root with leftovers + + device = '\\\\' + firstPart + '\\' + path.slice(last, j); + rootEnd = j; + } + } + } + } else { + rootEnd = 1; + } + } else if ((code >= 65/*A*/ && code <= 90/*Z*/) || + (code >= 97/*a*/ && code <= 122/*z*/)) { + // Possible device root + + code = path.charCodeAt(1); + if (path.charCodeAt(1) === 58/*:*/) { + device = path.slice(0, 2); + rootEnd = 2; + if (len > 2) { + code = path.charCodeAt(2); + if (code === 47/*/*/ || code === 92/*\*/) { + // Treat separator following drive name as an absolute path + // indicator + isAbsolute = true; + rootEnd = 3; + } + } + } + } + } else if (code === 47/*/*/ || code === 92/*\*/) { + // `path` contains just a path separator, exit early to avoid unnecessary + // work + return '\\'; } - if (resolvedDevice && resolvedAbsolute) { - break; + code = path.charCodeAt(len - 1); + var trailingSeparator = (code === 47/*/*/ || code === 92/*\*/); + var tail; + if (rootEnd < len) + tail = normalizeStringWin32(path.slice(rootEnd), !isAbsolute); + else + tail = ''; + if (tail.length === 0 && !isAbsolute) + tail = '.'; + if (tail.length > 0 && trailingSeparator) + tail += '\\'; + if (device === undefined) { + if (isAbsolute) { + if (tail.length > 0) + return '\\' + tail; + else + return '\\'; + } else if (tail.length > 0) { + return tail; + } else { + return ''; + } + } else { + if (isAbsolute) { + if (tail.length > 0) + return device + '\\' + tail; + else + return device + '\\'; + } else if (tail.length > 0) { + return device + tail; + } else { + return device; + } } - } + }, - // Convert slashes to backslashes when `resolvedDevice` points to an UNC - // root. Also squash multiple slashes into a single one where appropriate. - if (isUnc) { - resolvedDevice = normalizeUNCRoot(resolvedDevice); - } - // At this point the path should be resolved to a full absolute path, - // but handle relative paths to be safe (might happen when process.cwd() - // fails) + isAbsolute: function isAbsolute(path) { + assertPath(path); + const len = path.length; + if (len === 0) + return false; + var code = path.charCodeAt(0); + if (len > 1) { + if (code === 47/*/*/ || code === 92/*\*/) { + // Possible UNC root + + code = path.charCodeAt(1); + if (code === 47/*/*/ || code === 92/*\*/) { + // Matched double path separator at beginning + var j = 2; + var last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + code = path.charCodeAt(j); + if (code === 47/*/*/ || code === 92/*\*/) + break; + } + if (j < len && j !== last) { + // Matched! + last = j; + // Match 1 or more path separators + for (; j < len; ++j) { + code = path.charCodeAt(j); + if (code !== 47/*/*/ && code !== 92/*\*/) + break; + } + if (j < len && j !== last) { + // Matched! + last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + code = path.charCodeAt(j); + if (code === 47/*/*/ || code === 92/*\*/) + break; + } + if (j !== last) + return true; + } + } + } + } else if ((code >= 65/*A*/ && code <= 90/*Z*/) || + (code >= 97/*a*/ && code <= 122/*z*/)) { + // Possible device root + + code = path.charCodeAt(1); + if (path.charCodeAt(1) === 58/*:*/ && len > 2) { + code = path.charCodeAt(2); + if (code === 47/*/*/ || code === 92/*\*/) + return true; + } + } + } else if (code === 47/*/*/ || code === 92/*\*/) { + return true; + } + return false; + }, + + + join: function join() { + if (arguments.length === 0) + return '.'; + + var joined; + var firstPart; + for (var i = 0; i < arguments.length; ++i) { + var arg = arguments[i]; + assertPath(arg); + if (arg.length > 0) { + if (joined === undefined) + joined = firstPart = arg; + else + joined += '\\' + arg; + } + } - // Normalize the tail path - resolvedTail = normalizeArray(resolvedTail.split(/[\\\/]+/), - !resolvedAbsolute).join('\\'); + if (joined === undefined) + return '.'; + + // Make sure that the joined path doesn't start with two slashes, because + // normalize() will mistake it for an UNC path then. + // + // This step is skipped when it is very clear that the user actually + // intended to point at an UNC path. This is assumed when the first + // non-empty string arguments starts with exactly two slashes followed by + // at least one more non-slash character. + // + // Note that for normalize() to treat a path as an UNC path it needs to + // have at least 2 components, so we don't filter for that here. + // This means that the user can use join to construct UNC paths from + // a server name and a share name; for example: + // path.join('//server', 'share') -> '\\\\server\\share\\') + //var firstPart = paths[0]; + var needsReplace = true; + var slashCount = 0; + var code = firstPart.charCodeAt(0); + if (code === 47/*/*/ || code === 92/*\*/) { + ++slashCount; + const firstLen = firstPart.length; + if (firstLen > 1) { + code = firstPart.charCodeAt(1); + if (code === 47/*/*/ || code === 92/*\*/) { + ++slashCount; + if (firstLen > 2) { + code = firstPart.charCodeAt(2); + if (code === 47/*/*/ || code === 92/*\*/) + ++slashCount; + else { + // We matched a UNC path in the first part + needsReplace = false; + } + } + } + } + } + if (needsReplace) { + // Find any more consecutive slashes we need to replace + for (; slashCount < joined.length; ++slashCount) { + code = joined.charCodeAt(slashCount); + if (code !== 47/*/*/ && code !== 92/*\*/) + break; + } - return (resolvedDevice + (resolvedAbsolute ? '\\' : '') + resolvedTail) || - '.'; -}; + // Replace the slashes if needed + if (slashCount >= 2) + joined = '\\' + joined.slice(slashCount); + } + return win32.normalize(joined); + }, -win32.normalize = function(path) { - assertPath(path); - const result = win32StatPath(path); - var device = result.device; - const isUnc = result.isUnc; - const isAbsolute = result.isAbsolute; - var tail = result.tail; - const trailingSlash = /[\\\/]$/.test(tail); + // It will solve the relative path from `from` to `to`, for instance: + // from = 'C:\\orandea\\test\\aaa' + // to = 'C:\\orandea\\impl\\bbb' + // The output of the function should be: '..\\..\\impl\\bbb' + relative: function relative(from, to) { + assertPath(from); + assertPath(to); - // Normalize the tail path - tail = normalizeArray(tail.split(/[\\\/]+/), !isAbsolute).join('\\'); + if (from === to) + return ''; - if (!tail && !isAbsolute) { - tail = '.'; - } - if (tail && trailingSlash) { - tail += '\\'; - } + var fromOrig = win32.resolve(from); + var toOrig = win32.resolve(to); - // Convert slashes to backslashes when `device` points to an UNC root. - // Also squash multiple slashes into a single one where appropriate. - if (isUnc) { - device = normalizeUNCRoot(device); - } + if (fromOrig === toOrig) + return ''; - return device + (isAbsolute ? '\\' : '') + tail; -}; + from = fromOrig.toLowerCase(); + to = toOrig.toLowerCase(); + if (from === to) + return ''; -win32.isAbsolute = function(path) { - assertPath(path); - return win32StatPath(path).isAbsolute; -}; + // Trim any leading backslashes + var fromStart = 0; + for (; fromStart < from.length; ++fromStart) { + if (from.charCodeAt(fromStart) !== 92/*\*/) + break; + } + var fromEnd = from.length; + var fromLen = (fromEnd - fromStart); + + // Trim any leading backslashes + var toStart = 0; + for (; toStart < to.length; ++toStart) { + if (to.charCodeAt(toStart) !== 92/*\*/) + break; + } + var toEnd = to.length; + var toLen = (toEnd - toStart); + + // Compare paths to find the longest common path from root + var length = (fromLen < toLen ? fromLen : toLen); + var lastCommonSep = -1; + var i = 0; + for (; i <= length; ++i) { + if (i === length) { + if (lastCommonSep > 2 && // ignore separator match following device root + toLen > length && + to.charCodeAt(i) === 92/*\*/) { + // We get here if `from` is the exact base path for `to`. + // For example: from='C:\\foo\\bar'; to='C:\\foo\\bar\\baz' + return toOrig.slice(i + 1); + } + lastCommonSep = i; + break; + } + var fromCode = from.charCodeAt(fromStart + i); + var toCode = to.charCodeAt(toStart + i); + if (fromCode !== toCode) + break; + else if (fromCode === 92/*\*/) + lastCommonSep = i; + } -win32.join = function() { - var paths = []; - for (var i = 0; i < arguments.length; i++) { - var arg = arguments[i]; - assertPath(arg); - if (arg) { - paths.push(arg); + // We found a mismatch before the first common path separator was seen, so + // return the original `to`. + // TODO: do this just for device roots (and not UNC paths)? + if (i !== length && lastCommonSep === -1) { + if (toStart > 0) + return toOrig.slice(toStart); + else + return toOrig; } - } - var joined = paths.join('\\'); - - // Make sure that the joined path doesn't start with two slashes, because - // normalize() will mistake it for an UNC path then. - // - // This step is skipped when it is very clear that the user actually - // intended to point at an UNC path. This is assumed when the first - // non-empty string arguments starts with exactly two slashes followed by - // at least one more non-slash character. - // - // Note that for normalize() to treat a path as an UNC path it needs to - // have at least 2 components, so we don't filter for that here. - // This means that the user can use join to construct UNC paths from - // a server name and a share name; for example: - // path.join('//server', 'share') -> '\\\\server\\share\') - if (!/^[\\\/]{2}[^\\\/]/.test(paths[0])) { - joined = joined.replace(/^[\\\/]{2,}/, '\\'); - } + var out = ''; + if (lastCommonSep === -1) + lastCommonSep = 0; + // Generate the relative path based on the path difference between `to` and + // `from` + for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) { + if (i === fromEnd || from.charCodeAt(i) === 92/*\*/) { + if (out.length === 0) + out += '..'; + else + out += '\\..'; + } + } - return win32.normalize(joined); -}; + // Lastly, append the rest of the destination (`to`) path that comes after + // the common path parts + if (out.length > 0) + return out + toOrig.slice(toStart + lastCommonSep); + else { + toStart += lastCommonSep; + if (toOrig.charCodeAt(toStart) === 92/*\*/) + ++toStart; + return toOrig.slice(toStart); + } + }, -// path.relative(from, to) -// it will solve the relative path from 'from' to 'to', for instance: -// from = 'C:\\orandea\\test\\aaa' -// to = 'C:\\orandea\\impl\\bbb' -// The output of the function should be: '..\\..\\impl\\bbb' -win32.relative = function(from, to) { - assertPath(from); - assertPath(to); + _makeLong: function _makeLong(path) { + // Note: this will *probably* throw somewhere. + if (typeof path !== 'string') + return path; - from = win32.resolve(from); - to = win32.resolve(to); + if (path.length === 0) { + return ''; + } - // windows is not case sensitive - var lowerFrom = from.toLowerCase(); - var lowerTo = to.toLowerCase(); + const resolvedPath = win32.resolve(path); + + if (resolvedPath.length >= 3) { + var code = resolvedPath.charCodeAt(0); + if (code === 92/*\*/) { + // Possible UNC root + + if (resolvedPath.charCodeAt(1) === 92/*\*/) { + code = resolvedPath.charCodeAt(2); + if (code !== 63/*?*/ && code !== 46/*.*/) { + // Matched non-long UNC root, convert the path to a long UNC path + return '\\\\?\\UNC\\' + resolvedPath.slice(2); + } + } + } else if ((code >= 65/*A*/ && code <= 90/*Z*/) || + (code >= 97/*a*/ && code <= 122/*z*/)) { + // Possible device root + + if (resolvedPath.charCodeAt(1) === 58/*:*/ && + resolvedPath.charCodeAt(2) === 92/*\*/) { + // Matched device root, convert the path to a long UNC path + return '\\\\?\\' + resolvedPath; + } + } + } - var toParts = trimArray(to.split('\\')); + return path; + }, - var lowerFromParts = trimArray(lowerFrom.split('\\')); - var lowerToParts = trimArray(lowerTo.split('\\')); - var length = Math.min(lowerFromParts.length, lowerToParts.length); - var samePartsLength = length; - for (var i = 0; i < length; i++) { - if (lowerFromParts[i] !== lowerToParts[i]) { - samePartsLength = i; - break; + dirname: function dirname(path) { + assertPath(path); + const len = path.length; + if (len === 0) + return '.'; + var rootEnd = -1; + var end = -1; + var matchedSlash = true; + var offset = 0; + var code = path.charCodeAt(0); + + // Try to match a root + if (len > 1) { + if (code === 47/*/*/ || code === 92/*\*/) { + // Possible UNC root + + rootEnd = offset = 1; + + code = path.charCodeAt(1); + if (code === 47/*/*/ || code === 92/*\*/) { + // Matched double path separator at beginning + var j = 2; + var last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + code = path.charCodeAt(j); + if (code === 47/*/*/ || code === 92/*\*/) + break; + } + if (j < len && j !== last) { + // Matched! + last = j; + // Match 1 or more path separators + for (; j < len; ++j) { + code = path.charCodeAt(j); + if (code !== 47/*/*/ && code !== 92/*\*/) + break; + } + if (j < len && j !== last) { + // Matched! + last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + code = path.charCodeAt(j); + if (code === 47/*/*/ || code === 92/*\*/) + break; + } + if (j === len) { + // We matched a UNC root only + return path; + } + if (j !== last) { + // We matched a UNC root with leftovers + + // Offset by 1 to include the separator after the UNC root to + // treat it as a "normal root" on top of a (UNC) root + rootEnd = offset = j + 1; + } + } + } + } + } else if ((code >= 65/*A*/ && code <= 90/*Z*/) || + (code >= 97/*a*/ && code <= 122/*z*/)) { + // Possible device root + + code = path.charCodeAt(1); + if (path.charCodeAt(1) === 58/*:*/) { + rootEnd = offset = 2; + if (len > 2) { + code = path.charCodeAt(2); + if (code === 47/*/*/ || code === 92/*\*/) + rootEnd = offset = 3; + } + } + } + } else if (code === 47/*/*/ || code === 92/*\*/) { + return path[0]; } - } - if (samePartsLength === 0) { - return to; - } - - var outputParts = []; - for (var j = samePartsLength; j < lowerFromParts.length; j++) { - outputParts.push('..'); - } + for (var i = len - 1; i >= offset; --i) { + code = path.charCodeAt(i); + if (code === 47/*/*/ || code === 92/*\*/) { + if (!matchedSlash) { + end = i; + break; + } + } else { + // We saw the first non-path separator + matchedSlash = false; + } + } - outputParts = outputParts.concat(toParts.slice(samePartsLength)); + if (end === -1) { + if (rootEnd === -1) + end = len; + else + end = rootEnd; + } + return path.slice(0, end); + }, - return outputParts.join('\\'); -}; + basename: function basename(path, ext) { + assertPath(path); + if (ext !== undefined && typeof ext !== 'string') + throw new TypeError('"ext" argument must be a string'); + var start = 0; + var end = -1; + var matchedSlash = true; + var i; + + // Check for a drive letter prefix so as not to mistake the following + // path separator as an extra separator at the end of the path that can be + // disregarded + if (path.length >= 2) { + const drive = path.charCodeAt(0); + if ((drive >= 65/*A*/ && drive <= 90/*Z*/) || + (drive >= 97/*a*/ && drive <= 122/*z*/)) { + if (path.charCodeAt(1) === 58/*:*/) + start = 2; + } + } -win32._makeLong = function(path) { - // Note: this will *probably* throw somewhere. - if (typeof path !== 'string') - return path; + if (ext !== undefined && ext.length > 0 && ext.length <= path.length) { + if (ext.length === path.length && ext === path) + return ''; + var extIdx = ext.length - 1; + var firstNonSlashEnd = -1; + for (i = path.length - 1; i >= start; --i) { + const code = path.charCodeAt(i); + if (code === 47/*/*/ || code === 92/*\*/) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else { + if (firstNonSlashEnd === -1) { + // We saw the first non-path separator, remember this index in case + // we need it if the extension ends up not matching + matchedSlash = false; + firstNonSlashEnd = i + 1; + } + if (extIdx >= 0) { + // Try to match the explicit extension + if (code === ext.charCodeAt(extIdx)) { + if (--extIdx === -1) { + // We matched the extension, so mark this as the end of our path + // component + end = i; + } + } else { + // Extension does not match, so our result is the entire path + // component + extIdx = -1; + end = firstNonSlashEnd; + } + } + } + } - if (!path) { - return ''; - } + if (end === -1) + return ''; + return path.slice(start, end); + } else { + for (i = path.length - 1; i >= start; --i) { + const code = path.charCodeAt(i); + if (code === 47/*/*/ || code === 92/*\*/) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // path component + matchedSlash = false; + end = i + 1; + } + } - var resolvedPath = win32.resolve(path); + if (end === -1) + return ''; + return path.slice(start, end); + } + }, - if (/^[a-zA-Z]\:\\/.test(resolvedPath)) { - // path is local filesystem path, which needs to be converted - // to long UNC path. - return '\\\\?\\' + resolvedPath; - } else if (/^\\\\[^?.]/.test(resolvedPath)) { - // path is network UNC path, which needs to be converted - // to long UNC path. - return '\\\\?\\UNC\\' + resolvedPath.substring(2); - } - return path; -}; + extname: function extname(path) { + assertPath(path); + var startDot = -1; + var startPart = 0; + var end = -1; + var matchedSlash = true; + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + var preDotState = 0; + for (var i = path.length - 1; i >= 0; --i) { + const code = path.charCodeAt(i); + if (code === 47/*/*/ || code === 92/*\*/) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + if (code === 46/*.*/) { + // If this is our first dot, mark it as the start of our extension + if (startDot === -1) + startDot = i; + else if (preDotState !== 1) + preDotState = 1; + } else if (startDot !== -1) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = -1; + } + } + if (startDot === -1 || + end === -1 || + // We saw a non-dot character immediately before the dot + preDotState === 0 || + // The (right-most) trimmed path component is exactly '..' + (preDotState === 1 && + startDot === end - 1 && + startDot === startPart + 1)) { + return ''; + } + return path.slice(startDot, end); + }, -win32.dirname = function(path) { - const result = win32SplitPath(path); - const root = result[0]; - var dir = result[1]; - if (!root && !dir) { - // No dirname whatsoever - return '.'; - } + format: function format(pathObject) { + if (pathObject === null || typeof pathObject !== 'object') { + throw new TypeError( + 'Parameter "pathObject" must be an object, not ' + typeof pathObject + ); + } - if (dir) { - // It has a dirname, strip trailing slash - dir = dir.substr(0, dir.length - 1); - } + var dir = pathObject.dir || pathObject.root; + var base = pathObject.base || + ((pathObject.name || '') + (pathObject.ext || '')); + if (!dir) { + return base; + } + if (dir === pathObject.root) { + return dir + base; + } + return dir + win32.sep + base; + }, - return root + dir; -}; + parse: function parse(path) { + assertPath(path); -win32.basename = function(path, ext) { - if (ext !== undefined && typeof ext !== 'string') - throw new TypeError('"ext" argument must be a string'); + var ret = { root: '', dir: '', base: '', ext: '', name: '' }; + if (path.length === 0) + return ret; + + var len = path.length; + var rootEnd = 0; + var code = path.charCodeAt(0); + var isAbsolute = false; + + // Try to match a root + if (len > 1) { + if (code === 47/*/*/ || code === 92/*\*/) { + // Possible UNC root + + isAbsolute = true; + + code = path.charCodeAt(1); + if (code === 47/*/*/ || code === 92/*\*/) { + rootEnd = 1; + // Matched double path separator at beginning + var j = 2; + var last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + code = path.charCodeAt(j); + if (code === 47/*/*/ || code === 92/*\*/) + break; + } + if (j < len && j !== last) { + // Matched! + last = j; + // Match 1 or more path separators + for (; j < len; ++j) { + code = path.charCodeAt(j); + if (code !== 47/*/*/ && code !== 92/*\*/) + break; + } + if (j < len && j !== last) { + // Matched! + last = j; + // Match 1 or more non-path separators + for (; j < len; ++j) { + code = path.charCodeAt(j); + if (code === 47/*/*/ || code === 92/*\*/) + break; + } + if (j === len) { + // We matched a UNC root only + + rootEnd = j; + } else if (j !== last) { + // We matched a UNC root with leftovers + + rootEnd = j + 1; + } + } + } + } + } else if ((code >= 65/*A*/ && code <= 90/*Z*/) || + (code >= 97/*a*/ && code <= 122/*z*/)) { + // Possible device root + + code = path.charCodeAt(1); + if (path.charCodeAt(1) === 58/*:*/) { + rootEnd = 2; + if (len > 2) { + code = path.charCodeAt(2); + if (code === 47/*/*/ || code === 92/*\*/) { + if (len === 3) { + // `path` contains just a drive root, exit early to avoid + // unnecessary work + ret.root = ret.dir = path.slice(0, 3); + return ret; + } + isAbsolute = true; + rootEnd = 3; + } + } else { + // `path` contains just a drive root, exit early to avoid + // unnecessary work + ret.root = ret.dir = path.slice(0, 2); + return ret; + } + } + } + } else if (code === 47/*/*/ || code === 92/*\*/) { + // `path` contains just a path separator, exit early to avoid + // unnecessary work + ret.root = ret.dir = path[0]; + return ret; + } - var f = win32SplitPath(path)[2]; - // TODO: make this comparison case-insensitive on windows? - if (ext && f.substr(-1 * ext.length) === ext) { - f = f.substr(0, f.length - ext.length); - } - return f; -}; + if (rootEnd > 0) + ret.root = path.slice(0, rootEnd); + + var startDot = -1; + var startPart = 0; + var end = -1; + var matchedSlash = true; + var i = path.length - 1; + + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + var preDotState = 0; + + // Get non-dir info + for (; i >= rootEnd; --i) { + code = path.charCodeAt(i); + if (code === 47/*/*/ || code === 92/*\*/) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + if (code === 46/*.*/) { + // If this is our first dot, mark it as the start of our extension + if (startDot === -1) + startDot = i; + else if (preDotState !== 1) + preDotState = 1; + } else if (startDot !== -1) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = -1; + } + } + if (startDot === -1 || + end === -1 || + // We saw a non-dot character immediately before the dot + preDotState === 0 || + // The (right-most) trimmed path component is exactly '..' + (preDotState === 1 && + startDot === end - 1 && + startDot === startPart + 1)) { + if (end !== -1) { + if (startPart === 0 && isAbsolute) + ret.base = ret.name = path.slice(rootEnd, end); + else + ret.base = ret.name = path.slice(startPart, end); + } + } else { + if (startPart === 0 && isAbsolute) { + ret.name = path.slice(rootEnd, startDot); + ret.base = path.slice(rootEnd, end); + } else { + ret.name = path.slice(startPart, startDot); + ret.base = path.slice(startPart, end); + } + ret.ext = path.slice(startDot, end); + } -win32.extname = function(path) { - return win32SplitPath(path)[3]; -}; + if (startPart > 0) + ret.dir = path.slice(0, startPart - 1); + else if (isAbsolute) + ret.dir = path.slice(0, rootEnd); + return ret; + }, -win32.format = function(pathObject) { - if (pathObject === null || typeof pathObject !== 'object') { - throw new TypeError( - 'Parameter "pathObject" must be an object, not ' + typeof pathObject - ); - } - var dir = pathObject.dir || pathObject.root; - var base = pathObject.base || - ((pathObject.name || '') + (pathObject.ext || '')); - if (!dir) { - return base; - } - if (dir === pathObject.root) { - return dir + base; - } - return dir + win32.sep + base; + sep: '\\', + delimiter: ';', + win32: null, + posix: null }; -win32.parse = function(pathString) { - assertPath(pathString); - - var allParts = win32SplitPath(pathString); - return { - root: allParts[0], - dir: allParts[0] + allParts[1].slice(0, -1), - base: allParts[2], - ext: allParts[3], - name: allParts[2].slice(0, allParts[2].length - allParts[3].length) - }; -}; +const posix = { + // path.resolve([from ...], to) + resolve: function resolve() { + var resolvedPath = ''; + var resolvedAbsolute = false; + var cwd; + + for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { + var path; + if (i >= 0) + path = arguments[i]; + else { + if (cwd === undefined) + cwd = process.cwd(); + path = cwd; + } + assertPath(path); -win32.sep = '\\'; -win32.delimiter = ';'; + // Skip empty entries + if (path.length === 0) { + continue; + } + resolvedPath = path + '/' + resolvedPath; + resolvedAbsolute = path.charCodeAt(0) === 47/*/*/; + } -// Split a filename into [root, dir, basename, ext], unix version -// 'root' is just a slash, or nothing. -const splitPathRe = - /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/; -var posix = {}; + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + // Normalize the path + resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute); -function posixSplitPath(filename) { - const out = splitPathRe.exec(filename); - out.shift(); - return out; -} + if (resolvedAbsolute) { + if (resolvedPath.length > 0) + return '/' + resolvedPath; + else + return '/'; + } else if (resolvedPath.length > 0) { + return resolvedPath; + } else { + return '.'; + } + }, -// path.resolve([from ...], to) -// posix version -posix.resolve = function() { - var resolvedPath = ''; - var resolvedAbsolute = false; + normalize: function normalize(path) { + assertPath(path); - for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { - var path = (i >= 0) ? arguments[i] : process.cwd(); + if (path.length === 0) + return '.'; - assertPath(path); + const isAbsolute = path.charCodeAt(0) === 47/*/*/; + const trailingSeparator = path.charCodeAt(path.length - 1) === 47/*/*/; - // Skip empty entries - if (path === '') { - continue; - } + // Normalize the path + path = normalizeStringPosix(path, !isAbsolute); - resolvedPath = path + '/' + resolvedPath; - resolvedAbsolute = path[0] === '/'; - } + if (path.length === 0 && !isAbsolute) + path = '.'; + if (path.length > 0 && trailingSeparator) + path += '/'; - // At this point the path should be resolved to a full absolute path, but - // handle relative paths to be safe (might happen when process.cwd() fails) + if (isAbsolute) + return '/' + path; + return path; + }, - // Normalize the path - resolvedPath = normalizeArray(resolvedPath.split('/'), - !resolvedAbsolute).join('/'); - return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; -}; + isAbsolute: function isAbsolute(path) { + assertPath(path); + return path.length > 0 && path.charCodeAt(0) === 47/*/*/; + }, + + + join: function join() { + if (arguments.length === 0) + return '.'; + var joined; + for (var i = 0; i < arguments.length; ++i) { + var arg = arguments[i]; + assertPath(arg); + if (arg.length > 0) { + if (joined === undefined) + joined = arg; + else + joined += '/' + arg; + } + } + if (joined === undefined) + return '.'; + return posix.normalize(joined); + }, -// path.normalize(path) -// posix version -posix.normalize = function(path) { - assertPath(path); - const isAbsolute = posix.isAbsolute(path); - const trailingSlash = path && path[path.length - 1] === '/'; + relative: function relative(from, to) { + assertPath(from); + assertPath(to); - // Normalize the path - path = normalizeArray(path.split('/'), !isAbsolute).join('/'); + if (from === to) + return ''; - if (!path && !isAbsolute) { - path = '.'; - } - if (path && trailingSlash) { - path += '/'; - } + from = posix.resolve(from); + to = posix.resolve(to); - return (isAbsolute ? '/' : '') + path; -}; + if (from === to) + return ''; -// posix version -posix.isAbsolute = function(path) { - assertPath(path); - return !!path && path[0] === '/'; -}; + // Trim any leading backslashes + var fromStart = 1; + for (; fromStart < from.length; ++fromStart) { + if (from.charCodeAt(fromStart) !== 47/*/*/) + break; + } + var fromEnd = from.length; + var fromLen = (fromEnd - fromStart); + + // Trim any leading backslashes + var toStart = 1; + for (; toStart < to.length; ++toStart) { + if (to.charCodeAt(toStart) !== 47/*/*/) + break; + } + var toEnd = to.length; + var toLen = (toEnd - toStart); + + // Compare paths to find the longest common path from root + var length = (fromLen < toLen ? fromLen : toLen); + var lastCommonSep = -1; + var i = 0; + for (; i <= length; ++i) { + if (i === length) { + if (lastCommonSep === -1) { + lastCommonSep = i; + } else if (toLen > length && to.charCodeAt(i + 1) === 47/*/*/) { + // We get here if `from` is the exact base path for `to`. + // For example: from='/foo/bar'; to='/foo/bar/baz' + return to.slice(i + 2); + } + break; + } + var fromCode = from.charCodeAt(fromStart + i); + var toCode = to.charCodeAt(toStart + i); + if (fromCode !== toCode) + break; + else if (fromCode === 47/*/*/) + lastCommonSep = i; + } -// posix version -posix.join = function() { - var path = ''; - for (var i = 0; i < arguments.length; i++) { - var segment = arguments[i]; - assertPath(segment); - if (segment) { - if (!path) { - path += segment; - } else { - path += '/' + segment; + var out = ''; + // Generate the relative path based on the path difference between `to` + // and `from` + for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) { + if (i === fromEnd || from.charCodeAt(i) === 47/*/*/) { + if (out.length === 0) + out += '..'; + else + out += '/..'; } } - } - return posix.normalize(path); -}; + // Lastly, append the rest of the destination (`to`) path that comes after + // the common path parts + if (out.length > 0) + return out + to.slice(toStart + lastCommonSep); + else { + toStart += lastCommonSep; + if (to.charCodeAt(toStart) === 47/*/*/) + ++toStart; + return to.slice(toStart); + } + }, -// path.relative(from, to) -// posix version -posix.relative = function(from, to) { - assertPath(from); - assertPath(to); - from = posix.resolve(from).substr(1); - to = posix.resolve(to).substr(1); + _makeLong: function _makeLong(path) { + return path; + }, - var fromParts = trimArray(from.split('/')); - var toParts = trimArray(to.split('/')); - var length = Math.min(fromParts.length, toParts.length); - var samePartsLength = length; - for (var i = 0; i < length; i++) { - if (fromParts[i] !== toParts[i]) { - samePartsLength = i; - break; + dirname: function dirname(path) { + assertPath(path); + if (path.length === 0) + return '.'; + var code = path.charCodeAt(0); + var hasRoot = (code === 47/*/*/); + var end = -1; + var matchedSlash = true; + for (var i = path.length - 1; i >= 1; --i) { + code = path.charCodeAt(i); + if (code === 47/*/*/) { + if (!matchedSlash) { + end = i; + break; + } + } else { + // We saw the first non-path separator + matchedSlash = false; + } } - } - var outputParts = []; - for (var j = samePartsLength; j < fromParts.length; j++) { - outputParts.push('..'); - } + if (end === -1) + return hasRoot ? '/' : '.'; + if (hasRoot && end === 1) + return '//'; + return path.slice(0, end); + }, - outputParts = outputParts.concat(toParts.slice(samePartsLength)); - - return outputParts.join('/'); -}; - - -posix._makeLong = function(path) { - return path; -}; + basename: function basename(path, ext) { + assertPath(path); + if (ext !== undefined && typeof ext !== 'string') + throw new TypeError('"ext" argument must be a string'); + + var start = 0; + var end = -1; + var matchedSlash = true; + var i; + + if (ext !== undefined && ext.length > 0 && ext.length <= path.length) { + if (ext.length === path.length && ext === path) + return ''; + var extIdx = ext.length - 1; + var firstNonSlashEnd = -1; + for (i = path.length - 1; i >= 0; --i) { + const code = path.charCodeAt(i); + if (code === 47/*/*/) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else { + if (firstNonSlashEnd === -1) { + // We saw the first non-path separator, remember this index in case + // we need it if the extension ends up not matching + matchedSlash = false; + firstNonSlashEnd = i + 1; + } + if (extIdx >= 0) { + // Try to match the explicit extension + if (code === ext.charCodeAt(extIdx)) { + if (--extIdx === -1) { + // We matched the extension, so mark this as the end of our path + // component + end = i; + } + } else { + // Extension does not match, so our result is the entire path + // component + extIdx = -1; + end = firstNonSlashEnd; + } + } + } + } -posix.dirname = function(path) { - const result = posixSplitPath(path); - const root = result[0]; - var dir = result[1]; + if (end === -1) + return ''; + return path.slice(start, end); + } else { + for (i = path.length - 1; i >= 0; --i) { + if (path.charCodeAt(i) === 47/*/*/) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // path component + matchedSlash = false; + end = i + 1; + } + } - if (!root && !dir) { - // No dirname whatsoever - return '.'; - } + if (end === -1) + return ''; + return path.slice(start, end); + } + }, - if (dir) { - // It has a dirname, strip trailing slash - dir = dir.substr(0, dir.length - 1); - } - return root + dir; -}; + extname: function extname(path) { + assertPath(path); + var startDot = -1; + var startPart = 0; + var end = -1; + var matchedSlash = true; + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + var preDotState = 0; + for (var i = path.length - 1; i >= 0; --i) { + const code = path.charCodeAt(i); + if (code === 47/*/*/) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + if (code === 46/*.*/) { + // If this is our first dot, mark it as the start of our extension + if (startDot === -1) + startDot = i; + else if (preDotState !== 1) + preDotState = 1; + } else if (startDot !== -1) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = -1; + } + } + if (startDot === -1 || + end === -1 || + // We saw a non-dot character immediately before the dot + preDotState === 0 || + // The (right-most) trimmed path component is exactly '..' + (preDotState === 1 && + startDot === end - 1 && + startDot === startPart + 1)) { + return ''; + } + return path.slice(startDot, end); + }, -posix.basename = function(path, ext) { - if (ext !== undefined && typeof ext !== 'string') - throw new TypeError('"ext" argument must be a string'); - var f = posixSplitPath(path)[2]; + format: function format(pathObject) { + if (pathObject === null || typeof pathObject !== 'object') { + throw new TypeError( + 'Parameter "pathObject" must be an object, not ' + typeof pathObject + ); + } - if (ext && f.substr(-1 * ext.length) === ext) { - f = f.substr(0, f.length - ext.length); - } - return f; -}; + var dir = pathObject.dir || pathObject.root; + var base = pathObject.base || + ((pathObject.name || '') + (pathObject.ext || '')); + if (!dir) { + return base; + } + if (dir === pathObject.root) { + return dir + base; + } + return dir + posix.sep + base; + }, -posix.extname = function(path) { - return posixSplitPath(path)[3]; -}; + parse: function parse(path) { + assertPath(path); + var ret = { root: '', dir: '', base: '', ext: '', name: '' }; + if (path.length === 0) + return ret; + var code = path.charCodeAt(0); + var isAbsolute = (code === 47/*/*/); + var start; + if (isAbsolute) { + ret.root = '/'; + start = 1; + } else { + start = 0; + } + var startDot = -1; + var startPart = 0; + var end = -1; + var matchedSlash = true; + var i = path.length - 1; + + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + var preDotState = 0; + + // Get non-dir info + for (; i >= start; --i) { + code = path.charCodeAt(i); + if (code === 47/*/*/) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + if (code === 46/*.*/) { + // If this is our first dot, mark it as the start of our extension + if (startDot === -1) + startDot = i; + else if (preDotState !== 1) + preDotState = 1; + } else if (startDot !== -1) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = -1; + } + } -posix.format = function(pathObject) { - if (pathObject === null || typeof pathObject !== 'object') { - throw new TypeError( - 'Parameter "pathObject" must be an object, not ' + typeof pathObject - ); - } + if (startDot === -1 || + end === -1 || + // We saw a non-dot character immediately before the dot + preDotState === 0 || + // The (right-most) trimmed path component is exactly '..' + (preDotState === 1 && + startDot === end - 1 && + startDot === startPart + 1)) { + if (end !== -1) { + if (startPart === 0 && isAbsolute) + ret.base = ret.name = path.slice(1, end); + else + ret.base = ret.name = path.slice(startPart, end); + } + } else { + if (startPart === 0 && isAbsolute) { + ret.name = path.slice(1, startDot); + ret.base = path.slice(1, end); + } else { + ret.name = path.slice(startPart, startDot); + ret.base = path.slice(startPart, end); + } + ret.ext = path.slice(startDot, end); + } - var dir = pathObject.dir || pathObject.root; - var base = pathObject.base || - ((pathObject.name || '') + (pathObject.ext || '')); - if (!dir) { - return base; - } - if (dir === pathObject.root) { - return dir + base; - } - return dir + posix.sep + base; -}; + if (startPart > 0) + ret.dir = path.slice(0, startPart - 1); + else if (isAbsolute) + ret.dir = '/'; + return ret; + }, -posix.parse = function(pathString) { - assertPath(pathString); - var allParts = posixSplitPath(pathString); - return { - root: allParts[0], - dir: allParts[0] + allParts[1].slice(0, -1), - base: allParts[2], - ext: allParts[3], - name: allParts[2].slice(0, allParts[2].length - allParts[3].length) - }; + sep: '/', + delimiter: ':', + win32: null, + posix: null }; -posix.sep = '/'; -posix.delimiter = ':'; +posix.win32 = win32.win32 = win32; +posix.posix = win32.posix = posix; -if (isWindows) +if (process.platform === 'win32') module.exports = win32; -else /* posix */ +else module.exports = posix; - -module.exports.posix = posix; -module.exports.win32 = win32;