diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 3d9ea6defaf..572effc70a9 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -433,7 +433,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { private async getCompatibleExtensionByEngine(arg1: IExtensionIdentifier | IGalleryExtension, version?: string): Promise { const extension: IGalleryExtension | null = isIExtensionIdentifier(arg1) ? null : arg1; - if (extension && extension.properties.engine && isEngineValid(extension.properties.engine, this.productService.version)) { + if (extension && extension.properties.engine && isEngineValid(extension.properties.engine, this.productService.version, this.productService.date)) { return extension; } const { id, uuid } = extension ? extension.identifier : arg1; @@ -458,7 +458,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { const versionAsset = rawExtension.versions.filter(v => v.version === version)[0]; if (versionAsset) { const extension = toExtension(rawExtension, versionAsset, 0, query); - if (extension.properties.engine && isEngineValid(extension.properties.engine, this.productService.version)) { + if (extension.properties.engine && isEngineValid(extension.properties.engine, this.productService.version, this.productService.date)) { return extension; } } @@ -711,7 +711,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { try { engine = await this.getEngine(v); } catch (error) { /* Ignore error and skip version */ } - if (engine && isEngineValid(engine, this.productService.version)) { + if (engine && isEngineValid(engine, this.productService.version, this.productService.date)) { result.push({ version: v!.version, date: v!.lastUpdated }); } })); @@ -774,7 +774,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { if (!engine) { return null; } - if (isEngineValid(engine, this.productService.version)) { + if (isEngineValid(engine, this.productService.version, this.productService.date)) { return version; } } @@ -809,7 +809,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { const version = versions[0]; const engine = await this.getEngine(version); - if (!isEngineValid(engine, this.productService.version)) { + if (!isEngineValid(engine, this.productService.version, this.productService.date)) { return this.getLastValidExtensionVersionRecursively(extension, versions.slice(1)); } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index ed907661490..871c6f93080 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -170,7 +170,7 @@ export class ExtensionManagementService extends Disposable implements IExtension const manifest = await getManifest(zipPath); const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; let operation: InstallOperation = InstallOperation.Install; - if (manifest.engines && manifest.engines.vscode && !isEngineValid(manifest.engines.vscode, product.version)) { + if (manifest.engines && manifest.engines.vscode && !isEngineValid(manifest.engines.vscode, product.version, product.date)) { throw new Error(nls.localize('incompatible', "Unable to install extension '{0}' as it is not compatible with VS Code '{1}'.", identifier.id, product.version)); } diff --git a/src/vs/platform/extensions/common/extensionValidator.ts b/src/vs/platform/extensions/common/extensionValidator.ts index 77f125ff220..ca498f9305b 100644 --- a/src/vs/platform/extensions/common/extensionValidator.ts +++ b/src/vs/platform/extensions/common/extensionValidator.ts @@ -24,10 +24,12 @@ export interface INormalizedVersion { minorMustEqual: boolean; patchBase: number; patchMustEqual: boolean; + notBefore: number; /* milliseconds timestamp, or 0 */ isMinimum: boolean; } const VERSION_REGEXP = /^(\^|>=)?((\d+)|x)\.((\d+)|x)\.((\d+)|x)(\-.*)?$/; +const NOT_BEFORE_REGEXP = /^-(?\d{4})(?\d{2})(?\d{2})$/; export function isValidVersionStr(version: string): boolean { version = version.trim(); @@ -93,6 +95,14 @@ export function normalizeVersion(version: IParsedVersion | null): INormalizedVer } } + let notBefore = 0; + if (version.preRelease) { + const match = NOT_BEFORE_REGEXP.exec(version.preRelease); + if (match?.groups) { + notBefore = Date.UTC(Number(match.groups.year), Number(match.groups.month) - 1, Number(match.groups.day)); + } + } + return { majorBase: majorBase, majorMustEqual: majorMustEqual, @@ -100,16 +110,24 @@ export function normalizeVersion(version: IParsedVersion | null): INormalizedVer minorMustEqual: minorMustEqual, patchBase: patchBase, patchMustEqual: patchMustEqual, - isMinimum: version.hasGreaterEquals + isMinimum: version.hasGreaterEquals, + notBefore, }; } -export function isValidVersion(_version: string | INormalizedVersion, _desiredVersion: string | INormalizedVersion): boolean { +export function isValidVersion(_inputVersion: string | INormalizedVersion, _inputDate: ProductDate, _desiredVersion: string | INormalizedVersion): boolean { let version: INormalizedVersion | null; - if (typeof _version === 'string') { - version = normalizeVersion(parseVersion(_version)); + if (typeof _inputVersion === 'string') { + version = normalizeVersion(parseVersion(_inputVersion)); } else { - version = _version; + version = _inputVersion; + } + + let productTs: number | undefined; + if (_inputDate instanceof Date) { + productTs = _inputDate.getTime(); + } else if (typeof _inputDate === 'string') { + productTs = new Date(_inputDate).getTime(); } let desiredVersion: INormalizedVersion | null; @@ -130,6 +148,7 @@ export function isValidVersion(_version: string | INormalizedVersion, _desiredVe let desiredMajorBase = desiredVersion.majorBase; let desiredMinorBase = desiredVersion.minorBase; let desiredPatchBase = desiredVersion.patchBase; + let desiredNotBefore = desiredVersion.notBefore; let majorMustEqual = desiredVersion.majorMustEqual; let minorMustEqual = desiredVersion.minorMustEqual; @@ -152,6 +171,10 @@ export function isValidVersion(_version: string | INormalizedVersion, _desiredVe return false; } + if (productTs && productTs < desiredNotBefore) { + return false; + } + return patchBase >= desiredPatchBase; } @@ -200,6 +223,11 @@ export function isValidVersion(_version: string | INormalizedVersion, _desiredVe } // at this point, patchBase are equal + + if (productTs && productTs < desiredNotBefore) { + return false; + } + return true; } @@ -211,22 +239,24 @@ export interface IReducedExtensionDescription { main?: string; } -export function isValidExtensionVersion(version: string, extensionDesc: IReducedExtensionDescription, notices: string[]): boolean { +type ProductDate = string | Date | undefined; + +export function isValidExtensionVersion(version: string, date: ProductDate, extensionDesc: IReducedExtensionDescription, notices: string[]): boolean { if (extensionDesc.isBuiltin || typeof extensionDesc.main === 'undefined') { // No version check for builtin or declarative extensions return true; } - return isVersionValid(version, extensionDesc.engines.vscode, notices); + return isVersionValid(version, date, extensionDesc.engines.vscode, notices); } -export function isEngineValid(engine: string, version: string): boolean { +export function isEngineValid(engine: string, version: string, date: ProductDate): boolean { // TODO@joao: discuss with alex '*' doesn't seem to be a valid engine version - return engine === '*' || isVersionValid(version, engine); + return engine === '*' || isVersionValid(version, date, engine); } -export function isVersionValid(currentVersion: string, requestedVersion: string, notices: string[] = []): boolean { +function isVersionValid(currentVersion: string, date: ProductDate, requestedVersion: string, notices: string[] = []): boolean { let desiredVersion = normalizeVersion(parseVersion(requestedVersion)); if (!desiredVersion) { @@ -251,7 +281,7 @@ export function isVersionValid(currentVersion: string, requestedVersion: string, } } - if (!isValidVersion(currentVersion, desiredVersion)) { + if (!isValidVersion(currentVersion, date, desiredVersion)) { notices.push(nls.localize('versionMismatch', "Extension is not compatible with Code {0}. Extension requires: {1}.", currentVersion, requestedVersion)); return false; } diff --git a/src/vs/platform/extensions/test/common/extensionValidator.test.ts b/src/vs/platform/extensions/test/common/extensionValidator.test.ts index 999789c85a6..5fd309aac0f 100644 --- a/src/vs/platform/extensions/test/common/extensionValidator.test.ts +++ b/src/vs/platform/extensions/test/common/extensionValidator.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import { INormalizedVersion, IParsedVersion, IReducedExtensionDescription, isValidExtensionVersion, isValidVersion, isValidVersionStr, normalizeVersion, parseVersion } from 'vs/platform/extensions/common/extensionValidator'; suite('Extension Version Validator', () => { + const productVersion = '2021-05-11T21:54:30.577Z'; test('isValidVersionStr', () => { assert.strictEqual(isValidVersionStr('0.10.0-dev'), true); @@ -53,13 +54,16 @@ suite('Extension Version Validator', () => { }); test('normalizeVersion', () => { - function assertNormalizeVersion(version: string, majorBase: number, majorMustEqual: boolean, minorBase: number, minorMustEqual: boolean, patchBase: number, patchMustEqual: boolean, isMinimum: boolean): void { + function assertNormalizeVersion(version: string, majorBase: number, majorMustEqual: boolean, minorBase: number, minorMustEqual: boolean, patchBase: number, patchMustEqual: boolean, isMinimum: boolean, notBefore = 0): void { const actual = normalizeVersion(parseVersion(version)); - const expected: INormalizedVersion = { majorBase, majorMustEqual, minorBase, minorMustEqual, patchBase, patchMustEqual, isMinimum }; + const expected: INormalizedVersion = { majorBase, majorMustEqual, minorBase, minorMustEqual, patchBase, patchMustEqual, isMinimum, notBefore }; assert.deepStrictEqual(actual, expected, 'parseVersion for ' + version); } - assertNormalizeVersion('0.10.0-dev', 0, true, 10, true, 0, true, false); + assertNormalizeVersion('0.10.0-dev', 0, true, 10, true, 0, true, false, 0); + assertNormalizeVersion('0.10.0-222222222', 0, true, 10, true, 0, true, false, 0); + assertNormalizeVersion('0.10.0-20210511', 0, true, 10, true, 0, true, false, new Date('2021-05-11T00:00:00Z').getTime()); + assertNormalizeVersion('0.10.0', 0, true, 10, true, 0, true, false); assertNormalizeVersion('0.10.1', 0, true, 10, true, 1, true, false); assertNormalizeVersion('0.10.100', 0, true, 10, true, 100, true, false); @@ -75,11 +79,12 @@ suite('Extension Version Validator', () => { assertNormalizeVersion('>=0.0.1', 0, true, 0, true, 1, true, true); assertNormalizeVersion('>=2.4.3', 2, true, 4, true, 3, true, true); + assertNormalizeVersion('>=2.4.3', 2, true, 4, true, 3, true, true); }); test('isValidVersion', () => { function testIsValidVersion(version: string, desiredVersion: string, expectedResult: boolean): void { - let actual = isValidVersion(version, desiredVersion); + let actual = isValidVersion(version, productVersion, desiredVersion); assert.strictEqual(actual, expectedResult, 'extension - vscode: ' + version + ', desiredVersion: ' + desiredVersion + ' should be ' + expectedResult); } @@ -211,7 +216,7 @@ suite('Extension Version Validator', () => { main: hasMain ? 'something' : undefined }; let reasons: string[] = []; - let actual = isValidExtensionVersion(version, desc, reasons); + let actual = isValidExtensionVersion(version, productVersion, desc, reasons); assert.strictEqual(actual, expectedResult, 'version: ' + version + ', desiredVersion: ' + desiredVersion + ', desc: ' + JSON.stringify(desc) + ', reasons: ' + JSON.stringify(reasons)); } @@ -390,5 +395,12 @@ suite('Extension Version Validator', () => { testIsValidVersion('2.0.0', '^1.100.0', false); testIsValidVersion('2.0.0', '^2.0.0', true); testIsValidVersion('2.0.0', '*', false); // fails due to lack of specificity + + // date tags + testIsValidVersion('1.10.0', '^1.10.0-20210511', true); // current date + testIsValidVersion('1.10.0', '^1.10.0-20210510', true); // before date + testIsValidVersion('1.10.0', '^1.10.0-20210512', false); // future date + testIsValidVersion('1.10.1', '^1.10.0-20200101', true); // before date, but ahead version + testIsValidVersion('1.11.0', '^1.10.0-20200101', true); }); }); diff --git a/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts index 792a4cc3816..c441c61c1f6 100644 --- a/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts @@ -69,9 +69,10 @@ export class CachedExtensionScanner { const version = this._productService.version; const commit = this._productService.commit; + const date = this._productService.date; const devMode = !!process.env['VSCODE_DEV']; const locale = platform.language; - const input = new ExtensionScannerInput(version, commit, locale, devMode, path, isBuiltin, false, translations); + const input = new ExtensionScannerInput(version, date, commit, locale, devMode, path, isBuiltin, false, translations); return ExtensionScanner.scanSingleExtension(input, log); } @@ -245,6 +246,7 @@ export class CachedExtensionScanner { const version = productService.version; const commit = productService.commit; + const date = productService.date; const devMode = !!process.env['VSCODE_DEV']; const locale = platform.language; @@ -253,7 +255,7 @@ export class CachedExtensionScanner { notificationService, environmentService, BUILTIN_MANIFEST_CACHE_FILE, - new ExtensionScannerInput(version, commit, locale, devMode, getSystemExtensionsRoot(), true, false, translations), + new ExtensionScannerInput(version, date, commit, locale, devMode, getSystemExtensionsRoot(), true, false, translations), log ); @@ -266,7 +268,7 @@ export class CachedExtensionScanner { const controlFile = fs.promises.readFile(controlFilePath, 'utf8') .then(raw => JSON.parse(raw), () => ({} as any)); - const input = new ExtensionScannerInput(version, commit, locale, devMode, getExtraDevSystemExtensionsRoot(), true, false, translations); + const input = new ExtensionScannerInput(version, date, commit, locale, devMode, getExtraDevSystemExtensionsRoot(), true, false, translations); const extraBuiltinExtensions = Promise.all([builtInExtensions, controlFile]) .then(([builtInExtensions, control]) => new ExtraBuiltInExtensionResolver(builtInExtensions, control)) .then(resolver => ExtensionScanner.scanExtensions(input, log, resolver)); @@ -279,7 +281,7 @@ export class CachedExtensionScanner { notificationService, environmentService, USER_MANIFEST_CACHE_FILE, - new ExtensionScannerInput(version, commit, locale, devMode, environmentService.extensionsPath, false, false, translations), + new ExtensionScannerInput(version, date, commit, locale, devMode, environmentService.extensionsPath, false, false, translations), log )); @@ -288,7 +290,7 @@ export class CachedExtensionScanner { if (environmentService.isExtensionDevelopment && environmentService.extensionDevelopmentLocationURI) { const extDescsP = environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file).map(extLoc => { return ExtensionScanner.scanOneOrMultipleExtensions( - new ExtensionScannerInput(version, commit, locale, devMode, originalFSPath(extLoc), false, true, translations), log + new ExtensionScannerInput(version, date, commit, locale, devMode, originalFSPath(extLoc), false, true, translations), log ); }); developedExtensions = Promise.all(extDescsP).then((extDescArrays: IExtensionDescription[][]) => { diff --git a/src/vs/workbench/services/extensions/node/extensionPoints.ts b/src/vs/workbench/services/extensions/node/extensionPoints.ts index 5506bddc81d..076ad50b84c 100644 --- a/src/vs/workbench/services/extensions/node/extensionPoints.ts +++ b/src/vs/workbench/services/extensions/node/extensionPoints.ts @@ -30,14 +30,16 @@ export interface NlsConfiguration { abstract class ExtensionManifestHandler { protected readonly _ourVersion: string; + protected readonly _ourProductDate: string | undefined; protected readonly _log: ILog; protected readonly _absoluteFolderPath: string; protected readonly _isBuiltin: boolean; protected readonly _isUnderDevelopment: boolean; protected readonly _absoluteManifestPath: string; - constructor(ourVersion: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean) { + constructor(ourVersion: string, ourProductDate: string | undefined, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean) { this._ourVersion = ourVersion; + this._ourProductDate = ourProductDate; this._log = log; this._absoluteFolderPath = absoluteFolderPath; this._isBuiltin = isBuiltin; @@ -91,8 +93,8 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { private readonly _nlsConfig: NlsConfiguration; - constructor(ourVersion: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration) { - super(ourVersion, log, absoluteFolderPath, isBuiltin, isUnderDevelopment); + constructor(ourVersion: string, ourProductDate: string | undefined, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration) { + super(ourVersion, ourProductDate, log, absoluteFolderPath, isBuiltin, isUnderDevelopment); this._nlsConfig = nlsConfig; } @@ -318,7 +320,7 @@ class ExtensionManifestValidator extends ExtensionManifestHandler { extensionDescription.isUnderDevelopment = this._isUnderDevelopment; let notices: string[] = []; - if (!ExtensionManifestValidator.isValidExtensionDescription(this._ourVersion, this._absoluteFolderPath, extensionDescription, notices)) { + if (!ExtensionManifestValidator.isValidExtensionDescription(this._ourVersion, this._ourProductDate, this._absoluteFolderPath, extensionDescription, notices)) { notices.forEach((error) => { this._log.error(this._absoluteFolderPath, error); }); @@ -344,7 +346,7 @@ class ExtensionManifestValidator extends ExtensionManifestHandler { return extensionDescription; } - private static isValidExtensionDescription(version: string, extensionFolderPath: string, extensionDescription: IExtensionDescription, notices: string[]): boolean { + private static isValidExtensionDescription(version: string, productDate: string | undefined, extensionFolderPath: string, extensionDescription: IExtensionDescription, notices: string[]): boolean { if (!ExtensionManifestValidator.baseIsValidExtensionDescription(extensionFolderPath, extensionDescription, notices)) { return false; @@ -355,7 +357,7 @@ class ExtensionManifestValidator extends ExtensionManifestHandler { return false; } - return isValidExtensionVersion(version, extensionDescription, notices); + return isValidExtensionVersion(version, productDate, extensionDescription, notices); } private static baseIsValidExtensionDescription(extensionFolderPath: string, extensionDescription: IExtensionDescription, notices: string[]): boolean { @@ -453,6 +455,7 @@ export class ExtensionScannerInput { constructor( public readonly ourVersion: string, + public readonly ourProductDate: string | undefined, public readonly commit: string | undefined, public readonly locale: string | undefined, public readonly devMode: boolean, @@ -476,6 +479,7 @@ export class ExtensionScannerInput { public static equals(a: ExtensionScannerInput, b: ExtensionScannerInput): boolean { return ( a.ourVersion === b.ourVersion + && a.ourProductDate === b.ourProductDate && a.commit === b.commit && a.locale === b.locale && a.devMode === b.devMode @@ -512,23 +516,23 @@ export class ExtensionScanner { /** * Read the extension defined in `absoluteFolderPath` */ - private static scanExtension(version: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration): Promise { + private static scanExtension(version: string, productDate: string | undefined, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration): Promise { absoluteFolderPath = path.normalize(absoluteFolderPath); - let parser = new ExtensionManifestParser(version, log, absoluteFolderPath, isBuiltin, isUnderDevelopment); + let parser = new ExtensionManifestParser(version, productDate, log, absoluteFolderPath, isBuiltin, isUnderDevelopment); return parser.parse().then((extensionDescription) => { if (extensionDescription === null) { return null; } - let nlsReplacer = new ExtensionManifestNLSReplacer(version, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig); + let nlsReplacer = new ExtensionManifestNLSReplacer(version, productDate, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig); return nlsReplacer.replaceNLS(extensionDescription); }).then((extensionDescription) => { if (extensionDescription === null) { return null; } - let validator = new ExtensionManifestValidator(version, log, absoluteFolderPath, isBuiltin, isUnderDevelopment); + let validator = new ExtensionManifestValidator(version, productDate, log, absoluteFolderPath, isBuiltin, isUnderDevelopment); return validator.validate(extensionDescription); }); } @@ -566,7 +570,7 @@ export class ExtensionScanner { } const nlsConfig = ExtensionScannerInput.createNLSConfig(input); - let _extensionDescriptions = await Promise.all(refs.map(r => this.scanExtension(input.ourVersion, log, r.path, isBuiltin, isUnderDevelopment, nlsConfig))); + let _extensionDescriptions = await Promise.all(refs.map(r => this.scanExtension(input.ourVersion, input.ourProductDate, log, r.path, isBuiltin, isUnderDevelopment, nlsConfig))); let extensionDescriptions = arrays.coalesce(_extensionDescriptions); extensionDescriptions = extensionDescriptions.filter(item => item !== null && !obsolete[new ExtensionIdentifierWithVersion({ id: getGalleryExtensionId(item.publisher, item.name) }, item.version).key()]); @@ -601,7 +605,7 @@ export class ExtensionScanner { return pfs.SymlinkSupport.existsFile(path.join(absoluteFolderPath, MANIFEST_FILE)).then((exists) => { if (exists) { const nlsConfig = ExtensionScannerInput.createNLSConfig(input); - return this.scanExtension(input.ourVersion, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig).then((extensionDescription) => { + return this.scanExtension(input.ourVersion, input.ourProductDate, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig).then((extensionDescription) => { if (extensionDescription === null) { return []; } @@ -620,7 +624,7 @@ export class ExtensionScanner { const isBuiltin = input.isBuiltin; const isUnderDevelopment = input.isUnderDevelopment; const nlsConfig = ExtensionScannerInput.createNLSConfig(input); - return this.scanExtension(input.ourVersion, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig); + return this.scanExtension(input.ourVersion, input.ourProductDate, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig); } public static mergeBuiltinExtensions(builtinExtensions: Promise, extraBuiltinExtensions: Promise): Promise {