Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/extensions/common/extensionValidator.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { isEqualOrParent, joinPath } from '../../../base/common/resources.js';
7
import Severity from '../../../base/common/severity.js';
8
import { URI } from '../../../base/common/uri.js';
9
import * as nls from '../../../nls.js';
10
import * as semver from '../../../base/common/semver/semver.js';
11
import { IExtensionManifest, parseApiProposals } from './extensions.js';
12
import { allApiProposals } from './extensionsApiProposals.js';
13
14
export interface IParsedVersion {
15
hasCaret: boolean;
16
hasGreaterEquals: boolean;
17
majorBase: number;
18
majorMustEqual: boolean;
19
minorBase: number;
20
minorMustEqual: boolean;
21
patchBase: number;
22
patchMustEqual: boolean;
23
preRelease: string | null;
24
}
25
26
export interface INormalizedVersion {
27
majorBase: number;
28
majorMustEqual: boolean;
29
minorBase: number;
30
minorMustEqual: boolean;
31
patchBase: number;
32
patchMustEqual: boolean;
33
notBefore: number; /* milliseconds timestamp, or 0 */
34
isMinimum: boolean;
35
}
36
37
const VERSION_REGEXP = /^(\^|>=)?((\d+)|x)\.((\d+)|x)\.((\d+)|x)(\-.*)?$/;
38
const NOT_BEFORE_REGEXP = /^-(\d{4})(\d{2})(\d{2})$/;
39
40
export function isValidVersionStr(version: string): boolean {
41
version = version.trim();
42
return (version === '*' || VERSION_REGEXP.test(version));
43
}
44
45
export function parseVersion(version: string): IParsedVersion | null {
46
if (!isValidVersionStr(version)) {
47
return null;
48
}
49
50
version = version.trim();
51
52
if (version === '*') {
53
return {
54
hasCaret: false,
55
hasGreaterEquals: false,
56
majorBase: 0,
57
majorMustEqual: false,
58
minorBase: 0,
59
minorMustEqual: false,
60
patchBase: 0,
61
patchMustEqual: false,
62
preRelease: null
63
};
64
}
65
66
const m = version.match(VERSION_REGEXP);
67
if (!m) {
68
return null;
69
}
70
return {
71
hasCaret: m[1] === '^',
72
hasGreaterEquals: m[1] === '>=',
73
majorBase: m[2] === 'x' ? 0 : parseInt(m[2], 10),
74
majorMustEqual: (m[2] === 'x' ? false : true),
75
minorBase: m[4] === 'x' ? 0 : parseInt(m[4], 10),
76
minorMustEqual: (m[4] === 'x' ? false : true),
77
patchBase: m[6] === 'x' ? 0 : parseInt(m[6], 10),
78
patchMustEqual: (m[6] === 'x' ? false : true),
79
preRelease: m[8] || null
80
};
81
}
82
83
export function normalizeVersion(version: IParsedVersion | null): INormalizedVersion | null {
84
if (!version) {
85
return null;
86
}
87
88
const majorBase = version.majorBase;
89
const majorMustEqual = version.majorMustEqual;
90
const minorBase = version.minorBase;
91
let minorMustEqual = version.minorMustEqual;
92
const patchBase = version.patchBase;
93
let patchMustEqual = version.patchMustEqual;
94
95
if (version.hasCaret) {
96
if (majorBase === 0) {
97
patchMustEqual = false;
98
} else {
99
minorMustEqual = false;
100
patchMustEqual = false;
101
}
102
}
103
104
let notBefore = 0;
105
if (version.preRelease) {
106
const match = NOT_BEFORE_REGEXP.exec(version.preRelease);
107
if (match) {
108
const [, year, month, day] = match;
109
notBefore = Date.UTC(Number(year), Number(month) - 1, Number(day));
110
}
111
}
112
113
return {
114
majorBase: majorBase,
115
majorMustEqual: majorMustEqual,
116
minorBase: minorBase,
117
minorMustEqual: minorMustEqual,
118
patchBase: patchBase,
119
patchMustEqual: patchMustEqual,
120
isMinimum: version.hasGreaterEquals,
121
notBefore,
122
};
123
}
124
125
export function isValidVersion(_inputVersion: string | INormalizedVersion, _inputDate: ProductDate, _desiredVersion: string | INormalizedVersion): boolean {
126
let version: INormalizedVersion | null;
127
if (typeof _inputVersion === 'string') {
128
version = normalizeVersion(parseVersion(_inputVersion));
129
} else {
130
version = _inputVersion;
131
}
132
133
let productTs: number | undefined;
134
if (_inputDate instanceof Date) {
135
productTs = _inputDate.getTime();
136
} else if (typeof _inputDate === 'string') {
137
productTs = new Date(_inputDate).getTime();
138
}
139
140
let desiredVersion: INormalizedVersion | null;
141
if (typeof _desiredVersion === 'string') {
142
desiredVersion = normalizeVersion(parseVersion(_desiredVersion));
143
} else {
144
desiredVersion = _desiredVersion;
145
}
146
147
if (!version || !desiredVersion) {
148
return false;
149
}
150
151
const majorBase = version.majorBase;
152
const minorBase = version.minorBase;
153
const patchBase = version.patchBase;
154
155
let desiredMajorBase = desiredVersion.majorBase;
156
let desiredMinorBase = desiredVersion.minorBase;
157
let desiredPatchBase = desiredVersion.patchBase;
158
const desiredNotBefore = desiredVersion.notBefore;
159
160
let majorMustEqual = desiredVersion.majorMustEqual;
161
let minorMustEqual = desiredVersion.minorMustEqual;
162
let patchMustEqual = desiredVersion.patchMustEqual;
163
164
if (desiredVersion.isMinimum) {
165
if (majorBase > desiredMajorBase) {
166
return true;
167
}
168
169
if (majorBase < desiredMajorBase) {
170
return false;
171
}
172
173
if (minorBase > desiredMinorBase) {
174
return true;
175
}
176
177
if (minorBase < desiredMinorBase) {
178
return false;
179
}
180
181
if (productTs && productTs < desiredNotBefore) {
182
return false;
183
}
184
185
return patchBase >= desiredPatchBase;
186
}
187
188
// Anything < 1.0.0 is compatible with >= 1.0.0, except exact matches
189
if (majorBase === 1 && desiredMajorBase === 0 && (!majorMustEqual || !minorMustEqual || !patchMustEqual)) {
190
desiredMajorBase = 1;
191
desiredMinorBase = 0;
192
desiredPatchBase = 0;
193
majorMustEqual = true;
194
minorMustEqual = false;
195
patchMustEqual = false;
196
}
197
198
if (majorBase < desiredMajorBase) {
199
// smaller major version
200
return false;
201
}
202
203
if (majorBase > desiredMajorBase) {
204
// higher major version
205
return (!majorMustEqual);
206
}
207
208
// at this point, majorBase are equal
209
210
if (minorBase < desiredMinorBase) {
211
// smaller minor version
212
return false;
213
}
214
215
if (minorBase > desiredMinorBase) {
216
// higher minor version
217
return (!minorMustEqual);
218
}
219
220
// at this point, minorBase are equal
221
222
if (patchBase < desiredPatchBase) {
223
// smaller patch version
224
return false;
225
}
226
227
if (patchBase > desiredPatchBase) {
228
// higher patch version
229
return (!patchMustEqual);
230
}
231
232
// at this point, patchBase are equal
233
234
if (productTs && productTs < desiredNotBefore) {
235
return false;
236
}
237
238
return true;
239
}
240
241
type ProductDate = string | Date | undefined;
242
243
export function validateExtensionManifest(productVersion: string, productDate: ProductDate, extensionLocation: URI, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean, validateApiVersion: boolean): readonly [Severity, string][] {
244
const validations: [Severity, string][] = [];
245
if (typeof extensionManifest.publisher !== 'undefined' && typeof extensionManifest.publisher !== 'string') {
246
validations.push([Severity.Error, nls.localize('extensionDescription.publisher', "property publisher must be of type `string`.")]);
247
return validations;
248
}
249
if (typeof extensionManifest.name !== 'string') {
250
validations.push([Severity.Error, nls.localize('extensionDescription.name', "property `{0}` is mandatory and must be of type `string`", 'name')]);
251
return validations;
252
}
253
if (typeof extensionManifest.version !== 'string') {
254
validations.push([Severity.Error, nls.localize('extensionDescription.version', "property `{0}` is mandatory and must be of type `string`", 'version')]);
255
return validations;
256
}
257
if (!extensionManifest.engines) {
258
validations.push([Severity.Error, nls.localize('extensionDescription.engines', "property `{0}` is mandatory and must be of type `object`", 'engines')]);
259
return validations;
260
}
261
if (typeof extensionManifest.engines.vscode !== 'string') {
262
validations.push([Severity.Error, nls.localize('extensionDescription.engines.vscode', "property `{0}` is mandatory and must be of type `string`", 'engines.vscode')]);
263
return validations;
264
}
265
if (typeof extensionManifest.extensionDependencies !== 'undefined') {
266
if (!isStringArray(extensionManifest.extensionDependencies)) {
267
validations.push([Severity.Error, nls.localize('extensionDescription.extensionDependencies', "property `{0}` can be omitted or must be of type `string[]`", 'extensionDependencies')]);
268
return validations;
269
}
270
}
271
if (typeof extensionManifest.activationEvents !== 'undefined') {
272
if (!isStringArray(extensionManifest.activationEvents)) {
273
validations.push([Severity.Error, nls.localize('extensionDescription.activationEvents1', "property `{0}` can be omitted or must be of type `string[]`", 'activationEvents')]);
274
return validations;
275
}
276
if (typeof extensionManifest.main === 'undefined' && typeof extensionManifest.browser === 'undefined') {
277
validations.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')]);
278
return validations;
279
}
280
}
281
if (typeof extensionManifest.extensionKind !== 'undefined') {
282
if (typeof extensionManifest.main === 'undefined') {
283
validations.push([Severity.Warning, nls.localize('extensionDescription.extensionKind', "property `{0}` can be defined only if property `main` is also defined.", 'extensionKind')]);
284
// not a failure case
285
}
286
}
287
if (typeof extensionManifest.main !== 'undefined') {
288
if (typeof extensionManifest.main !== 'string') {
289
validations.push([Severity.Error, nls.localize('extensionDescription.main1', "property `{0}` can be omitted or must be of type `string`", 'main')]);
290
return validations;
291
} else {
292
const mainLocation = joinPath(extensionLocation, extensionManifest.main);
293
if (!isEqualOrParent(mainLocation, extensionLocation)) {
294
validations.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)]);
295
// not a failure case
296
}
297
}
298
}
299
if (typeof extensionManifest.browser !== 'undefined') {
300
if (typeof extensionManifest.browser !== 'string') {
301
validations.push([Severity.Error, nls.localize('extensionDescription.browser1', "property `{0}` can be omitted or must be of type `string`", 'browser')]);
302
return validations;
303
} else {
304
const browserLocation = joinPath(extensionLocation, extensionManifest.browser);
305
if (!isEqualOrParent(browserLocation, extensionLocation)) {
306
validations.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)]);
307
// not a failure case
308
}
309
}
310
}
311
312
if (!semver.valid(extensionManifest.version)) {
313
validations.push([Severity.Error, nls.localize('notSemver', "Extension version is not semver compatible.")]);
314
return validations;
315
}
316
317
const notices: string[] = [];
318
const validExtensionVersion = isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices);
319
if (!validExtensionVersion) {
320
for (const notice of notices) {
321
validations.push([Severity.Error, notice]);
322
}
323
}
324
325
if (validateApiVersion && extensionManifest.enabledApiProposals?.length) {
326
const incompatibleNotices: string[] = [];
327
if (!areApiProposalsCompatible([...extensionManifest.enabledApiProposals], incompatibleNotices)) {
328
for (const notice of incompatibleNotices) {
329
validations.push([Severity.Error, notice]);
330
}
331
}
332
}
333
334
return validations;
335
}
336
337
export function isValidExtensionVersion(productVersion: string, productDate: ProductDate, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean, notices: string[]): boolean {
338
339
if (extensionIsBuiltin || (typeof extensionManifest.main === 'undefined' && typeof extensionManifest.browser === 'undefined')) {
340
// No version check for builtin or declarative extensions
341
return true;
342
}
343
344
return isVersionValid(productVersion, productDate, extensionManifest.engines.vscode, notices);
345
}
346
347
export function isEngineValid(engine: string, version: string, date: ProductDate): boolean {
348
// TODO@joao: discuss with alex '*' doesn't seem to be a valid engine version
349
return engine === '*' || isVersionValid(version, date, engine);
350
}
351
352
export function areApiProposalsCompatible(apiProposals: string[]): boolean;
353
export function areApiProposalsCompatible(apiProposals: string[], notices: string[]): boolean;
354
export function areApiProposalsCompatible(apiProposals: string[], productApiProposals: Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>): boolean;
355
export function areApiProposalsCompatible(apiProposals: string[], arg1?: any): boolean {
356
if (apiProposals.length === 0) {
357
return true;
358
}
359
const notices: string[] | undefined = Array.isArray(arg1) ? arg1 : undefined;
360
const productApiProposals: Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }> = (notices ? undefined : arg1) ?? allApiProposals;
361
const incompatibleProposals: string[] = [];
362
const parsedProposals = parseApiProposals(apiProposals);
363
for (const { proposalName, version } of parsedProposals) {
364
if (!version) {
365
continue;
366
}
367
const existingProposal = productApiProposals[proposalName];
368
if (existingProposal?.version !== version) {
369
incompatibleProposals.push(proposalName);
370
}
371
}
372
if (incompatibleProposals.length) {
373
if (notices) {
374
if (incompatibleProposals.length === 1) {
375
notices.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]));
376
} else {
377
notices.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.",
378
incompatibleProposals.slice(0, incompatibleProposals.length - 1).map(p => `'${p}'`).join(', '),
379
incompatibleProposals[incompatibleProposals.length - 1]));
380
}
381
}
382
return false;
383
}
384
return true;
385
}
386
387
function isVersionValid(currentVersion: string, date: ProductDate, requestedVersion: string, notices: string[] = []): boolean {
388
389
const desiredVersion = normalizeVersion(parseVersion(requestedVersion));
390
if (!desiredVersion) {
391
notices.push(nls.localize('versionSyntax', "Could not parse `engines.vscode` value {0}. Please use, for example: ^1.22.0, ^1.22.x, etc.", requestedVersion));
392
return false;
393
}
394
395
// enforce that a breaking API version is specified.
396
// for 0.X.Y, that means up to 0.X must be specified
397
// otherwise for Z.X.Y, that means Z must be specified
398
if (desiredVersion.majorBase === 0) {
399
// force that major and minor must be specific
400
if (!desiredVersion.majorMustEqual || !desiredVersion.minorMustEqual) {
401
notices.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));
402
return false;
403
}
404
} else {
405
// force that major must be specific
406
if (!desiredVersion.majorMustEqual) {
407
notices.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));
408
return false;
409
}
410
}
411
412
if (!isValidVersion(currentVersion, date, desiredVersion)) {
413
notices.push(nls.localize('versionMismatch', "Extension is not compatible with Code {0}. Extension requires: {1}.", currentVersion, requestedVersion));
414
return false;
415
}
416
417
return true;
418
}
419
420
function isStringArray(arr: string[]): boolean {
421
if (!Array.isArray(arr)) {
422
return false;
423
}
424
for (let i = 0, len = arr.length; i < len; i++) {
425
if (typeof arr[i] !== 'string') {
426
return false;
427
}
428
}
429
return true;
430
}
431
432