Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions apps/desktop/src/preview/BrowserImport/Sources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ describe("Linux Chromium secret applications", () => {
opera: "opera",
helium: "chromium",
firefox: undefined,
zen: undefined,
},
);
});
Expand Down Expand Up @@ -672,6 +673,187 @@ describe("cookieDatabaseCandidatePaths", () => {
});

const firefox = BROWSER_IMPORT_SOURCES.find((source) => source.id === "firefox")!;
const zen = BROWSER_IMPORT_SOURCES.find((source) => source.id === "zen")!;

describe("Zen", () => {
it.effect("resolves Zen's native user-data roots on every supported platform", () =>
run(
Effect.gen(function* () {
const darwinContext = yield* sourcePathContext.pipe(
Effect.provideService(HostProcessEnvironment, { HOME: "/Users/zen-user" }),
Effect.provideService(HostProcessPlatform, "darwin"),
);
const windowsContext = yield* sourcePathContext.pipe(
Effect.provideService(HostProcessEnvironment, {
HOME: "C:\\Users\\zen-user",
APPDATA: "C:\\Users\\zen-user\\AppData\\Roaming",
}),
Effect.provideService(HostProcessPlatform, "win32"),
);
const linuxContext = yield* sourcePathContext.pipe(
Effect.provideService(HostProcessEnvironment, { HOME: "/home/zen-user" }),
Effect.provideService(HostProcessPlatform, "linux"),
);

assert.equal(zen.name, "Zen");
assert.equal(zen.engine, "firefox");
assert.deepEqual([...zen.platforms], ["darwin", "win32", "linux"]);
assert.equal(
zen.userDataDirectory(darwinContext),
darwinContext.path.join("/Users/zen-user", "Library", "Application Support", "zen"),
);
assert.equal(
zen.userDataDirectory(windowsContext),
windowsContext.path.join("C:\\Users\\zen-user\\AppData\\Roaming", "zen"),
);
assert.equal(
zen.userDataDirectory(linuxContext),
linuxContext.path.join("/home/zen-user", ".config", "zen"),
);
assert.deepEqual(zen.alternateLinuxRoots?.(linuxContext), [
linuxContext.path.join("/home/zen-user", ".zen"),
]);
}),
),
);

it.effect("discovers Zen profiles through its Firefox-compatible metadata", () =>
run(
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-zen-" });
const context = yield* sourcePathContext.pipe(
Effect.provideService(HostProcessEnvironment, { HOME: home }),
Effect.provideService(HostProcessPlatform, "darwin"),
);
const root = zen.userDataDirectory(context)!;
yield* fileSystem.makeDirectory(root, { recursive: true });
const profile = context.path.join(root, "Profiles", "zen.default");
yield* fileSystem.makeDirectory(profile, { recursive: true });
yield* writeFirefoxCookieDatabase(context.path.join(profile, "cookies.sqlite"), 2, 1);
yield* fileSystem.makeDirectory(context.path.join(root, "Profiles", "empty.default"), {
recursive: true,
});
yield* fileSystem.writeFileString(
context.path.join(root, "profiles.ini"),
[
"[Profile0]",
"Name=Zen",
"IsRelative=1",
"Path=Profiles/zen.default",
"ZenAvatarPath=chrome://browser/content/zen-avatars/avatar-55.svg",
"Default=1",
"",
"[Profile1]",
"Name=Empty",
"IsRelative=1",
"Path=Profiles/empty.default",
"",
].join("\n"),
);

assert.deepEqual(yield* listSourceProfiles(zen, context), [
{
directory: context.path.join("Profiles", "zen.default"),
name: "Zen",
cookieCount: 2,
},
]);
assert.isTrue(yield* isSourceInstalled(zen, context));
assert.isFalse(yield* isSourceRunning(zen, context));
assert.equal(
yield* resolveCookieDatabase(zen, context, context.path.join("Profiles", "zen.default")),
context.path.join(profile, "cookies.sqlite"),
);
}),
),
);

it.effect("discovers Zen profiles in both current and legacy Linux roots", () =>
run(
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-zen-linux-" });
const context = yield* sourcePathContext.pipe(
Effect.provideService(HostProcessEnvironment, { HOME: home }),
Effect.provideService(HostProcessPlatform, "linux"),
);
const currentRoot = context.path.join(home, ".config", "zen");
const legacyRoot = context.path.join(home, ".zen");
const currentProfile = context.path.join(currentRoot, "current.default");
const legacyProfile = context.path.join(legacyRoot, "legacy.default");

for (const [root, profile, name] of [
[currentRoot, currentProfile, "Current"],
[legacyRoot, legacyProfile, "Legacy"],
] as const) {
yield* fileSystem.makeDirectory(profile, { recursive: true });
yield* writeFirefoxCookieDatabase(context.path.join(profile, "cookies.sqlite"), 1, 0);
yield* fileSystem.writeFileString(
context.path.join(root, "profiles.ini"),
[
"[Profile0]",
`Name=${name}`,
"IsRelative=1",
`Path=${context.path.basename(profile)}`,
].join("\n"),
);
}

assert.deepEqual(yield* listSourceProfiles(zen, context), [
{ directory: "current.default", name: "Current", cookieCount: 1 },
{ directory: legacyProfile, name: "Legacy", cookieCount: 1 },
]);
assert.isTrue(yield* isSourceInstalled(zen, context));
}),
),
);

it.effect("does not mistake Firefox Snap installs for Zen profiles", () =>
run(
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-zen-snap-" });
const context = yield* sourcePathContext.pipe(
Effect.provideService(HostProcessEnvironment, { HOME: home }),
Effect.provideService(HostProcessPlatform, "linux"),
);
const zenRoot = context.path.join(home, ".config", "zen");
const snapRoot = context.path.join(
home,
"snap",
"firefox",
"common",
".mozilla",
"firefox",
);
const zenProfile = context.path.join(zenRoot, "zen.default");
const snapProfile = context.path.join(snapRoot, "snap.default");
for (const root of [zenRoot, snapRoot]) {
const profile = root === zenRoot ? zenProfile : snapProfile;
yield* fileSystem.makeDirectory(profile, { recursive: true });
yield* writeFirefoxCookieDatabase(context.path.join(profile, "cookies.sqlite"), 1, 0);
yield* fileSystem.writeFileString(
context.path.join(root, "profiles.ini"),
[
"[Profile0]",
"Name=Personal",
"IsRelative=1",
`Path=${profile === zenProfile ? "zen.default" : "snap.default"}`,
].join("\n"),
);
}

const profiles = yield* listSourceProfiles(zen, context);
assert.deepEqual(
profiles.map((profile) => profile.directory),
["zen.default"],
);
assert.isTrue(yield* isSourceInstalled(zen, context));
}),
),
);
});

describe("Firefox Snap profiles", () => {
it.effect.skipIf(!symlinksSupported)(
Expand Down
30 changes: 23 additions & 7 deletions apps/desktop/src/preview/BrowserImport/Sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export interface BrowserImportSourceDefinition {
/** Platforms the definition has paths for. */
readonly platforms: ReadonlyArray<NodeJS.Platform>;
readonly userDataDirectory: (context: BrowserImportPathContext) => string | undefined;
/** Parallel Linux roots for package variants or legacy layouts. */
readonly alternateLinuxRoots?: (context: BrowserImportPathContext) => ReadonlyArray<string>;
/** Chromium on macOS only: where the OSCrypt key lives in the keychain. */
readonly keychainService?: string;
readonly keychainAccount?: string;
Expand Down Expand Up @@ -212,6 +214,23 @@ export const BROWSER_IMPORT_SOURCES: ReadonlyArray<BrowserImportSourceDefinition
}
return context.path.join(context.home, ".mozilla", "firefox");
},
alternateLinuxRoots: (context) => [
context.path.join(context.home, "snap", "firefox", "common", ".mozilla", "firefox"),
],
},
{
id: "zen",
name: "Zen",
engine: "firefox",
platforms: ["darwin", "win32", "linux"],
userDataDirectory: (context) => {
if (context.platform === "darwin") return macApplicationSupport(context, "zen");
if (context.platform === "win32") {
return context.appData ? context.path.join(context.appData, "zen") : undefined;
}
return context.path.join(context.home, ".config", "zen");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
alternateLinuxRoots: (context) => [context.path.join(context.home, ".zen")],
},
];

Expand Down Expand Up @@ -571,9 +590,9 @@ const listSourceProfilesInDirectory = Effect.fnUntraced(function* (
});

/**
* Include Firefox's Snap home alongside its native home. Snap profiles use
* absolute directories so cookie reads and lock checks keep pointing at the
* installation they came from, even when both installs use the same name.
* Include alternate Linux homes alongside the native home. Alternate profiles
* use absolute directories so cookie reads and lock checks keep pointing at
* the installation they came from, even when both installs use the same name.
*/
export const listSourceProfiles = Effect.fn("BrowserImportSources.listSourceProfiles")(function* (
definition: BrowserImportSourceDefinition,
Expand All @@ -585,10 +604,7 @@ export const listSourceProfiles = Effect.fn("BrowserImportSources.listSourceProf

const root = definition.userDataDirectory(context);
if (root === undefined) return [];
const roots = [
root,
context.path.join(context.home, "snap", "firefox", "common", ".mozilla", "firefox"),
];
const roots = [root, ...(definition.alternateLinuxRoots?.(context) ?? [])];
const profiles = new Map<string, BrowserImportSourceProfile>();
for (const directory of roots) {
const found = yield* listSourceProfilesInDirectory(
Expand Down
1 change: 1 addition & 0 deletions packages/contracts/src/browserImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const BROWSER_IMPORT_SOURCE_IDS = [
"arc",
"helium",
"firefox",
"zen",
"safari",
] as const;

Expand Down
Loading