Skip to content

4.X: fix bitfield off-by-one encoding - #108

Merged
Tejasshack merged 2 commits into
IABTechLab:4.Xfrom
yuzawa-san:4.X-roundtrip-check
Sep 30, 2026
Merged

Tejasshack merged 2 commits into
IABTechLab:4.Xfrom
yuzawa-san:4.X-roundtrip-check

Conversation

@yuzawa-san

Copy link
Copy Markdown
Contributor

this addresses the feedback done in #83 (comment)

from one of the tcf specs:

PurposeConsent
Bitfield(24)

One bit for each Purpose:
1 Consent
0 No Consent
The user’s consent value for each Purpose established on the legal basis of consent.

The Purposes are numerically identified and published in the Global Vendor List. From left to right, Purpose 1 maps to the 0th bit, purpose 24 maps to the bit at index 23. Special Purposes are a different ID space and not included in this field.

@Tejasshack

Copy link
Copy Markdown
Collaborator

PR 108 — Fixed Bitfield

  1. The encoding change matches the specification.
    The spec defines bit 0 as ID 1, bit 1 as ID 2, and so on. The change from containsInt(i) to containsInt(i + 1) correctly aligns encoding with the existing decode behavior. This is not a spec issue.

  2. Actual bug: the last valid ID cannot be stored through the API.
    EncodableFixedBitfield.initialize() creates the IntegerSet with adjustment = 0 and to = numElements, so the set only accepts IDs 0..numElements-1. After the encoding fix, the valid IDs are 1..numElements.

  3. Example:
    For Bitfield(24), IDs 1..23 work, but ID 24 is rejected by IntegerSet.addInt() and is never stored. Therefore, Purpose 24 cannot be set through setFieldValue().

  4. Recommended fix:
    Initialize the set with adjustment = 1:

    @Override
    protected IntegerSet initialize() {
      return new IntegerSet(new BitSet(numElements), 0, numElements, 1);
    }
  5. Add a boundary round-trip test.
    Please verify the highest ID, not only middle values:

    Set<Integer> purposeConsents = Set.of(1, 24);
    // encode -> decode -> assert equality

    The same boundary check should be added for Bitfield(12) so Special Feature Opt-In ID 12 also survives setFieldValue().

Main point: PR 108 is spec-correct on bit indexing; the remaining issue is an off-by-one storage bug that prevents the highest valid ID from being set.

Specification for reference
https://github.com/InteractiveAdvertisingBureau/Global-Privacy-Platform

@Tejasshack
Tejasshack self-requested a review September 21, 2026 20:55
@yuzawa-san

Copy link
Copy Markdown
Contributor Author

@Tejasshack i believe i have applied the desired fixes in my most recent commits. i actually dropped support for IntegerSet with adjustment of 0 (it is now always 1) and the GppModelTest.consistencyTest does the range 1...24 in one of its cases successfully.

@Tejasshack

Copy link
Copy Markdown
Collaborator

@yuzawa-san I’ve reviewed the changes. Thanks for adding the fixes. This looks good to me.

@chuff chuff 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.

Verified locally: pulled the branch, ran mvn test (344 tests, 0 failures). The off-by-one fix removes the adjustment abstraction and bakes in 1-based indexing directly in IntegerSet, consistent with every remaining call site (vendor ranges, purpose bitfields, custom-purpose flexible bitfields) which are all 1-indexed per the TCF spec. Boundary case (purpose/vendor 24) round-trips correctly. LGTM.

@Tejasshack
Tejasshack merged commit 5342c29 into IABTechLab:4.X Sep 30, 2026
2 checks passed
Tejasshack added a commit that referenced this pull request Sep 30, 2026
Co-authored-by: Cursor <cursoragent@cursor.com>
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.

3 participants