Skip to content

fix/issue 222#230

Merged
Jakubk15 merged 11 commits into
masterfrom
fix/issue-222
Jul 3, 2026
Merged

fix/issue 222#230
Jakubk15 merged 11 commits into
masterfrom
fix/issue-222

Conversation

@Jakubk15

@Jakubk15 Jakubk15 commented Jul 2, 2026

Copy link
Copy Markdown
Member
  • fix: strip null/empty item stacks from parcel content (Exception in future due to NPE #221)
  • fix: stop using legacy ItemStack serialization for parcel content
  • Update src/main/java/com/eternalcode/parcellockers/content/ParcelContent.java
  • test: cover null items list in ParcelContent guard
  • fix: guard against null parcel description in AdminParcelEditGui
  • fix: restrict parcel collection to destination locker (Every parcel can be collected from every locker #222)
  • refactor: filter collectible parcels in the query, not after paging

Jakubk15 and others added 9 commits June 22, 2026 18:59
Empty/air inventory slots could slip past the GUI write filters and
round-trip through the persister as null elements, causing a
NullPointerException when CollectionGui read itemStack.getType() on a
collected parcel.

Normalize the item list in the ParcelContent record's compact
constructor so null and empty stacks are dropped and the exposed list is
immutable. This is the single chokepoint every read and write path goes
through, so it heals already-corrupted rows on read and prevents bad
writes.

Adds Mockito to mock ItemStack in unit tests, since paper-api ItemStacks
cannot be constructed without a running server (RegistryAccess).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R4YqhTV42FvN95HanC7XJc
Legacy (Spigot map) ItemStack serialization drops empty/air stacks to
null, the upstream cause of the issue #221 NPE. The jackson-bukkit Paper
module defaults to an NBT byte-array format (ItemStack#serializeAsBytes)
that round-trips empties safely, and its deserializer auto-detects the
format, so previously stored legacy-format rows still read correctly and
are rewritten in the new format on next save.

The ParcelContent null/empty guard remains as defense in depth.

Note: the serialization round-trip cannot be exercised by the existing
test harness (paper-api ItemStacks require a running server, and the
integration tests are Docker-gated), so this relies on the library's
documented backward-compatible auto-detection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R4YqhTV42FvN95HanC7XJc
…ent.java

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Adds a case verifying the compact constructor defaults a null items list
to an empty list, closing the coverage gap on the defensive guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R4YqhTV42FvN95HanC7XJc
A parcel description may legitimately be null (SendingGui stores a blank
description as null). AdminParcelEditGui.button() passed the value straight
to String.replace(), which throws NPE when the replacement is null, crashing
the admin edit GUI on InventoryClickEvent.

Coerce null placeholder values to an empty string via a testable
nullToEmpty() helper and cover it with unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Egi4whB4fVKQL3XH4PpkX9
Parcels were collectible from any locker because CollectionGui listed
every DELIVERED parcel of the receiver regardless of the locker opened.
Filter the collection list to parcels destined for the current locker,
guarded by the new PluginConfig.Settings#allowCollectingFromAnyLocker
flag (default false) which restores the legacy collect-anywhere behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R4YqhTV42FvN95HanC7XJc
CollectionGui paged receiver parcels (28/page) then filtered DELIVERED
and destination-locker client-side, so a page could render fewer than its
size and the has-next probe counted ineligible rows. Move the receiver +
DELIVERED (+ optional destination locker) filter into the ORMLite query via
findCollectible, so pagination operates on the eligible set. Drops the now
redundant client-side filters and the unused Parcel#isDestinedFor helper;
adds a Docker-gated integration test for the paged query.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R4YqhTV42FvN95HanC7XJc

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces a new configuration option allowCollectingFromAnyLocker to control whether players can collect parcels from any locker or only their designated destination locker. It refactors parcel retrieval to filter collectible parcels (status DELIVERED and matching locker, if restricted) directly in the database query rather than in-memory, improving pagination consistency. Additionally, it addresses NPE issues (such as Issue #221) by filtering out null/empty items in ParcelContent and removing legacy item stack serialization in ItemStackPersister, and adds a helper to handle null placeholders in AdminParcelEditGui. Feedback is provided regarding a potential logic bypass in GuiManager#getCollectibleParcels where a null currentLocker could unintentionally bypass the locker restriction when allowCollectingFromAnyLocker is false.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +62 to 65
public CompletableFuture<PageResult<Parcel>> getCollectibleParcels(UUID receiver, UUID currentLocker, Page page) {
UUID destinationLocker = this.allowCollectingFromAnyLocker ? null : currentLocker;
return this.parcelService.getCollectible(receiver, destinationLocker, page);
}

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.

security-high high

If allowCollectingFromAnyLocker is false but currentLocker is null, destinationLocker will be set to null. In ParcelRepositoryOrmLite#findCollectible, a null destination locker bypasses the locker restriction filter entirely, allowing the player to collect parcels from any locker. To prevent this security/logic bypass, we should ensure that a non-existent/dummy UUID is used when currentLocker is null and restriction is active.

Suggested change
public CompletableFuture<PageResult<Parcel>> getCollectibleParcels(UUID receiver, UUID currentLocker, Page page) {
UUID destinationLocker = this.allowCollectingFromAnyLocker ? null : currentLocker;
return this.parcelService.getCollectible(receiver, destinationLocker, page);
}
public CompletableFuture<PageResult<Parcel>> getCollectibleParcels(UUID receiver, UUID currentLocker, Page page) {
UUID destinationLocker = this.allowCollectingFromAnyLocker ? null : (currentLocker != null ? currentLocker : new UUID(0L, 0L));
return this.parcelService.getCollectible(receiver, destinationLocker, page);
}

Jakubk15 and others added 2 commits July 2, 2026 22:10
Addresses PR #230 review: getCollectibleParcels passed currentLocker
straight through, and getCollectible/findCollectible treat a null
destination locker as "any locker". A null currentLocker while
allowCollectingFromAnyLocker is false would therefore silently drop the
locker restriction (fail-open authorization bypass). Not reachable today
(the locker UUID always comes from a real Locker via
LockerInteractionController), but the invariant was implicit. Make it
explicit and fail-closed with Objects.requireNonNull, and only pass null
on the deliberate collect-from-any-locker path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWGThsFyex5LiEyN63L3Wu
@Jakubk15 Jakubk15 merged commit 1e2041e into master Jul 3, 2026
1 check passed
@Jakubk15

Jakubk15 commented Jul 4, 2026

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Jul 4, 2026

Copy link
Copy Markdown

Claude finished @Jakubk15's task in 1m 22s —— View job


Review of fix/issue-222 (merged)

Todo list:

  • Gather context (PR diff, changed files, prior review comments)
  • Read changed files in detail
  • Verify/assess the Gemini bot's flagged issue in GuiManager.getCollectibleParcels
  • Review remaining changed files for bugs/quality issues
  • Post final review

Gemini's flagged bug — resolved correctly

Gemini flagged that a null currentLocker with allowCollectingFromAnyLocker = false would silently bypass the locker restriction (since findCollectible treats null destination locker as "any locker"). Checking GuiManager.java:67-74 on the current master, this has already been fixed — but with a better approach than Gemini's suggested dummy-UUID sentinel:

public CompletableFuture<PageResult<Parcel>> getCollectibleParcels(UUID receiver, UUID currentLocker, Page page) {
    if (this.allowCollectingFromAnyLocker) {
        return this.parcelService.getCollectible(receiver, null, page);
    }
    Objects.requireNonNull(currentLocker,
        "currentLocker must not be null when collection is restricted to the destination locker");
    return this.parcelService.getCollectible(receiver, currentLocker, page);
}

Failing fast with requireNonNull is preferable to a dummy new UUID(0L, 0L) sentinel — the latter would silently return an empty (but technically not bypassed) result set if a caller ever passed null, masking a caller bug. requireNonNull surfaces it immediately.

I traced the only caller path (LockerInteractionControllerLockerGui.showCollectionGui) and currentLocker/entryLocker always originates from a real Locker.uuid(), so this can't currently throw in practice — the guard is a correct defensive invariant, not a live bug.

Rest of the diff

  • ParcelRepositoryOrmLite.findCollectible — filtering DELIVERED status + optional destination-locker match directly in the ORMLite query builder is the right fix for the original pagination bug (previously filtering post-page would under-fill/mis-count pages).
  • ParcelFindCollectibleIntegrationTest — good coverage: verifies pagination counts only eligible parcels across the restricted-locker case and the allowCollectingFromAnyLocker/null-locker case.
  • PluginConfig.allowCollectingFromAnyLocker — defaults to false (secure by default, matches the new restrictive behavior), with a clear comment describing both modes.
  • ParcelService/ParcelRepository interfaces and impls — consistent null-checks (Objects.requireNonNull) added on the new parameter path; Javadoc on findCollectible/getCollectible clearly documents the null = "any locker" contract, which is what made the original bug easy to introduce and now makes it easy to avoid.

No further issues found. Nice fix — the requireNonNull fail-fast approach is arguably safer than the review suggestion since it converts a security-relevant silent-bypass class of bug into a loud, unmissable failure.

Branch ·

@Jakubk15 Jakubk15 deleted the fix/issue-222 branch July 4, 2026 17:17
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.

1 participant