Skip to content

Commit

Permalink
Fix ensure hashed directory for extension (#7188)
Browse files Browse the repository at this point in the history
* Fix ensure hashed directory for extension

Earlier open lens versions stored extension_data directory based on
path of extension's package.json. This causes problems
because extensions have moved to new location. This ensures
backward compatibility that extension will get the same
directory than before the change.

Signed-off-by: Juho Heikka <juho.heikka@gmail.com>

* Reconstruct the key for old hashed directories

Signed-off-by: Juho Heikka <juho.heikka@gmail.com>

* Remove unnecessary return

Signed-off-by: Juho Heikka <juho.heikka@gmail.com>

* Use sync version of random bytes

Signed-off-by: Juho Heikka <juho.heikka@gmail.com>

* Add migration to new type of key

Signed-off-by: Juho Heikka <juho.heikka@gmail.com>

* Fix global override for random bytes to not return a promise

Signed-off-by: Juho Heikka <juho.heikka@gmail.com>

* Make registeredExtensions a dependency

Signed-off-by: Juho Heikka <juho.heikka@gmail.com>

---------

Signed-off-by: Juho Heikka <juho.heikka@gmail.com>
  • Loading branch information
jweak committed Feb 21, 2023
1 parent b3ae5a0 commit d8acff6
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { getGlobalOverride } from "../test-utils/get-global-override";
import randomBytesInjectable from "./random-bytes.injectable";

export default getGlobalOverride(randomBytesInjectable, () => async (size) => {
export default getGlobalOverride(randomBytesInjectable, () => (size) => {
const res = Buffer.alloc(size);

for (let i = 0; i < size; i += 1) {
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/common/utils/random-bytes.injectable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { randomBytes } from "crypto";
import { promisify } from "util";

export type RandomBytes = (size: number) => Promise<Buffer>;
export type RandomBytes = (size: number) => Buffer;

const randomBytesInjectable = getInjectable({
id: "random-bytes",
instantiate: (): RandomBytes => promisify(randomBytes),
instantiate: (): RandomBytes => randomBytes,
causesSideEffects: true,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/

import type { ObservableMap } from "mobx";
import { getInjectable } from "@ogre-tools/injectable";

import { getOrInsertWithAsync } from "../../../common/utils";
import { getOrInsert } from "../../../common/utils";
import randomBytesInjectable from "../../../common/utils/random-bytes.injectable";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
import directoryForExtensionDataInjectable from "./directory-for-extension-data.injectable";
import ensureDirInjectable from "../../../common/fs/ensure-dir.injectable";
import getHashInjectable from "./get-hash.injectable";
import getPathToLegacyPackageJsonInjectable from "./get-path-to-legacy-package-json.injectable";
import { registeredExtensionsInjectable } from "./registered-extensions.injectable";

export type EnsureHashedDirectoryForExtension = (extensionName: string, registeredExtensions: ObservableMap<string, string>) => Promise<string>;
export type EnsureHashedDirectoryForExtension = (extensionName: string) => Promise<string>;

const ensureHashedDirectoryForExtensionInjectable = getInjectable({
id: "ensure-hashed-directory-for-extension",
Expand All @@ -24,14 +25,27 @@ const ensureHashedDirectoryForExtensionInjectable = getInjectable({
const directoryForExtensionData = di.inject(directoryForExtensionDataInjectable);
const ensureDirectory = di.inject(ensureDirInjectable);
const getHash = di.inject(getHashInjectable);
const getPathToLegacyPackageJson = di.inject(getPathToLegacyPackageJsonInjectable);
const registeredExtensions = di.inject(registeredExtensionsInjectable);

return async (extensionName, registeredExtensions) => {
const dirPath = await getOrInsertWithAsync(registeredExtensions, extensionName, async () => {
const salt = (await randomBytes(32)).toString("hex");
return async (extensionName) => {
let dirPath: string;

const legacyDirPath = getPathToLegacyPackageJson(extensionName);
const hashedDirectoryForLegacyDirPath = registeredExtensions.get(legacyDirPath);

if (hashedDirectoryForLegacyDirPath) {
registeredExtensions.set(extensionName, hashedDirectoryForLegacyDirPath);
registeredExtensions.delete(legacyDirPath);
dirPath = hashedDirectoryForLegacyDirPath;
} else {
const salt = randomBytes(32).toString("hex");
const hashedName = getHash(`${extensionName}/${salt}`);

return joinPaths(directoryForExtensionData, hashedName);
});
const hashedExtensionDirectory = joinPaths(directoryForExtensionData, hashedName);

dirPath = getOrInsert(registeredExtensions, extensionName, hashedExtensionDirectory);
}

await ensureDirectory(dirPath);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ObservableMap } from "mobx";
import { runInAction } from "mobx";
import { getDiForUnitTesting } from "../../../main/getDiForUnitTesting";
import type { EnsureHashedDirectoryForExtension } from "./ensure-hashed-directory-for-extension.injectable";
import ensureHashedDirectoryForExtensionInjectable from "./ensure-hashed-directory-for-extension.injectable";
import ensureDirInjectable from "../../../common/fs/ensure-dir.injectable";
import directoryForExtensionDataInjectable from "./directory-for-extension-data.injectable";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { registeredExtensionsInjectable } from "./registered-extensions.injectable";

describe("ensure-hashed-directory-for-extension", () => {
let ensureHashedDirectoryForExtension: EnsureHashedDirectoryForExtension;
let ensureDirMock: jest.Mock;
let registeredExtensions: ObservableMap<string, string>;

beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });

ensureDirMock = jest.fn();

di.override(ensureDirInjectable, () => ensureDirMock);
di.override(directoryForExtensionDataInjectable, () => "some-directory-for-extension-data");
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");

ensureHashedDirectoryForExtension = di.inject(
ensureHashedDirectoryForExtensionInjectable,
);

registeredExtensions = di.inject(registeredExtensionsInjectable);
});

it("given registered extension exists, returns existing directory", async () => {
runInAction(() => {
registeredExtensions.set("some-extension-name", "some-directory");
});

const actual = await ensureHashedDirectoryForExtension(
"some-extension-name",
);

expect(actual).toBe("some-directory");
});

it("given registered extension does not exist, returns random directory", async () => {
const actual = await ensureHashedDirectoryForExtension(
"some-extension-name",
);

expect(actual).toBe("some-directory-for-extension-data/a37a1cfefc0391af3733f23cb6b29443f596a2b8ffe6d116c35df7bc3cd99ef6");
});

describe("given extension directory was saved based on extension's package.json path", () => {
beforeEach(() => {
runInAction(() => {
registeredExtensions.set("/some-directory-for-user-data/node_modules/some-extension-name/package.json", "some-directory");
});
ensureDirMock.mockClear();
});

it("returns existing directory", async () => {
const actual = await ensureHashedDirectoryForExtension(
"some-extension-name",
);

expect(actual).toBe("some-directory");
});

it("ensure dir is called with some directory", async () => {
await ensureHashedDirectoryForExtension(
"some-extension-name",
);

expect(ensureDirMock).toHaveBeenCalledWith("some-directory");
});

it("is migrated to use the extension name as key", async () => {
await ensureHashedDirectoryForExtension(
"some-extension-name",
);

expect(registeredExtensions.get("some-extension-name")).toEqual("some-directory");
});

it("old key is removed", async () => {
await ensureHashedDirectoryForExtension(
"some-extension-name",
);

expect(registeredExtensions.has("/some-directory-for-user-data/node_modules/some-extension-name/package.json")).toEqual(false);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { MigrationDeclaration } from "../../../common/base-store/migrations.injectable";

export const fileSystemProvisionerStoreInjectionToken = getInjectionToken<MigrationDeclaration>({
id: "file-system-provisioner-store-injection-token",
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { persistStateToConfigInjectionToken } from "../../../common/base-store/s
import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable";
import { enlistMessageChannelListenerInjectionToken } from "../../../common/utils/channel/enlist-message-channel-listener-injection-token";
import ensureHashedDirectoryForExtensionInjectable from "./ensure-hashed-directory-for-extension.injectable";
import { registeredExtensionsInjectable } from "./registered-extensions.injectable";

const fileSystemProvisionerStoreInjectable = getInjectable({
id: "file-system-provisioner-store",
Expand All @@ -30,6 +31,7 @@ const fileSystemProvisionerStoreInjectable = getInjectable({
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
ensureHashedDirectoryForExtension: di.inject(ensureHashedDirectoryForExtensionInjectable),
registeredExtensions: di.inject(registeredExtensionsInjectable),
}),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/

import { action, makeObservable, observable } from "mobx";
import type { ObservableMap } from "mobx";
import { action, makeObservable } from "mobx";
import type { BaseStoreDependencies } from "../../../common/base-store/base-store";
import { BaseStore } from "../../../common/base-store/base-store";
import type { LensExtensionId } from "../../lens-extension";
Expand All @@ -16,11 +17,10 @@ interface FSProvisionModel {

interface Dependencies extends BaseStoreDependencies {
ensureHashedDirectoryForExtension: EnsureHashedDirectoryForExtension;
registeredExtensions: ObservableMap<LensExtensionId, string>;
}

export class FileSystemProvisionerStore extends BaseStore<FSProvisionModel> {
readonly registeredExtensions = observable.map<LensExtensionId, string>();

constructor(protected readonly dependencies: Dependencies) {
super(dependencies, {
configName: "lens-filesystem-provisioner-store",
Expand All @@ -37,17 +37,17 @@ export class FileSystemProvisionerStore extends BaseStore<FSProvisionModel> {
* @returns path to the folder that the extension can safely write files to.
*/
async requestDirectory(extensionName: string): Promise<string> {
return this.dependencies.ensureHashedDirectoryForExtension(extensionName, this.registeredExtensions);
return this.dependencies.ensureHashedDirectoryForExtension(extensionName);
}

@action
protected fromStore({ extensions }: FSProvisionModel = { extensions: {}}): void {
this.registeredExtensions.merge(extensions);
this.dependencies.registeredExtensions.merge(extensions);
}

toJSON(): FSProvisionModel {
return toJS({
extensions: Object.fromEntries(this.registeredExtensions),
extensions: Object.fromEntries(this.dependencies.registeredExtensions),
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";

export type GetPathToLegacyPackageJson = (extensionName: string) => string;

const getPathToLegacyPackageJson = getInjectable({
id: "get-path-to-legacy-package-json",

instantiate: (di): GetPathToLegacyPackageJson => {
const directoryForUserData = di.inject(directoryForUserDataInjectable);
const joinPaths = di.inject(joinPathsInjectable);

return (extensionName: string) => joinPaths(directoryForUserData, "node_modules", extensionName, "package.json");
},
});

export default getPathToLegacyPackageJson;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { observable } from "mobx";
import type { LensExtensionId } from "../../lens-extension";

export const registeredExtensionsInjectable = getInjectable({
id: "registered-extensions",
instantiate: () => observable.map<LensExtensionId, string>(),
});

0 comments on commit d8acff6

Please sign in to comment.