Repository: microsoft/vscode (TypeScript)
Date: 2023-02-01 14:54:09+00:00
Task type: Bug Fix

Issue:
Cannot install extensions
Sanity testing 1.75 on macOS and I found that installing extensions would always fail due to following error

```
2023-01-31 18:21:18.888 [error] Invalid extensions content in vscode-userdata:/Users/penlv/.vscode/extensions/extensions.json: Error: Invalid extensions content in vscode-userdata:/Users/penlv/.vscode/extensions/extensions.json
    at I.p (vscode-file://vscode-app/private/var/folders/j7/py1_06vs0y3bbcdpsn0pd66h0000gn/T/AppTranslocation/2287E3F7-7106-459F-A86E-6AFEAEE53302/d/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/sharedProcess/sharedProcessMain.js:88:94382)
    at Object.factory (vscode-file://vscode-app/private/var/folders/j7/py1_06vs0y3bbcdpsn0pd66h0000gn/T/AppTranslocation/2287E3F7-7106-459F-A86E-6AFEAEE53302/d/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/code/electron-browser/sharedProcess/sharedProcessMain.js:88:93857)

```

I was using 1.74.3 before, tried following steps:

* Switch back to 1.74.3, installing extensions work
* Switch back to 1.75, installing extensions continues to fail for the same errors
* Removing extensions.json and start 1.74.3 again, installing extensions starts to work

My `extensions.json` content

```
[{"identifier":{"id":"rebornix.nova","uuid":"258ae097-be50-4084-a73a-c8b1a36a8970"},"version":"0.2.2","location":{"$mid":1,"scheme":"rebornix.nova-0.2.2"},"metadata":{"id":"258ae097-be50-4084-a73a-c8b1a36a8970","publisherId":"d30b9513-547b-4112-9c9c-0fcffa960258","publisherDisplayName":"Peng Lv","isPreReleaseVersion":false,"installedTimestamp":1641933915286}},{"identifier":{"id":"github.codespaces","uuid":"4023d3e5-c840-4cdd-8b54-51c77548aa3f"},"version":"1.13.3","location":{"$mid":1,"scheme":"github.codespaces-1.13.3"},"metadata":{"id":"4023d3e5-c840-4cdd-8b54-51c77548aa3f","publisherDisplayName":"GitHub","publisherId":"7c1c19cd-78eb-4dfb-8999-99caf7679002","isPreReleaseVersion":false,"isApplicationScoped":false,"installedTimestamp":1670275424102,"pinned":true}},{"identifier":{"id":"ms-toolsai.jupyter-keymap","uuid":"9f6dc8db-620c-4844-b8c5-e74914f1be27"},"version":"1.0.0","location":{"$mid":1,"scheme":"ms-toolsai.jupyter-keymap-1.0.0"},"metadata":{"id":"9f6dc8db-620c-4844-b8c5-e74914f1be27","publisherId":"ac8eb7c9-3e59-4b39-8040-f0484d8170ce","publisherDisplayName":"Microsoft","targetPlatform":"undefined","isApplicationScoped":false,"updated":false,"isPreReleaseVersion":false,"installedTimestamp":1670431017420,"preRelease":false}},{"identifier":{"id":"ms-toolsai.vscode-jupyter-cell-tags","uuid":"ab4fb32a-befb-4102-adf9-1652d0cd6a5e"},"version":"0.1.6","location":{"$mid":1,"scheme":"ms-toolsai.vscode-jupyter-cell-tags-0.1.6"},"metadata":{"id":"ab4fb32a-befb-4102-adf9-1652d0cd6a5e","publisherId":"ac8eb7c9-3e59-4b39-8040-f0484d8170ce","publisherDisplayName":"Microsoft","targetPlatform":"undefined","isApplicationScoped":false,"updated":false,"isPreReleaseVersion":false,"installedTimestamp":1670431017429,"preRelease":false}},{"identifier":{"id":"ms-toolsai.vscode-jupyter-slideshow","uuid":"e153ca70-b543-4865-b4c5-b31d34185948"},"version":"0.1.5","location":{"$mid":1,"scheme":"ms-toolsai.vscode-jupyter-slideshow-0.1.5"},"metadata":{"id":"e153ca70-b543-4865-b4c5-b31d34185948","publisherId":"ac8eb7c9-3e59-4b39-8040-f0484d8170ce","publisherDisplayName":"Microsoft","targetPlatform":"undefined","isApplicationScoped":false,"updated":false,"isPreReleaseVersion":false,"installedTimestamp":1670431017433,"preRelease":false}},{"identifier":{"id":"github.vscode-pull-request-github","uuid":"69ddd764-339a-4ecc-97c1-9c4ece58e36d"},"version":"0.56.0","location":{"$mid":1,"scheme":"github.vscode-pull-request-github-0.56.0"},"metadata":{"id":"69ddd764-339a-4ecc-97c1-9c4ece58e36d","publisherId":"7c1c19cd-78eb-4dfb-8999-99caf7679002","publisherDisplayName":"GitHub","targetPlatform":"undefined","isApplicationScoped":false,"updated":true,"isPreReleaseVersion":false,"installedTimestamp":1670474216065,"preRelease":false}},{"identifier":{"id":"ms-toolsai.jupyter","uuid":"6c2f1801-1e7f-45b2-9b5c-7782f1e076e8"},"version":"2022.11.1003412109","location":{"$mid":1,"scheme":"ms-toolsai.jupyter-2022.11.1003412109"},"metadata":{"id":"6c2f1801-1e7f-45b2-9b5c-7782f1e076e8","publisherId":"ac8eb7c9-3e59-4b39-8040-f0484d8170ce","publisherDisplayName":"Microsoft","targetPlatform":"undefined","isApplicationScoped":false,"updated":true,"isPreReleaseVersion":false,"installedTimestamp":1670474216027,"preRelease":false}},{"identifier":{"id":"ms-vscode.vscode-github-issue-notebooks","uuid":"3be1cece-c179-4cd6-b59b-3bae29e1a166"},"version":"0.0.128","location":{"$mid":1,"scheme":"ms-vscode.vscode-github-issue-notebooks-0.0.128"},"metadata":{"id":"3be1cece-c179-4cd6-b59b-3bae29e1a166","publisherId":"5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee","publisherDisplayName":"Microsoft","targetPlatform":"undefined","isApplicationScoped":false,"updated":true,"isPreReleaseVersion":false,"installedTimestamp":1670603827473,"preRelease":false}},{"identifier":{"id":"eamodio.gitlens","uuid":"4de763bd-505d-4978-9575-2b7696ecf94e"},"version":"13.2.0","location":{"$mid":1,"scheme":"eamodio.gitlens-13.2.0"},"metadata":{"id":"4de763bd-505d-4978-9575-2b7696ecf94e","publisherId":"678d198b-9b2e-49d3-96ff-6d801c9575df","publisherDisplayName":"GitKraken","targetPlatform":"undefined","isApplicationScoped":false,"updated":true,"isPreReleaseVersion":false,"installedTimestamp":1671736733466,"preRelease":false}},{"identifier":{"id":"github.github-vscode-theme","uuid":"7328a705-91fc-49e6-8293-da6f112e482d"},"version":"6.3.3","location":{"$mid":1,"scheme":"github.github-vscode-theme-6.3.3"},"metadata":{"id":"7328a705-91fc-49e6-8293-da6f112e482d","publisherId":"7c1c19cd-78eb-4dfb-8999-99caf7679002","publisherDisplayName":"GitHub","targetPlatform":"undefined","isApplicationScoped":false,"updated":true,"isPreReleaseVersion":false,"installedTimestamp":1674599403280,"preRelease":false}},{"identifier":{"id":"ms-toolsai.jupyter-renderers","uuid":"b15c72f8-d5fe-421a-a4f7-27ed9f6addbf"},"version":"1.0.14","location":{"$mid":1,"scheme":"ms-toolsai.jupyter-renderers-1.0.14"},"metadata":{"id":"b15c72f8-d5fe-421a-a4f7-27ed9f6addbf","publisherId":"ac8eb7c9-3e59-4b39-8040-f0484d8170ce","publisherDisplayName":"Microsoft","targetPlatform":"undefined","isApplicationScoped":false,"updated":true,"isPreReleaseVersion":false,"installedTimestamp":1674599403050,"preRelease":false}},{"identifier":{"id":"ms-python.python","uuid":"f1f59ae4-9318-4f3c-a9b5-81b2eaa5f8a5"},"version":"2022.20.2","location":{"$mid":1,"scheme":"ms-python.python-2022.20.2"},"metadata":{"id":"f1f59ae4-9318-4f3c-a9b5-81b2eaa5f8a5","publisherId":"998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8","publisherDisplayName":"Microsoft","targetPlatform":"undefined","isApplicationScoped":false,"updated":true,"isPreReleaseVersion":false,"installedTimestamp":1674599402614,"preRelease":false}},{"identifier":{"id":"ms-python.isort","uuid":"4ad0ce32-ff3f-49f0-83b5-93e5dc00cfff"},"version":"2022.8.0","location":{"$mid":1,"fsPath":"/Users/penlv/.vscode/extensions/ms-python.isort-2022.8.0","path":"/Users/penlv/.vscode/extensions/ms-python.isort-2022.8.0","scheme":"file"},"metadata":{"id":"4ad0ce32-ff3f-49f0-83b5-93e5dc00cfff","publisherId":"998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8","publisherDisplayName":"Microsoft","targetPlatform":"undefined","isApplicationScoped":false,"updated":true,"isPreReleaseVersion":false,"installedTimestamp":1674686475980,"preRelease":false}},{"identifier":{"id":"ms-python.vscode-pylance","uuid":"364d2426-116a-433a-a5d8-a5098dc3afbd"},"version":"2023.1.40","location":{"$mid":1,"fsPath":"/Users/penlv/.vscode/extensions/ms-python.vscode-pylance-2023.1.40","path":"/Users/penlv/.vscode/extensions/ms-python.vscode-pylance-2023.1.40","scheme":"file"},"metadata":{"id":"364d2426-116a-433a-a5d8-a5098dc3afbd","publisherId":"998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8","publisherDisplayName":"Microsoft","targetPlatform":"undefined","isApplicationScoped":false,"updated":true,"isPreReleaseVersion":false,"installedTimestamp":1674686475974,"preRelease":false}}]
```




Issue discussion:
On 1.74.3, I actually got following errors:

```
2023-01-31 18:36:48.143 [error] Error: [UriError]: cannot call joinPath on URI without path
    at c.joinPath (vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js:88:29104)
    at S.joinPath (vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js:89:6466)
    at W.scanExtensionManifest (vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js:769:40990)
    at W.scanExtension (vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js:769:40080)
    at vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js:769:39656
    at Array.map (<anonymous>)
    at W.m (vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js:769:39389)
    at async W.l (vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js:769:38977)
    at async W.scanExtensions (vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js:769:38158)
    at async W.scanExtensions (vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js:769:44069)
    at async c.scanUserExtensions (vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js:769:31089)
    at async Promise.all (index 1)
    at async u.j (vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js:2434:38337)
    at async u.startScanningExtensions (vscode-file://vscode-app/Applications/Visual%20Studio%20Code.app/Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js:2434:38046)
```

I don't recall modifying extensions.json file myself as the content is a big one line. It seems in 1.74.3, even though we have some errors, extension service still works but on 1.75, it fails completely.
Seems like it doesn't like the `location` format. For me, it's always a string like `"location": "amodio.amethyst-theme-4.5.0"`
There are two points here

1. How does the extensions.json file got invalid content. In your case, location seems to be an invalid URI - `"location":{"$mid":1,"scheme":"rebornix.nova-0.2.2"}`. Its not a coincidence that locations of all extension entries in this file are converted into this format. This needs to be investigated.

2. Shall VS Code ignore these invalid extensions or bail out like it is now?
Ok, I nailed it. Here are the steps:

- Install 1.74.3 and install some extensions
- Upgrade to 1.75.0 - this migrates the extensions.json file to use relative location instead of absolute location.
- Downgrade to 1.74.3 and install an extension - this messes up the format of extensions.json file
- Upgrade to 1.75.0 - extensions are not loaded and cannot be installed because of invalid manifest file

Workaround: Delete the `extensions.json` file

Solution patch:
diff --git a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts
--- a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts
+++ b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts
@@ -17,13 +17,14 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
 import { ILogService } from 'vs/platform/log/common/log';
 import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
 import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
-import { Mutable, isObject, isString } from 'vs/base/common/types';
+import { Mutable, isObject, isString, isUndefined } from 'vs/base/common/types';
 import { getErrorMessage } from 'vs/base/common/errors';
 import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
 
 interface IStoredProfileExtension {
 	identifier: IExtensionIdentifier;
 	location: UriComponents | string;
+	relativeLocation: string | undefined;
 	version: string;
 	metadata?: Metadata;
 }
@@ -247,14 +248,23 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable
 						this.reportAndThrowInvalidConentError(file);
 					}
 					let location: URI;
-					if (isString(e.location)) {
+					if (isString(e.relativeLocation)) {
+						// Extension in new format. No migration needed.
+						location = this.resolveExtensionLocation(e.relativeLocation);
+					} else if (isString(e.location)) {
+						// Extension in intermediate format. Migrate to new format.
 						location = this.resolveExtensionLocation(e.location);
+						migrate = true;
+						e.relativeLocation = e.location;
+						// retain old format so that old clients can read it
+						e.location = location.toJSON();
 					} else {
 						location = URI.revive(e.location);
 						const relativePath = this.toRelativePath(location);
 						if (relativePath) {
+							// Extension in old format. Migrate to new format.
 							migrate = true;
-							e.location = relativePath;
+							e.relativeLocation = relativePath;
 						}
 					}
 					extensions.push({
@@ -275,7 +285,9 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable
 				const storedProfileExtensions: IStoredProfileExtension[] = extensions.map(e => ({
 					identifier: e.identifier,
 					version: e.version,
-					location: this.toRelativePath(e.location) ?? e.location.toJSON(),
+					// retain old format so that old clients can read it
+					location: e.location.toJSON(),
+					relativeLocation: this.toRelativePath(e.location),
 					metadata: e.metadata
 				}));
 				await this.fileService.writeFile(file, VSBuffer.fromString(JSON.stringify(storedProfileExtensions)));
@@ -376,6 +388,7 @@ function isStoredProfileExtension(candidate: any): candidate is IStoredProfileEx
 	return isObject(candidate)
 		&& isIExtensionIdentifier(candidate.identifier)
 		&& (isUriComponents(candidate.location) || isString(candidate.location))
+		&& (isUndefined(candidate.relativeLocation) || isString(candidate.relativeLocation))
 		&& candidate.version && isString(candidate.version);
 }
 


Test patch:
diff --git a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts
new file mode 100644
--- /dev/null
+++ b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts
@@ -0,0 +1,425 @@
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as assert from 'assert';
+import { VSBuffer } from 'vs/base/common/buffer';
+import { DisposableStore } from 'vs/base/common/lifecycle';
+import { joinPath } from 'vs/base/common/resources';
+import { URI } from 'vs/base/common/uri';
+import { IEnvironmentService } from 'vs/platform/environment/common/environment';
+import { AbstractExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService';
+import { ExtensionType, IExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions';
+import { FileService } from 'vs/platform/files/common/fileService';
+import { IFileService } from 'vs/platform/files/common/files';
+import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
+import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
+import { ILogService, NullLogService } from 'vs/platform/log/common/log';
+import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
+import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
+import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
+import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService';
+import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
+
+class TestObject extends AbstractExtensionsProfileScannerService { }
+
+suite('ExtensionsProfileScannerService', () => {
+
+	const ROOT = URI.file('/ROOT');
+	const disposables = new DisposableStore();
+
+	const extensionsLocation = joinPath(ROOT, 'extensions');
+	let instantiationService: TestInstantiationService;
+
+	setup(async () => {
+		instantiationService = new TestInstantiationService();
+		const logService = new NullLogService();
+		const fileService = disposables.add(new FileService(logService));
+		const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider());
+		fileService.registerProvider(ROOT.scheme, fileSystemProvider);
+		instantiationService.stub(ILogService, logService);
+		instantiationService.stub(IFileService, fileService);
+		instantiationService.stub(ITelemetryService, NullTelemetryService);
+		const uriIdentityService = instantiationService.stub(IUriIdentityService, new UriIdentityService(fileService));
+		const environmentService = instantiationService.stub(IEnvironmentService, { userRoamingDataHome: ROOT });
+		const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService);
+		instantiationService.stub(IUserDataProfilesService, userDataProfilesService);
+	});
+
+	test('write extensions located in the same extensions folder', async () => {
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+		const extension = aExtension('pub.a', joinPath(extensionsLocation, 'pub.a-1.0.0'));
+		await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest);
+
+		const actual = await testObject.scanProfileExtensions(extensionsManifest);
+		assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: undefined }]);
+	});
+
+	test('write extensions located in the different folder', async () => {
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+		const extension = aExtension('pub.a', joinPath(ROOT, 'pub.a-1.0.0'));
+		await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest);
+
+		const actual = await testObject.scanProfileExtensions(extensionsManifest);
+		assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: undefined }]);
+	});
+
+	test('write extensions located in the same extensions folder has relative location ', async () => {
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+		const extension = aExtension('pub.a', joinPath(extensionsLocation, 'pub.a-1.0.0'));
+		await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest);
+
+		const actual = JSON.parse((await instantiationService.get(IFileService).readFile(extensionsManifest)).value.toString());
+		assert.deepStrictEqual(actual, [{ identifier: extension.identifier, location: extension.location.toJSON(), relativeLocation: 'pub.a-1.0.0', version: extension.manifest.version }]);
+	});
+
+	test('write extensions located in different extensions folder does not has relative location ', async () => {
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+		const extension = aExtension('pub.a', joinPath(ROOT, 'pub.a-1.0.0'));
+		await testObject.addExtensionsToProfile([[extension, undefined]], extensionsManifest);
+
+		const actual = JSON.parse((await instantiationService.get(IFileService).readFile(extensionsManifest)).value.toString());
+		assert.deepStrictEqual(actual, [{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version }]);
+	});
+
+	test('extension in old format is read and migrated', async () => {
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+		const extension = aExtension('pub.a', joinPath(extensionsLocation, 'pub.a-1.0.0'));
+		await instantiationService.get(IFileService).writeFile(extensionsManifest, VSBuffer.fromString(JSON.stringify([{
+			identifier: extension.identifier,
+			location: extension.location.toJSON(),
+			version: extension.manifest.version,
+		}])));
+
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		const actual = await testObject.scanProfileExtensions(extensionsManifest);
+		assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: undefined }]);
+
+		const manifestContent = JSON.parse((await instantiationService.get(IFileService).readFile(extensionsManifest)).value.toString());
+		assert.deepStrictEqual(manifestContent, [{ identifier: extension.identifier, location: extension.location.toJSON(), relativeLocation: 'pub.a-1.0.0', version: extension.manifest.version }]);
+	});
+
+	test('extension in old format is not migrated if not exists in same location', async () => {
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+		const extension = aExtension('pub.a', joinPath(ROOT, 'pub.a-1.0.0'));
+		await instantiationService.get(IFileService).writeFile(extensionsManifest, VSBuffer.fromString(JSON.stringify([{
+			identifier: extension.identifier,
+			location: extension.location.toJSON(),
+			version: extension.manifest.version,
+		}])));
+
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		const actual = await testObject.scanProfileExtensions(extensionsManifest);
+		assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: undefined }]);
+
+		const manifestContent = JSON.parse((await instantiationService.get(IFileService).readFile(extensionsManifest)).value.toString());
+		assert.deepStrictEqual(manifestContent, [{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version }]);
+	});
+
+	test('extension in old format is read and migrated during write', async () => {
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+		const extension = aExtension('pub.a', joinPath(extensionsLocation, 'pub.a-1.0.0'));
+		await instantiationService.get(IFileService).writeFile(extensionsManifest, VSBuffer.fromString(JSON.stringify([{
+			identifier: extension.identifier,
+			location: extension.location.toJSON(),
+			version: extension.manifest.version,
+		}])));
+
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+		const extension2 = aExtension('pub.b', joinPath(extensionsLocation, 'pub.b-1.0.0'));
+		await testObject.addExtensionsToProfile([[extension2, undefined]], extensionsManifest);
+
+		const actual = await testObject.scanProfileExtensions(extensionsManifest);
+		assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [
+			{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: undefined },
+			{ identifier: extension2.identifier, location: extension2.location.toJSON(), version: extension2.manifest.version, metadata: undefined }
+		]);
+
+		const manifestContent = JSON.parse((await instantiationService.get(IFileService).readFile(extensionsManifest)).value.toString());
+		assert.deepStrictEqual(manifestContent, [
+			{ identifier: extension.identifier, location: extension.location.toJSON(), relativeLocation: 'pub.a-1.0.0', version: extension.manifest.version },
+			{ identifier: extension2.identifier, location: extension2.location.toJSON(), relativeLocation: 'pub.b-1.0.0', version: extension2.manifest.version }
+		]);
+	});
+
+	test('extensions in old format and new format is read and migrated', async () => {
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+		const extension = aExtension('pub.a', joinPath(extensionsLocation, 'pub.a-1.0.0'));
+		const extension2 = aExtension('pub.b', joinPath(extensionsLocation, 'pub.b-1.0.0'));
+		await instantiationService.get(IFileService).writeFile(extensionsManifest, VSBuffer.fromString(JSON.stringify([{
+			identifier: extension.identifier,
+			location: extension.location.toJSON(),
+			version: extension.manifest.version,
+		}, {
+			identifier: extension2.identifier,
+			location: extension2.location.toJSON(),
+			relativeLocation: 'pub.b-1.0.0',
+			version: extension2.manifest.version,
+		}])));
+
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		const actual = await testObject.scanProfileExtensions(extensionsManifest);
+		assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [
+			{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: undefined },
+			{ identifier: extension2.identifier, location: extension2.location.toJSON(), version: extension2.manifest.version, metadata: undefined }
+		]);
+
+		const manifestContent = JSON.parse((await instantiationService.get(IFileService).readFile(extensionsManifest)).value.toString());
+		assert.deepStrictEqual(manifestContent, [
+			{ identifier: extension.identifier, location: extension.location.toJSON(), relativeLocation: 'pub.a-1.0.0', version: extension.manifest.version },
+			{ identifier: extension2.identifier, location: extension2.location.toJSON(), relativeLocation: 'pub.b-1.0.0', version: extension2.manifest.version }
+		]);
+	});
+
+	test('extension in intermediate format is read and migrated', async () => {
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+		const extension = aExtension('pub.a', joinPath(extensionsLocation, 'pub.a-1.0.0'));
+		await instantiationService.get(IFileService).writeFile(extensionsManifest, VSBuffer.fromString(JSON.stringify([{
+			identifier: extension.identifier,
+			location: 'pub.a-1.0.0',
+			version: extension.manifest.version,
+		}])));
+
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		const actual = await testObject.scanProfileExtensions(extensionsManifest);
+		assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: undefined }]);
+
+		const manifestContent = JSON.parse((await instantiationService.get(IFileService).readFile(extensionsManifest)).value.toString());
+		assert.deepStrictEqual(manifestContent, [{ identifier: extension.identifier, location: extension.location.toJSON(), relativeLocation: 'pub.a-1.0.0', version: extension.manifest.version }]);
+	});
+
+	test('extension in intermediate format is read and migrated during write', async () => {
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+		const extension = aExtension('pub.a', joinPath(extensionsLocation, 'pub.a-1.0.0'));
+		await instantiationService.get(IFileService).writeFile(extensionsManifest, VSBuffer.fromString(JSON.stringify([{
+			identifier: extension.identifier,
+			location: 'pub.a-1.0.0',
+			version: extension.manifest.version,
+		}])));
+
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+		const extension2 = aExtension('pub.b', joinPath(extensionsLocation, 'pub.b-1.0.0'));
+		await testObject.addExtensionsToProfile([[extension2, undefined]], extensionsManifest);
+
+		const actual = await testObject.scanProfileExtensions(extensionsManifest);
+		assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [
+			{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: undefined },
+			{ identifier: extension2.identifier, location: extension2.location.toJSON(), version: extension2.manifest.version, metadata: undefined }
+		]);
+
+		const manifestContent = JSON.parse((await instantiationService.get(IFileService).readFile(extensionsManifest)).value.toString());
+		assert.deepStrictEqual(manifestContent, [
+			{ identifier: extension.identifier, location: extension.location.toJSON(), relativeLocation: 'pub.a-1.0.0', version: extension.manifest.version },
+			{ identifier: extension2.identifier, location: extension2.location.toJSON(), relativeLocation: 'pub.b-1.0.0', version: extension2.manifest.version }
+		]);
+	});
+
+	test('extensions in intermediate and new format is read and migrated', async () => {
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+		const extension = aExtension('pub.a', joinPath(extensionsLocation, 'pub.a-1.0.0'));
+		const extension2 = aExtension('pub.b', joinPath(extensionsLocation, 'pub.b-1.0.0'));
+		await instantiationService.get(IFileService).writeFile(extensionsManifest, VSBuffer.fromString(JSON.stringify([{
+			identifier: extension.identifier,
+			location: 'pub.a-1.0.0',
+			version: extension.manifest.version,
+		}, {
+			identifier: extension2.identifier,
+			location: extension2.location.toJSON(),
+			relativeLocation: 'pub.b-1.0.0',
+			version: extension2.manifest.version,
+		}])));
+
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		const actual = await testObject.scanProfileExtensions(extensionsManifest);
+		assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [
+			{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: undefined },
+			{ identifier: extension2.identifier, location: extension2.location.toJSON(), version: extension2.manifest.version, metadata: undefined }
+		]);
+
+		const manifestContent = JSON.parse((await instantiationService.get(IFileService).readFile(extensionsManifest)).value.toString());
+		assert.deepStrictEqual(manifestContent, [
+			{ identifier: extension.identifier, location: extension.location.toJSON(), relativeLocation: 'pub.a-1.0.0', version: extension.manifest.version },
+			{ identifier: extension2.identifier, location: extension2.location.toJSON(), relativeLocation: 'pub.b-1.0.0', version: extension2.manifest.version }
+		]);
+	});
+
+	test('extensions in mixed format is read and migrated', async () => {
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+		const extension1 = aExtension('pub.a', joinPath(extensionsLocation, 'pub.a-1.0.0'));
+		const extension2 = aExtension('pub.b', joinPath(extensionsLocation, 'pub.b-1.0.0'));
+		const extension3 = aExtension('pub.c', joinPath(extensionsLocation, 'pub.c-1.0.0'));
+		const extension4 = aExtension('pub.d', joinPath(ROOT, 'pub.d-1.0.0'));
+		await instantiationService.get(IFileService).writeFile(extensionsManifest, VSBuffer.fromString(JSON.stringify([{
+			identifier: extension1.identifier,
+			location: 'pub.a-1.0.0',
+			version: extension1.manifest.version,
+		}, {
+			identifier: extension2.identifier,
+			location: extension2.location.toJSON(),
+			version: extension2.manifest.version,
+		}, {
+			identifier: extension3.identifier,
+			location: extension3.location.toJSON(),
+			relativeLocation: 'pub.c-1.0.0',
+			version: extension3.manifest.version,
+		}, {
+			identifier: extension4.identifier,
+			location: extension4.location.toJSON(),
+			version: extension4.manifest.version,
+		}])));
+
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		const actual = await testObject.scanProfileExtensions(extensionsManifest);
+		assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [
+			{ identifier: extension1.identifier, location: extension1.location.toJSON(), version: extension1.manifest.version, metadata: undefined },
+			{ identifier: extension2.identifier, location: extension2.location.toJSON(), version: extension2.manifest.version, metadata: undefined },
+			{ identifier: extension3.identifier, location: extension3.location.toJSON(), version: extension3.manifest.version, metadata: undefined },
+			{ identifier: extension4.identifier, location: extension4.location.toJSON(), version: extension4.manifest.version, metadata: undefined }
+		]);
+
+		const manifestContent = JSON.parse((await instantiationService.get(IFileService).readFile(extensionsManifest)).value.toString());
+		assert.deepStrictEqual(manifestContent, [
+			{ identifier: extension1.identifier, location: extension1.location.toJSON(), relativeLocation: 'pub.a-1.0.0', version: extension1.manifest.version },
+			{ identifier: extension2.identifier, location: extension2.location.toJSON(), relativeLocation: 'pub.b-1.0.0', version: extension2.manifest.version },
+			{ identifier: extension3.identifier, location: extension3.location.toJSON(), relativeLocation: 'pub.c-1.0.0', version: extension3.manifest.version },
+			{ identifier: extension4.identifier, location: extension4.location.toJSON(), version: extension4.manifest.version }
+		]);
+	});
+
+	test('throws error if extension has invalid relativePath', async () => {
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+		const extension = aExtension('pub.a', joinPath(extensionsLocation, 'pub.a-1.0.0'));
+		await instantiationService.get(IFileService).writeFile(extensionsManifest, VSBuffer.fromString(JSON.stringify([{
+			identifier: extension.identifier,
+			location: extension.location.toJSON(),
+			version: extension.manifest.version,
+			relativePath: 2
+		}])));
+
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		try {
+			await testObject.scanProfileExtensions(extensionsManifest);
+			assert.fail('Should throw error');
+		} catch (error) { /*expected*/ }
+	});
+
+	test('throws error if extension has no location', async () => {
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+		const extension = aExtension('pub.a', joinPath(extensionsLocation, 'pub.a-1.0.0'));
+		await instantiationService.get(IFileService).writeFile(extensionsManifest, VSBuffer.fromString(JSON.stringify([{
+			identifier: extension.identifier,
+			version: extension.manifest.version,
+			relativePath: 'pub.a-1.0.0'
+		}])));
+
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		try {
+			await testObject.scanProfileExtensions(extensionsManifest);
+			assert.fail('Should throw error');
+		} catch (error) { /*expected*/ }
+	});
+
+	test('throws error if extension location is invalid', async () => {
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+		const extension = aExtension('pub.a', joinPath(extensionsLocation, 'pub.a-1.0.0'));
+		await instantiationService.get(IFileService).writeFile(extensionsManifest, VSBuffer.fromString(JSON.stringify([{
+			identifier: extension.identifier,
+			location: {},
+			version: extension.manifest.version,
+			relativePath: 'pub.a-1.0.0'
+		}])));
+
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		try {
+			await testObject.scanProfileExtensions(extensionsManifest);
+			assert.fail('Should throw error');
+		} catch (error) { /*expected*/ }
+	});
+
+	test('throws error if extension has no identifier', async () => {
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+		const extension = aExtension('pub.a', joinPath(extensionsLocation, 'pub.a-1.0.0'));
+		await instantiationService.get(IFileService).writeFile(extensionsManifest, VSBuffer.fromString(JSON.stringify([{
+			location: extension.location.toJSON(),
+			version: extension.manifest.version,
+		}])));
+
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		try {
+			await testObject.scanProfileExtensions(extensionsManifest);
+			assert.fail('Should throw error');
+		} catch (error) { /*expected*/ }
+	});
+
+	test('throws error if extension identifier is invalid', async () => {
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+		const extension = aExtension('pub.a', joinPath(extensionsLocation, 'pub.a-1.0.0'));
+		await instantiationService.get(IFileService).writeFile(extensionsManifest, VSBuffer.fromString(JSON.stringify([{
+			identifier: 'pub.a',
+			location: extension.location.toJSON(),
+			version: extension.manifest.version,
+		}])));
+
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		try {
+			await testObject.scanProfileExtensions(extensionsManifest);
+			assert.fail('Should throw error');
+		} catch (error) { /*expected*/ }
+	});
+
+	test('throws error if extension has no version', async () => {
+		const extensionsManifest = joinPath(extensionsLocation, 'extensions.json');
+		const extension = aExtension('pub.a', joinPath(extensionsLocation, 'pub.a-1.0.0'));
+		await instantiationService.get(IFileService).writeFile(extensionsManifest, VSBuffer.fromString(JSON.stringify([{
+			identifier: extension.identifier,
+			location: extension.location.toJSON(),
+		}])));
+
+		const testObject = instantiationService.createInstance(TestObject, extensionsLocation);
+
+		try {
+			await testObject.scanProfileExtensions(extensionsManifest);
+			assert.fail('Should throw error');
+		} catch (error) { /*expected*/ }
+	});
+
+	function aExtension(id: string, location: URI, e?: Partial<IExtension>): IExtension {
+		return {
+			identifier: { id },
+			location,
+			type: ExtensionType.User,
+			targetPlatform: TargetPlatform.DARWIN_X64,
+			isBuiltin: false,
+			manifest: {
+				name: 'name',
+				publisher: 'publisher',
+				version: '1.0.0',
+				engines: { vscode: '1.0.0' },
+			},
+			isValid: true,
+			validations: [],
+			...e
+		};
+	}
+
+});