From 05c7defec14cd492864db029306c4aaff77f4bf0 Mon Sep 17 00:00:00 2001 From: Amolith Date: Tue, 30 Jun 2026 16:52:31 -0600 Subject: [PATCH] Require exact integer values for sliders Check integer range bounds, current values, and options as XML integer text before parsing them as floats. Also require each parsed value to round-trip to the same integer and reject no-option unit-step ranges outside float's consecutive exact integer window. --- .../conversations/entities/Conversation.java | 22 ++++++++++++++++--- .../entities/ConversationTest.java | 21 ++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 606e6f039d3cd2b9d167235cc594ff5460686e90..e5347fd244a16b39cae96c19b86369df8bf92212 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -1878,14 +1878,14 @@ public class Conversation extends AbstractEntity final String minValue = range == null ? null : range.getAttribute("min"); final String maxValue = range == null ? null : range.getAttribute("max"); final boolean integerDatatype = isIntegerDatatype(datatype); - if (integerDatatype && (!isIntegerLexicalValue(minValue) || !isIntegerLexicalValue(maxValue))) return null; + if (integerDatatype && (!isExactIntegerSliderValue(minValue) || !isExactIntegerSliderValue(maxValue))) return null; final Float min = parseFloat(minValue); final Float max = parseFloat(maxValue); if (min == null || max == null || min >= max) return null; final String value = firstValue(field); if (value == null || value.equals("")) return null; - if (integerDatatype && !isIntegerLexicalValue(value)) return null; + if (integerDatatype && !isExactIntegerSliderValue(value)) return null; final Float parsedValue = parseFloat(value); if (parsedValue == null || parsedValue < min || parsedValue > max) return null; @@ -1894,6 +1894,7 @@ public class Conversation extends AbstractEntity final Float step; if (options.size() < 2) { + if (integerDatatype && !isExactIntegerUnitRange(minValue, maxValue)) return null; step = integerDatatype ? 1f : 0f; } else { step = uniformStep(options); @@ -1932,7 +1933,7 @@ public class Conversation extends AbstractEntity final List values = new ArrayList<>(); for (final Option option : Option.forField(field)) { final String optionValue = option.getValue(); - if (integerDatatype && !isIntegerLexicalValue(optionValue)) return null; + if (integerDatatype && !isExactIntegerSliderValue(optionValue)) return null; final Float value = parseFloat(optionValue); if (value == null) return null; values.add(value); @@ -1968,6 +1969,21 @@ public class Conversation extends AbstractEntity return Math.abs(Math.rint(multiple) - multiple) < 0.0001f; } + private static boolean isExactIntegerSliderValue(final String value) { + if (!isIntegerLexicalValue(value)) return false; + final Float parsed = parseFloat(value); + return parsed != null + && new BigDecimal(Float.toString(parsed)).compareTo(new BigDecimal(value)) == 0; + } + + private static boolean isExactIntegerUnitRange(final String minValue, final String maxValue) { + final BigDecimal min = new BigDecimal(minValue); + final BigDecimal max = new BigDecimal(maxValue); + final BigDecimal maxConsecutiveInteger = new BigDecimal("16777216"); + return min.compareTo(maxConsecutiveInteger.negate()) >= 0 + && max.compareTo(maxConsecutiveInteger) <= 0; + } + private static boolean isIntegerLexicalValue(final String value) { if (value == null || value.equals("")) return false; final int start = value.charAt(0) == '+' || value.charAt(0) == '-' ? 1 : 0; diff --git a/src/test/java/eu/siacs/conversations/entities/ConversationTest.java b/src/test/java/eu/siacs/conversations/entities/ConversationTest.java index 5f8b86169067f01926d90b99c819c290baa5c9ea..bc380469f5c77cf8ad0d11307a9adf88f58118c4 100644 --- a/src/test/java/eu/siacs/conversations/entities/ConversationTest.java +++ b/src/test/java/eu/siacs/conversations/entities/ConversationTest.java @@ -241,6 +241,27 @@ public class ConversationTest { Assert.assertNull(Conversation.steppedSliderStep(field)); } + @Test + public void steppedSliderStepRejectsFractionalIntegerValueAtFloatPrecisionBoundary() { + final var field = sliderField("xs:integer", "16777216", "16777218", "16777216.5"); + + Assert.assertNull(Conversation.steppedSliderStep(field)); + } + + @Test + public void steppedSliderStepRejectsIntegerValueRoundedByFloatParsing() { + final var field = sliderField("xs:integer", "16777216", "16777218", "16777217"); + + Assert.assertNull(Conversation.steppedSliderStep(field)); + } + + @Test + public void steppedSliderStepRejectsIntegerRangeWithUnrepresentableIntermediateValue() { + final var field = sliderField("xs:integer", "16777216", "16777218", "16777216"); + + Assert.assertNull(Conversation.steppedSliderStep(field)); + } + @Test public void formatSliderValueDoesNotClampLargeIntegerDatatypesToInt() { Assert.assertEquals("3000000000", Conversation.formatSliderValue(3_000_000_000f, "xs:long"));