extensions: allow date-based engines constraint

Fixes #121749
This commit is contained in:
Connor Peet 2021-05-11 15:39:06 -07:00
parent 05d4a5c56d
commit 3ef8ea56b6
No known key found for this signature in database
GPG key ID: CF8FD2EA0DBC61BD
6 changed files with 88 additions and 40 deletions

View file

@ -433,7 +433,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
private async getCompatibleExtensionByEngine(arg1: IExtensionIdentifier | IGalleryExtension, version?: string): Promise<IGalleryExtension | null> {
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 : <IExtensionIdentifier>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));
}

View file

@ -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));
}

View file

@ -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 = /^-(?<year>\d{4})(?<month>\d{2})(?<day>\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;
}

View file

@ -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);
});
});

View file

@ -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<IBuiltInExtensionControl>(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[][]) => {

View file

@ -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<IExtensionDescription | null> {
private static scanExtension(version: string, productDate: string | undefined, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration): Promise<IExtensionDescription | null> {
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<IExtensionDescription | null>((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<IExtensionDescription[]>, extraBuiltinExtensions: Promise<IExtensionDescription[]>): Promise<IExtensionDescription[]> {