Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/extensions/common/extensionValidator.ts
5251 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})(\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, hours, minutes] = match;
109
notBefore = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hours) || 0, Number(minutes) || 0);
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.extensionAffinity !== 'undefined') {
272
if (!isStringArray(extensionManifest.extensionAffinity)) {
273
validations.push([Severity.Error, nls.localize('extensionDescription.extensionAffinity', "property `{0}` can be omitted or must be of type `string[]`", 'extensionAffinity')]);
274
return validations;
275
}
276
}
277
if (typeof extensionManifest.activationEvents !== 'undefined') {
278
if (!isStringArray(extensionManifest.activationEvents)) {
279
validations.push([Severity.Error, nls.localize('extensionDescription.activationEvents1', "property `{0}` can be omitted or must be of type `string[]`", 'activationEvents')]);
280
return validations;
281
}
282
if (typeof extensionManifest.main === 'undefined' && typeof extensionManifest.browser === 'undefined') {
283
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')]);
284
return validations;
285
}
286
}
287
if (typeof extensionManifest.extensionKind !== 'undefined') {
288
if (typeof extensionManifest.main === 'undefined') {
289
validations.push([Severity.Warning, nls.localize('extensionDescription.extensionKind', "property `{0}` can be defined only if property `main` is also defined.", 'extensionKind')]);
290
// not a failure case
291
}
292
}
293
if (typeof extensionManifest.main !== 'undefined') {
294
if (typeof extensionManifest.main !== 'string') {
295
validations.push([Severity.Error, nls.localize('extensionDescription.main1', "property `{0}` can be omitted or must be of type `string`", 'main')]);
296
return validations;
297
} else {
298
const mainLocation = joinPath(extensionLocation, extensionManifest.main);
299
if (!isEqualOrParent(mainLocation, extensionLocation)) {
300
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)]);
301
// not a failure case
302
}
303
}
304
}
305
if (typeof extensionManifest.browser !== 'undefined') {
306
if (typeof extensionManifest.browser !== 'string') {
307
validations.push([Severity.Error, nls.localize('extensionDescription.browser1', "property `{0}` can be omitted or must be of type `string`", 'browser')]);
308
return validations;
309
} else {
310
const browserLocation = joinPath(extensionLocation, extensionManifest.browser);
311
if (!isEqualOrParent(browserLocation, extensionLocation)) {
312
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)]);
313
// not a failure case
314
}
315
}
316
}
317
318
if (!semver.valid(extensionManifest.version)) {
319
validations.push([Severity.Error, nls.localize('notSemver', "Extension version is not semver compatible.")]);
320
return validations;
321
}
322
323
const notices: string[] = [];
324
const validExtensionVersion = isValidExtensionVersion(productVersion, productDate, extensionManifest, extensionIsBuiltin, notices);
325
if (!validExtensionVersion) {
326
for (const notice of notices) {
327
validations.push([Severity.Error, notice]);
328
}
329
}
330
331
if (validateApiVersion && extensionManifest.enabledApiProposals?.length) {
332
const incompatibleNotices: string[] = [];
333
if (!areApiProposalsCompatible([...extensionManifest.enabledApiProposals], incompatibleNotices)) {
334
for (const notice of incompatibleNotices) {
335
validations.push([Severity.Error, notice]);
336
}
337
}
338
}
339
340
return validations;
341
}
342
343
export function isValidExtensionVersion(productVersion: string, productDate: ProductDate, extensionManifest: IExtensionManifest, extensionIsBuiltin: boolean, notices: string[]): boolean {
344
345
if (extensionIsBuiltin || (typeof extensionManifest.main === 'undefined' && typeof extensionManifest.browser === 'undefined')) {
346
// No version check for builtin or declarative extensions
347
return true;
348
}
349
350
return isVersionValid(productVersion, productDate, extensionManifest.engines.vscode, notices);
351
}
352
353
export function isEngineValid(engine: string, version: string, date: ProductDate): boolean {
354
// TODO@joao: discuss with alex '*' doesn't seem to be a valid engine version
355
return engine === '*' || isVersionValid(version, date, engine);
356
}
357
358
export function areApiProposalsCompatible(apiProposals: string[]): boolean;
359
export function areApiProposalsCompatible(apiProposals: string[], notices: string[]): boolean;
360
export function areApiProposalsCompatible(apiProposals: string[], productApiProposals: Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>): boolean;
361
export function areApiProposalsCompatible(apiProposals: string[], arg1?: string[] | Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>): boolean {
362
if (apiProposals.length === 0) {
363
return true;
364
}
365
const notices: string[] | undefined = Array.isArray(arg1) ? arg1 : undefined;
366
const productApiProposals: Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }> = (Array.isArray(arg1) ? undefined : arg1) ?? allApiProposals;
367
const incompatibleProposals: string[] = [];
368
const parsedProposals = parseApiProposals(apiProposals);
369
for (const { proposalName, version } of parsedProposals) {
370
if (!version) {
371
continue;
372
}
373
const existingProposal = productApiProposals[proposalName];
374
if (existingProposal?.version !== version) {
375
incompatibleProposals.push(proposalName);
376
}
377
}
378
if (incompatibleProposals.length) {
379
if (notices) {
380
if (incompatibleProposals.length === 1) {
381
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]));
382
} else {
383
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.",
384
incompatibleProposals.slice(0, incompatibleProposals.length - 1).map(p => `'${p}'`).join(', '),
385
incompatibleProposals[incompatibleProposals.length - 1]));
386
}
387
}
388
return false;
389
}
390
return true;
391
}
392
393
function isVersionValid(currentVersion: string, date: ProductDate, requestedVersion: string, notices: string[] = []): boolean {
394
395
const desiredVersion = normalizeVersion(parseVersion(requestedVersion));
396
if (!desiredVersion) {
397
notices.push(nls.localize('versionSyntax', "Could not parse `engines.vscode` value {0}. Please use, for example: ^1.22.0, ^1.22.x, etc.", requestedVersion));
398
return false;
399
}
400
401
// enforce that a breaking API version is specified.
402
// for 0.X.Y, that means up to 0.X must be specified
403
// otherwise for Z.X.Y, that means Z must be specified
404
if (desiredVersion.majorBase === 0) {
405
// force that major and minor must be specific
406
if (!desiredVersion.majorMustEqual || !desiredVersion.minorMustEqual) {
407
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));
408
return false;
409
}
410
} else {
411
// force that major must be specific
412
if (!desiredVersion.majorMustEqual) {
413
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));
414
return false;
415
}
416
}
417
418
if (!isValidVersion(currentVersion, date, desiredVersion)) {
419
notices.push(nls.localize('versionMismatch', "Extension is not compatible with Code {0}. Extension requires: {1}.", currentVersion, requestedVersion));
420
return false;
421
}
422
423
return true;
424
}
425
426
function isStringArray(arr: readonly string[]): boolean {
427
if (!Array.isArray(arr)) {
428
return false;
429
}
430
for (let i = 0, len = arr.length; i < len; i++) {
431
if (typeof arr[i] !== 'string') {
432
return false;
433
}
434
}
435
return true;
436
}
437
438