From 58479e80ab899402283b522da173c34867d7c78f Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 30 Oct 2019 10:28:41 +0100 Subject: [PATCH] Revert "Merge pull request #83060 from microsoft/joh/uri-parse" This reverts commit 30886e2f677953a108daa22aecd008e4b3c0980a, reversing changes made to 525f1e4754201c9f833de8ef131cfc87cb6d3f6f. --- src/vs/base/common/strings.ts | 30 +- src/vs/base/common/uri.ts | 485 ++++++++---------- src/vs/base/test/{node => common}/uri.test.ts | 377 +++----------- .../editor/browser/services/openerService.ts | 2 +- src/vs/monaco.d.ts | 4 +- src/vs/vscode.d.ts | 4 +- .../api/common/extHostTypeConverters.ts | 2 +- .../debug/test/common/debugSource.test.ts | 2 +- .../contrib/webview/common/portMapping.ts | 8 +- src/vs/workbench/electron-browser/window.ts | 2 +- .../test/node/ripgrepTextSearchEngine.test.ts | 25 +- .../api/extHostTypeConverter.test.ts | 6 +- .../electron-browser/api/extHostTypes.test.ts | 4 +- 13 files changed, 305 insertions(+), 646 deletions(-) rename src/vs/base/test/{node => common}/uri.test.ts (54%) diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index a9d88083981..78cf46bd2c3 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -338,34 +338,6 @@ export function compareIgnoreCase(a: string, b: string): number { } } -/** - * [0-9] - */ -export function isAsciiDigit(code: number): boolean { - return code >= CharCode.Digit0 && code <= CharCode.Digit9; -} - -/** - * [a-f] - */ -export function isLowerAsciiHex(code: number): boolean { - return code >= CharCode.a && code <= CharCode.f; -} - -/** - * [A-F] - */ -export function isUpperAsciiHex(code: number): boolean { - return code >= CharCode.A && code <= CharCode.F; -} - -/** - * [0-9a-fA-F] - */ -export function isAsciiHex(code: number): boolean { - return isAsciiDigit(code) || isLowerAsciiHex(code) || isUpperAsciiHex(code); -} - export function isLowerAsciiLetter(code: number): boolean { return code >= CharCode.a && code <= CharCode.z; } @@ -374,7 +346,7 @@ export function isUpperAsciiLetter(code: number): boolean { return code >= CharCode.A && code <= CharCode.Z; } -export function isAsciiLetter(code: number): boolean { +function isAsciiLetter(code: number): boolean { return isLowerAsciiLetter(code) || isUpperAsciiLetter(code); } diff --git a/src/vs/base/common/uri.ts b/src/vs/base/common/uri.ts index 81160d2a633..7df9153b440 100644 --- a/src/vs/base/common/uri.ts +++ b/src/vs/base/common/uri.ts @@ -5,9 +5,10 @@ import { isWindows } from 'vs/base/common/platform'; import { CharCode } from 'vs/base/common/charCode'; -import { isHighSurrogate, isLowSurrogate, isLowerAsciiHex, isAsciiLetter } from 'vs/base/common/strings'; -const _schemeRegExp = /^\w[\w\d+.-]*$/; +const _schemePattern = /^\w[\w\d+.-]*$/; +const _singleSlashStart = /^\//; +const _doubleSlashStart = /^\/\//; function _validateUri(ret: URI): void { @@ -18,7 +19,7 @@ function _validateUri(ret: URI): void { // scheme, https://tools.ietf.org/html/rfc3986#section-3.1 // ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) - if (ret.scheme && !_schemeRegExp.test(ret.scheme)) { + if (ret.scheme && !_schemePattern.test(ret.scheme)) { throw new Error('[UriError]: Scheme contains illegal characters.'); } @@ -29,11 +30,11 @@ function _validateUri(ret: URI): void { // with two slash characters ("//"). if (ret.path) { if (ret.authority) { - if (ret.path.charCodeAt(0) !== CharCode.Slash) { + if (!_singleSlashStart.test(ret.path)) { throw new Error('[UriError]: If a URI contains an authority component, then the path component must either be empty or begin with a slash ("/") character'); } } else { - if (ret.path.charCodeAt(0) === CharCode.Slash && ret.path.charCodeAt(1) === CharCode.Slash) { + if (_doubleSlashStart.test(ret.path)) { throw new Error('[UriError]: If a URI does not contain an authority component, then the path cannot begin with two slash characters ("//")'); } } @@ -41,14 +42,10 @@ function _validateUri(ret: URI): void { } // graceful behaviour when scheme is missing: fallback to using 'file'-scheme -function _schemeFix(scheme: string, strict?: boolean): string { +function _schemeFix(scheme: string): string { if (!scheme) { - if (strict) { - throw new Error('[UriError]: A scheme must be provided'); - } else { - // console.trace('BAD uri lacks scheme, falling back to file-scheme.'); - scheme = 'file'; - } + console.trace('BAD uri lacks scheme, falling back to file-scheme.'); + scheme = 'file'; } return scheme; } @@ -65,29 +62,18 @@ function _referenceResolution(scheme: string, path: string): string { case 'http': case 'file': if (!path) { - path = '/'; - } else if (path[0].charCodeAt(0) !== CharCode.Slash) { - path = '/' + path; + path = _slash; + } else if (path[0] !== _slash) { + path = _slash + path; } break; } return path; } -const _uriRegExp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; - -const enum MatchIndex { - scheme = 2, - authority = 4, - path = 5, - query = 7, - fragment = 9 -} - -const _percentRegExp = /%/g; -const _hashRegExp = /#/g; -const _backslashRegExp = /\\/g; -const _slashRegExp = /\//g; +const _empty = ''; +const _slash = '/'; +const _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; /** * Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986. @@ -165,20 +151,20 @@ export class URI implements UriComponents { protected constructor(schemeOrData: string | UriComponents, authority?: string, path?: string, query?: string, fragment?: string) { if (typeof schemeOrData === 'object') { - this.scheme = schemeOrData.scheme || ''; - this.authority = schemeOrData.authority || ''; - this.path = schemeOrData.path || ''; - this.query = schemeOrData.query || ''; - this.fragment = schemeOrData.fragment || ''; + this.scheme = schemeOrData.scheme || _empty; + this.authority = schemeOrData.authority || _empty; + this.path = schemeOrData.path || _empty; + this.query = schemeOrData.query || _empty; + this.fragment = schemeOrData.fragment || _empty; // no validation because it's this URI // that creates uri components. // _validateUri(this); } else { this.scheme = _schemeFix(schemeOrData); - this.authority = authority || ''; - this.path = _referenceResolution(this.scheme, path || ''); - this.query = query || ''; - this.fragment = fragment || ''; + this.authority = authority || _empty; + this.path = _referenceResolution(this.scheme, path || _empty); + this.query = query || _empty; + this.fragment = fragment || _empty; _validateUri(this); } @@ -211,7 +197,10 @@ export class URI implements UriComponents { * with URIs that represent files on disk (`file` scheme). */ get fsPath(): string { - return _toFsPath(this.scheme, this.authority, this.path); + // if (this.scheme !== 'file') { + // console.warn(`[UriError] calling fsPath with scheme ${this.scheme}`); + // } + return _makeFsPath(this); } // ---- modify to new ------------------------- @@ -226,27 +215,27 @@ export class URI implements UriComponents { if (scheme === undefined) { scheme = this.scheme; } else if (scheme === null) { - scheme = ''; + scheme = _empty; } if (authority === undefined) { authority = this.authority; } else if (authority === null) { - authority = ''; + authority = _empty; } if (path === undefined) { path = this.path; } else if (path === null) { - path = ''; + path = _empty; } if (query === undefined) { query = this.query; } else if (query === null) { - query = ''; + query = _empty; } if (fragment === undefined) { fragment = this.fragment; } else if (fragment === null) { - fragment = ''; + fragment = _empty; } if (scheme === this.scheme @@ -267,31 +256,20 @@ export class URI implements UriComponents { * Creates a new URI from a string, e.g. `http://www.msft.com/some/path`, * `file:///usr/home`, or `scheme:with/path`. * - * *Note:* When the input lacks a scheme then `file` is used. - * * @param value A string which represents an URI (see `URI#toString`). */ - static parse(value: string, strict?: boolean): URI { - const match = _uriRegExp.exec(value); + static parse(value: string): URI { + const match = _regexp.exec(value); if (!match) { - throw new Error(`[UriError]: Invalid input: ${value}`); + return new _URI(_empty, _empty, _empty, _empty, _empty); } - - const scheme = _schemeFix(match[MatchIndex.scheme], strict) || ''; - const authority = match[MatchIndex.authority] || ''; - const path = _referenceResolution(scheme, match[MatchIndex.path] || ''); - const query = match[MatchIndex.query] || ''; - const fragment = match[MatchIndex.fragment] || ''; - - const result = new _URI( - scheme, - percentDecode(authority), - percentDecode(path), - percentDecode(query), - percentDecode(fragment), + return new _URI( + match[2] || _empty, + decodeURIComponent(match[4] || _empty), + decodeURIComponent(match[5] || _empty), + decodeURIComponent(match[7] || _empty), + decodeURIComponent(match[9] || _empty) ); - result._external = _toExternal(_normalEncoder, scheme, authority, path, query, fragment); - return result; } /** @@ -317,41 +295,29 @@ export class URI implements UriComponents { */ static file(path: string): URI { - let authority = ''; + let authority = _empty; // normalize to fwd-slashes on windows, // on other systems bwd-slashes are valid // filename character, eg /f\oo/ba\r.txt if (isWindows) { - path = path.replace(_backslashRegExp, '/'); + path = path.replace(/\\/g, _slash); } // check for authority as used in UNC shares // or use the path as given - if (path.charCodeAt(0) === CharCode.Slash && path.charCodeAt(1) === CharCode.Slash) { - const idx = path.indexOf('/', 2); + if (path[0] === _slash && path[1] === _slash) { + const idx = path.indexOf(_slash, 2); if (idx === -1) { authority = path.substring(2); - path = '/'; + path = _slash; } else { authority = path.substring(2, idx); - path = path.substring(idx) || '/'; + path = path.substring(idx) || _slash; } } - // ensures that path starts with / - path = _referenceResolution('file', path); - - // escape some vital characters - authority = authority.replace(_percentRegExp, '%25'); - path = path.replace(_percentRegExp, '%25'); - path = path.replace(_hashRegExp, '%23'); - - if (!isWindows) { - path = path.replace(_backslashRegExp, '%5C'); - } - - return URI.parse('file://' + authority + path); + return new _URI('file', authority, path, _empty, _empty); } static from(components: { scheme: string; authority?: string; path?: string; query?: string; fragment?: string; }): URI { @@ -378,7 +344,7 @@ export class URI implements UriComponents { * @param skipEncoding Do not encode the result, default is `false` */ toString(skipEncoding: boolean = false): string { - return _toExternal(skipEncoding ? _minimalEncoder : _normalEncoder, this.scheme, this.authority, this.path, this.query, this.fragment); + return _asFormatted(this, skipEncoding); } toJSON(): UriComponents { @@ -396,7 +362,7 @@ export class URI implements UriComponents { return data; } else { const result = new _URI(data); - result._external = (data).external; + result._formatted = (data).external; result._fsPath = (data)._sep === _pathSepMarker ? (data).fsPath : null; return result; } @@ -423,25 +389,26 @@ const _pathSepMarker = isWindows ? 1 : undefined; // tslint:disable-next-line:class-name class _URI extends URI { - _external: string | null = null; + _formatted: string | null = null; _fsPath: string | null = null; get fsPath(): string { if (!this._fsPath) { - this._fsPath = _toFsPath(this.scheme, this.authority, this.path); + this._fsPath = _makeFsPath(this); } return this._fsPath; } toString(skipEncoding: boolean = false): string { - if (skipEncoding) { + if (!skipEncoding) { + if (!this._formatted) { + this._formatted = _asFormatted(this, false); + } + return this._formatted; + } else { // we don't cache that - return _toExternal(_minimalEncoder, this.scheme, this.authority, this.path, this.query, this.fragment); - } - if (!this._external) { - this._external = _toExternal(_normalEncoder, this.scheme, this.authority, this.path, this.query, this.fragment); + return _asFormatted(this, true); } - return this._external; } toJSON(): UriComponents { @@ -453,8 +420,8 @@ class _URI extends URI { res.fsPath = this._fsPath; res._sep = _pathSepMarker; } - if (this._external) { - res.external = this._external; + if (this._formatted) { + res.external = this._formatted; } // uri components if (this.path) { @@ -476,225 +443,205 @@ class _URI extends URI { } } -/** - * Compute `fsPath` for the given uri - */ -function _toFsPath(scheme: string, authority: string, path: string): string { +// reserved characters: https://tools.ietf.org/html/rfc3986#section-2.2 +const encodeTable: { [ch: number]: string; } = { + [CharCode.Colon]: '%3A', // gen-delims + [CharCode.Slash]: '%2F', + [CharCode.QuestionMark]: '%3F', + [CharCode.Hash]: '%23', + [CharCode.OpenSquareBracket]: '%5B', + [CharCode.CloseSquareBracket]: '%5D', + [CharCode.AtSign]: '%40', + + [CharCode.ExclamationMark]: '%21', // sub-delims + [CharCode.DollarSign]: '%24', + [CharCode.Ampersand]: '%26', + [CharCode.SingleQuote]: '%27', + [CharCode.OpenParen]: '%28', + [CharCode.CloseParen]: '%29', + [CharCode.Asterisk]: '%2A', + [CharCode.Plus]: '%2B', + [CharCode.Comma]: '%2C', + [CharCode.Semicolon]: '%3B', + [CharCode.Equals]: '%3D', + + [CharCode.Space]: '%20', +}; + +function encodeURIComponentFast(uriComponent: string, allowSlash: boolean): string { + let res: string | undefined = undefined; + let nativeEncodePos = -1; + + for (let pos = 0; pos < uriComponent.length; pos++) { + const code = uriComponent.charCodeAt(pos); + + // unreserved characters: https://tools.ietf.org/html/rfc3986#section-2.3 + if ( + (code >= CharCode.a && code <= CharCode.z) + || (code >= CharCode.A && code <= CharCode.Z) + || (code >= CharCode.Digit0 && code <= CharCode.Digit9) + || code === CharCode.Dash + || code === CharCode.Period + || code === CharCode.Underline + || code === CharCode.Tilde + || (allowSlash && code === CharCode.Slash) + ) { + // check if we are delaying native encode + if (nativeEncodePos !== -1) { + res += encodeURIComponent(uriComponent.substring(nativeEncodePos, pos)); + nativeEncodePos = -1; + } + // check if we write into a new string (by default we try to return the param) + if (res !== undefined) { + res += uriComponent.charAt(pos); + } - let value: string; - if (authority && path.length > 1 && scheme === 'file') { - // unc path: file://shares/c$/far/boo - value = `//${authority}${path}`; - } else if ( - path.charCodeAt(0) === CharCode.Slash - && isAsciiLetter(path.charCodeAt(1)) - && path.charCodeAt(2) === CharCode.Colon - ) { - // windows drive letter: file:///c:/far/boo - value = path[1].toLowerCase() + path.substr(2); - } else { - // other path - value = path; - } - if (isWindows) { - value = value.replace(_slashRegExp, '\\'); - } - return value; -} + } else { + // encoding needed, we need to allocate a new string + if (res === undefined) { + res = uriComponent.substr(0, pos); + } + // check with default table first + const escaped = encodeTable[code]; + if (escaped !== undefined) { -//#region ---- decode + // check if we are delaying native encode + if (nativeEncodePos !== -1) { + res += encodeURIComponent(uriComponent.substring(nativeEncodePos, pos)); + nativeEncodePos = -1; + } -function decodeURIComponentGraceful(str: string): string { - try { - return decodeURIComponent(str); - } catch { - if (str.length > 3) { - return str.substr(0, 3) + decodeURIComponentGraceful(str.substr(3)); - } else { - return str; + // append escaped variant to result + res += escaped; + + } else if (nativeEncodePos === -1) { + // use native encode only when needed + nativeEncodePos = pos; + } } } -} -const _encodedAsHexRegExp = /(%[0-9A-Za-z][0-9A-Za-z])+/g; - -function percentDecode(str: string): string { - if (!str.match(_encodedAsHexRegExp)) { - return str; + if (nativeEncodePos !== -1) { + res += encodeURIComponent(uriComponent.substring(nativeEncodePos)); } - return str.replace(_encodedAsHexRegExp, (match) => decodeURIComponentGraceful(match)); -} - -//#endregion - -//#region ---- encode - -// https://url.spec.whatwg.org/#percent-encoded-bytes -// "The C0 control percent-encode set are the C0 controls and all code points greater than U+007E (~)." -function isC0ControlPercentEncodeSet(code: number): boolean { - return code <= 0x1F || code > 0x7E; -} -// "The fragment percent-encode set is the C0 control percent-encode set and U+0020 SPACE, U+0022 ("), U+003C (<), U+003E (>), and U+0060 (`)." -function isFragmentPercentEncodeSet(code: number): boolean { - return isC0ControlPercentEncodeSet(code) - || code === 0x20 || code === 0x22 || code === 0x3C || code === 0x3E || code === 0x60; -} -// "The path percent-encode set is the fragment percent-encode set and U+0023 (#), U+003F (?), U+007B ({), and U+007D (})." -function isPathPercentEncodeSet(code: number): boolean { - return isFragmentPercentEncodeSet(code) - || code === 0x23 || code === 0x3F || code === 0x7B || code === 0x7D; -} -// "The userinfo percent-encode set is the path percent-encode set and U+002F (/), U+003A (:), U+003B (;), U+003D (=), U+0040 (@), U+005B ([), U+005C (\), U+005D (]), U+005E (^), and U+007C (|)." -function isUserInfoPercentEncodeSet(code: number): boolean { - return isPathPercentEncodeSet(code) - || code === 0x2F || code === 0x3A || code === 0x3B || code === 0x3D || code === 0x40 - || code === 0x5B || code === 0x5C || code === 0x5D || code === 0x5E || code === 0x7C; -} - -// https://url.spec.whatwg.org/#query-state -function isQueryPrecentEncodeSet(code: number): boolean { - return code < 0x21 || code > 0x7E - || code === 0x22 || code === 0x23 || code === 0x3C || code === 0x3E - || code === 0x27; // <- todo@joh https://url.spec.whatwg.org/#is-special -} - -// this is non-standard and uses for `URI.toString(true)` -function isHashOrQuestionMark(code: number): boolean { - return code === CharCode.Hash || code === CharCode.QuestionMark; + return res !== undefined ? res : uriComponent; } -const _encodeTable: string[] = (function () { - let table: string[] = []; - for (let code = 0; code < 128; code++) { - if (code < 16) { - table[code] = `%0${code.toString(16).toUpperCase()}`; - } else { - table[code] = `%${code.toString(16).toUpperCase()}`; - } - } - return table; -})(); - -function percentEncode(str: string, mustEncode: (code: number) => boolean): string { - let lazyOutStr: string | undefined; - for (let pos = 0; pos < str.length; pos++) { - const code = str.charCodeAt(pos); - - // invoke encodeURIComponent when needed - if (mustEncode(code)) { - if (!lazyOutStr) { - lazyOutStr = str.substr(0, pos); +function encodeURIComponentMinimal(path: string): string { + let res: string | undefined = undefined; + for (let pos = 0; pos < path.length; pos++) { + const code = path.charCodeAt(pos); + if (code === CharCode.Hash || code === CharCode.QuestionMark) { + if (res === undefined) { + res = path.substr(0, pos); } - if (isHighSurrogate(code)) { - // Append encoded version of this surrogate pair (2 characters) - if (pos + 1 < str.length && isLowSurrogate(str.charCodeAt(pos + 1))) { - lazyOutStr += encodeURIComponent(str.substr(pos, 2)); - pos += 1; - } else { - // broken surrogate pair - lazyOutStr += str.charAt(pos); - } - } else { - // Append encoded version of the current character, use lookup table - // to speed up repeated encoding of the same characters. - if (code < _encodeTable.length) { - lazyOutStr += _encodeTable[code]; - } else { - lazyOutStr += encodeURIComponent(str.charAt(pos)); - } - } - continue; - } - - // normalize percent encoded sequences to upper case - // todo@joh also changes invalid sequences - if (code === CharCode.PercentSign - && pos + 2 < str.length - && (isLowerAsciiHex(str.charCodeAt(pos + 1)) || isLowerAsciiHex(str.charCodeAt(pos + 2))) - ) { - if (!lazyOutStr) { - lazyOutStr = str.substr(0, pos); + res += encodeTable[code]; + } else { + if (res !== undefined) { + res += path[pos]; } - lazyOutStr += '%' + str.substr(pos + 1, 2).toUpperCase(); - pos += 2; - continue; - } - - // once started, continue to build up lazy output - if (lazyOutStr) { - lazyOutStr += str.charAt(pos); } } - return lazyOutStr || str; + return res !== undefined ? res : path; } -const enum EncodePart { - user, authority, path, query, fragment -} -const _normalEncoder: { (code: number): boolean }[] = [isUserInfoPercentEncodeSet, isC0ControlPercentEncodeSet, isPathPercentEncodeSet, isQueryPrecentEncodeSet, isFragmentPercentEncodeSet]; -const _minimalEncoder: { (code: number): boolean }[] = [isHashOrQuestionMark, isHashOrQuestionMark, isHashOrQuestionMark, isHashOrQuestionMark, () => false]; +/** + * Compute `fsPath` for the given uri + */ +function _makeFsPath(uri: URI): string { -const _driveLetterRegExp = /^(\/?[a-z])(:|%3a)/i; + let value: string; + if (uri.authority && uri.path.length > 1 && uri.scheme === 'file') { + // unc path: file://shares/c$/far/boo + value = `//${uri.authority}${uri.path}`; + } else if ( + uri.path.charCodeAt(0) === CharCode.Slash + && (uri.path.charCodeAt(1) >= CharCode.A && uri.path.charCodeAt(1) <= CharCode.Z || uri.path.charCodeAt(1) >= CharCode.a && uri.path.charCodeAt(1) <= CharCode.z) + && uri.path.charCodeAt(2) === CharCode.Colon + ) { + // windows drive letter: file:///c:/far/boo + value = uri.path[1].toLowerCase() + uri.path.substr(2); + } else { + // other path + value = uri.path; + } + if (isWindows) { + value = value.replace(/\//g, '\\'); + } + return value; +} /** * Create the external version of a uri */ -function _toExternal(encoder: { (code: number): boolean }[], scheme: string, authority: string, path: string, query: string, fragment: string): string { +function _asFormatted(uri: URI, skipEncoding: boolean): string { + + const encoder = !skipEncoding + ? encodeURIComponentFast + : encodeURIComponentMinimal; let res = ''; + let { scheme, authority, path, query, fragment } = uri; if (scheme) { res += scheme; res += ':'; } if (authority || scheme === 'file') { - res += '//'; + res += _slash; + res += _slash; } if (authority) { - const idxUserInfo = authority.indexOf('@'); - if (idxUserInfo !== -1) { - // - const userInfo = authority.substr(0, idxUserInfo); - const idxPasswordOrToken = userInfo.indexOf(':'); - if (idxPasswordOrToken !== -1) { - res += percentEncode(userInfo.substr(0, idxPasswordOrToken), encoder[EncodePart.user]); - res += ':'; - res += percentEncode(userInfo.substr(idxPasswordOrToken + 1), encoder[EncodePart.user]); + let idx = authority.indexOf('@'); + if (idx !== -1) { + // @ + const userinfo = authority.substr(0, idx); + authority = authority.substr(idx + 1); + idx = userinfo.indexOf(':'); + if (idx === -1) { + res += encoder(userinfo, false); } else { - res += percentEncode(userInfo, encoder[EncodePart.user]); + // :@ + res += encoder(userinfo.substr(0, idx), false); + res += ':'; + res += encoder(userinfo.substr(idx + 1), false); } res += '@'; } - authority = authority.substr(idxUserInfo + 1).toLowerCase(); - const idxPort = authority.indexOf(':'); - if (idxPort !== -1) { - // : - res += percentEncode(authority.substr(0, idxPort), encoder[EncodePart.authority]); - res += ':'; + authority = authority.toLowerCase(); + idx = authority.indexOf(':'); + if (idx === -1) { + res += encoder(authority, false); + } else { + // : + res += encoder(authority.substr(0, idx), false); + res += authority.substr(idx); } - res += percentEncode(authority.substr(idxPort + 1), encoder[EncodePart.authority]); } if (path) { - // encode the path - let pathEncoded = percentEncode(path, encoder[EncodePart.path]); - - // lower-case windows drive letters in /C:/fff or C:/fff and escape `:` - let match = _driveLetterRegExp.exec(pathEncoded); - if (match) { - pathEncoded = match[1].toLowerCase() + '%3A' + pathEncoded.substr(match[0].length); + // lower-case windows drive letters in /C:/fff or C:/fff + if (path.length >= 3 && path.charCodeAt(0) === CharCode.Slash && path.charCodeAt(2) === CharCode.Colon) { + const code = path.charCodeAt(1); + if (code >= CharCode.A && code <= CharCode.Z) { + path = `/${String.fromCharCode(code + 32)}:${path.substr(3)}`; // "/c:".length === 3 + } + } else if (path.length >= 2 && path.charCodeAt(1) === CharCode.Colon) { + const code = path.charCodeAt(0); + if (code >= CharCode.A && code <= CharCode.Z) { + path = `${String.fromCharCode(code + 32)}:${path.substr(2)}`; // "/c:".length === 3 + } } - res += pathEncoded; + // encode the rest of the path + res += encoder(path, true); } if (query) { res += '?'; - res += percentEncode(query, encoder[EncodePart.query]); + res += encoder(query, false); } if (fragment) { res += '#'; - res += percentEncode(fragment, encoder[EncodePart.fragment]); + res += !skipEncoding ? encodeURIComponentFast(fragment, false) : fragment; } return res; } - -//#endregion diff --git a/src/vs/base/test/node/uri.test.ts b/src/vs/base/test/common/uri.test.ts similarity index 54% rename from src/vs/base/test/node/uri.test.ts rename to src/vs/base/test/common/uri.test.ts index b9fbd24de8c..3255575ae57 100644 --- a/src/vs/base/test/node/uri.test.ts +++ b/src/vs/base/test/common/uri.test.ts @@ -2,11 +2,10 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - import * as assert from 'assert'; import { URI, UriComponents } from 'vs/base/common/uri'; import { isWindows } from 'vs/base/common/platform'; -import { pathToFileURL, fileURLToPath } from 'url'; + suite('URI', () => { test('file#toString', () => { @@ -64,8 +63,8 @@ suite('URI', () => { assert.equal(URI.from({ scheme: 'http', authority: '', path: 'my/path' }).toString(), 'http:/my/path'); assert.equal(URI.from({ scheme: 'http', authority: '', path: '/my/path' }).toString(), 'http:/my/path'); //http://a-test-site.com/#test=true - assert.equal(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: 'test=true' }).toString(), 'http://a-test-site.com/?test=true'); - assert.equal(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: '', fragment: 'test=true' }).toString(), 'http://a-test-site.com/#test=true'); + assert.equal(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: 'test=true' }).toString(), 'http://a-test-site.com/?test%3Dtrue'); + assert.equal(URI.from({ scheme: 'http', authority: 'a-test-site.com', path: '/', query: '', fragment: 'test=true' }).toString(), 'http://a-test-site.com/#test%3Dtrue'); }); test('http#toString, encode=FALSE', () => { @@ -88,7 +87,7 @@ suite('URI', () => { assert.equal(uri2.fragment, uri3.fragment); }); - test('URI#with, identity', () => { + test('with, identity', () => { let uri = URI.parse('foo:bar/path'); let uri2 = uri.with(null!); @@ -101,17 +100,17 @@ suite('URI', () => { assert.ok(uri === uri2); }); - test('URI#with, changes', () => { + test('with, changes', () => { assert.equal(URI.parse('before:some/file/path').with({ scheme: 'after' }).toString(), 'after:some/file/path'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'http', path: '/api/files/test.me', query: 't=1234' }).toString(), 'http:/api/files/test.me?t=1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'http', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'http:/api/files/test.me?t=1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'https', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'https:/api/files/test.me?t=1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'HTTP', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'HTTP:/api/files/test.me?t=1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'HTTPS', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'HTTPS:/api/files/test.me?t=1234'); - assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'boo', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'boo:/api/files/test.me?t=1234'); + assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'http', path: '/api/files/test.me', query: 't=1234' }).toString(), 'http:/api/files/test.me?t%3D1234'); + assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'http', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'http:/api/files/test.me?t%3D1234'); + assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'https', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'https:/api/files/test.me?t%3D1234'); + assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'HTTP', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'HTTP:/api/files/test.me?t%3D1234'); + assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'HTTPS', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'HTTPS:/api/files/test.me?t%3D1234'); + assert.equal(URI.from({ scheme: 's' }).with({ scheme: 'boo', authority: '', path: '/api/files/test.me', query: 't=1234', fragment: '' }).toString(), 'boo:/api/files/test.me?t%3D1234'); }); - test('URI#with, remove components #8465', () => { + test('with, remove components #8465', () => { assert.equal(URI.parse('scheme://authority/path').with({ authority: '' }).toString(), 'scheme:/path'); assert.equal(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ authority: '' }).toString(), 'scheme:/path'); assert.equal(URI.parse('scheme:/path').with({ authority: 'authority' }).with({ authority: null }).toString(), 'scheme:/path'); @@ -121,7 +120,7 @@ suite('URI', () => { assert.equal(URI.parse('scheme:/path').with({ authority: null }).toString(), 'scheme:/path'); }); - test('URI#with, validation', () => { + test('with, validation', () => { let uri = URI.parse('foo:bar/path'); assert.throws(() => uri.with({ scheme: 'fai:l' })); assert.throws(() => uri.with({ scheme: 'fäil' })); @@ -235,11 +234,6 @@ suite('URI', () => { assert.throws(() => URI.parse('file:////shares/files/p.cs')); }); - test('URI#parse, missing scheme', () => { - assert.throws(() => URI.parse('/foo/bar', true)); - assertToString('/foo/bar', 'file:///foo/bar'); - }); - test('URI#file, win-speciale', () => { if (isWindows) { let value = URI.file('c:\\test\\drive'); @@ -260,7 +254,7 @@ suite('URI', () => { assert.equal(value.fsPath, '\\\\localhost\\c$\\GitDevelopment\\express'); assert.equal(value.query, ''); assert.equal(value.fragment, ''); - assert.equal(value.toString(), 'file://localhost/c$/GitDevelopment/express'); + assert.equal(value.toString(), 'file://localhost/c%24/GitDevelopment/express'); value = URI.file('c:\\test with %\\path'); assert.equal(value.path, '/c:/test with %/path'); @@ -315,78 +309,50 @@ suite('URI', () => { assert.equal(value.toString(), 'file:///a.file'); }); - - function assertToString(input: string | URI, expected: string) { - if (typeof input === 'string') { - input = URI.parse(input); - } - const actual = input.toString(); - assert.equal(actual, expected.toString()); - } - - function assertComponents(input: string | URI, scheme: string, authority: string, path: string, query: string, fragment: string) { - if (typeof input === 'string') { - input = URI.parse(input); - } - assert.equal(input.scheme, scheme); - assert.equal(input.authority, authority); - assert.equal(input.path, path); - assert.equal(input.query, query); - assert.equal(input.fragment, fragment); - } - - function assertEqualUri(input: string | URI, other: string | URI) { - if (typeof input === 'string') { - input = URI.parse(input); - } - if (typeof other === 'string') { - other = URI.parse(other); - } - assert.equal(input.scheme, other.scheme); - assert.equal(input.authority, other.authority); - assert.equal(input.path, other.path); - assert.equal(input.query, other.query); - assert.equal(input.fragment, other.fragment); - assert.equal(input.toString(), other.toString()); - } - test('URI.toString, only scheme and query', () => { - assertToString('stuff:?qüery', 'stuff:?q%C3%BCery'); + const value = URI.parse('stuff:?qüery'); + assert.equal(value.toString(), 'stuff:?q%C3%BCery'); }); test('URI#toString, upper-case percent espaces', () => { - assertToString('file://sh%c3%a4res/path', 'file://sh%C3%A4res/path'); - assertToString('file://sh%c3%z4res/path', 'file://sh%C3%z4res/path'); - assertToString('file:///sh%a0res/path', 'file:///sh%A0res/path'); // also upper-cased invalid sequence + const value = URI.parse('file://sh%c3%a4res/path'); + assert.equal(value.toString(), 'file://sh%C3%A4res/path'); }); test('URI#toString, lower-case windows drive letter', () => { - assertToString('untitled:c:/Users/jrieken/Code/abc.txt', 'untitled:c%3A/Users/jrieken/Code/abc.txt'); - assertToString('untitled:C:/Users/jrieken/Code/abc.txt', 'untitled:c%3A/Users/jrieken/Code/abc.txt'); + assert.equal(URI.parse('untitled:c:/Users/jrieken/Code/abc.txt').toString(), 'untitled:c%3A/Users/jrieken/Code/abc.txt'); + assert.equal(URI.parse('untitled:C:/Users/jrieken/Code/abc.txt').toString(), 'untitled:c%3A/Users/jrieken/Code/abc.txt'); }); test('URI#toString, escape all the bits', () => { + const value = URI.file('/Users/jrieken/Code/_samples/18500/Mödel + Other Thîngß/model.js'); - assertToString(value, 'file:///Users/jrieken/Code/_samples/18500/M%C3%B6del%20+%20Other%20Th%C3%AEng%C3%9F/model.js'); + assert.equal(value.toString(), 'file:///Users/jrieken/Code/_samples/18500/M%C3%B6del%20%2B%20Other%20Th%C3%AEng%C3%9F/model.js'); }); test('URI#toString, don\'t encode port', () => { let value = URI.parse('http://localhost:8080/far'); - assertToString(value, 'http://localhost:8080/far'); + assert.equal(value.toString(), 'http://localhost:8080/far'); value = URI.from({ scheme: 'http', authority: 'löcalhost:8080', path: '/far', query: undefined, fragment: undefined }); - assertToString(value, 'http://l%C3%B6calhost:8080/far'); + assert.equal(value.toString(), 'http://l%C3%B6calhost:8080/far'); }); test('URI#toString, user information in authority', () => { - assertToString('http://foo:bar@localhost/far', 'http://foo:bar@localhost/far'); - assertToString('http://foo@localhost/far', 'http://foo@localhost/far'); - assertToString('http://foo:bAr@localhost:8080/far', 'http://foo:bAr@localhost:8080/far'); - assertToString('http://foo@localhost:8080/far', 'http://foo@localhost:8080/far'); - assertToString( - URI.from({ scheme: 'http', authority: 'föö:bör@löcalhost:8080', path: '/far', query: undefined, fragment: undefined }), - 'http://f%C3%B6%C3%B6:b%C3%B6r@l%C3%B6calhost:8080/far' - ); + let value = URI.parse('http://foo:bar@localhost/far'); + assert.equal(value.toString(), 'http://foo:bar@localhost/far'); + + value = URI.parse('http://foo@localhost/far'); + assert.equal(value.toString(), 'http://foo@localhost/far'); + + value = URI.parse('http://foo:bAr@localhost:8080/far'); + assert.equal(value.toString(), 'http://foo:bAr@localhost:8080/far'); + + value = URI.parse('http://foo@localhost:8080/far'); + assert.equal(value.toString(), 'http://foo@localhost:8080/far'); + + value = URI.from({ scheme: 'http', authority: 'föö:bör@löcalhost:8080', path: '/far', query: undefined, fragment: undefined }); + assert.equal(value.toString(), 'http://f%C3%B6%C3%B6:b%C3%B6r@l%C3%B6calhost:8080/far'); }); test('correctFileUriToFilePath2', () => { @@ -410,7 +376,7 @@ suite('URI', () => { let uri = URI.parse('https://go.microsoft.com/fwlink/?LinkId=518008'); assert.equal(uri.query, 'LinkId=518008'); assert.equal(uri.toString(true), 'https://go.microsoft.com/fwlink/?LinkId=518008'); - assert.equal(uri.toString(), 'https://go.microsoft.com/fwlink/?LinkId=518008'); + assert.equal(uri.toString(), 'https://go.microsoft.com/fwlink/?LinkId%3D518008'); let uri2 = URI.parse(uri.toString()); assert.equal(uri2.query, 'LinkId=518008'); @@ -419,7 +385,7 @@ suite('URI', () => { uri = URI.parse('https://go.microsoft.com/fwlink/?LinkId=518008&foö&ké¥=üü'); assert.equal(uri.query, 'LinkId=518008&foö&ké¥=üü'); assert.equal(uri.toString(true), 'https://go.microsoft.com/fwlink/?LinkId=518008&foö&ké¥=üü'); - assert.equal(uri.toString(), 'https://go.microsoft.com/fwlink/?LinkId=518008&fo%C3%B6&k%C3%A9%C2%A5=%C3%BC%C3%BC'); + assert.equal(uri.toString(), 'https://go.microsoft.com/fwlink/?LinkId%3D518008%26fo%C3%B6%26k%C3%A9%C2%A5%3D%C3%BC%C3%BC'); uri2 = URI.parse(uri.toString()); assert.equal(uri2.query, 'LinkId=518008&foö&ké¥=üü'); @@ -461,122 +427,47 @@ suite('URI', () => { }); test('Unable to open \'%A0.txt\': URI malformed #76506', function () { - let uriFromPath = URI.file('/foo/%A0.txt'); - let uriFromStr = URI.parse(uriFromPath.toString()); - assert.equal(uriFromPath.scheme, uriFromStr.scheme); - assert.equal(uriFromPath.path, uriFromStr.path); - assert.equal(uriFromPath.toString(), 'file:///foo/%25A0.txt'); - assert.equal(uriFromStr.toString(), 'file:///foo/%25A0.txt'); - }); - test('Unable to open \'%2e.txt\'', function () { - let uriFromPath = URI.file('/foo/%2e.txt'); - let uriFromStr = URI.parse(uriFromPath.toString()); - assert.equal(uriFromPath.scheme, uriFromStr.scheme); - assert.equal(uriFromPath.path, uriFromStr.path); - assert.equal(uriFromPath.toString(), 'file:///foo/%252e.txt'); - assert.equal(uriFromStr.toString(), 'file:///foo/%252e.txt'); + let uri = URI.file('/foo/%A0.txt'); + let uri2 = URI.parse(uri.toString()); + assert.equal(uri.scheme, uri2.scheme); + assert.equal(uri.path, uri2.path); + + uri = URI.file('/foo/%2e.txt'); + uri2 = URI.parse(uri.toString()); + assert.equal(uri.scheme, uri2.scheme); + assert.equal(uri.path, uri2.path); }); + test('Links in markdown are broken if url contains encoded parameters #79474', function () { + this.skip(); let strIn = 'https://myhost.com/Redirect?url=http%3A%2F%2Fwww.bing.com%3Fsearch%3Dtom'; let uri1 = URI.parse(strIn); - assertToString(uri1, strIn); let strOut = uri1.toString(); let uri2 = URI.parse(strOut); - assertEqualUri(uri1, uri2); - assert.equal(strIn, strOut); + + assert.equal(uri1.scheme, uri2.scheme); + assert.equal(uri1.authority, uri2.authority); + assert.equal(uri1.path, uri2.path); + assert.equal(uri1.query, uri2.query); + assert.equal(uri1.fragment, uri2.fragment); + assert.equal(strIn, strOut); // fails here!! }); test('Uri#parse can break path-component #45515', function () { + this.skip(); let strIn = 'https://firebasestorage.googleapis.com/v0/b/brewlangerie.appspot.com/o/products%2FzVNZkudXJyq8bPGTXUxx%2FBetterave-Sesame.jpg?alt=media&token=0b2310c4-3ea6-4207-bbde-9c3710ba0437'; let uri1 = URI.parse(strIn); - assertToString(uri1, strIn); - - assertComponents(uri1, - 'https', - 'firebasestorage.googleapis.com', - '/v0/b/brewlangerie.appspot.com/o/products/zVNZkudXJyq8bPGTXUxx/Betterave-Sesame.jpg', // INCORRECT: %2F got decoded but for compat reasons we cannot change this anymore... - 'alt=media&token=0b2310c4-3ea6-4207-bbde-9c3710ba0437', - '' - ); - let strOut = uri1.toString(); let uri2 = URI.parse(strOut); - assertEqualUri(uri1, uri2); - }); - - test('Nonce does not match on login #75755', function () { - let uri = URI.parse('http://localhost:60371/signin?nonce=iiK1zRI%2BHyDCKb2zatvrYA%3D%3D'); - assertComponents(uri, 'http', 'localhost:60371', '/signin', 'nonce=iiK1zRI+HyDCKb2zatvrYA==', ''); - assertToString(uri, 'http://localhost:60371/signin?nonce=iiK1zRI%2BHyDCKb2zatvrYA%3D%3D'); - }); - - test('URI.parse() failes with `Cannot read property \'toLowerCase\' of undefined` #75344', function () { - try { - URI.parse('abc'); - assert.ok(false); - } catch (e) { - assert.ok(e instanceof Error && e.message.indexOf('[UriError]:')); - } - }); - - test('vscode.Uri.parse is double encoding certain characters', function () { - const inStr = 'https://github.com/PowerShell/vscode-powershell#reporting-problems'; - assertToString(inStr, inStr); - assertComponents(inStr, 'https', 'github.com', '/PowerShell/vscode-powershell', '', 'reporting-problems'); - }); - - test('Symbols in URL fragment should not be encoded #76635', function () { - const inStr = 'http://source.roslyn.io/#Microsoft.CodeAnalysis.CSharp/CSharpCompilationOptions.cs,20'; - assertToString(inStr, inStr); - assertComponents(inStr, 'http', 'source.roslyn.io', '/', '', 'Microsoft.CodeAnalysis.CSharp/CSharpCompilationOptions.cs,20'); - }); - - test('vscode.env.openExternal is not working correctly because of unnecessary escape processing. #76606', function () { - const inStr = 'x-github-client://openRepo/https://github.com/wraith13/open-in-github-desktop-vscode.git'; - assertToString(inStr, 'x-github-client://openrepo/https://github.com/wraith13/open-in-github-desktop-vscode.git'); // lower-cased authory - assertComponents(inStr, 'x-github-client', 'openRepo', '/https://github.com/wraith13/open-in-github-desktop-vscode.git', '', ''); - }); - - test('When I click on a link in the terminal, browser opens with a URL which seems to be the link, but run through decodeURIComponent #52211', function () { - const inStr = 'http://localhost:8448/#/repository?path=%2Fhome%2Fcapaj%2Fgit_projects%2Fartillery'; - assertToString(inStr, inStr); - assertComponents(inStr, 'http', 'localhost:8448', '/', '', '/repository?path=/home/capaj/git_projects/artillery'); // INCORRECT %2F lost - }); - test('Terminal breaks weblink for fish shell #44278', function () { - const inStr = 'https://eu-west-1.console.aws.amazon.com/cloudformation/home\\?region=eu-west-1#/stacks\\?filter=active'; - assertToString(inStr, inStr); - assertComponents(inStr, 'https', 'eu-west-1.console.aws.amazon.com', '/cloudformation/home\\', 'region=eu-west-1', '/stacks\\?filter=active'); - }); - - test('Markdown mode cannot open links that contains some codes by percent-encoding. #32026', function () { - const inStr = 'https://www.google.co.jp/search?q=%91%E5'; - assertToString(inStr, inStr); - assertComponents(inStr, 'https', 'www.google.co.jp', '/search', 'q=%91%E5', ''); - }); - - test('URI#parse creates normalized output', function () { - function assertToString(input: string, output: string = input): void { - const uri = URI.parse(input); - assert.equal(uri.toString(), output); - } - - // don't break query string, encoded characters - assertToString('https://firebasestorage.googleapis.com/v0/b/brewlangerie.appspot.com/o/products%2FzVNZkudXJyq8bPGTXUxx%2FBetterave-Sesame.jpg?alt=media&token=0b2310c4-3ea6-4207-bbde-9c3710ba0437'); - assertToString('https://go.microsoft.com/fwlink/?LinkId=518008'); - assertToString('https://twitter.com/search?src=typd&q=%23tag'); - assertToString('http://localhost:3000/#/foo?bar=baz'); - assertToString('https://myhost.com/Redirect?url=http%3A%2F%2Fwww.bing.com%3Fsearch%3Dtom'); - assertToString('https://myhost.com/Redirect?url=http%3a%2f%2Fwww.bing.com%3Fsearch%3Dtom', 'https://myhost.com/Redirect?url=http%3A%2F%2Fwww.bing.com%3Fsearch%3Dtom'); // upper-case hex - assertToString('https://go.microsoft.com/fwlink/?LinkId=518008&foö&ké¥=üü', 'https://go.microsoft.com/fwlink/?LinkId=518008&fo%C3%B6&k%C3%A9%C2%A5=%C3%BC%C3%BC'); // encode umlaute and friends - - // normalize things like - assertToString('file:///c:/test/me', 'file:///c%3A/test/me'); // drive letter treatment - assertToString('file:///C:/test/me', 'file:///c%3A/test/me'); - assertToString('file:///c%3A/test/me', 'file:///c%3A/test/me'); - assertToString('file:///C%3A/test/me', 'file:///c%3A/test/me'); + assert.equal(uri1.scheme, uri2.scheme); + assert.equal(uri1.authority, uri2.authority); + assert.equal(uri1.path, uri2.path); + assert.equal(uri1.query, uri2.query); + assert.equal(uri1.fragment, uri2.fragment); + assert.equal(strIn, strOut); // fails here!! }); test('URI - (de)serialize', function () { @@ -608,142 +499,4 @@ suite('URI', () => { // } // console.profileEnd(); }); - - // ------ check against standard URL and nodejs-file-url utils - - function assertUriFromFsPath(path: string, recurse = true): void { - let actual = URI.file(path).toString(); - if (isWindows) { - // we always encode windows drive letters and since nodejs - // never does. to compare we need to undo our encoding... - actual = actual.replace(/(\/[a-z])%3A/, '$1:'); - } - let expected = pathToFileURL(path).href; - assert.equal(actual, expected, path); - if (recurse) { - assertFsPathFromUri(expected, false); - } - } - - function assertFsPathFromUri(uri: string, recurse = true): void { - let actual = URI.parse(uri).fsPath; - let expected = fileURLToPath(uri); - assert.equal(actual, expected, uri); - if (recurse) { - assertUriFromFsPath(actual, false); - } - } - - test('URI.file and pathToFileURL', function () { - // nodejs is strict to the platform on which it runs - if (isWindows) { - assertUriFromFsPath('d:/foo/bar'); - assertUriFromFsPath('d:/foo/%2e.txt'); - assertUriFromFsPath('d:/foo/%A0.txt'); - assertUriFromFsPath('d:/foo/ü.txt'); - assertUriFromFsPath('d:/foo/ß.txt'); - assertUriFromFsPath('d:/my/c#project/d.cs'); - assertUriFromFsPath('c:\\win\\path'); - assertUriFromFsPath('c:\\test\\drive'); - assertUriFromFsPath('c:\\test with %\\path'); - assertUriFromFsPath('c:\\test with %25\\path'); - assertUriFromFsPath('c:\\test with %25\\c#code'); - // assertUriFromFsPath('\\\\shäres\\path\\c#\\plugin.json'); // nodejs doesn't accept UNC paths as paths - // assertUriFromFsPath('\\\\localhost\\c$\\GitDevelopment\\express'); - // assertUriFromFsPath('\\\\shares'); - // assertUriFromFsPath('\\\\shares\\'); - } else { - assertUriFromFsPath('/foo/bar'); - assertUriFromFsPath('/foo/%2e.txt'); - assertUriFromFsPath('/foo/%A0.txt'); - assertUriFromFsPath('/foo/ü.txt'); - assertUriFromFsPath('/foo/ß.txt'); - assertUriFromFsPath('/my/c#project/d.cs'); - assertUriFromFsPath('/c\\win\\path'); - } - }); - - - test('URI.fsPath and fileURLToPath', function () { - // nodejs is strict to the platform on which it runs - if (isWindows) { - assertFsPathFromUri('file:///f:/foo/bar'); - assertFsPathFromUri('file:///f:/fo%25/bar'); - assertFsPathFromUri('file:///f:/foo/b ar/text.cs'); - assertFsPathFromUri('file:///f:/foö/bar'); - assertFsPathFromUri('file:///f:/fo%C3%B6/bar'); - assertFsPathFromUri('file:///f:/'); - assertFsPathFromUri('file:///f:/my/c%23project/c.cs'); - assertFsPathFromUri('file:///c:/bar/foo'); - assertFsPathFromUri('file:///c:/alex.txt'); - assertFsPathFromUri('file:///c:/Source/Z%C3%BCrich%20or%20Zurich%20(%CB%88zj%CA%8A%C9%99r%C9%AAk,/Code/resources/app/plugins'); - assertFsPathFromUri('file://unc-host/foö/bar', false); - // assertFsPathFromUri('file://unc-host/', false); //nodejs \\unc-host\ vs code \ - assertFsPathFromUri('file://monacotools/folder/isi.txt', false); - assertFsPathFromUri('file://monacotools1/certificates/SSL/', false); - } else { - assertFsPathFromUri('file:///foo/bar'); - assertFsPathFromUri('file:///fo%25/bar'); - assertFsPathFromUri('file:///foo/b ar/text.cs'); - assertFsPathFromUri('file:///foö/bar'); - assertFsPathFromUri('file:///fo%C3%B6/bar'); - assertFsPathFromUri('file:///'); - assertFsPathFromUri('file:///my/c%23project/c.cs'); - assertFsPathFromUri('file:///foo%5cbar'); - assertFsPathFromUri('file:///foo%5Cbar'); - assertFsPathFromUri('file:///foo%5C%5cbar'); - assertFsPathFromUri('file:///Source/Z%C3%BCrich%20or%20Zurich%20(%CB%88zj%CA%8A%C9%99r%C9%AAk,/Code/resources/app/plugins'); - } - }); - - // ---- check against standard url - test('URI.toString equals (whatwg) URL.toString', function () { - - function assertToString(uri: string): void { - const actual = URI.parse(uri).toString(); - const expected = new URL(uri).href; - assert.equal(actual, expected); - } - - assertToString('before:some/file/path'); - assertToString('scheme://authority/path'); - assertToString('scheme:/path'); - assertToString('foo:bar/path'); - // assertToString('http:/api/files/test.me?t=1234'); // URL makes api the hostname, - assertToString('http://api/files/test.me?t=1234'); - // assertToString('file:///c:/test/me'); // we encode the colon - // assertToString('file:///c:/Source/Z%C3%BCrich%20or%20Zurich%20(%CB%88zj%CA%8A%C9%99r%C9%AAk,/Code/resources/app/plugins/c%23/plugin.json'); - // assertToString('file:///c:/test %25/path'); - assertToString('file://shares/files/c%23/p.cs'); - assertToString('inmemory:'); - assertToString('foo:api/files/test'); - assertToString('f3ile:?q'); - assertToString('f3ile:#d'); - assertToString('foo+bar:path'); - assertToString('foo-bar:path'); - assertToString('foo.bar:path'); - assertToString('file:///_:/path'); - assertToString('https://firebasestorage.googleapis.com/v0/b/brewlangerie.appspot.com/o/products%2FzVNZkudXJyq8bPGTXUxx%2FBetterave-Sesame.jpg?alt=media&token=0b2310c4-3ea6-4207-bbde-9c3710ba0437'); - assertToString('https://myhost.com/Redirect?url=http%3A%2F%2Fwww.bing.com%3Fsearch%3Dtom'); - assertToString('debug:internalModule.js?session=aDebugSessionId&ref=11'); - assertToString('debug:internalModule.js?session%3DaDebugSessionId%26ref%3D11'); - assertToString('https://github.com/microsoft/vscode/issues/33746#issuecomment-545345356'); - assertToString('http://localhost:3000/#/foo?bar=baz'); - assertToString('https://myhost.com/Redirect?url=http%3A%2F%2Fwww.bing.com%3Fsearch%3Dtom'); - assertToString('https://myhost.com/my/pãth/ìß/hē®ę'); - assertToString('http://foo:bar@localhost/far'); - assertToString('http://foo@localhost/far'); - assertToString('http://foo:bAr@localhost:8080/far'); - assertToString('http://foo@localhost:8080/far'); - assertToString('http://localhost:60371/signin?nonce=iiK1zRI%2BHyDCKb2zatvrYA%3D%3D'); - assertToString('https://github.com/PowerShell/vscode-powershell#reporting-problems'); - assertToString('http://source.roslyn.io/#Microsoft.CodeAnalysis.CSharp/CSharpCompilationOptions.cs,20'); - // assertToString('x-github-client://openRepo/https://github.com/wraith13/open-in-github-desktop-vscode.git'); we lower-case - assertToString('x-github-client://openrepo/https://github.com/wraith13/open-in-github-desktop-vscode.git'); - assertToString('http://www.google.com/?parameter1=\'http://imageserver.domain.com/?parameter2=1\''); - assertToString('http://some.ws/page?id=123&select=%22quoted_string%22'); - // assertToString('https://eu-west-1.console.aws.amazon.com/cloudformation/home\\?region=eu-west-1#/stacks\\?filter=active'); URL makes slash out of backslash - assertToString('http://localhost/?user=test%2B1@example.com'); - assertToString('https://www.google.co.jp/search?q=%91%E5'); - }); }); diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index bea55b0182f..9902634b3e2 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -149,7 +149,7 @@ export class OpenerService extends Disposable implements IOpenerService { private async _doOpenExternal(resource: URI, options: OpenOptions | undefined): Promise { const { resolved } = await this.resolveExternalUri(resource, options); - dom.windowOpenNoOpener(resolved.toString()); + dom.windowOpenNoOpener(encodeURI(resolved.toString(true))); return true; } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 0b73970348b..a0d77aa6d6c 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -129,11 +129,9 @@ declare namespace monaco { * Creates a new Uri from a string, e.g. `http://www.msft.com/some/path`, * `file:///usr/home`, or `scheme:with/path`. * - * *Note:* When the input lacks a scheme then `file` is used. - * * @param value A string which represents an Uri (see `Uri#toString`). */ - static parse(value: string, strict?: boolean): Uri; + static parse(value: string): Uri; /** * Creates a new Uri from a file system path, e.g. `c:\my\files`, * `/usr/home`, or `\\server\share\some\path`. diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index b4da25fc53f..25d57792ec3 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -1236,8 +1236,8 @@ declare module 'vscode' { * `file:///usr/home`, or `scheme:with/path`. * * *Note* that for a while uris without a `scheme` were accepted. That is not correct - * as all uris should have a scheme. When missing the `file`-scheme is being used unless - * the `strict`-argument is `true` in which case an error is thrown. + * as all uris should have a scheme. To avoid breakage of existing code the optional + * `strict`-argument has been added. We *strongly* advise to use it, e.g. `Uri.parse('my:uri', true)` * * @see [Uri.toString](#Uri.toString) * @param value The string value of an Uri. diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index ad2bf7ed0ca..b8b227bead3 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -260,7 +260,7 @@ export namespace MarkdownString { const collectUri = (href: string): string => { try { - let uri = URI.parse(href, true); + let uri = URI.parse(href); uri = uri.with({ query: _uriMassage(uri.query, resUris) }); resUris[href] = uri; } catch (e) { diff --git a/src/vs/workbench/contrib/debug/test/common/debugSource.test.ts b/src/vs/workbench/contrib/debug/test/common/debugSource.test.ts index 33073efdda3..f44747a9fcf 100644 --- a/src/vs/workbench/contrib/debug/test/common/debugSource.test.ts +++ b/src/vs/workbench/contrib/debug/test/common/debugSource.test.ts @@ -36,7 +36,7 @@ suite('Debug - Source', () => { assert.equal(source.name, 'internalModule.js'); assert.equal(source.inMemory, true); assert.equal(source.reference, 11); - assert.equal(source.uri.toString(), 'debug:internalModule.js?session=aDebugSessionId&ref=11'); + assert.equal(source.uri.toString(), 'debug:internalModule.js?session%3DaDebugSessionId%26ref%3D11'); }); test('get encoded debug data', () => { diff --git a/src/vs/workbench/contrib/webview/common/portMapping.ts b/src/vs/workbench/contrib/webview/common/portMapping.ts index 79904ad749c..69c216631ba 100644 --- a/src/vs/workbench/contrib/webview/common/portMapping.ts +++ b/src/vs/workbench/contrib/webview/common/portMapping.ts @@ -37,16 +37,16 @@ export class WebviewPortMappingManager extends Disposable { if (tunnel.tunnelLocalPort === mapping.webviewPort) { return undefined; } - return uri.with({ + return encodeURI(uri.with({ authority: `127.0.0.1:${tunnel.tunnelLocalPort}`, - }).toString(); + }).toString(true)); } } if (mapping.webviewPort !== mapping.extensionHostPort) { - return uri.with({ + return encodeURI(uri.with({ authority: `${requestLocalHostInfo.address}:${mapping.extensionHostPort}` - }).toString(); + }).toString(true)); } } } diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 06ef5bbbc45..23ea962fdf8 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -408,7 +408,7 @@ export class ElectronWindow extends Disposable { // the main process to prevent window focus issues. if (this.shouldOpenExternal(resource, options)) { const { resolved } = await this.openerService.resolveExternalUri(resource, options); - const success = await this.electronService.openExternal(resolved.toString()); + const success = await this.electronService.openExternal(encodeURI(resolved.toString(true))); if (!success && resolved.scheme === Schemas.file) { // if opening failed, and this is a file, we can still try to reveal it await this.electronService.showItemInFolder(resolved.fsPath); diff --git a/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts b/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts index 2c89f7bc9d4..d1b0f31042f 100644 --- a/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts +++ b/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts @@ -72,17 +72,6 @@ suite('RipgrepTextSearchEngine', () => { suite('RipgrepParser', () => { const TEST_FOLDER = URI.file('/foo/bar'); - function joinPathExt(uri: URI, path: string): URI { - const result = joinPath(uri, path); - result.toString(); - // ^^^^^^^^ - // doing this to init the URI._formatted-field because this - // test compares the output of URI.toJSON and because - // calling URI.file (called by RipgrepParser) will also - // initialize URI._formatted - return result; - } - function testParser(inputData: string[], expectedResults: TextSearchResult[]): void { const testParser = new RipgrepParser(1000, TEST_FOLDER.fsPath); @@ -130,7 +119,7 @@ suite('RipgrepTextSearchEngine', () => { text: 'foobar', matches: [new Range(0, 3, 0, 6)] }, - uri: joinPathExt(TEST_FOLDER, 'file1.js'), + uri: joinPath(TEST_FOLDER, 'file1.js'), ranges: [new Range(3, 3, 3, 6)] } ]); @@ -149,7 +138,7 @@ suite('RipgrepTextSearchEngine', () => { text: 'foobar', matches: [new Range(0, 3, 0, 6)] }, - uri: joinPathExt(TEST_FOLDER, 'file1.js'), + uri: joinPath(TEST_FOLDER, 'file1.js'), ranges: [new Range(3, 3, 3, 6)] }, { @@ -157,7 +146,7 @@ suite('RipgrepTextSearchEngine', () => { text: 'foobar', matches: [new Range(0, 3, 0, 6)] }, - uri: joinPathExt(TEST_FOLDER, 'app/file2.js'), + uri: joinPath(TEST_FOLDER, 'app/file2.js'), ranges: [new Range(3, 3, 3, 6)] }, { @@ -165,7 +154,7 @@ suite('RipgrepTextSearchEngine', () => { text: 'foobar', matches: [new Range(0, 3, 0, 6)] }, - uri: joinPathExt(TEST_FOLDER, 'app2/file3.js'), + uri: joinPath(TEST_FOLDER, 'app2/file3.js'), ranges: [new Range(3, 3, 3, 6)] } ]); @@ -194,7 +183,7 @@ suite('RipgrepTextSearchEngine', () => { text: 'foo bar', matches: [new Range(0, 3, 0, 7)] }, - uri: joinPathExt(TEST_FOLDER, 'file1.js'), + uri: joinPath(TEST_FOLDER, 'file1.js'), ranges: [new Range(3, 3, 3, 7)] }, { @@ -202,7 +191,7 @@ suite('RipgrepTextSearchEngine', () => { text: 'foobar', matches: [new Range(0, 3, 0, 6)] }, - uri: joinPathExt(TEST_FOLDER, 'app/file2.js'), + uri: joinPath(TEST_FOLDER, 'app/file2.js'), ranges: [new Range(3, 3, 3, 6)] }, { @@ -210,7 +199,7 @@ suite('RipgrepTextSearchEngine', () => { text: 'foobar', matches: [new Range(0, 3, 0, 6)] }, - uri: joinPathExt(TEST_FOLDER, 'app2/file3.js'), + uri: joinPath(TEST_FOLDER, 'app2/file3.js'), ranges: [new Range(3, 3, 3, 6)] } ]); diff --git a/src/vs/workbench/test/electron-browser/api/extHostTypeConverter.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTypeConverter.test.ts index c9d7dbe85b9..63f13266677 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTypeConverter.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTypeConverter.test.ts @@ -22,11 +22,13 @@ suite('ExtHostTypeConverter', function () { data = MarkdownString.from('Hello [link](foo)'); assert.equal(data.value, 'Hello [link](foo)'); - assert.equal(size(data.uris!), 0); + assert.equal(size(data.uris!), 1); + assert.ok(!!data.uris!['foo']); data = MarkdownString.from('Hello [link](www.noscheme.bad)'); assert.equal(data.value, 'Hello [link](www.noscheme.bad)'); - assert.equal(size(data.uris!), 0); + assert.equal(size(data.uris!), 1); + assert.ok(!!data.uris!['www.noscheme.bad']); data = MarkdownString.from('Hello [link](foo:path)'); assert.equal(data.value, 'Hello [link](foo:path)'); diff --git a/src/vs/workbench/test/electron-browser/api/extHostTypes.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTypes.test.ts index 05321a9875d..f33a4d5f6e6 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTypes.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTypes.test.ts @@ -22,8 +22,7 @@ suite('ExtHostTypes', function () { assert.deepEqual(uri.toJSON(), { $mid: 1, scheme: 'file', - path: '/path/test.file', - external: 'file:///path/test.file' + path: '/path/test.file' }); assert.ok(uri.fsPath); @@ -31,7 +30,6 @@ suite('ExtHostTypes', function () { $mid: 1, scheme: 'file', path: '/path/test.file', - external: 'file:///path/test.file', fsPath: '/path/test.file'.replace(/\//g, isWindows ? '\\' : '/'), _sep: isWindows ? 1 : undefined, }); -- GitLab