Skip to content

Fix cursor icon search paths to follow XDG Icon Theme Specification#4731

Open
Copilot wants to merge 11 commits intomainfrom
copilot/fix-cursor-theming-issues
Open

Fix cursor icon search paths to follow XDG Icon Theme Specification#4731
Copilot wants to merge 11 commits intomainfrom
copilot/fix-cursor-theming-issues

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 27, 2026

Fixes cursor icon search paths in src/miral/xcursor.c to comply with the XDG Icon Theme Specification. Previously, Mir used a hardcoded XCURSORPATH that omitted the XDG-specified directories. This introduces a dynamic path builder that constructs the correct XDG-spec-compliant search order at runtime.

What's new?

  • Added _XcursorBuildXdgPath() that dynamically constructs the cursor search path per the XDG Icon Theme spec in the following order:
    1. $HOME/.icons
    2. $XDG_DATA_HOME/icons (defaults to $HOME/.local/share/icons when XDG_DATA_HOME is unset or empty)
    3. $HOME/.cursors
    4. $XDG_DATA_DIRS/icons entries (defaults to /usr/local/share:/usr/share)
    5. /usr/share/pixmaps
    6. Legacy-only XCURSORLEGACYPATH entries (/usr/share/cursors/xorg-x11, ICONDIR)
  • XcursorLibraryPath() uses XCURSOR_PATH env var when set, otherwise calls _XcursorBuildXdgPath(), with XCURSORPATH macro as last-resort fallback
  • Empty components in XDG_DATA_DIRS (e.g. leading/trailing : or ::) are skipped to avoid bogus /icons entries
  • component_len > PATH_MAX guard aborts with a message — an oversized component in XDG_DATA_DIRS is treated as a fatal misconfiguration
  • Introduced _xcursor_path_append() realloc-based helper that grows the path buffer on demand — eliminates the duplicated pre-calculation phase, building the path in a single forward pass
  • Thread-safe lazy initialisation via pthread_once
  • No duplicate entries: XCURSORLEGACYPATH excludes paths already covered by the XDG prefix entries

How to test

  • Install a cursor theme under ~/.local/share/icons/<theme-name>/cursors/ and verify it is picked up by a Mir-based compositor
  • Set XDG_DATA_HOME to a custom path, install a cursor theme under $XDG_DATA_HOME/icons/, and verify it is found
  • Set XCURSOR_PATH to override the path entirely and verify the override is respected
  • Verify that an unset or empty XDG_DATA_DIRS falls back to /usr/local/share:/usr/share

Checklist

  • Tests added and pass
  • Adequate documentation added
  • (optional) Added Screenshots or videos

Copilot AI changed the title [WIP] Fix cursor theming by updating search paths Fix cursor icon search paths to follow XDG Icon Theme Specification Feb 27, 2026
@AlanGriffiths
Copy link
Copy Markdown
Contributor

@copilot it is poor style to declare variables before they can be initialized

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 2, 2026

@copilot it is poor style to declare variables before they can be initialized

Fixed in bc8d61c — variables are now declared at the point where they can be initialized, using for-init declarations for the loop variables and int const written declared inside each block.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes cursor icon search paths in src/miral/xcursor.c to comply with the XDG Icon Theme Specification. Previously, Mir used a hardcoded XCURSORPATH that omitted $XDG_DATA_DIRS/icons. The fix introduces a dynamic path builder that constructs the correct XDG-spec-compliant search order at runtime, falling back to the legacy path.

Changes:

  • Added _XcursorBuildXdgPath() that dynamically constructs the cursor search path per the XDG Icon Theme spec: $HOME/.icons, $XDG_DATA_DIRS/icons (expanded), /usr/share/pixmaps, then legacy XCURSORPATH entries
  • XcursorLibraryPath() updated to call _XcursorBuildXdgPath() when XCURSOR_PATH env var is not set, with XCURSORPATH macro as last resort fallback

Comment thread src/miral/xcursor.c Outdated
Copilot AI and others added 3 commits March 23, 2026 15:26
Co-authored-by: AlanGriffiths <9048879+AlanGriffiths@users.noreply.github.com>
…lization

Co-authored-by: AlanGriffiths <9048879+AlanGriffiths@users.noreply.github.com>
…nly path

Co-authored-by: AlanGriffiths <9048879+AlanGriffiths@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comment thread src/miral/xcursor.cpp Outdated
Comment thread src/miral/xcursor.cpp Outdated
Comment on lines +659 to +666
for (char const *p = xdg_data_dirs; p && *p; )
{
char const *colon = strchr (p, ':');
int const written = colon
? snprintf (p_out, remaining, "%.*s/icons:", (int)(colon - p), p)
: snprintf (p_out, remaining, "%s/icons:", p);
p = colon ? colon + 1 : NULL;
if (written < 0 || (size_t)written >= remaining) goto error;
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When XDG_DATA_DIRS contains empty components (e.g. "::" or a leading ':'), this loop will emit "/icons:" because (colon - p) can be 0. That adds a bogus search directory. Consider skipping empty components (advance past ':' and continue) before calling snprintf, and apply the same logic in the length-calculation loop above.

Copilot uses AI. Check for mistakes.
Comment thread src/miral/xcursor.c Outdated
Co-authored-by: AlanGriffiths <9048879+AlanGriffiths@users.noreply.github.com>
Agent-Logs-Url: https://github.com/canonical/mir/sessions/e1039739-4f60-4451-ac58-bd396023af5d
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 23, 2026

@copilot it is poor style to declare variables before they can be initialized

The remaining occurrence (static const char *path; in XcursorLibraryPath) is resolved in d06a619 — the function now uses pthread_once with two statics both initialized at their declaration (PTHREAD_ONCE_INIT and NULL).

Copilot AI requested a review from AlanGriffiths March 23, 2026 16:42
@AlanGriffiths AlanGriffiths marked this pull request as ready for review March 23, 2026 16:56
@AlanGriffiths AlanGriffiths requested a review from a team as a code owner March 23, 2026 16:56
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment thread src/miral/xcursor.c Outdated
Comment on lines +621 to +631
char const *xdg_data_dirs = getenv ("XDG_DATA_DIRS");

if (!xdg_data_dirs || xdg_data_dirs[0] == '\0')
xdg_data_dirs = "/usr/local/share:/usr/share";

size_t len = 0;

/* $HOME/.icons: and $HOME/.cursors: */
if (home)
len += strlen (home) + strlen ("/.icons") + 1 /* +1 for ':' */
+ strlen (home) + strlen ("/.cursors") + 1;
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_XcursorBuildXdgPath() builds an XDG-based icon search path from $HOME and $XDG_DATA_DIRS, but it never includes $XDG_DATA_HOME/icons (defaulting to $HOME/.local/share/icons). The XDG Icon Theme search order includes that directory, so cursor themes installed under ~/.local/share/icons won’t be discovered with the generated path. Consider adding an $XDG_DATA_HOME/icons entry (with the correct default when XDG_DATA_HOME is unset) between $HOME/.icons and $XDG_DATA_DIRS/icons in both the length calculation and snprintf construction.

Suggested change
char const *xdg_data_dirs = getenv ("XDG_DATA_DIRS");
if (!xdg_data_dirs || xdg_data_dirs[0] == '\0')
xdg_data_dirs = "/usr/local/share:/usr/share";
size_t len = 0;
/* $HOME/.icons: and $HOME/.cursors: */
if (home)
len += strlen (home) + strlen ("/.icons") + 1 /* +1 for ':' */
+ strlen (home) + strlen ("/.cursors") + 1;
char const *xdg_data_home = getenv ("XDG_DATA_HOME");
char const *xdg_data_dirs = getenv ("XDG_DATA_DIRS");
char *default_xdg_data_home = NULL;
if ((!xdg_data_home || xdg_data_home[0] == '\0') && home)
{
size_t const default_xdg_data_home_len = strlen (home) + strlen ("/.local/share") + 1;
default_xdg_data_home = malloc (default_xdg_data_home_len);
if (!default_xdg_data_home)
return NULL;
snprintf (default_xdg_data_home, default_xdg_data_home_len, "%s/.local/share", home);
xdg_data_home = default_xdg_data_home;
}
if (!xdg_data_dirs || xdg_data_dirs[0] == '\0')
xdg_data_dirs = "/usr/local/share:/usr/share";
size_t len = 0;
/* $HOME/.icons:, $XDG_DATA_HOME/icons:, and $HOME/.cursors: */
if (home)
len += strlen (home) + strlen ("/.icons") + 1; /* +1 for ':' */
if (xdg_data_home)
len += strlen (xdg_data_home) + strlen ("/icons") + 1;
if (home)
len += strlen (home) + strlen ("/.cursors") + 1;

Copilot uses AI. Check for mistakes.
@AlanGriffiths
Copy link
Copy Markdown
Contributor

@copilot apply changes based on the comments in this thread

Also, the commit body seems to have lost the overall purpose of this PR and just contains the latest change

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 17, 2026

@copilot apply changes based on the comments in this thread

Also, the commit body seems to have lost the overall purpose o...

Done in 3e99acf. _XcursorBuildXdgPath now includes $XDG_DATA_HOME/icons (defaulting to $HOME/.local/share/icons when XDG_DATA_HOME is unset/empty), positioned between $HOME/.icons and $HOME/.cursors in the search order. The PR description has also been updated to reflect the full purpose of the PR.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment thread src/miral/xcursor.c
Comment on lines +709 to +712
{
fprintf (stderr, "xcursor: XDG_DATA_DIRS component length exceeds INT_MAX\n");
abort ();
}
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling fprintf(stderr, ...) + abort() when an XDG_DATA_DIRS component exceeds INT_MAX makes the compositor/library abort based on untrusted environment input (user-controllable denial-of-service). Prefer handling this case gracefully (e.g., treat it as a build-path failure and return NULL so XcursorLibraryPath() falls back to XCURSORPATH, or skip the offending component) rather than terminating the process.

Suggested change
{
fprintf (stderr, "xcursor: XDG_DATA_DIRS component length exceeds INT_MAX\n");
abort ();
}
goto error;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@mattkae mattkae left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good 👍

@AlanGriffiths AlanGriffiths enabled auto-merge April 17, 2026 16:23
Copy link
Copy Markdown
Contributor

@RAOF RAOF left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The INT_MAX -> PATH_MAX change is an easy one.

I think it'd be good to factor out the appending code, but I won't block on that.

Comment thread src/miral/xcursor.c Outdated
p = colon ? colon + 1 : NULL;
if (component_len == 0)
continue;
if (component_len > INT_MAX)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know why this is comparing against INT_MAX; being paranoid here is maybe reasonable, but the thing to be comparing against is PATH_MAX.

(I know POSIX doesn't mandate that PATH_MAX exists; it does on Linux and I don't think we care about portability to GNU Hurd)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to PATH_MAX in 4db18aa. Oversized components are now skipped with a fprintf(stderr, ...) warning rather than aborting.

Comment thread src/miral/xcursor.c Outdated
int const written = snprintf (p_out, remaining, "%s/.icons:", home);
if (written < 0 || (size_t)written >= remaining) goto error;
p_out += written;
remaining -= (size_t)written;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is crying out to be factored into a function char* format_into_buffer(char* start, char const* end, char const* format, ...) that takes the insertion point, (one past) the end of the buffer, the format string and formatting arguments and returns a pointer to the null byte written or NULL on error (including insufficient buffer space).

Then all these become

if (!(p_out = format_into_buffer(p_out, end, "%s/icons:", xdg_data_home)))
   goto error;

And, actually, since this is only called once at startup, we could make this dramatically simpler by just repeatedly reallocing and having the function be char* append_into_buffer(char* buffer, char* end, char const* format, ...). That way we don't have this duplicated, somewhat gnarly code that also needs to be kept in sync with the length calculation above.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 4db18aa. Introduced _xcursor_path_append(char **path_ptr, char const *str) which realloc-grows the buffer and appends str. The duplicated length-calculation loop is gone — _XcursorBuildXdgPath now just calls _xcursor_path_append for each entry in a single forward pass.

@AlanGriffiths
Copy link
Copy Markdown
Contributor

@copilot please address the concerns raised in this thread: #4731 (review)

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 20, 2026

@copilot please address the concerns raised in this thread: #4731 (review)

Done in 4db18aa. Both concerns from the review are addressed:

  1. INT_MAXPATH_MAX: the component guard now compares against PATH_MAX, and an oversized component is skipped with a warning rather than aborting (graceful degradation).
  2. Factored out appending: introduced _xcursor_path_append(), a small realloc-based helper that grows the path buffer on demand. This eliminates the duplicated pre-calculation loop entirely — the function now simply builds the path in a single forward pass.

@AlanGriffiths
Copy link
Copy Markdown
Contributor

@copilot the changes to error handling introduced in the last commit are unwanted: if the configuration is incorrect we prefer not to start (i.e. abort) rather than carry on in an unexpected configuration

Agent-Logs-Url: https://github.com/canonical/mir/sessions/fa656584-a756-4791-9083-8f937c694fac

Co-authored-by: AlanGriffiths <9048879+AlanGriffiths@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 20, 2026

@copilot the changes to error handling introduced in the last commit are unwanted: if the configuration is incorrect we prefer not to start (i.e. abort) rather than carry on in an unexpected configura...

Fixed in 1bf5ee4. The PATH_MAX guard now calls abort() again — an oversized XDG_DATA_DIRS component is treated as a fatal misconfiguration.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment thread src/miral/xcursor.c
Comment on lines +659 to +672
/* Temporary buffer for formatted entries; PATH_MAX covers any valid path
* component and "/icons:" suffix. */
char buf[PATH_MAX + 8];

if (home)
{
snprintf (buf, sizeof (buf), "%s/.icons:", home);
if (!_xcursor_path_append (&path, buf)) goto done;
}

if (xdg_data_home)
{
snprintf (buf, sizeof (buf), "%s/icons:", xdg_data_home);
if (!_xcursor_path_append (&path, buf)) goto done;
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

snprintf() return values are ignored when formatting entries into the fixed-size buf. If $HOME or $XDG_DATA_HOME is long enough to truncate the formatted string, a truncated (and likely invalid) path element will be appended silently, changing the search path in unexpected ways. Consider either (a) checking snprintf() for truncation and treating it as an error (skip/fallback), or (b) avoiding the fixed buffer by appending pieces directly with the realloc-growing helper.

Copilot uses AI. Check for mistakes.
Comment thread src/miral/xcursor.c
Comment on lines +691 to +692
fprintf (stderr, "xcursor: XDG_DATA_DIRS component length exceeds PATH_MAX\n");
abort ();
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XDG_DATA_DIRS with an oversized component currently triggers fprintf(...); abort();. Because this value is user-controlled environment input, this introduces a new hard-crash path that can take down a Mir-based compositor just by setting an invalid env var. Consider handling this more defensively (e.g. skip the oversized component and continue, or fall back to the default XDG dirs) rather than aborting the process.

Suggested change
fprintf (stderr, "xcursor: XDG_DATA_DIRS component length exceeds PATH_MAX\n");
abort ();
fprintf (stderr, "xcursor: ignoring XDG_DATA_DIRS component length exceeding PATH_MAX\n");
continue;

Copilot uses AI. Check for mistakes.
@AlanGriffiths AlanGriffiths requested a review from RAOF April 21, 2026 13:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants