Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
200 changes: 200 additions & 0 deletions apps/desktop/src/preview/BrowserImport/Sources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,206 @@ 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(HostProcess.Environment, { HOME: "/Users/zen-user" }),
Effect.provideService(HostProcess.Platform, "darwin"),
);
const windowsContext = yield* sourcePathContext.pipe(
Effect.provideService(HostProcess.Environment, {
HOME: "C:\\Users\\zen-user",
APPDATA: "C:\\Users\\zen-user\\AppData\\Roaming",
}),
Effect.provideService(HostProcess.Platform, "win32"),
);
const linuxContext = yield* sourcePathContext.pipe(
Effect.provideService(HostProcess.Environment, { HOME: "/home/zen-user" }),
Effect.provideService(HostProcess.Platform, "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(HostProcess.Environment, { HOME: home }),
Effect.provideService(HostProcess.Platform, "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(HostProcess.Environment, { HOME: home }),
Effect.provideService(HostProcess.Platform, "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));
assert.equal(
yield* resolveCookieDatabase(zen, context, legacyProfile),
context.path.join(legacyProfile, "cookies.sqlite"),
);

yield* fileSystem.writeFileString(
context.path.join(currentRoot, "profiles.ini"),
`[Profile0]\nName=Shared\nIsRelative=0\nPath=${legacyProfile}\n`,
);
assert.deepEqual(yield* listSourceProfiles(zen, context), [
{ directory: legacyProfile, name: "Shared", cookieCount: 1 },
]);

yield* fileSystem.remove(currentRoot, { recursive: true });
assert.isTrue(yield* isSourceInstalled(zen, context));
assert.deepEqual(yield* listSourceProfiles(zen, context), [
{ directory: legacyProfile, name: "Legacy", cookieCount: 1 },
]);
}),
),
);

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(HostProcess.Environment, { HOME: home }),
Effect.provideService(HostProcess.Platform, "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)(
"finds Snap profiles with or without profiles.ini and checks their locks",
Expand Down
25 changes: 20 additions & 5 deletions apps/desktop/src/preview/BrowserImport/Sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface BrowserImportSourceDefinition {
readonly engine: BrowserImportEngine;
readonly platforms: ReadonlyArray<NodeJS.Platform>;
readonly userDataDirectory: (context: BrowserImportPathContext) => string | undefined;
readonly alternateLinuxRoots?: (context: BrowserImportPathContext) => ReadonlyArray<string>;
readonly keychainService?: string;
readonly keychainAccount?: string;
readonly linuxSecretApplication?: string;
Expand Down Expand Up @@ -169,6 +170,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");
},
alternateLinuxRoots: (context) => [context.path.join(context.home, ".zen")],
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
},
];

Expand Down Expand Up @@ -470,7 +488,7 @@ const listSourceProfilesInDirectory = Effect.fnUntraced(function* (
);
});

/** Keep Snap profile paths absolute so reads and locks distinguish native and Snap installations. */
/** Keep alternate-root profile paths absolute so cookie reads and locks use the correct installation. */
export const listSourceProfiles = Effect.fn("BrowserImportSources.listSourceProfiles")(function* (
definition: BrowserImportSourceDefinition,
context: BrowserImportPathContext,
Expand All @@ -481,10 +499,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
2 changes: 1 addition & 1 deletion docs/user/browser-import.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ System Settings permission list, and turn access on. **Continue** becomes availa
is detected. macOS may require you to quit and reopen T3 Code before the grant applies; reopen
the import wizard afterward. You can revoke Full Disk Access once the import is done.

On Windows, import supports Firefox and Helium profiles that use standard profile encryption.
On Windows, import supports Firefox, Zen, and Helium profiles that use standard profile encryption.
Other Chromium-based browsers use app-bound encryption and cannot be imported. Partitioned cookies
are skipped on all platforms.
1 change: 1 addition & 0 deletions packages/contracts/src/browserImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const BROWSER_IMPORT_SOURCE_IDS = [
"arc",
"helium",
"firefox",
"zen",
"safari",
] as const;

Expand Down
Loading