fix(user): replace Math.abs(hashCode()) with Math.floorMod in computeColor #490
Reference in New Issue
Block a user
Delete Branch "fix/issue-471-floor-mod-hashcode"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #471
Problem
AppUser.computeColorusedMath.abs(id.hashCode()) % PALETTE.lengthto select an avatar color.Math.abs(Integer.MIN_VALUE)overflows back toInteger.MIN_VALUE(negative) because-Integer.MIN_VALUEexceedsInteger.MAX_VALUE. This is the only Priority-1 SpotBugs CORRECTNESS finding in the codebase (RV_ABSOLUTE_VALUE_OF_HASHCODE).With
PALETTE.length = 8(a power of 2 that dividesInteger.MIN_VALUEexactly), the bug is latent —Integer.MIN_VALUE % 8 = 0, a valid index. But if the palette size ever changes to a value that doesn't evenly divideInteger.MIN_VALUE, the code would throwArrayIndexOutOfBoundsExceptionduring a JPA lifecycle callback (@PrePersist,@PreUpdate,@PostLoad), causing an entity save to fail with a 500.Fix
Math.floorModalways returns a non-negative residue in[0, n-1]regardless of the dividend's sign. No overflow, no edge case.Tests
Added
computeColor_returnsValidPaletteColorForIntegerMinValueHashinAppUserTest:UUID.fromString("80000000-0000-0000-0000-000000000000")hashashCode() == Integer.MIN_VALUE(the triggering input)computeColorreturns a valid palette color for this UUID👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ⚠️ Approved with concerns
Code Quality
The production fix is exactly right: one character change (
Math.abs(x) % n→Math.floorMod(x, n)), no logic changes, no side effects. The inline comment passes my "explains WHY" test —Math.floorModisn't obviously superior toMath.absunless you know theInteger.MIN_VALUEoverflow trap, so the comment is earned.TDD Gap (not a blocker, but worth documenting)
The PR description is honest about it: the test is not red before the fix.
Integer.MIN_VALUE % 8 = 0because 8 is a power of 2 that evenly dividesInteger.MIN_VALUE, so the old code happens to returnPALETTE[0]without throwing. The test documents the edge case and guards against future palette size changes, but it does not prove the old code was broken in the way TDD demands.This is an acceptable exception — SpotBugs findings don't always have a runnable RED test (the finding IS the red signal). The PR description explains the situation clearly. I would not block on this, but I'd note it so the team doesn't take this as a precedent for skipping red.
One concrete improvement for the future: if the palette ever grows to an odd size (7, 9, etc.), add a test that explicitly confirms the result for
Integer.MIN_VALUE-hash UUID is non-negative — at that point the RED test is achievable.Suggestions (non-blocking)
The test asserts
assertThat(EXPECTED_PALETTE).contains(AppUser.computeColor(minHashId))but does not assert that the returned value is a specific expected color. IfPALETTE[0]is always the expected result for this UUID (deterministic), we could pin it:assertThat(AppUser.computeColor(minHashId)).isEqualTo("#7a4f9a"). This makes the test more precise but also more brittle if the palette reorders. Current form is acceptable.Summary
Correct fix, good comment, honest PR description. The TDD gap is acknowledged and understood. Ship it.
🔒 Nora "NullX" Steiner — Application Security Engineer
Verdict: ✅ Approved
Vulnerability Analysis
CWE-190 (Integer Overflow/Wraparound) / CWE-682 (Incorrect Calculation) — resolved correctly.
The old pattern
Math.abs(hashCode()) % nis a well-documented Java pitfall.Math.abs(Integer.MIN_VALUE)returnsInteger.MIN_VALUEbecause two's complement has no positive representation for-2^31. The fix usingMath.floorModis the canonical Java remedy — it is defined to always return a value in[0, n-1]regardless of the sign of the dividend, with no overflow case.The SpotBugs rule
RV_ABSOLUTE_VALUE_OF_HASHCODEexists precisely for this pattern. Eliminating it is the right call.Production Comment
The inline comment in
computeColoris appropriate here — this is exactly the "why" case that Nora's style guide requires. A future auditor would otherwise remove thefloorModand revert toMath.absas a "simplification."JPA Lifecycle Context
The PR description correctly identifies that
computeColorruns inside@PrePersist,@PreUpdate, and@PostLoadcallbacks. An uncaughtArrayIndexOutOfBoundsExceptionduring@PrePersistwould cause the entity save to fail with a JPA exception, which would bubble through theGlobalExceptionHandleras a 500. WithPALETTE.length = 8this was not triggerable, but the fix eliminates the latent risk entirely and prevents it from surfacing if the palette ever grows to a non-power-of-2 size.No New Concerns
No auth or authorization changes. No logging of sensitive data. No new endpoints. LGTM.
🏛️ Markus Keller — Application Architect
Verdict: ✅ Approved
Architecture Check
AppUser.computeColor— a pure static method. No service, no repository, no module boundary touched.computeColoris correctly placed in the entity (as my comment on the issue recommended — do not move it to a utility class). The color derivation is the user domain's responsibility.deriveColor()only sets color when it's null/empty, so persisted values are immutable after first persist. This is the correct pattern for a computed field used for visual identity. The fix doesn't change this behaviour.Documentation Check
Per my table: no new migrations, no new packages or controllers, no new routes, no new ErrorCode or Permission values. No doc updates required. ✓
Nothing to Flag
Clean, minimal, self-contained fix. The approach of using
Math.floorModis idiomatic Java and has been standard since Java 8. No architectural concerns.🧪 Sara Holt — Senior QA Engineer
Verdict: ⚠️ Approved with concerns
Test Coverage Assessment
What the test does well:
computeColor_returnsValidPaletteColorForIntegerMinValueHash— reads as a sentence, passes my naming check.assertThat(minHashId.hashCode()).isEqualTo(Integer.MIN_VALUE)— this is critical. If Java ever changesUUID.hashCode()(unlikely but defensive), the assumption fails loudly rather than silently passing with the wrong UUID.Concern: No true RED phase
The PR description is honest:
Integer.MIN_VALUE % 8 = 0(because 8 = 2³ divides 2³¹ exactly), so the old code returnsPALETTE[0]without throwing, and the test passes even before the fix. This is an acknowledged deviation from the red/green discipline.For this specific type of fix (static analysis finding for a latent arithmetic bug), an achievable RED test would require either:
PALETTE.lengthto an odd number in the test (not realistic without production code modification), orNeither is worth doing for an XS fix. I accept the pragmatic approach but would flag it: this sets a precedent that some SpotBugs fixes won't have a RED test, and that's OK when the reasoning is documented — as it is here.
Missing coverage (suggestion, non-blocking):
Sara's issue comment recommended two tests — one arithmetic boundary and one
doesNotThrowAnyExceptioncoverage test. Only one was added. ThedoesNotThrowAnyExceptionis arguably redundant given the arithmetic is correct, but it would give more behavioral coverage:Not a blocker — the existing test plus the fix are sufficient. Just noting the gap.
Full Suite
1,547 tests pass. No regressions. The XS scope is clean.
⚙️ Tobias Wendt — DevOps & Platform Engineer
Verdict: ✅ Approved
No infrastructure, CI, Docker Compose, or deployment changes in this PR. Two Java files changed, both backend-only.
One relevant note from my issue review: the acceptance criterion "SpotBugs no longer reports
RV_ABSOLUTE_VALUE_OF_HASHCODE" requires running./mvnw verify(not just./mvnw test) if SpotBugs is wired into the verify phase. The PR description doesn't mention verify output, but the fix is mathematically correct — the SpotBugs rule flags exactly theMath.abs(hashCode())pattern, which is now gone.The fix is safe to deploy without downtime:
coloris derived on load if empty; existing persisted values are unchanged; no Flyway migration required. Nothing to flag from a platform perspective.📋 Elicit — Requirements Engineer
Verdict: ✅ Approved
Requirements Traceability
Issue #471 acceptance criteria:
AppUser.computeColorusesMath.floorMod(overflow-safe arithmetic)RV_ABSOLUTE_VALUE_OF_HASHCODEis eliminated (the pattern is gone)Integer.MIN_VALUEcaseThe issue's note about
PALETTE.length(use the actual length, not 360 from the HSL example) is correctly observed in the fix —Math.floorMod(id.hashCode(), PALETTE.length)not360.No scope creep. No new requirements introduced. The amber palette contrast issue (raised by Leonie in the issue comments) is correctly deferred to a separate issue. Exactly what a well-scoped XS fix should look like.
🎨 Leonie Voss — UX Designer & Accessibility Strategist
Verdict: ✅ Approved
Backend-only arithmetic fix — no Svelte components, no CSS, no i18n strings, no layout changes.
Visual impact:
Math.floorModreturns the same result as the oldMath.abs(x) % nfor all hash values exceptInteger.MIN_VALUE. For theInteger.MIN_VALUEedge case withPALETTE.length = 8, the result is also identical (0in both cases —PALETTE[0] = "#7a4f9a"). So zero visual change for any existing user.Amber palette concern: I flagged
#c17a00(WCAG AA contrast failure for white text at 10px) in the issue discussion. This PR correctly defers that to a separate accessibility issue to keep the scope clean. I'll track it independently.No UX impact to review. LGTM.