Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package libinput for openSUSE:Factory 
checked in at 2026-06-05 14:56:01
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/libinput (Old)
 and      /work/SRC/openSUSE:Factory/.libinput.new.2375 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "libinput"

Fri Jun  5 14:56:01 2026 rev:132 rq:1357044 version:1.31.3

Changes:
--------
--- /work/SRC/openSUSE:Factory/libinput/libinput.changes        2026-05-16 
19:23:46.692203032 +0200
+++ /work/SRC/openSUSE:Factory/.libinput.new.2375/libinput.changes      
2026-06-05 14:56:02.209727500 +0200
@@ -1,0 +2,14 @@
+Thu Jun  4 01:55:06 UTC 2026 - Jan Engelhardt <[email protected]>
+
+- Update to release 1.31.3
+  * libinput-device-group now sanitizes the PHYS value which
+    prevents local privilege escalation through udev property
+    injection.
+  * `libinput record`: the --autorestart interval handling was
+    broken for intervals 5s and higher
+  * `libinput recor`: added a convenience fix for running with
+    --autorestart.
+  * Eraser buttons can now be mapped to any button (previously only
+    BTN_STYLUS, BTN_STYULUS2 and BTN_STYLUS3 were permitted).
+
+-------------------------------------------------------------------

Old:
----
  libinput-1.31.2.tar.gz

New:
----
  libinput-1.31.3.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ libinput.spec ++++++
--- /var/tmp/diff_new_pack.BDX1g2/_old  2026-06-05 14:56:03.305772828 +0200
+++ /var/tmp/diff_new_pack.BDX1g2/_new  2026-06-05 14:56:03.309772993 +0200
@@ -37,7 +37,7 @@
 %define lname  libinput10
 %define pname  libinput
 Name:           libinput%{?xsuffix}
-Version:        1.31.2
+Version:        1.31.3
 Release:        0
 Summary:        Input device and event processing library
 License:        MIT

++++++ _scmsync.obsinfo ++++++
--- /var/tmp/diff_new_pack.BDX1g2/_old  2026-06-05 14:56:03.361775144 +0200
+++ /var/tmp/diff_new_pack.BDX1g2/_new  2026-06-05 14:56:03.365775309 +0200
@@ -1,5 +1,5 @@
-mtime: 1778803125
-commit: 35d2d9dc284600b2191c7ce44233899d3564df78e84a2dee5caf6ab7c6b50bbb
+mtime: 1780538190
+commit: d7355c8f4dc61a1e7d37c0b5ac864194a4eff5025e4d9cd5464991bc9805ee7b
 url: https://src.opensuse.org/jengelh/libinput
 revision: master
 

++++++ build.specials.obscpio ++++++

++++++ build.specials.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/.gitignore new/.gitignore
--- old/.gitignore      1970-01-01 01:00:00.000000000 +0100
+++ new/.gitignore      2026-06-04 03:56:30.000000000 +0200
@@ -0,0 +1 @@
+.osc

++++++ libinput-1.31.2.tar.gz -> libinput-1.31.3.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libinput-1.31.2/meson.build 
new/libinput-1.31.3/meson.build
--- old/libinput-1.31.2/meson.build     2026-05-14 12:56:17.000000000 +0200
+++ new/libinput-1.31.3/meson.build     2026-06-04 03:05:01.000000000 +0200
@@ -1,5 +1,5 @@
 project('libinput', 'c',
-       version : '1.31.2',
+       version : '1.31.3',
        license : 'MIT/Expat',
        default_options : [ 'c_std=gnu99', 'warning_level=2' ],
        meson_version : '>= 0.64.0')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libinput-1.31.2/src/evdev-tablet-pad.c 
new/libinput-1.31.3/src/evdev-tablet-pad.c
--- old/libinput-1.31.2/src/evdev-tablet-pad.c  2026-05-14 12:56:17.000000000 
+0200
+++ new/libinput-1.31.3/src/evdev-tablet-pad.c  2026-06-04 03:05:01.000000000 
+0200
@@ -237,6 +237,9 @@
 static inline double
 normalize_wacom_strip(const struct input_absinfo *absinfo)
 {
+       if (absinfo->maximum <= 1 || absinfo->value <= 0)
+               return 0.0;
+
        /* strip axes don't use a proper value, they just shift the bit left
         * for each position. 0 isn't a real value either, it's only sent on
         * finger release */
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libinput-1.31.2/src/evdev-tablet.c 
new/libinput-1.31.3/src/evdev-tablet.c
--- old/libinput-1.31.2/src/evdev-tablet.c      2026-05-14 12:56:17.000000000 
+0200
+++ new/libinput-1.31.3/src/evdev-tablet.c      2026-06-04 03:05:01.000000000 
+0200
@@ -1311,18 +1311,6 @@
 static enum libinput_config_status
 eraser_button_set_button(struct libinput_tablet_tool *tool, uint32_t button)
 {
-       switch (button) {
-       case BTN_STYLUS:
-       case BTN_STYLUS2:
-       case BTN_STYLUS3:
-               break;
-       default:
-               log_bug_libinput(libinput_device_get_context(tool->last_device),
-                                "Unsupported eraser button 0x%x",
-                                button);
-               return LIBINPUT_CONFIG_STATUS_INVALID;
-       }
-
        tool->eraser_button.want_button = button;
 
        eraser_button_toggle(tool);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libinput-1.31.2/src/evdev-totem.c 
new/libinput-1.31.3/src/evdev-totem.c
--- old/libinput-1.31.2/src/evdev-totem.c       2026-05-14 12:56:17.000000000 
+0200
+++ new/libinput-1.31.3/src/evdev-totem.c       2026-06-04 03:05:01.000000000 
+0200
@@ -753,7 +753,7 @@
                libevdev_get_abs_maximum(evdev, ABS_MT_TOOL_TYPE) >= 
MT_TOOL_DIAL;
        has_size = evdev_device_get_size(device, &w, &h) == 0;
        has_touch_size =
-               libevdev_get_abs_resolution(device->evdev, ABS_MT_TOUCH_MAJOR) 
> 0 ||
+               libevdev_get_abs_resolution(device->evdev, ABS_MT_TOUCH_MAJOR) 
> 0 &&
                libevdev_get_abs_resolution(device->evdev, ABS_MT_TOUCH_MINOR) 
> 0;
 
        if (has_xy && has_slot && has_tool_dial && has_size && has_touch_size)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libinput-1.31.2/src/evdev.c 
new/libinput-1.31.3/src/evdev.c
--- old/libinput-1.31.2/src/evdev.c     2026-05-14 12:56:17.000000000 +0200
+++ new/libinput-1.31.3/src/evdev.c     2026-06-04 03:05:01.000000000 +0200
@@ -1635,6 +1635,16 @@
                return true;
 
        absinfo = libevdev_get_abs_info(evdev, code);
+
+       if (((uint64_t)absinfo->maximum - (uint64_t)absinfo->minimum) > 
INT32_MAX / 2) {
+               evdev_log_bug_kernel(device,
+                                    "kernel axis range [%d, %d] on %s too 
extreme\n",
+                                    absinfo->minimum,
+                                    absinfo->maximum,
+                                    libevdev_event_code_get_name(EV_ABS, 
code));
+               return false;
+       }
+
        if (absinfo->minimum == absinfo->maximum) {
                /* Some devices have a sort-of legitimate min/max of 0 for
                 * ABS_MISC and above (e.g. Roccat Kone XTD). Don't ignore
@@ -1654,6 +1664,18 @@
                                libevdev_event_code_get_name(EV_ABS, code));
                        return false;
                }
+       } else if (absinfo->minimum > absinfo->maximum) {
+               evdev_log_bug_kernel(device,
+                                    "device has min > max on %s\n",
+                                    libevdev_event_code_get_name(EV_ABS, 
code));
+               return false;
+       }
+
+       if (absinfo->resolution < 0) {
+               evdev_log_bug_kernel(device,
+                                    "kernel resolution is negative on %s\n",
+                                    libevdev_event_code_get_name(EV_ABS, 
code));
+               return false;
        }
 
        return true;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libinput-1.31.2/src/libinput.c 
new/libinput-1.31.3/src/libinput.c
--- old/libinput-1.31.2/src/libinput.c  2026-05-14 12:56:17.000000000 +0200
+++ new/libinput-1.31.3/src/libinput.c  2026-06-04 03:05:01.000000000 +0200
@@ -5216,16 +5216,9 @@
        if (!libinput_tablet_tool_config_eraser_button_get_modes(tool))
                return LIBINPUT_CONFIG_STATUS_UNSUPPORTED;
 
-       switch (button) {
-       case BTN_STYLUS:
-       case BTN_STYLUS2:
-       case BTN_STYLUS3:
-               break;
-       default:
-               if (!libinput_tablet_tool_has_button(tool, button))
-                       return LIBINPUT_CONFIG_STATUS_INVALID;
-               break;
-       }
+       evdev_usage_t usage = evdev_usage_from_code(EV_KEY, button);
+       if (!evdev_usage_is_button(usage))
+               return LIBINPUT_CONFIG_STATUS_INVALID;
 
        return tool->config.eraser_button.set_button(tool, button);
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libinput-1.31.2/src/libinput.h 
new/libinput-1.31.3/src/libinput.h
--- old/libinput-1.31.2/src/libinput.h  2026-05-14 12:56:17.000000000 +0200
+++ new/libinput-1.31.3/src/libinput.h  2026-06-04 03:05:01.000000000 +0200
@@ -7371,14 +7371,9 @@
  * the eraser mode to @ref LIBINPUT_CONFIG_ERASER_BUTTON_BUTTON via
  * libinput_tablet_tool_config_eraser_button_set_mode().
  *
- * The buttons BTN_STYLUS, BTN_STYLUS2 and BTN_STYLUS2 are always
- * allowed, even if libinput_tablet_tool_has_button() returns zero
- * for the button. Otherwise, the button must be one that
- * libinput_tablet_tool_has_button() returns a nonzero value for.
- *
  * @param tool The libinput tool
- * @param button The button, usually one of BTN_STYLUS, BTN_STYLUS2 or
- * BTN_STYLUS3
+ * @param button The button code. Must be a valid button (e.g. BTN_STYLUS)
+ * excluding fake buttons (e.g. BTN_TOOL_*) and keys (KEY_*)
  *
  * @return A config status code
  *
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libinput-1.31.2/src/util-strings.h 
new/libinput-1.31.3/src/util-strings.h
--- old/libinput-1.31.2/src/util-strings.h      2026-05-14 12:56:17.000000000 
+0200
+++ new/libinput-1.31.3/src/util-strings.h      2026-06-04 03:05:01.000000000 
+0200
@@ -543,7 +543,10 @@
 
 /**
  * Return a copy of str with all % converted to %% to make the string
- * acceptable as printf format.
+ * acceptable as printf format, and all non-NUL control characters
+ * (bytes 0x01-0x1f, 0x7f) replaced with '?' to prevent terminal
+ * escape sequence injection. NUL bytes are excluded implicitly
+ * because the string is null-terminated.
  */
 static inline char *
 str_sanitize(const char *str)
@@ -551,19 +554,34 @@
        if (!str)
                return NULL;
 
-       if (!strchr(str, '%'))
-               return strdup(str);
-
        size_t slen = strlen(str);
        slen = min(slen, 512);
+
+       bool needs_sanitization = false;
+       for (size_t i = 0; i < slen; i++) {
+               unsigned char c = str[i];
+               if (c == '%' || c < 0x20 || c == 0x7f) {
+                       needs_sanitization = true;
+                       break;
+               }
+       }
+       if (!needs_sanitization)
+               return strdup(str);
+
        char *sanitized = zalloc(2 * slen + 1);
        const char *src = str;
        char *dst = sanitized;
 
        for (size_t i = 0; i < slen; i++) {
-               if (*src == '%')
+               unsigned char c = *src++;
+               if (c == '%') {
+                       *dst++ = '%';
                        *dst++ = '%';
-               *dst++ = *src++;
+               } else if (c < 0x20 || c == 0x7f) {
+                       *dst++ = '?';
+               } else {
+                       *dst++ = c;
+               }
        }
        *dst = '\0';
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libinput-1.31.2/test/litest.h 
new/libinput-1.31.3/test/litest.h
--- old/libinput-1.31.2/test/litest.h   2026-05-14 12:56:17.000000000 +0200
+++ new/libinput-1.31.3/test/litest.h   2026-06-04 03:05:01.000000000 +0200
@@ -40,6 +40,8 @@
 #include "litest-runner.h"
 #include "quirks.h"
 
+DEFINE_DESTROY_CLEANUP_FUNC(libevdev_uinput);
+
 #define START_TEST(func_)  \
    static enum litest_runner_result func_(const struct litest_runner_test_env 
*test_env) { \
        int _i _unused_ = test_env->rangeval;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libinput-1.31.2/test/test-device.c 
new/libinput-1.31.3/test/test-device.c
--- old/libinput-1.31.2/test/test-device.c      2026-05-14 12:56:17.000000000 
+0200
+++ new/libinput-1.31.3/test/test-device.c      2026-06-04 03:05:01.000000000 
+0200
@@ -1656,6 +1656,77 @@
 }
 END_TEST
 
+enum extreme_axis_range_idx {
+       EXTREME_RANGE_0_TO_INT32MAX,
+       EXTREME_RANGE_INT32MIN_TO_0,
+       EXTREME_RANGE_NEGHALF_TO_POSHALF,
+};
+
+START_TEST(abs_device_extreme_axis_range)
+{
+       struct libevdev_uinput *uinput;
+       struct libinput_device *device;
+       int idx = litest_test_param_get_i32(test_env->params, "range");
+       /* All ranges exceed INT32_MAX/2, device should be rejected */
+       const struct {
+               int32_t min, max;
+       } ranges[] = {
+               [EXTREME_RANGE_0_TO_INT32MAX] = { 0, INT32_MAX },
+               [EXTREME_RANGE_INT32MIN_TO_0] = { INT32_MIN, 0 },
+               [EXTREME_RANGE_NEGHALF_TO_POSHALF] = { -(INT32_MAX / 2),
+                                                      INT32_MAX / 2 },
+       };
+       struct input_absinfo absinfo[] = {
+               { ABS_X, ranges[idx].min, ranges[idx].max, 0, 0, 0 },
+               { ABS_Y, 0, 1000, 0, 0, 0 },
+               { -1, -1, -1, -1, -1, -1 },
+       };
+
+       _litest_context_destroy_ struct libinput *li = litest_create_context();
+       litest_disable_log_handler(li);
+       /* clang-format off */
+       uinput = litest_create_uinput_abs_device("test device", NULL,
+                                                absinfo,
+                                                EV_KEY, BTN_LEFT,
+                                                EV_KEY, BTN_RIGHT,
+                                                -1);
+       /* clang-format on */
+       device = libinput_path_add_device(li, 
libevdev_uinput_get_devnode(uinput));
+       litest_restore_log_handler(li);
+       litest_assert_ptr_null(device);
+
+       libevdev_uinput_destroy(uinput);
+}
+END_TEST
+
+START_TEST(abs_device_negative_resolution)
+{
+       struct libevdev_uinput *uinput;
+       struct libinput_device *device;
+       struct input_absinfo absinfo[] = {
+               { ABS_X, 0, 1000, 0, 0, -1 }, /* negative resolution */
+               { ABS_Y, 0, 1000, 0, 0, -1 }, /* negative resolution */
+               { -1, -1, -1, -1, -1, -1 },
+       };
+
+       _litest_context_destroy_ struct libinput *li = litest_create_context();
+       litest_disable_log_handler(li);
+       /* clang-format off */
+       uinput = litest_create_uinput_abs_device("test device", NULL,
+                                                absinfo,
+                                                EV_KEY, BTN_LEFT,
+                                                EV_KEY, BTN_RIGHT,
+                                                -1);
+       /* clang-format on */
+       device = libinput_path_add_device(li, 
libevdev_uinput_get_devnode(uinput));
+       litest_restore_log_handler(li);
+       /* Device should be rejected */
+       litest_assert_ptr_null(device);
+
+       libevdev_uinput_destroy(uinput);
+}
+END_TEST
+
 TEST_COLLECTION(device)
 {
        /* clang-format off */
@@ -1713,6 +1784,13 @@
 
        litest_add(device_wheel_only, LITEST_WHEEL, 
LITEST_RELATIVE|LITEST_ABSOLUTE|LITEST_TABLET);
        litest_add_no_device(device_accelerometer);
+       litest_with_parameters(params, "range", 'I', 3,
+                              litest_named_i32(EXTREME_RANGE_0_TO_INT32MAX, 
"0-to-INT32MAX"),
+                              litest_named_i32(EXTREME_RANGE_INT32MIN_TO_0, 
"INT32MIN-to-0"),
+                              
litest_named_i32(EXTREME_RANGE_NEGHALF_TO_POSHALF, 
"-INT32MAX/2-to-INT32MAX/2")) {
+               
litest_add_parametrized_no_device(abs_device_extreme_axis_range, params);
+       }
+       litest_add_no_device(abs_device_negative_resolution);
 
        litest_add(device_udev_tag_wacom_tablet, LITEST_TABLET, LITEST_TOTEM);
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libinput-1.31.2/test/test-pad.c 
new/libinput-1.31.3/test/test-pad.c
--- old/libinput-1.31.2/test/test-pad.c 2026-05-14 12:56:17.000000000 +0200
+++ new/libinput-1.31.3/test/test-pad.c 2026-06-04 03:05:01.000000000 +0200
@@ -1113,6 +1113,54 @@
 }
 END_TEST
 
+START_TEST(pad_strip_wacom_degenerate_max)
+{
+       struct libinput_event *ev;
+       struct libinput_event_tablet_pad *pev;
+       double pos;
+
+       /* Override ABS_RX to have maximum=1, which would cause
+        * log2(1) = 0 and division by zero in normalize_wacom_strip()
+        * without the guard. */
+       /* clang-format off */
+       struct input_absinfo abs_override[] = {
+               { ABS_RX, 0, 1, 0, 0, 0 },
+               { -1, -1, -1, -1, -1, -1 },
+       };
+       /* clang-format on */
+
+       _litest_context_destroy_ struct libinput *li = litest_create_context();
+       struct litest_device *dev =
+               litest_add_device_with_overrides(li,
+                                                LITEST_WACOM_INTUOS3_PAD,
+                                                NULL,
+                                                NULL,
+                                                abs_override,
+                                                NULL);
+
+       litest_drain_events(li);
+
+       /* Send strip events - value 1 with max 1 triggers
+        * log2(1)/log2(1) = 0/0 = NaN without the guard */
+       litest_pad_strip_start(dev, 100);
+       litest_dispatch(li);
+
+       ev = libinput_get_event(li);
+       pev = litest_is_pad_strip_event(ev, 0, 
LIBINPUT_TABLET_PAD_STRIP_SOURCE_FINGER);
+       pos = libinput_event_tablet_pad_get_strip_position(pev);
+       /* Without the fix, pos would be NaN from 0/0 division.
+        * With the fix, pos must be a valid finite number. */
+       litest_assert_double_ge(pos, 0.0);
+       litest_assert_double_le(pos, 1.0);
+       libinput_event_destroy(ev);
+
+       litest_pad_strip_end(dev);
+       litest_drain_events(li);
+
+       litest_device_destroy(dev);
+}
+END_TEST
+
 START_TEST(pad_send_events_disabled)
 {
        struct litest_device *dev = litest_current_device();
@@ -1171,6 +1219,7 @@
        litest_add(pad_has_strip, LITEST_STRIP, LITEST_ANY);
        litest_add(pad_strip, LITEST_STRIP, LITEST_ANY);
        litest_add(pad_strip_finger_up, LITEST_STRIP, LITEST_ANY);
+       litest_add_no_device(pad_strip_wacom_degenerate_max);
 
        litest_add(pad_has_dial, LITEST_DIAL, LITEST_ANY);
        litest_add(pad_dial_low_res, LITEST_DIAL, LITEST_ANY);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libinput-1.31.2/test/test-tablet.c 
new/libinput-1.31.3/test/test-tablet.c
--- old/libinput-1.31.2/test/test-tablet.c      2026-05-14 12:56:17.000000000 
+0200
+++ new/libinput-1.31.3/test/test-tablet.c      2026-06-04 03:05:01.000000000 
+0200
@@ -7883,6 +7883,189 @@
 }
 END_TEST
 
+START_TEST(tablet_eraser_button_different_buttons)
+{
+       struct litest_device *dev = litest_current_device();
+       struct libinput *li = dev->libinput;
+       struct axis_replacement axes[] = {
+               { ABS_DISTANCE, 10 },
+               { ABS_PRESSURE, 0 },
+               { -1, -1 },
+       };
+       _unref_(libinput_tablet_tool) *pen = NULL;
+
+       uint32_t eraser_button_mapping =
+               litest_test_param_get_i32(test_env->params, 
"eraser-button-mapping");
+
+       if (!libevdev_has_event_code(dev->evdev, EV_KEY, BTN_TOOL_RUBBER))
+               return LITEST_NOT_APPLICABLE;
+
+       litest_log_group("Prox in/out to disable proximity timer") {
+               litest_tablet_proximity_in(dev, 25, 25, axes);
+               litest_tablet_proximity_out(dev);
+               litest_timeout_tablet_proxout(li);
+
+               litest_checkpoint(
+                       "Eraser prox in/out to force-disable config on broken 
tablets");
+               litest_tablet_set_tool_type(dev, BTN_TOOL_RUBBER);
+               litest_tablet_proximity_in(dev, 25, 25, axes);
+               litest_tablet_proximity_out(dev);
+               litest_timeout_tablet_proxout(li);
+       }
+
+       litest_drain_events(li);
+
+       litest_log_group("Proximity in for pen") {
+               litest_tablet_set_tool_type(dev, BTN_TOOL_PEN);
+               litest_tablet_proximity_in(dev, 20, 20, axes);
+               litest_dispatch(li);
+               _destroy_(libinput_event) *ev = libinput_get_event(li);
+               auto tev = litest_is_proximity_event(
+                       ev,
+                       LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_IN);
+               pen = libinput_event_tablet_tool_get_tool(tev);
+               litest_assert_enum_eq(libinput_tablet_tool_get_type(pen),
+                                     LIBINPUT_TABLET_TOOL_TYPE_PEN);
+               pen = libinput_tablet_tool_ref(pen);
+       }
+
+       if (!libinput_tablet_tool_config_eraser_button_get_modes(pen))
+               return LITEST_NOT_APPLICABLE;
+
+       auto status = libinput_tablet_tool_config_eraser_button_set_mode(
+               pen,
+               LIBINPUT_CONFIG_ERASER_BUTTON_BUTTON);
+       litest_assert_enum_eq(status, LIBINPUT_CONFIG_STATUS_SUCCESS);
+       status = libinput_tablet_tool_config_eraser_button_set_button(
+               pen,
+               eraser_button_mapping);
+       litest_assert_enum_eq(status, LIBINPUT_CONFIG_STATUS_SUCCESS);
+
+       litest_log_group("Prox out to apply changed settings") {
+               litest_tablet_proximity_out(dev);
+               litest_timeout_tablet_proxout(li);
+               litest_drain_events(li);
+       }
+
+       litest_mark_test_start();
+
+       litest_tablet_proximity_in(dev, 10, 10, axes);
+       litest_drain_events(li);
+
+       /* Make sure the button still works as-is */
+       if (libinput_tablet_tool_has_button(pen, eraser_button_mapping)) {
+               litest_log_group("Testing button on pen") {
+                       litest_event(dev, EV_KEY, eraser_button_mapping, 1);
+                       litest_event(dev, EV_SYN, SYN_REPORT, 0);
+                       litest_dispatch(li);
+                       litest_event(dev, EV_KEY, eraser_button_mapping, 0);
+                       litest_event(dev, EV_SYN, SYN_REPORT, 0);
+                       litest_dispatch(li);
+                       litest_assert_tablet_button_event(
+                               li,
+                               eraser_button_mapping,
+                               LIBINPUT_BUTTON_STATE_PRESSED);
+                       litest_assert_tablet_button_event(
+                               li,
+                               eraser_button_mapping,
+                               LIBINPUT_BUTTON_STATE_RELEASED);
+               }
+       }
+
+       litest_dispatch(li);
+
+       litest_log_group("Prox out for the pen ...") {
+               litest_with_event_frame(dev) {
+                       litest_tablet_set_tool_type(dev, BTN_TOOL_PEN);
+                       litest_tablet_proximity_out(dev);
+               }
+               litest_dispatch(li);
+       }
+
+       litest_log_group("...and prox in for the eraser") {
+               litest_with_event_frame(dev) {
+                       litest_tablet_set_tool_type(dev, BTN_TOOL_RUBBER);
+                       litest_tablet_proximity_in(dev, 12, 12, axes);
+               }
+               litest_dispatch(li);
+       }
+
+       litest_drain_events_of_type(li, LIBINPUT_EVENT_TABLET_TOOL_AXIS);
+
+       litest_log_group("Expect button event") {
+               _destroy_(libinput_event) *ev = libinput_get_event(li);
+               auto tev =
+                       litest_is_tablet_event(ev, 
LIBINPUT_EVENT_TABLET_TOOL_BUTTON);
+               
litest_assert_enum_eq(libinput_event_tablet_tool_get_button_state(tev),
+                                     LIBINPUT_BUTTON_STATE_PRESSED);
+               litest_assert_int_eq(libinput_event_tablet_tool_get_button(tev),
+                                    eraser_button_mapping);
+               litest_assert_ptr_eq(libinput_event_tablet_tool_get_tool(tev), 
pen);
+       }
+
+       litest_log_group("Prox out for the eraser...") {
+               litest_with_event_frame(dev) {
+                       litest_tablet_proximity_out(dev);
+               }
+               litest_dispatch(li);
+       }
+
+       litest_log_group("...and prox in for the pen") {
+               litest_with_event_frame(dev) {
+                       litest_tablet_set_tool_type(dev, BTN_TOOL_PEN);
+                       litest_tablet_proximity_in(dev, 12, 12, axes);
+               }
+               litest_dispatch(li);
+       }
+
+       litest_drain_events_of_type(li, LIBINPUT_EVENT_TABLET_TOOL_AXIS);
+
+       litest_log_group("Expect button event") {
+               _destroy_(libinput_event) *ev = libinput_get_event(li);
+               auto tev =
+                       litest_is_tablet_event(ev, 
LIBINPUT_EVENT_TABLET_TOOL_BUTTON);
+               litest_assert_int_eq(libinput_event_tablet_tool_get_button(tev),
+                                    eraser_button_mapping);
+               litest_assert_ptr_eq(libinput_event_tablet_tool_get_tool(tev), 
pen);
+       }
+}
+END_TEST
+
+START_TEST(tablet_eraser_button_invalid_buttons)
+{
+       struct litest_device *dev = litest_current_device();
+       struct libinput *li = dev->libinput;
+       struct axis_replacement axes[] = {
+               { ABS_DISTANCE, 10 },
+               { ABS_PRESSURE, 0 },
+               { -1, -1 },
+       };
+
+       uint32_t eraser_button_mapping =
+               litest_test_param_get_i32(test_env->params, 
"eraser-button-mapping");
+
+       if (!libevdev_has_event_code(dev->evdev, EV_KEY, BTN_TOOL_RUBBER))
+               return LITEST_NOT_APPLICABLE;
+
+       litest_drain_events(li);
+       litest_tablet_proximity_in(dev, 20, 20, axes);
+       litest_dispatch(li);
+
+       _destroy_(libinput_event) *ev = libinput_get_event(li);
+       auto tev =
+               litest_is_proximity_event(ev, 
LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_IN);
+       auto tool = libinput_event_tablet_tool_get_tool(tev);
+
+       if (!libinput_tablet_tool_config_eraser_button_get_modes(tool))
+               return LITEST_NOT_APPLICABLE;
+
+       auto status = libinput_tablet_tool_config_eraser_button_set_button(
+               tool,
+               eraser_button_mapping);
+       litest_assert_enum_eq(status, LIBINPUT_CONFIG_STATUS_INVALID);
+}
+END_TEST
+
 START_TEST(tablet_eraser_button_config_after_device_removal)
 {
        _litest_context_destroy_ struct libinput *li = litest_create_context();
@@ -8099,7 +8282,21 @@
                               "with-motion-events", 'b') {
                litest_add_parametrized(tablet_eraser_button_disabled, 
LITEST_TABLET, LITEST_TOTEM|LITEST_FORCED_PROXOUT, params);
        }
-
+       litest_with_parameters(params,
+                              "eraser-button-mapping", 'I', 4,
+                                       litest_named_i32(BTN_STYLUS),
+                                       litest_named_i32(BTN_STYLUS3),
+                                       litest_named_i32(BTN_LEFT),
+                                       litest_named_i32(BTN_BACK)){
+               litest_add_parametrized(tablet_eraser_button_different_buttons, 
LITEST_TABLET, LITEST_TOTEM|LITEST_FORCED_PROXOUT, params);
+       }
+       litest_with_parameters(params,
+                              "eraser-button-mapping", 'I', 3,
+                                       litest_named_i32(BTN_TOUCH),
+                                       litest_named_i32(BTN_TOOL_FINGER),
+                                       litest_named_i32(KEY_A)) {
+               litest_add_parametrized(tablet_eraser_button_invalid_buttons, 
LITEST_TABLET, LITEST_TOTEM|LITEST_FORCED_PROXOUT, params);
+       }
        litest_add_no_device(tablet_eraser_button_config_after_device_removal);
        /* clang-format on */
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libinput-1.31.2/test/test-totem.c 
new/libinput-1.31.3/test/test-totem.c
--- old/libinput-1.31.2/test/test-totem.c       2026-05-14 12:56:17.000000000 
+0200
+++ new/libinput-1.31.3/test/test-totem.c       2026-06-04 03:05:01.000000000 
+0200
@@ -556,6 +556,57 @@
 }
 END_TEST
 
+START_TEST(totem_touch_size_missing_resolution)
+{
+       struct libinput_device *device;
+       /* Match the Dell Canvas Totem quirk: vendor=0x2575, product=0x0204,
+        * name matching "*System Multi Axis", bus=USB */
+       struct input_id id = {
+               .bustype = 0x3,
+               .vendor = 0x2575,
+               .product = 0x0204,
+       };
+       struct input_absinfo absinfo[] = {
+               { ABS_MT_SLOT, 0, 4, 0, 0, 0 },
+               { ABS_MT_TOUCH_MAJOR, 0, 32767, 0, 0, 10 },
+               { ABS_MT_TOUCH_MINOR, 0, 32767, 0, 0, 0 }, /* resolution 
missing */
+               { ABS_MT_ORIENTATION, -89, 89, 0, 0, 0 },
+               { ABS_MT_POSITION_X, 0, 32767, 0, 0, 55 },
+               { ABS_MT_POSITION_Y, 0, 32767, 0, 0, 98 },
+               { ABS_MT_TOOL_TYPE, 9, 10, 0, 0, 0 },
+               { ABS_MT_TRACKING_ID, 0, 65535, 0, 0, 0 },
+               { -1, -1, -1, -1, -1, -1 },
+       };
+       /* clang-format off */
+       int events[] = {
+               EV_KEY, BTN_0,
+               INPUT_PROP_MAX, INPUT_PROP_DIRECT,
+               -1, -1,
+       };
+       /* clang-format on */
+
+       _destroy_(libevdev_uinput) *uinput =
+               litest_create_uinput_device_from_description(
+                       "CoolTouch System Multi Axis",
+                       &id,
+                       absinfo,
+                       events);
+       _litest_context_destroy_ struct libinput *li = litest_create_context();
+       litest_disable_log_handler(li);
+       device = libinput_path_add_device(li, 
libevdev_uinput_get_devnode(uinput));
+       litest_restore_log_handler(li);
+
+       /* The device matches the Dell Canvas Totem quirk but has
+        * ABS_MT_TOUCH_MINOR resolution missing. The totem dispatch
+        * should reject it, so it must not be recognized as a tablet
+        * tool device. */
+       if (device)
+               litest_assert(!libinput_device_has_capability(
+                       device,
+                       LIBINPUT_DEVICE_CAP_TABLET_TOOL));
+}
+END_TEST
+
 TEST_COLLECTION(totem)
 {
        /* clang-format off */
@@ -571,6 +622,7 @@
        litest_add(totem_button, LITEST_TOTEM, LITEST_ANY);
        litest_add(totem_button_down_on_init, LITEST_TOTEM, LITEST_ANY);
        litest_add_no_device(totem_button_up_on_delete);
+       litest_add_no_device(totem_touch_size_missing_resolution);
 
        litest_add(totem_arbitration_below, LITEST_TOTEM, LITEST_ANY);
        litest_add(totem_arbitration_during, LITEST_TOTEM, LITEST_ANY);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libinput-1.31.2/test/test-utils.c 
new/libinput-1.31.3/test/test-utils.c
--- old/libinput-1.31.2/test/test-utils.c       2026-05-14 12:56:17.000000000 
+0200
+++ new/libinput-1.31.3/test/test-utils.c       2026-06-04 03:05:01.000000000 
+0200
@@ -2199,6 +2199,16 @@
                { "x %", "x %%" },
                { "%sx", "%%sx" },
                { "%s%s", "%%s%%s" },
+               { "\t", "?" },
+               { "\n", "?" },
+               { "\r", "?" },
+               { "\x1b[31m", "?[31m" },
+               { "foo\tbar", "foo?bar" },
+               { "foo\nbar", "foo?bar" },
+               { "\x01\x1f\x7f", "???" },
+               { "clean", "clean" },
+               { "a\x1b[0mb", "a?[0mb" },
+               { "%\n", "%%?" },
                { NULL, NULL },
        };
        /* clang-format on */
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libinput-1.31.2/tools/libinput-record.c 
new/libinput-1.31.3/tools/libinput-record.c
--- old/libinput-1.31.2/tools/libinput-record.c 2026-05-14 12:56:17.000000000 
+0200
+++ new/libinput-1.31.3/tools/libinput-record.c 2026-06-04 03:05:01.000000000 
+0200
@@ -38,7 +38,6 @@
 #include <sys/epoll.h>
 #include <sys/signalfd.h>
 #include <sys/stat.h>
-#include <sys/timerfd.h>
 #include <sys/utsname.h>
 #include <time.h>
 #include <unistd.h>
@@ -108,7 +107,7 @@
 };
 
 struct record_context {
-       int timeout;
+       usec_t timeout;
        bool show_keycodes;
 
        usec_t offset;
@@ -130,8 +129,7 @@
        struct list sources;
 
        struct {
-               bool had_events_since_last_time;
-               bool skipped_timer_print;
+               usec_t last_wall_time;
        } timestamps;
 
        bool had_events;
@@ -1353,13 +1351,13 @@
 }
 
 static void
-print_libinput_header(FILE *fp, int timeout)
+print_libinput_header(FILE *fp, usec_t timeout)
 {
        iprintf(fp, I_TOPLEVEL, "libinput:\n");
        iprintf(fp, I_LIBINPUT, "version: \"%s\"\n", LIBINPUT_VERSION);
        iprintf(fp, I_LIBINPUT, "git: \"%s\"\n", LIBINPUT_GIT_VERSION);
-       if (timeout > 0)
-               iprintf(fp, I_LIBINPUT, "autorestart: %d\n", timeout);
+       if (!usec_is_zero(timeout))
+               iprintf(fp, I_LIBINPUT, "autorestart: %u\n", 
usec_to_seconds(timeout));
 }
 
 static void
@@ -1519,7 +1517,8 @@
                break;
        }
 
-       iprintf(fp, I_EVDEV, "# Name: %s\n", libevdev_get_name(dev));
+       _autofree_ char *name = str_sanitize(libevdev_get_name(dev));
+       iprintf(fp, I_EVDEV, "# Name: %s\n", name ? name : "");
        iprintf(fp,
                I_EVDEV,
                "# ID: bus 0x%04x%svendor 0x%04x product 0x%04x version 
0x%04x\n",
@@ -1570,7 +1569,8 @@
 static void
 print_bits_info(FILE *fp, struct libevdev *dev)
 {
-       iprintf(fp, I_EVDEV, "name: \"%s\"\n", libevdev_get_name(dev));
+       _autofree_ char *name = str_sanitize(libevdev_get_name(dev));
+       iprintf(fp, I_EVDEV, "name: \"%s\"\n", name ? name : "");
        iprintf(fp,
                I_EVDEV,
                "id: [%d, %d, %d, %d]\n",
@@ -1936,7 +1936,8 @@
                if (rc != 0)
                        continue;
 
-               fprintf(stderr, "%s%s:  %s\n", prefix, path, 
libevdev_get_name(device));
+               _autofree_ char *name = str_sanitize(libevdev_get_name(device));
+               fprintf(stderr, "%s%s:  %s\n", prefix, path, name ? name : "");
                libevdev_free(device);
                available_devices++;
        }
@@ -2071,21 +2072,6 @@
        }
 }
 
-static void
-arm_timer(int timerfd)
-{
-       time_t t = time(NULL);
-       struct tm tm;
-       struct itimerspec interval = {
-               .it_value = { 0, 0 },
-               .it_interval = { 5, 0 },
-       };
-
-       localtime_r(&t, &tm);
-       interval.it_value.tv_sec = 5 - (tm.tm_sec % 5);
-       timerfd_settime(timerfd, 0, &interval, NULL);
-}
-
 static struct source *
 add_source(struct record_context *ctx,
           int fd,
@@ -2132,33 +2118,11 @@
 }
 
 static void
-timefd_dispatch(struct record_context *ctx, int fd, void *data)
-{
-       char discard[64];
-
-       (void)read(fd, discard, sizeof(discard));
-
-       if (ctx->timestamps.had_events_since_last_time) {
-               print_wall_time(ctx);
-               ctx->timestamps.had_events_since_last_time = false;
-               ctx->timestamps.skipped_timer_print = false;
-       } else {
-               ctx->timestamps.skipped_timer_print = true;
-       }
-}
-
-static void
 evdev_dispatch(struct record_context *ctx, int fd, void *data)
 {
        struct record_device *this_device = data;
 
-       if (ctx->timestamps.skipped_timer_print) {
-               print_wall_time(ctx);
-               ctx->timestamps.skipped_timer_print = false;
-       }
-
        ctx->had_events = true;
-       ctx->timestamps.had_events_since_last_time = true;
 
        handle_events(ctx, this_device);
 }
@@ -2179,7 +2143,6 @@
        struct hidraw *hidraw = data;
 
        ctx->had_events = true;
-       ctx->timestamps.had_events_since_last_time = true;
        handle_hidraw(hidraw);
 }
 
@@ -2189,15 +2152,29 @@
        struct source *source;
        struct epoll_event ep[64];
        int i, count;
+       int timeout = usec_to_millis(ctx->timeout);
 
-       count = epoll_wait(ctx->epoll_fd, ep, ARRAY_LENGTH(ep), ctx->timeout);
+       count = epoll_wait(ctx->epoll_fd,
+                          ep,
+                          ARRAY_LENGTH(ep),
+                          timeout > 0 ? timeout : -1);
        if (count < 0)
                return -errno;
 
+       if (count > 0) {
+               usec_t now = usec_from_now();
+               usec_t dt = usec_delta(now, ctx->timestamps.last_wall_time);
+               if (usec_cmp(dt, usec_from_seconds(5)) > 0) {
+                       ctx->timestamps.last_wall_time = now;
+                       print_wall_time(ctx);
+               }
+       }
+
        for (i = 0; i < count; ++i) {
                source = ep[i].data.ptr;
                if (source->fd == -1)
                        continue;
+
                source->dispatch(ctx, source->fd, source->user_data);
        }
 
@@ -2207,13 +2184,12 @@
 static int
 mainloop(struct record_context *ctx)
 {
-       bool autorestart = (ctx->timeout > 0);
+       bool autorestart = !usec_is_zero(ctx->timeout);
        struct source *source;
        struct record_device *d = NULL;
        sigset_t mask;
-       int sigfd, timerfd;
+       int sigfd;
 
-       assert(ctx->timeout != 0);
        assert(!list_empty(&ctx->devices));
 
        ctx->epoll_fd = epoll_create1(0);
@@ -2227,10 +2203,6 @@
        sigfd = signalfd(-1, &mask, SFD_NONBLOCK);
        add_source(ctx, sigfd, signalfd_dispatch, NULL);
 
-       timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
-       add_source(ctx, timerfd, timefd_dispatch, NULL);
-       arm_timer(timerfd);
-
        list_for_each(d, &ctx->devices, link) {
                struct hidraw *hidraw;
 
@@ -2279,8 +2251,8 @@
                if (autorestart)
                        iprintf(ctx->first_device->fp,
                                I_NONE,
-                               "# Autorestart timeout: %d\n",
-                               ctx->timeout);
+                               "# Autorestart timeout: %u\n",
+                               usec_to_seconds(ctx->timeout));
 
                iprintf(ctx->first_device->fp, I_TOPLEVEL, "devices:\n");
 
@@ -2290,6 +2262,8 @@
                        print_device_description(d);
                        iprintf(d->fp, I_DEVICE, "events:\n");
                }
+
+               ctx->timestamps.last_wall_time = usec_from_now();
                print_wall_time(ctx);
 
                if (ctx->libinput) {
@@ -2324,8 +2298,8 @@
                        list_for_each(d, &ctx->devices, link) {
                                iprintf(d->fp,
                                        I_NONE,
-                                       "# Closing after %ds inactivity",
-                                       ctx->timeout / 1000);
+                                       "# Closing after %us inactivity",
+                                       usec_to_seconds(ctx->timeout));
                        }
                }
 
@@ -2651,8 +2625,9 @@
 main(int argc, char **argv)
 {
        struct record_context ctx = {
-               .timeout = -1,
+               .timeout = usec_from_uint64_t(0),
                .show_keycodes = false,
+               .timestamps.last_wall_time = usec_from_uint64_t(0),
        };
        struct option opts[] = {
                { "autorestart", required_argument, 0, OPT_AUTORESTART },
@@ -2690,14 +2665,16 @@
                        usage();
                        rc = EXIT_SUCCESS;
                        goto out;
-               case OPT_AUTORESTART:
-                       if (!safe_atoi(optarg, &ctx.timeout) || ctx.timeout <= 
0) {
+               case OPT_AUTORESTART: {
+                       int timeout;
+                       if (!safe_atoi(optarg, &timeout) || timeout <= 0) {
                                usage();
                                rc = EXIT_INVALID_USAGE;
                                goto out;
                        }
-                       ctx.timeout = ctx.timeout * 1000;
+                       ctx.timeout = usec_from_seconds(timeout);
                        break;
+               }
                case 'o':
                case OPT_OUTFILE:
                        output_arg = optarg;
@@ -2752,10 +2729,10 @@
                        optind++;
        }
 
-       if (ctx.timeout > 0 && output_arg == NULL) {
-               fprintf(stderr, "Option --autorestart requires 
--output-file\n");
-               rc = EXIT_INVALID_USAGE;
-               goto out;
+       if (!usec_is_zero(ctx.timeout) && output_arg == NULL) {
+               output_arg = "libinput-recording.yml";
+               fprintf(stderr,
+                       "Option --autorestart requires --output-file, 
defaulting to libinput-recording.yml\n");
        }
 
        ctx.output_file.name = safe_strdup(output_arg);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libinput-1.31.2/tools/test_tool_option_parsing.py 
new/libinput-1.31.3/tools/test_tool_option_parsing.py
--- old/libinput-1.31.2/tools/test_tool_option_parsing.py       2026-05-14 
12:56:17.000000000 +0200
+++ new/libinput-1.31.3/tools/test_tool_option_parsing.py       2026-06-04 
03:05:01.000000000 +0200
@@ -360,7 +360,7 @@
 
 def test_libinput_record_autorestart(libinput_record, recording):
     libinput_record.run_command_invalid(["--autorestart"])
-    libinput_record.run_command_invalid(["--autorestart=2"])
+    libinput_record.run_command_success(["--autorestart=2"])
     libinput_record.run_command_success(["-o", recording, "--autorestart=2"])
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libinput-1.31.2/udev/libinput-device-group.c 
new/libinput-1.31.3/udev/libinput-device-group.c
--- old/libinput-1.31.2/udev/libinput-device-group.c    2026-05-14 
12:56:17.000000000 +0200
+++ new/libinput-1.31.3/udev/libinput-device-group.c    2026-06-04 
03:05:01.000000000 +0200
@@ -107,7 +107,8 @@
 
        udev_list_entry_foreach(entry, udev_enumerate_get_list_entry(e)) {
                struct udev_device *d;
-               const char *path, *phys;
+               _autofree_ char *phys = NULL;
+               const char *path;
                const char *pidstr, *vidstr;
                int pid, vid, dist;
 
@@ -122,7 +123,7 @@
 
                vidstr = udev_device_get_property_value(d, "ID_VENDOR_ID");
                pidstr = udev_device_get_property_value(d, "ID_MODEL_ID");
-               phys = udev_device_get_sysattr_value(d, "phys");
+               phys = str_sanitize(udev_device_get_sysattr_value(d, "phys"));
 
                if (vidstr && pidstr && phys && safe_atoi_base(vidstr, &vid, 
16) &&
                    safe_atoi_base(pidstr, &pid, 16) && vid == VENDOR_ID_WACOM 
&&
@@ -134,7 +135,7 @@
                                best_dist = dist;
 
                                free(*phys_attr);
-                               *phys_attr = safe_strdup(phys);
+                               *phys_attr = steal(&phys);
                        }
                }
 
@@ -151,7 +152,8 @@
        int rc = 1;
        struct udev *udev = NULL;
        struct udev_device *device = NULL;
-       const char *syspath, *phys = NULL;
+       _autofree_ char *phys = NULL;
+       const char *syspath = NULL;
        const char *product;
        int bustype, vendor_id, product_id, version;
        char group[1024];
@@ -175,8 +177,7 @@
         * bit and use the remainder as device group identifier */
        while (device != NULL) {
                struct udev_device *parent;
-
-               phys = udev_device_get_sysattr_value(device, "phys");
+               phys = str_sanitize(udev_device_get_sysattr_value(device, 
"phys"));
                if (phys)
                        break;
 

Reply via email to