Path: blob/main/src/vs/platform/extensions/common/extensionValidator.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { isEqualOrParent, joinPath } from '../../../base/common/resources.js';6import Severity from '../../../base/common/severity.js';7import { URI } from '../../../base/common/uri.js';8import * as nls from '../../../nls.js';9import * as semver from '../../../base/common/semver/semver.js';10import { IExtensionManifest, parseApiProposals } from './extensions.js';11import { allApiProposals } from './extensionsApiProposals.js';1213export interface IParsedVersion {14hasCaret: boolean;15hasGreaterEquals: boolean;16majorBase: number;17majorMustEqual: boolean;18minorBase: number;19minorMustEqual: boolean;20patchBase: number;21patchMustEqual: boolean;22preRelease: string | null;23}2425export interface INormalizedVersion {26majorBase: number;27majorMustEqual: boolean;28minorBase: number;29minorMustEqual: boolean;30patchBase: number;31patchMustEqual: boolean;32notBefore: number; /* milliseconds timestamp, or 0 */33isMinimum: boolean;34}3536const VERSION_REGEXP = /^(\^|>=)?((\d+)|x)\.((\d+)|x)\.((\d+)|x)(\-.*)?$/;37const NOT_BEFORE_REGEXP = /^-(\d{4})(\d{2})(\d{2})$/;3839export function isValidVersionStr(version: string): boolean {40version = version.trim();41return (version === '*' || VERSION_REGEXP.test(version));42}4344export function parseVersion(version: string): IParsedVersion | null {45if (!isValidVersionStr(version)) {46return null;47}4849version = version.trim();5051if (version === '*') {52return {53hasCaret: false,54hasGreaterEquals: false,55majorBase: 0,56majorMustEqual: false,57minorBase: 0,58minorMustEqual: false,59patchBase: 0,60patchMustEqual: false,61preRelease: null62};63}6465const m = version.match(VERSION_REGEXP);66if (!m) {67return null;68}69return {70hasCaret: m[1] === '^',71hasGreaterEquals: m[1] === '>=',72majorBase: m[2] === 'x' ? 0 : parseInt(m[2], 10),73majorMustEqual: (m[2] === 'x' ? false : true),74minorBase: m[4] === 'x' ? 0 : parseInt(m[4], 10),75minorMustEqual: (m[4] === 'x' ? false : true),76patchBase: m[6] === 'x' ? 0 : parseInt(m[6], 10),77patchMustEqual: (m[6] === 'x' ? false : true),78preRelease: m[8] || null79};80}8182export function normalizeVersion(version: IParsedVersion | null): INormalizedVersion | null {83if (!version) {84return null;85}8687const majorBase = version.majorBase;88const majorMustEqual = version.majorMustEqual;89const minorBase = version.minorBase;90let minorMustEqual = version.minorMustEqual;91const patchBase = version.patchBase;92let patchMustEqual = version.patchMustEqual;9394if (version.hasCaret) {95if (majorBase === 0) {96patchMustEqual = false;97} else {98minorMustEqual = false;99patchMustEqual = false;100}101}102103let notBefore = 0;104if (version.preRelease) {105const match = NOT_BEFORE_REGEXP.exec(version.preRelease);106if (match) {107const [, year, month, day] = match;108notBefore = Date.UTC(Number(year), Number(month) - 1, Number(day));109}110}111112return {113majorBase: majorBase,114majorMustEqual: majorMustEqual,115minorBase: minorBase,116minorMustEqual: minorMustEqual,117patchBase: patchBase,118patchMustEqual: patchMustEqual,119isMinimum: version.hasGreaterEquals,120notBefore,121};122}123124export function isValidVersion(_inputVersion: string | INormalizedVersion, _inputDate: ProductDate, _desiredVersion: string | INormalizedVersion): boolean {125let version: INormalizedVersion | null;126if (typeof _inputVersion === 'string') {127version = normalizeVersion(parseVersion(_inputVersion));128} else {129version = _inputVersion;130}131132let productTs: number | undefined;133if (_inputDate instanceof Date) {134productTs = _inputDate.getTime();135} else if (typeof _inputDate === 'string') {136productTs = new Date(_inputDate).getTime();137}138139let desiredVersion: INormalizedVersion | null;140if (typeof _desiredVersion === 'string') {141desiredVersion = normalizeVersion(parseVersion(_desiredVersion));142} else {143desiredVersion = _desiredVersion;144}145146if (!version || !desiredVersion) {147return false;148}149150const majorBase = version.majorBase;151const minorBase = version.minorBase;152const patchBase = version.patchBase;153154let desiredMajorBase = desiredVersion.majorBase;155let desiredMinorBase = desiredVersion.minorBase;156let desiredPatchBase = desiredVersion.patchBase;157const desiredNotBefore = desiredVersion.notBefore;158159let majorMustEqual = desiredVersion.majorMustEqual;160let minorMustEqual = desiredVersion.minorMustEqual;161let patchMustEqual = desiredVersion.patchMustEqual;162163if (desiredVersion.isMinimum) {164if (majorBase > desiredMajorBase) {165return true;166}167168if (majorBase < desiredMajorBase) {169return false;170}171172if (minorBase > desiredMinorBase) {173return true;174}175176if (minorBase < desiredMinorBase) {177return false;178}179180if (productTs && productTs < desiredNotBefore) {181return false;182}183184return patchBase >= desiredPatchBase;185}186187// Anything < 1.0.0 is compatible with >= 1.0.0, except exact matches188if (majorBase === 1 && desiredMajorBase === 0 && (!majorMustEqual || !minorMustEqual || !patchMustEqual)) {189desiredMajorBase = 1;190desiredMinorBase = 0;191desiredPatchBase = 0;192majorMustEqual = true;193minorMustEqual = false;194patchMustEqual = false;195}196197if (majorBase < desiredMajorBase) {198// smaller major version199return false;200}201202if (majorBase > desiredMajorBase) {203// higher major version204return (!majorMustEqual);205}206207// at this point, majorBase are equal208209if (minorBase < desiredMinorBase) {210// smaller minor version211return false;212}213214if (minorBase > desiredMinorBase) {215// higher minor version216return (!minorMustEqual);217}218219// at this point, minorBase are equal220221if (patchBase < desiredPatchBase) {222// smaller patch version223return false;224}225226if (patchBase > desiredPatchBase) {227// higher patch version228return (!patchMustEqual);229}230231// at this point, patchBase are equal232233if (productTs && productTs < desiredNotBefore) {234return false;235}236237return true;238}239240type ProductDate = string | Date | undefined;241242export function validateExtensionManifest(productVersion: string, productDate: ProductDate, extensionLocation: URI, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean, validateApiVersion: boolean): readonly [Severity, string][] {243const validations: [Severity, string][] = [];244if (typeof extensionManifest.publisher !== 'undefined' && typeof extensionManifest.publisher !== 'string') {245validations.push([Severity.Error, nls.localize('extensionDescription.publisher', "property publisher must be of type `string`.")]);246return validations;247}248if (typeof extensionManifest.name !== 'string') {249validations.push([Severity.Error, nls.localize('extensionDescription.name', "property `{0}` is mandatory and must be of type `string`", 'name')]);250return validations;251}252if (typeof extensionManifest.version !== 'string') {253validations.push([Severity.Error, nls.localize('extensionDescription.version', "property `{0}` is mandatory and must be of type `string`", 'version')]);254return validations;255}256if (!extensionManifest.engines) {257validations.push([Severity.Error, nls.localize('extensionDescription.engines', "property `{0}` is mandatory and must be of type `object`", 'engines')]);258return validations;259}260if (typeof extensionManifest.engines.vscode !== 'string') {261validations.push([Severity.Error, nls.localize('extensionDescription.engines.vscode', "property `{0}` is mandatory and must be of type `string`", 'engines.vscode')]);262return validations;263}264if (typeof extensionManifest.extensionDependencies !== 'undefined') {265if (!isStringArray(extensionManifest.extensionDependencies)) {266validations.push([Severity.Error, nls.localize('extensionDescription.extensionDependencies', "property `{0}` can be omitted or must be of type `string[]`", 'extensionDependencies')]);267return validations;268}269}270if (typeof extensionManifest.activationEvents !== 'undefined') {271if (!isStringArray(extensionManifest.activationEvents)) {272validations.push([Severity.Error, nls.localize('extensionDescription.activationEvents1', "property `{0}` can be omitted or must be of type `string[]`", 'activationEvents')]);273return validations;274}275if (typeof extensionManifest.main === 'undefined' && typeof extensionManifest.browser === 'undefined') {276validations.push([Severity.Error, nls.localize('extensionDescription.activationEvents2', "property `{0}` should be omitted if the extension doesn't have a `{1}` or `{2}` property.", 'activationEvents', 'main', 'browser')]);277return validations;278}279}280if (typeof extensionManifest.extensionKind !== 'undefined') {281if (typeof extensionManifest.main === 'undefined') {282validations.push([Severity.Warning, nls.localize('extensionDescription.extensionKind', "property `{0}` can be defined only if property `main` is also defined.", 'extensionKind')]);283// not a failure case284}285}286if (typeof extensionManifest.main !== 'undefined') {287if (typeof extensionManifest.main !== 'string') {288validations.push([Severity.Error, nls.localize('extensionDescription.main1', "property `{0}` can be omitted or must be of type `string`", 'main')]);289return validations;290} else {291const mainLocation = joinPath(extensionLocation, extensionManifest.main);292if (!isEqualOrParent(mainLocation, extensionLocation)) {293validations.push([Severity.Warning, nls.localize('extensionDescription.main2', "Expected `main` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", mainLocation.path, extensionLocation.path)]);294// not a failure case295}296}297}298if (typeof extensionManifest.browser !== 'undefined') {299if (typeof extensionManifest.browser !== 'string') {300validations.push([Severity.Error, nls.localize('extensionDescription.browser1', "property `{0}` can be omitted or must be of type `string`", 'browser')]);301return validations;302} else {303const browserLocation = joinPath(extensionLocation, extensionManifest.browser);304if (!isEqualOrParent(browserLocation, extensionLocation)) {305validations.push([Severity.Warning, nls.localize('extensionDescription.browser2', "Expected `browser` ({0}) to be included inside extension's folder ({1}). This might make the extension non-portable.", browserLocation.path, extensionLocation.path)]);306// not a failure case307}308}309}310311if (!semver.valid(extensionManifest.version)) {312validations.push([Severity.Error, nls.localize('notSemver', "Extension version is not semver compatible.")]);313return validations;314}315316const notices: string[] = [];317const validExtensionVersion = isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices);318if (!validExtensionVersion) {319for (const notice of notices) {320validations.push([Severity.Error, notice]);321}322}323324if (validateApiVersion && extensionManifest.enabledApiProposals?.length) {325const incompatibleNotices: string[] = [];326if (!areApiProposalsCompatible([...extensionManifest.enabledApiProposals], incompatibleNotices)) {327for (const notice of incompatibleNotices) {328validations.push([Severity.Error, notice]);329}330}331}332333return validations;334}335336export function isValidExtensionVersion(productVersion: string, productDate: ProductDate, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean, notices: string[]): boolean {337338if (extensionIsBuiltin || (typeof extensionManifest.main === 'undefined' && typeof extensionManifest.browser === 'undefined')) {339// No version check for builtin or declarative extensions340return true;341}342343return isVersionValid(productVersion, productDate, extensionManifest.engines.vscode, notices);344}345346export function isEngineValid(engine: string, version: string, date: ProductDate): boolean {347// TODO@joao: discuss with alex '*' doesn't seem to be a valid engine version348return engine === '*' || isVersionValid(version, date, engine);349}350351export function areApiProposalsCompatible(apiProposals: string[]): boolean;352export function areApiProposalsCompatible(apiProposals: string[], notices: string[]): boolean;353export function areApiProposalsCompatible(apiProposals: string[], productApiProposals: Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>): boolean;354export function areApiProposalsCompatible(apiProposals: string[], arg1?: any): boolean {355if (apiProposals.length === 0) {356return true;357}358const notices: string[] | undefined = Array.isArray(arg1) ? arg1 : undefined;359const productApiProposals: Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }> = (notices ? undefined : arg1) ?? allApiProposals;360const incompatibleProposals: string[] = [];361const parsedProposals = parseApiProposals(apiProposals);362for (const { proposalName, version } of parsedProposals) {363if (!version) {364continue;365}366const existingProposal = productApiProposals[proposalName];367if (existingProposal?.version !== version) {368incompatibleProposals.push(proposalName);369}370}371if (incompatibleProposals.length) {372if (notices) {373if (incompatibleProposals.length === 1) {374notices.push(nls.localize('apiProposalMismatch1', "This extension is using the API proposal '{0}' that is not compatible with the current version of VS Code.", incompatibleProposals[0]));375} else {376notices.push(nls.localize('apiProposalMismatch2', "This extension is using the API proposals {0} and '{1}' that are not compatible with the current version of VS Code.",377incompatibleProposals.slice(0, incompatibleProposals.length - 1).map(p => `'${p}'`).join(', '),378incompatibleProposals[incompatibleProposals.length - 1]));379}380}381return false;382}383return true;384}385386function isVersionValid(currentVersion: string, date: ProductDate, requestedVersion: string, notices: string[] = []): boolean {387388const desiredVersion = normalizeVersion(parseVersion(requestedVersion));389if (!desiredVersion) {390notices.push(nls.localize('versionSyntax', "Could not parse `engines.vscode` value {0}. Please use, for example: ^1.22.0, ^1.22.x, etc.", requestedVersion));391return false;392}393394// enforce that a breaking API version is specified.395// for 0.X.Y, that means up to 0.X must be specified396// otherwise for Z.X.Y, that means Z must be specified397if (desiredVersion.majorBase === 0) {398// force that major and minor must be specific399if (!desiredVersion.majorMustEqual || !desiredVersion.minorMustEqual) {400notices.push(nls.localize('versionSpecificity1', "Version specified in `engines.vscode` ({0}) is not specific enough. For vscode versions before 1.0.0, please define at a minimum the major and minor desired version. E.g. ^0.10.0, 0.10.x, 0.11.0, etc.", requestedVersion));401return false;402}403} else {404// force that major must be specific405if (!desiredVersion.majorMustEqual) {406notices.push(nls.localize('versionSpecificity2', "Version specified in `engines.vscode` ({0}) is not specific enough. For vscode versions after 1.0.0, please define at a minimum the major desired version. E.g. ^1.10.0, 1.10.x, 1.x.x, 2.x.x, etc.", requestedVersion));407return false;408}409}410411if (!isValidVersion(currentVersion, date, desiredVersion)) {412notices.push(nls.localize('versionMismatch', "Extension is not compatible with Code {0}. Extension requires: {1}.", currentVersion, requestedVersion));413return false;414}415416return true;417}418419function isStringArray(arr: string[]): boolean {420if (!Array.isArray(arr)) {421return false;422}423for (let i = 0, len = arr.length; i < len; i++) {424if (typeof arr[i] !== 'string') {425return false;426}427}428return true;429}430431432