mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 18:52:48 +00:00
fix(desktop): ignore relative XDG bases in capture inventory
Desktop capture used XDG_* values without checking that they are absolute, so a relative XDG_CONFIG_HOME (or data, state, cache) moved state-directory, desktop-entry and build-secret lookups under the working directory. The XDG Base Directory Specification says to ignore a relative value; one xdgBase helper now applies that rule and falls back to the home default. Fixes #9702 Squashed from #9705 and rebased onto main. Setup's client registration paths are left unchanged: each client's own handling of a relative XDG_CONFIG_HOME has to be verified before REA writes elsewhere. Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
1 parent
0ffde0cd09
commit
4b5d00bebd
8 files changed
+131
-19
No files matched your search
@@ -35,7 +35,11 @@ rea capture-desktop-application --executable gnucash --argument /home/ada/books.
|
||||
|
||||
Desktop entries are looked up in `$XDG_DATA_HOME/applications` and then each
|
||||
`$XDG_DATA_DIRS` directory; an entry installed as a symbolic link, as flatpak
|
||||
and Nix do, is followed to its file. The entry's `Exec` line is expanded with no files;
|
||||
and Nix do, is followed to its file. An XDG base folder variable counts only
|
||||
when its value is an absolute path, as the XDG Base Directory Specification
|
||||
requires, so a relative value leaves the home default in use instead of
|
||||
naming a folder under the working directory. The entry's `Exec` line is
|
||||
expanded with no files;
|
||||
`--argument` values are appended in its place, and the application runs in the
|
||||
entry's `Path` or the home directory. When the line starts with
|
||||
`env NAME=value`, the program is found on the `PATH` those assignments set,
|
||||
|
||||
@@ -50,11 +50,14 @@ folder to build the environment; this step builds nothing.
|
||||
`flatpak run` launch), otherwise
|
||||
the dpkg owner of the launched desktop entry, or else of the executable,
|
||||
with its version, architecture, the apt sources listing the
|
||||
installed version and the `o=` origin apt reports for each source. The
|
||||
entry's package is the one that installs the application: LibreOffice
|
||||
Writer's entry belongs to `libreoffice-writer`, while the `libreoffice` it
|
||||
runs belongs to `libreoffice-common`, which does not depend on Writer. When
|
||||
the two differ, `executable_package` names the executable's, with
|
||||
installed version and the `o=` origin apt reports for each source. An XDG
|
||||
base folder variable names a folder only when its value is an absolute
|
||||
path, as the XDG Base Directory Specification requires, so a relative value
|
||||
leaves the home default in use instead of naming a folder under the working
|
||||
directory. The entry's package is the one that installs the application:
|
||||
LibreOffice Writer's entry belongs to `libreoffice-writer`, while the
|
||||
`libreoffice` it runs belongs to `libreoffice-common`, which does not
|
||||
depend on Writer. When the two differ, `executable_package` names the executable's, with
|
||||
`executable_package_reason` when neither the Ubuntu 24.04 archive nor a
|
||||
declared vendor repository offers it or apt could not say; each vendor
|
||||
repository records in `supplies` the library packages and executable's
|
||||
|
||||
@@ -2,6 +2,7 @@ import { lstat, readdir } from "node:fs/promises";
|
||||
import { basename, join } from "node:path";
|
||||
|
||||
import type { DesktopApplicationInventory } from "../domain/desktop/desktopCapture.js";
|
||||
import { xdgBase } from "./XdgBase.js";
|
||||
|
||||
/** The environment whose XDG folders, and the home whose dot-folders, may hold the application's state. */
|
||||
interface StateLocations {
|
||||
@@ -64,10 +65,10 @@ export const stateDirectories = async (
|
||||
extra: readonly string[],
|
||||
): Promise<DesktopApplicationInventory["state_directories"]> => {
|
||||
const bases = [
|
||||
environment.XDG_CONFIG_HOME || join(home, ".config"),
|
||||
environment.XDG_DATA_HOME || join(home, ".local/share"),
|
||||
environment.XDG_STATE_HOME || join(home, ".local/state"),
|
||||
environment.XDG_CACHE_HOME || join(home, ".cache"),
|
||||
xdgBase(environment.XDG_CONFIG_HOME, join(home, ".config")),
|
||||
xdgBase(environment.XDG_DATA_HOME, join(home, ".local/share")),
|
||||
xdgBase(environment.XDG_STATE_HOME, join(home, ".local/state")),
|
||||
xdgBase(environment.XDG_CACHE_HOME, join(home, ".cache")),
|
||||
];
|
||||
const variants = new Set(
|
||||
names
|
||||
|
||||
@@ -43,6 +43,11 @@ describe("build secret files", () => {
|
||||
expect(buildSecretsDirectory({ HOME: "/h" })).toBe(
|
||||
"/h/.config/rea/build-secrets",
|
||||
);
|
||||
// An absolute XDG_CONFIG_HOME decides the folder without consulting the
|
||||
// host account, which a Unix UID without a passwd entry cannot answer.
|
||||
expect(buildSecretsDirectory({ XDG_CONFIG_HOME: "/cfg" }, "linux")).toBe(
|
||||
"/cfg/rea/build-secrets",
|
||||
);
|
||||
});
|
||||
|
||||
it("reads a file only its owner can read, without its final line break", async () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
unlink,
|
||||
type FileHandle,
|
||||
} from "node:fs/promises";
|
||||
import { isAbsolute, join } from "node:path";
|
||||
import { join } from "node:path";
|
||||
|
||||
import {
|
||||
ArtifactFileCleanupFailure,
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
readStableFileBytes,
|
||||
} from "../filesystem/readStableFileBytes.js";
|
||||
import { RegularFileChangedError } from "../filesystem/RegularFile.js";
|
||||
import { xdgBase } from "./XdgBase.js";
|
||||
|
||||
/** The most bytes a secret file of at most `characters` may hold: those in UTF-8 and a line break. */
|
||||
const maxFileBytes = (characters: number) => characters * 4 + 2;
|
||||
@@ -44,11 +45,12 @@ export const buildSecretsDirectory = (
|
||||
environment: Readonly<NodeJS.ProcessEnv>,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): string => {
|
||||
const xdg = environment.XDG_CONFIG_HOME;
|
||||
const root =
|
||||
xdg !== undefined && isAbsolute(xdg)
|
||||
? xdg
|
||||
: join(homeDirectoryFromEnvironment(environment, platform), ".config");
|
||||
const root = xdgBase(environment.XDG_CONFIG_HOME, () =>
|
||||
// Resolve the home fallback only when it is the one that applies: on a host
|
||||
// without a passwd entry for the caller it can throw, and an absolute
|
||||
// XDG_CONFIG_HOME leaves it unused.
|
||||
join(homeDirectoryFromEnvironment(environment, platform), ".config"),
|
||||
);
|
||||
return join(root, "rea", "build-secrets");
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
symlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { isAbsolute, join } from "node:path";
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -647,7 +647,10 @@ const inventoryOf = async (
|
||||
{
|
||||
home,
|
||||
environment,
|
||||
}: Pick<Awaited<ReturnType<typeof fixture>>, "home" | "environment">,
|
||||
}: {
|
||||
home: string;
|
||||
environment: Readonly<Record<string, string | undefined>>;
|
||||
},
|
||||
) => {
|
||||
const inspected = await inspectLinuxApplication({
|
||||
desktopEntry,
|
||||
@@ -824,6 +827,76 @@ describe.skipIf(process.platform === "win32")(
|
||||
},
|
||||
);
|
||||
|
||||
describe.skipIf(process.platform === "win32")(
|
||||
"inspectLinuxApplication relative XDG bases",
|
||||
() => {
|
||||
it("ignores a relative XDG base and keeps the home default", async () => {
|
||||
const installed = await fixture();
|
||||
const { home, environment } = installed;
|
||||
const inventory = await inventoryOf("org.example.notes", {
|
||||
...installed,
|
||||
environment: {
|
||||
...environment,
|
||||
XDG_CONFIG_HOME: "relative-config",
|
||||
XDG_DATA_HOME: "relative-data",
|
||||
XDG_STATE_HOME: "relative-state",
|
||||
XDG_CACHE_HOME: "relative-cache",
|
||||
},
|
||||
});
|
||||
|
||||
const paths = inventory.state_directories.map((entry) => entry.path);
|
||||
expect(paths).toContain(join(home, ".config/notes"));
|
||||
expect(paths).toContain(join(home, ".local/share/notes"));
|
||||
expect(paths).toContain(join(home, ".local/state/notes"));
|
||||
expect(paths).toContain(join(home, ".cache/notes"));
|
||||
expect(paths.filter((path) => !isAbsolute(path))).toEqual([]);
|
||||
});
|
||||
|
||||
it("reads a relative XDG base an Exec env prefix sets as unset", async () => {
|
||||
const installed = await fixture();
|
||||
const { home, bin } = installed;
|
||||
await mkdir(join(home, ".config"), { recursive: true });
|
||||
await mkdir(join(home, ".local/share/applications"), {
|
||||
recursive: true,
|
||||
});
|
||||
await writeFile(
|
||||
join(
|
||||
home,
|
||||
".local/share/applications/org.example.env-relative.desktop",
|
||||
),
|
||||
`[Desktop Entry]\nType=Application\nName=Env\nExec=env XDG_CONFIG_HOME=relative-config notes-app\n`,
|
||||
);
|
||||
|
||||
const inventory = await inventoryOf(
|
||||
"org.example.env-relative",
|
||||
installed,
|
||||
);
|
||||
|
||||
expect(inventory.executable.path).toBe(join(bin, "notes-app"));
|
||||
expect(inventory.state_directories.map((entry) => entry.path)).toContain(
|
||||
join(home, ".config/notes-app"),
|
||||
);
|
||||
});
|
||||
|
||||
it("finds a desktop entry by id when XDG_DATA_HOME is relative", async () => {
|
||||
const installed = await fixture();
|
||||
const { home, environment } = installed;
|
||||
await mkdir(join(home, ".local/share/applications"), { recursive: true });
|
||||
await writeFile(
|
||||
join(home, ".local/share/applications/org.example.home-notes.desktop"),
|
||||
`[Desktop Entry]\nType=Application\nName=HomeNotes\nExec=notes-app\n`,
|
||||
);
|
||||
|
||||
const inventory = await inventoryOf("org.example.home-notes", {
|
||||
...installed,
|
||||
environment: { ...environment, XDG_DATA_HOME: "relative-data" },
|
||||
});
|
||||
|
||||
expect(inventory.desktop_entry).toMatchObject({ name: "HomeNotes" });
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === "win32").each([
|
||||
{ stage: "desktop entry", closeAt: 1, oversized: false, cancel: false },
|
||||
{
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from "../filesystem/readStableFileBytes.js";
|
||||
import { stateDirectories, stateNames } from "./ApplicationStateDirectories.js";
|
||||
import { APT_LOCATIONS } from "./AptRepositoryInventory.js";
|
||||
import { xdgBase } from "./XdgBase.js";
|
||||
import { inspectPackageFile } from "./DebianPackageFile.js";
|
||||
import {
|
||||
debianPackage,
|
||||
@@ -96,7 +97,10 @@ const desktopEntryDirectories = (
|
||||
environment: LinuxApplicationRequest["environment"],
|
||||
home: string,
|
||||
): readonly string[] => {
|
||||
const dataHome = environment.XDG_DATA_HOME || join(home, ".local/share");
|
||||
const dataHome = xdgBase(
|
||||
environment.XDG_DATA_HOME,
|
||||
join(home, ".local/share"),
|
||||
);
|
||||
const dataDirectories = (
|
||||
environment.XDG_DATA_DIRS || "/usr/local/share:/usr/share"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { isAbsolute } from "node:path";
|
||||
|
||||
/**
|
||||
* An XDG base folder: the variable's value when it is an absolute path, and
|
||||
* the home default otherwise. The XDG Base Directory Specification defines
|
||||
* these variables as absolute paths and says to ignore a relative value, so a
|
||||
* relative one must not redirect a lookup under the working directory.
|
||||
*
|
||||
* The fallback may be a thunk so a caller whose default needs the host account
|
||||
* never pays for that lookup when the variable already decides the folder.
|
||||
*/
|
||||
export const xdgBase = (
|
||||
value: string | undefined,
|
||||
fallback: string | (() => string),
|
||||
): string =>
|
||||
value !== undefined && isAbsolute(value)
|
||||
? value
|
||||
: typeof fallback === "function"
|
||||
? fallback()
|
||||
: fallback;
|
||||
Reference in new issue
Block a user