1198 lines · 47.7 KB · TypeScript
RawDownload
// @effect-diagnostics nodeBuiltinImport:off - Builds a Chromium-shaped cookie// table with the same native bindings the source reads.import * as NodePath from "node:path";import * as NodeServices from "@effect/platform-node/NodeServices";import { assert, describe, it } from "@effect/vitest";import {  HostProcessEnvironment,  HostProcessHostname,  HostProcessPlatform,} from "@t3tools/shared/hostProcess";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Path from "effect/Path";import * as PlatformError from "effect/PlatformError";import * as Scope from "effect/Scope";import * as Stream from "effect/Stream";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";import * as NodeSqlite from "node:sqlite"; import type { BrowserImportPathContext } from "./Sources.ts";import {  BROWSER_IMPORT_SOURCES,  chromiumProcessIsAlive,  chromiumSingletonLockIsHeld,  cookieDatabaseCandidatePaths,  firefoxSymlinkLockIsHeld,  resolveCookieDatabase,  isSourceInstalled,  isSourceRunning,  isWindowsLockHeldError,  posixLockIsHeld,  listSourceProfiles,  sourcePathContext,  windowsChromiumCookiesAreHeld,} from "./Sources.ts";import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; const helium = BROWSER_IMPORT_SOURCES.find((source) => source.id === "helium")!; describe("Linux Chromium secret applications", () => {  it("pins the libsecret application attribute for each supported fork", () => {    assert.deepEqual(      Object.fromEntries(        BROWSER_IMPORT_SOURCES.filter((source) => source.platforms.includes("linux")).map(          (source) => [source.id, source.linuxSecretApplication],        ),      ),      {        chrome: "chrome",        edge: "msedge",        brave: "brave",        vivaldi: "vivaldi",        opera: "opera",        helium: "chromium",        firefox: undefined,      },    );  });}); const platformError = (reasonTag: string): PlatformError.PlatformError =>  ({ _tag: "PlatformError", reason: { _tag: reasonTag } }) as never; describe("Windows browser lock errors", () => {  it("treats sharing and lock violations reported as Busy as held", () => {    assert.isTrue(isWindowsLockHeldError(platformError("Busy")));  });   it("does not treat access denied as proof of an active lock", () => {    assert.isFalse(isWindowsLockHeldError(platformError("PermissionDenied")));  });   it("does not treat a missing lock file as held", () => {    assert.isFalse(isWindowsLockHeldError(platformError("NotFound")));  });}); /** A scratch home with the source's user-data directory already created. */const withSourceHome = Effect.fnUntraced(function* () {  const fileSystem = yield* FileSystem.FileSystem;  const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-sources-" });  const context = yield* sourcePathContext.pipe(    Effect.provideService(HostProcessEnvironment, { HOME: home }),    Effect.provideService(HostProcessPlatform, "darwin"),  );  yield* fileSystem.makeDirectory(userDataDirectory(context), { recursive: true });  return context;}); /** Every case here runs on darwin, where Helium always resolves a directory. */const userDataDirectory = (context: BrowserImportPathContext) => {  const root = helium.userDataDirectory(context);  if (root === undefined) throw new Error("Helium has no macOS user-data directory");  return root;}; const run = <A, E>(  effect: Effect.Effect<    A,    E,    FileSystem.FileSystem | Path.Path | Scope.Scope | ChildProcessSpawner.ChildProcessSpawner  >,) => effect.pipe(Effect.provide(NodeServices.layer), Effect.scoped); /** Writes a Chromium-shaped cookie table with `count` rows. */const writeCookieDatabase = (file: string, count: number) =>  Effect.sync(() => {    const database = new NodeSqlite.DatabaseSync(file);    database.exec("create table cookies (host_key text, name text)");    const insert = database.prepare("insert into cookies (host_key, name) values (?, ?)");    for (let index = 0; index < count; index += 1) insert.run("example.test", `c${index}`);    database.close();  }); const writeFirefoxCookieDatabase = (  file: string,  defaultContainerCount: number,  containerCount: number,) =>  Effect.sync(() => {    const database = new NodeSqlite.DatabaseSync(file);    database.exec("create table moz_cookies (originAttributes text not null)");    const insert = database.prepare("insert into moz_cookies (originAttributes) values (?)");    for (let index = 0; index < defaultContainerCount; index += 1) insert.run("");    for (let index = 0; index < containerCount; index += 1) insert.run("^userContextId=2");    database.close();  }); describe("Helium on Linux", () => {  it.effect.skipIf(!symlinksSupported)("discovers its profiles and checks the user-data lock", () =>    run(      Effect.gen(function* () {        const fileSystem = yield* FileSystem.FileSystem;        const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-helium-linux-" });        const context = yield* sourcePathContext.pipe(          Effect.provideService(HostProcessEnvironment, { HOME: home }),          Effect.provideService(HostProcessPlatform, "linux"),        );        const root = `${home}/.config/net.imput.helium`;        yield* fileSystem.makeDirectory(`${root}/Default`, { recursive: true });        yield* writeCookieDatabase(`${root}/Default/Cookies`, 3);        yield* fileSystem.writeFileString(          `${root}/Local State`,          '{"profile":{"info_cache":{"Default":{"name":"Personal"}}}}',        );         assert.include(helium.platforms, "linux");        assert.isTrue(yield* isSourceInstalled(helium, context));        assert.deepEqual(yield* listSourceProfiles(helium, context), [          { directory: "Default", name: "Personal", cookieCount: 3 },        ]);        assert.isFalse(yield* isSourceRunning(helium, context));        yield* fileSystem.symlink("foreign-host-4242", `${root}/SingletonLock`);        assert.isTrue(yield* isSourceRunning(helium, context));      }),    ),  );}); describe("Helium on Windows", () => {  it.effect("uses Helium's local app-data profile while other Chromium forks stay disabled", () =>    run(      Effect.gen(function* () {        const context = yield* sourcePathContext.pipe(          Effect.provideService(HostProcessEnvironment, {            USERPROFILE: "C:\\Users\\browser-user",            LOCALAPPDATA: "C:\\Users\\browser-user\\AppData\\Local",          }),          Effect.provideService(HostProcessPlatform, "win32"),        );         assert.include(helium.platforms, "win32");        assert.equal(          helium.userDataDirectory(context),          context.path.join(            "C:\\Users\\browser-user\\AppData\\Local",            "imput",            "Helium",            "User Data",          ),        );        for (const source of BROWSER_IMPORT_SOURCES) {          if (source.engine === "chromium" && source.id !== "helium") {            assert.notInclude(source.platforms, "win32");          }        }      }),    ),  );}); describe("isSourceRunning", () => {  it.effect("uses the held cookie database as Chromium's Windows running signal", () =>    run(      Effect.gen(function* () {        const fileSystem = yield* FileSystem.FileSystem;        const home = yield* fileSystem.makeTempDirectoryScoped({          prefix: "t3code-helium-windows-lock-",        });        const context = yield* sourcePathContext.pipe(          Effect.provideService(HostProcessEnvironment, {            HOME: home,            LOCALAPPDATA: home,          }),          Effect.provideService(HostProcessPlatform, "win32"),        );        const profile = context.path.join(helium.userDataDirectory(context)!, "Default");        const database = context.path.join(profile, "Network", "Cookies");        yield* fileSystem.makeDirectory(context.path.join(profile, "Network"), { recursive: true });        yield* writeCookieDatabase(database, 1);         const probed: string[] = [];        assert.isTrue(          yield* windowsChromiumCookiesAreHeld(helium, context, (path) =>            Effect.sync(() => {              probed.push(path);              return true;            }),          ),        );        assert.deepEqual(probed, [database]);      }),    ),  );   it.effect.skipIf(!symlinksSupported)(    "reads Chromium's dangling SingletonLock symlink as a running browser",    () =>      run(        Effect.gen(function* () {          const fileSystem = yield* FileSystem.FileSystem;          const context = yield* withSourceHome();          assert.isFalse(yield* isSourceRunning(helium, context));           // Chromium points the lock at `<host>-<pid>`, a target that never          // exists on disk. A check that follows the link reports a running          // browser as closed, letting an import read a live, mid-write database.          yield* fileSystem.symlink(            "host-that-does-not-exist-1234",            `${userDataDirectory(context)}/SingletonLock`,          );           assert.isTrue(yield* isSourceRunning(helium, context));        }),      ),  );   it.effect.skipIf(!symlinksSupported)(    "uses the provided hostname to classify Chromium locks",    () =>      run(        Effect.gen(function* () {          const fileSystem = yield* FileSystem.FileSystem;          const paths = yield* withSourceHome();          yield* fileSystem.symlink(            "lock-owner-99999999",            `${helium.userDataDirectory(paths)}/SingletonLock`,          );           assert.isTrue(            yield* isSourceRunning(helium, paths).pipe(              Effect.provideService(HostProcessHostname, "another-host"),            ),          );          assert.isFalse(            yield* isSourceRunning(helium, paths).pipe(              Effect.provideService(HostProcessHostname, "lock-owner"),            ),          );        }),      ),  );}); describe("chromiumSingletonLockIsHeld", () => {  it.effect("ignores a positively dead PID on the current host", () =>    Effect.gen(function* () {      const checked: number[] = [];      const held = yield* chromiumSingletonLockIsHeld("current-host-4321", "current-host", (pid) =>        Effect.sync(() => {          checked.push(pid);          return false;        }),      );      assert.isFalse(held);      assert.deepEqual(checked, [4321]);    }),  );   it.effect("keeps a live PID on the current host", () =>    chromiumSingletonLockIsHeld("current-host-4321", "current-host", () =>      Effect.succeed(true),    ).pipe(Effect.tap((held) => Effect.sync(() => assert.isTrue(held)))),  );   it.effect("keeps foreign-host and malformed targets without probing a PID", () =>    Effect.gen(function* () {      let probes = 0;      const probe = (_pid: number) =>        Effect.sync(() => {          probes += 1;          return false;        });      assert.isTrue(yield* chromiumSingletonLockIsHeld("another-host-4321", "current-host", probe));      assert.isTrue(        yield* chromiumSingletonLockIsHeld("current-host-no-pid", "current-host", probe),      );      assert.isTrue(yield* chromiumSingletonLockIsHeld("current-host-0", "current-host", probe));      assert.strictEqual(probes, 0);    }),  );}); describe("chromiumProcessIsAlive", () => {  it.effect("returns false only when signal 0 reports a missing process", () =>    Effect.gen(function* () {      const missing = Object.assign(new Error("missing"), { code: "ESRCH" });      const denied = Object.assign(new Error("denied"), { code: "EPERM" });      assert.isFalse(        yield* chromiumProcessIsAlive(4321, () => {          throw missing;        }),      );      assert.isTrue(        yield* chromiumProcessIsAlive(4321, () => {          throw denied;        }),      );      assert.isTrue(        yield* chromiumProcessIsAlive(4321, () => {          throw undefined;        }),      );      assert.isTrue(        yield* chromiumProcessIsAlive(4321, () => {          throw "unknown failure";        }),      );      assert.isTrue(        yield* chromiumProcessIsAlive(4321, () => {          throw null;        }),      );      assert.isTrue(yield* chromiumProcessIsAlive(4321, () => true));    }),  );}); describe("isSourceInstalled", () => {  it.effect("ignores a user-data directory that holds no cookie database", () =>    run(      Effect.gen(function* () {        const fileSystem = yield* FileSystem.FileSystem;        const context = yield* withSourceHome();        const root = userDataDirectory(context);         // Installers for native messaging hosts create an empty user-data        // directory for every Chromium fork they know about, so treating the        // directory as evidence lists browsers the user does not have.        yield* fileSystem.makeDirectory(`${root}/NativeMessagingHosts`, { recursive: true });        assert.isFalse(yield* isSourceInstalled(helium, context));         yield* fileSystem.makeDirectory(`${root}/Default`, { recursive: true });        yield* fileSystem.writeFileString(`${root}/Default/Cookies`, "db");        assert.isTrue(yield* isSourceInstalled(helium, context));         // A real install whose cookies live outside `Default` still counts:        // reporting it as absent hides the source from the menu entirely.        yield* fileSystem.remove(`${root}/Default`, { recursive: true });        yield* fileSystem.makeDirectory(`${root}/Profile 1`, { recursive: true });        yield* fileSystem.writeFileString(`${root}/Profile 1/Cookies`, "db");        assert.isTrue(yield* isSourceInstalled(helium, context));         yield* fileSystem.remove(root, { recursive: true });        assert.isFalse(yield* isSourceInstalled(helium, context));      }),    ),  );   it.effect("detects a Chromium 127+ install with cookies under Network/", () =>    run(      Effect.gen(function* () {        const fileSystem = yield* FileSystem.FileSystem;        const context = yield* withSourceHome();        const root = userDataDirectory(context);         yield* fileSystem.makeDirectory(`${root}/Default/Network`, { recursive: true });        yield* fileSystem.writeFileString(`${root}/Default/Network/Cookies`, "db");        assert.isTrue(yield* isSourceInstalled(helium, context));      }),    ),  );   it.effect.skipIf(!symlinksSupported)(    "follows cookie database symlinks when detecting profiles",    () =>      run(        Effect.gen(function* () {          const fileSystem = yield* FileSystem.FileSystem;          const context = yield* withSourceHome();          const root = userDataDirectory(context);          yield* fileSystem.makeDirectory(`${root}/Default`, { recursive: true });          yield* fileSystem.symlink("missing-cookies", `${root}/Default/Cookies`);           assert.deepEqual(yield* listSourceProfiles(helium, context), []);          assert.isFalse(yield* isSourceInstalled(helium, context));           yield* fileSystem.writeFileString(`${root}/Default/missing-cookies`, "db");          assert.deepEqual(yield* listSourceProfiles(helium, context), [            { directory: "Default", name: "Default" },          ]);          assert.isTrue(yield* isSourceInstalled(helium, context));        }),      ),  );}); describe("listSourceProfiles", () => {  it.effect("ignores a profile whose Cookies entry is not a file", () =>    run(      Effect.gen(function* () {        const fileSystem = yield* FileSystem.FileSystem;        const paths = yield* withSourceHome();        const root = helium.userDataDirectory(paths);        // A directory named `Cookies` would list as importable and then fail        // the SQLite open, so only a regular file counts as a database.        yield* fileSystem.makeDirectory(`${root}/Broken/Cookies`, { recursive: true });        yield* fileSystem.makeDirectory(`${root}/Real`, { recursive: true });        yield* fileSystem.writeFileString(`${root}/Real/Cookies`, "db");         assert.deepEqual(yield* listSourceProfiles(helium, paths), [          { directory: "Real", name: "Real" },        ]);        assert.isTrue(yield* isSourceInstalled(helium, paths));      }),    ),  );   it.effect("discovers profiles by their cookie database when Local State is absent", () =>    run(      Effect.gen(function* () {        const fileSystem = yield* FileSystem.FileSystem;        const context = yield* withSourceHome();        const root = userDataDirectory(context);        // Assuming `Default` would report a browser whose cookies live in        // `Profile 1` as having nothing to import, and it is then hidden.        yield* fileSystem.makeDirectory(`${root}/Profile 1`, { recursive: true });        yield* fileSystem.writeFileString(`${root}/Profile 1/Cookies`, "db");        yield* fileSystem.makeDirectory(`${root}/NativeMessagingHosts`, { recursive: true });         assert.deepEqual(yield* listSourceProfiles(helium, context), [          { directory: "Profile 1", name: "Profile 1" },        ]);      }),    ),  );   it.effect("reads the profile names the browser shows", () =>    run(      Effect.gen(function* () {        const fileSystem = yield* FileSystem.FileSystem;        const context = yield* withSourceHome();        yield* fileSystem.writeFileString(          `${userDataDirectory(context)}/Local State`,          `{"profile":{"info_cache":{"Default":{"name":"You"},"Profile 2":{"name":"  "}}}}`,        );         assert.deepEqual(yield* listSourceProfiles(helium, context), [          { directory: "Default", name: "You" },          // Blank display name falls back to the directory rather than          // rendering an empty row.          { directory: "Profile 2", name: "Profile 2" },        ]);      }),    ),  );   it.effect("scans for profiles when Local State is malformed", () =>    run(      Effect.gen(function* () {        const fileSystem = yield* FileSystem.FileSystem;        const context = yield* withSourceHome();        const root = userDataDirectory(context);        yield* fileSystem.writeFileString(`${root}/Local State`, "{not-json");        yield* fileSystem.makeDirectory(`${root}/Default`, { recursive: true });        yield* fileSystem.writeFileString(`${root}/Default/Cookies`, "db");         assert.deepEqual(yield* listSourceProfiles(helium, context), [          { directory: "Default", name: "Default" },        ]);      }),    ),  );   it.effect("reports nothing when no directory holds a cookie database", () =>    run(      Effect.gen(function* () {        const context = yield* withSourceHome();        assert.deepEqual(yield* listSourceProfiles(helium, context), []);      }),

Lines 1–500 of 1198.