From 47ddf672e3ee161bfbc5c9c2d718f69160ed13cd Mon Sep 17 00:00:00 2001 From: James Yuzawa Date: Fri, 11 Sep 2026 09:13:14 -0400 Subject: [PATCH 1/2] fix bitfield off-by-one encoding --- .../encoder/FixedBitfieldEncoder.java | 2 +- .../com/iab/gpp/encoder/GppModelTest.java | 18 +++++++----------- .../encoder/FixedBitfieldEncoderTest.java | 19 ++++++++++++------- .../iab/gpp/encoder/section/TcfCaV1Test.java | 2 +- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/encoder/FixedBitfieldEncoder.java b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/encoder/FixedBitfieldEncoder.java index cb1e14a..c6be967 100644 --- a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/encoder/FixedBitfieldEncoder.java +++ b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/encoder/FixedBitfieldEncoder.java @@ -8,7 +8,7 @@ private FixedBitfieldEncoder() {} public static void encode(BitString builder, IntegerSet value, int bitStringLength) { for (int i = 0; i < bitStringLength; i++) { - builder.writeBoolean(value.containsInt(i)); + builder.writeBoolean(value.containsInt(i + 1)); } } } diff --git a/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/GppModelTest.java b/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/GppModelTest.java index 147c340..2fd6098 100644 --- a/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/GppModelTest.java +++ b/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/GppModelTest.java @@ -397,7 +397,7 @@ public void testEncodeUspV1AndTcfEuV2AndTcfCaV1() { String gppString = gppModel.encode(); Assertions.assertEquals( - "DBACOeA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA.IAAA~BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBgABABAAABAB4AACACAAA.fHHHA4444ao~1YNN", + "DBACOeA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA.IAAA~BPSG_8APSG_8AAyACAENGdCg_fA_AA_A_ABgABABAAABAB4AACACAAA.eOOOBxxxwcQ~1YNN", gppString); Assertions.assertEquals(4, gppString.split("~").length); @@ -824,12 +824,10 @@ public void testDecode9() { public void testConsistency() { GppModel fromObjectModel = new GppModel(); - fromObjectModel.setFieldValue( - TcfEuV2.NAME, TcfEuV2Field.PURPOSE_CONSENTS, Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); - fromObjectModel.setFieldValue( - TcfEuV2.NAME, - TcfEuV2Field.VENDOR_CONSENTS, - Arrays.asList(32, 128, 81, 210, 755, 21, 173, 238)); + Set purposeConsents = Set.of(1, 2, 4, 5, 6, 8, 9); + fromObjectModel.setFieldValue(TcfEuV2.NAME, TcfEuV2Field.PURPOSE_CONSENTS, purposeConsents); + Set vendorConsents = Set.of(32, 128, 81, 210, 755, 21, 173, 238); + fromObjectModel.setFieldValue(TcfEuV2.NAME, TcfEuV2Field.VENDOR_CONSENTS, vendorConsents); Assertions.assertEquals( fromObjectModel.getSection(TcfEuV2.NAME).encode(), @@ -839,11 +837,9 @@ public void testConsistency() { GppModel decodedModel = new GppModel(fromObjectModel.encode()); Assertions.assertEquals( - Set.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), - decodedModel.getFieldValue(TcfEuV2.NAME, TcfEuV2Field.PURPOSE_CONSENTS)); + purposeConsents, decodedModel.getFieldValue(TcfEuV2.NAME, TcfEuV2Field.PURPOSE_CONSENTS)); Assertions.assertEquals( - Set.of(21, 32, 81, 128, 173, 210, 238, 755), - decodedModel.getFieldValue(TcfEuV2.NAME, TcfEuV2Field.VENDOR_CONSENTS)); + vendorConsents, decodedModel.getFieldValue(TcfEuV2.NAME, TcfEuV2Field.VENDOR_CONSENTS)); } @Test diff --git a/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/datatype/encoder/FixedBitfieldEncoderTest.java b/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/datatype/encoder/FixedBitfieldEncoderTest.java index e8ea82e..d191d6d 100644 --- a/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/datatype/encoder/FixedBitfieldEncoderTest.java +++ b/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/datatype/encoder/FixedBitfieldEncoderTest.java @@ -13,8 +13,13 @@ public class FixedBitfieldEncoderTest { public void testEncode1() { BitString builder = new BitString(); IntegerSet set = new IntegerSet(); - FixedBitfieldEncoder.encode(builder, set, 2); - Assertions.assertEquals("00", builder.toString()); + set.add(2); + set.add(4); + set.add(6); + FixedBitfieldEncoder.encode(builder, set, 6); + String encoded = builder.toString(); + Assertions.assertEquals("010101", encoded); + Assertions.assertEquals(set, decode(encoded)); } @Test @@ -29,7 +34,7 @@ public void testEncode2() { public void testEncode3() { BitString builder = new BitString(); IntegerSet set = new IntegerSet(); - set.add(0); + set.add(1); FixedBitfieldEncoder.encode(builder, set, 1); Assertions.assertEquals("1", builder.toString()); } @@ -48,24 +53,24 @@ public void testEncode5() { IntegerSet set = new IntegerSet(); set.addInt(1); FixedBitfieldEncoder.encode(builder, set, 2); - Assertions.assertEquals("01", builder.toString()); + Assertions.assertEquals("10", builder.toString()); } @Test public void testEncode6() { BitString builder = new BitString(); IntegerSet set = new IntegerSet(); - set.addInt(0); + set.addInt(2); FixedBitfieldEncoder.encode(builder, set, 2); - Assertions.assertEquals("10", builder.toString()); + Assertions.assertEquals("01", builder.toString()); } @Test public void testEncode7() { BitString builder = new BitString(); IntegerSet set = new IntegerSet(); - set.addInt(0); set.addInt(1); + set.addInt(2); FixedBitfieldEncoder.encode(builder, set, 2); Assertions.assertEquals("11", builder.toString()); } diff --git a/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/section/TcfCaV1Test.java b/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/section/TcfCaV1Test.java index 9c184d5..fcf6749 100644 --- a/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/section/TcfCaV1Test.java +++ b/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/section/TcfCaV1Test.java @@ -66,7 +66,7 @@ public void testEncode2() { ZonedDateTime.of(2022, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC")).toInstant()); Assertions.assertEquals( - "BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBgABABAAABAB4AACACAAA.fHHHA4444ao", tcfCaV1.encode()); + "BPSG_8APSG_8AAyACAENGdCg_fA_AA_A_ABgABABAAABAB4AACACAAA.eOOOBxxxwcQ", tcfCaV1.encode()); } @Test From 7658f05901d9d2bbf5f2be3281c0a53b23929e02 Mon Sep 17 00:00:00 2001 From: James Yuzawa Date: Tue, 22 Sep 2026 19:11:40 -0400 Subject: [PATCH 2/2] clean up integerset --- .../iab/gpp/encoder/bitstring/BitString.java | 2 +- .../iab/gpp/encoder/datatype/IntegerSet.java | 14 ++++++------- .../com/iab/gpp/encoder/GppModelTest.java | 20 +++++++++---------- .../encoder/FixedBitfieldEncoderTest.java | 8 ++++---- .../datatype/encoder/IntegerSetTest.java | 14 ++++++------- .../iab/gpp/encoder/section/TcfCaV1Test.java | 6 +++--- 6 files changed, 30 insertions(+), 34 deletions(-) diff --git a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/bitstring/BitString.java b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/bitstring/BitString.java index f876bd0..6f4b4c6 100644 --- a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/bitstring/BitString.java +++ b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/bitstring/BitString.java @@ -205,7 +205,7 @@ public IntegerSet readIntegerSet(int length) { if (newReadIndex > writeIndex) { throw new DecodingException("Bit string access out of range"); } - IntegerSet out = new IntegerSet(bitSet, readIndex, newReadIndex, 1); + IntegerSet out = new IntegerSet(bitSet, readIndex, newReadIndex); readIndex = newReadIndex; return out; } diff --git a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/IntegerSet.java b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/IntegerSet.java index 19c0299..1d079a3 100644 --- a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/IntegerSet.java +++ b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/IntegerSet.java @@ -25,21 +25,19 @@ public final class IntegerSet extends AbstractSet implements Dirtyable private final BitSet bitSet; private final int from; private final int to; - private final int adjustment; - public IntegerSet(BitSet bitSet, int from, int to, int adjustment) { + public IntegerSet(BitSet bitSet, int from, int to) { this.bitSet = bitSet; this.from = from; this.to = to; - this.adjustment = adjustment; } public IntegerSet(int limit) { - this(new BitSet(limit), 0, limit, 0); + this(new BitSet(limit), 0, limit); } public IntegerSet() { - this(new BitSet(), 0, MAX_COLLECTION_SIZE, 0); + this(new BitSet(), 0, MAX_COLLECTION_SIZE); } @Override @@ -54,7 +52,7 @@ public int size() { } private int getOffset(int value) { - int offset = from - adjustment + value; + int offset = from - 1 + value; if (offset < from) { throw new IndexOutOfBoundsException("Negative index provided"); } @@ -73,7 +71,7 @@ public boolean isEmpty() { } public boolean containsInt(int value) { - if (value < adjustment) { + if (value < 1) { return false; } int offset = getOffset(value); @@ -102,7 +100,7 @@ public int nextInt() { } int next = cursor; cursor = bitSet.nextSetBit(cursor + 1); - return next - from + adjustment; + return next - from + 1; } }; } diff --git a/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/GppModelTest.java b/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/GppModelTest.java index 2fd6098..e72ec06 100644 --- a/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/GppModelTest.java +++ b/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/GppModelTest.java @@ -362,7 +362,7 @@ public void testEncodeUspV1AndTcfEuV2AndTcfCaV1() { gppModel.setFieldValue( TcfCaV1.NAME, TcfCaV1Field.PURPOSES_EXPRESS_CONSENT, - Arrays.asList(0, 1, 2, 3, 4, 5, 12, 13, 14, 15, 16, 17)); + Arrays.asList(1, 2, 3, 4, 5, 12, 13, 14, 15, 16, 17)); gppModel.setFieldValue( TcfCaV1.NAME, TcfCaV1Field.PURPOSES_IMPLIED_CONSENT, @@ -374,15 +374,14 @@ public void testEncodeUspV1AndTcfEuV2AndTcfCaV1() { gppModel.setFieldValue( TcfCaV1.NAME, TcfCaV1Field.PUB_PURPOSES_EXPRESS_CONSENT, - Arrays.asList(0, 1, 2, 6, 7, 8, 12, 13, 14, 18, 19, 20)); + Arrays.asList(1, 2, 6, 7, 8, 12, 13, 14, 18, 19, 20)); gppModel.setFieldValue( TcfCaV1.NAME, TcfCaV1Field.PUB_PURPOSES_IMPLIED_CONSENT, Arrays.asList(3, 4, 5, 9, 10, 11, 15, 16, 17, 21, 22, 23)); gppModel.setFieldValue(TcfCaV1.NAME, TcfCaV1Field.NUM_CUSTOM_PURPOSES, 3); gppModel.setFieldValue(TcfCaV1.NAME, TcfCaV1Field.CUSTOM_PURPOSES_EXPRESS_CONSENT, List.of(1)); - gppModel.setFieldValue( - TcfCaV1.NAME, TcfCaV1Field.CUSTOM_PURPOSES_IMPLIED_CONSENT, List.of(0, 2)); + gppModel.setFieldValue(TcfCaV1.NAME, TcfCaV1Field.CUSTOM_PURPOSES_IMPLIED_CONSENT, List.of(2)); gppModel.setFieldValue(TcfCaV1.NAME, TcfCaV1Field.CREATED, utcDateTime); gppModel.setFieldValue(TcfCaV1.NAME, TcfCaV1Field.LAST_UPDATED, utcDateTime); @@ -824,22 +823,21 @@ public void testDecode9() { public void testConsistency() { GppModel fromObjectModel = new GppModel(); - Set purposeConsents = Set.of(1, 2, 4, 5, 6, 8, 9); + Set purposeConsents = Set.of(1, 2, 23, 24); fromObjectModel.setFieldValue(TcfEuV2.NAME, TcfEuV2Field.PURPOSE_CONSENTS, purposeConsents); Set vendorConsents = Set.of(32, 128, 81, 210, 755, 21, 173, 238); fromObjectModel.setFieldValue(TcfEuV2.NAME, TcfEuV2Field.VENDOR_CONSENTS, vendorConsents); - Assertions.assertEquals( - fromObjectModel.getSection(TcfEuV2.NAME).encode(), - fromObjectModel.getSection(TcfEuV2.NAME).encode()); - Assertions.assertEquals(fromObjectModel.encode(), fromObjectModel.encode()); - - GppModel decodedModel = new GppModel(fromObjectModel.encode()); + String encoded = fromObjectModel.encode(); + GppModel decodedModel = new GppModel(encoded); Assertions.assertEquals( purposeConsents, decodedModel.getFieldValue(TcfEuV2.NAME, TcfEuV2Field.PURPOSE_CONSENTS)); Assertions.assertEquals( vendorConsents, decodedModel.getFieldValue(TcfEuV2.NAME, TcfEuV2Field.VENDOR_CONSENTS)); + + decodedModel.setDirty(true); + Assertions.assertEquals(encoded, decodedModel.encode()); } @Test diff --git a/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/datatype/encoder/FixedBitfieldEncoderTest.java b/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/datatype/encoder/FixedBitfieldEncoderTest.java index d191d6d..6340370 100644 --- a/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/datatype/encoder/FixedBitfieldEncoderTest.java +++ b/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/datatype/encoder/FixedBitfieldEncoderTest.java @@ -78,17 +78,17 @@ public void testEncode7() { @Test public void testEncode8() { IntegerSet set = new IntegerSet(5); - for (int i = 0; i <= 10; i++) { + for (int i = 1; i <= 10; i++) { set.addInt(i); } - Assertions.assertEquals(Set.of(0, 1, 2, 3, 4), set); + Assertions.assertEquals(Set.of(1, 2, 3, 4, 5), set); } @Test public void testEncode9() { IntegerSet set = new IntegerSet(5); - set.addRange(0, 10); - Assertions.assertEquals(Set.of(0, 1, 2, 3, 4), set); + set.addRange(1, 10); + Assertions.assertEquals(Set.of(1, 2, 3, 4, 5), set); } private IntegerSet decode(String str) { diff --git a/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/datatype/encoder/IntegerSetTest.java b/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/datatype/encoder/IntegerSetTest.java index 63e5e76..301edb2 100644 --- a/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/datatype/encoder/IntegerSetTest.java +++ b/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/datatype/encoder/IntegerSetTest.java @@ -17,29 +17,29 @@ void test() { IntegerSet set = new IntegerSet(5); assertFalse(set.isDirty()); assertTrue(set.isEmpty()); - set.add(0); + set.add(1); set.add(2); set.add(3); - assertEquals(Set.of(0, 2, 3), set); + assertEquals(Set.of(1, 2, 3), set); assertTrue(set.isDirty()); assertFalse(set.isEmpty()); assertEquals(3, set.size()); - assertFalse(set.remove(1)); + assertFalse(set.remove(4)); assertTrue(set.remove(2)); assertEquals(2, set.size()); set.addAll(List.of(3, 4)); - assertEquals(Set.of(0, 3, 4), set); + assertEquals(Set.of(1, 3, 4), set); List out = new ArrayList<>(); Iterator it = set.iterator(); it.forEachRemaining(out::add); - assertEquals(List.of(0, 3, 4), out); + assertEquals(List.of(1, 3, 4), out); assertFalse(set.contains(2)); assertTrue(set.contains(3)); assertTrue(set.isDirty()); set.setDirty(false); assertFalse(set.isDirty()); set.retainAll(Set.of(1, 2, 3)); - assertEquals(Set.of(3), set); + assertEquals(Set.of(1, 3), set); assertTrue(set.addAll(List.of(1, 3, 4))); assertFalse(set.addAll(List.of(1, 3, 4))); assertTrue(set.removeAll(List.of(3, 4))); @@ -59,7 +59,7 @@ void boundsTest() { bitSet.set(12); bitSet.set(13); - IntegerSet set = new IntegerSet(bitSet, 10, 13, 1); + IntegerSet set = new IntegerSet(bitSet, 10, 13); assertEquals(Set.of(1, 2, 3), set); assertThrows(IndexOutOfBoundsException.class, () -> set.add(0)); diff --git a/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/section/TcfCaV1Test.java b/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/section/TcfCaV1Test.java index fcf6749..2a6344e 100644 --- a/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/section/TcfCaV1Test.java +++ b/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/section/TcfCaV1Test.java @@ -42,7 +42,7 @@ public void testEncode2() { TcfCaV1Field.SPECIAL_FEATURE_EXPRESS_CONSENT, Arrays.asList(6, 7, 8, 9, 10, 11)); tcfCaV1.setFieldValue( TcfCaV1Field.PURPOSES_EXPRESS_CONSENT, - Arrays.asList(0, 1, 2, 3, 4, 5, 12, 13, 14, 15, 16, 17)); + Arrays.asList(1, 2, 3, 4, 5, 12, 13, 14, 15, 16, 17)); tcfCaV1.setFieldValue( TcfCaV1Field.PURPOSES_IMPLIED_CONSENT, Arrays.asList(6, 7, 8, 9, 10, 11, 18, 19, 20, 21, 22, 23)); @@ -50,13 +50,13 @@ public void testEncode2() { tcfCaV1.setFieldValue(TcfCaV1Field.VENDOR_IMPLIED_CONSENT, Arrays.asList(18, 30)); tcfCaV1.setFieldValue( TcfCaV1Field.PUB_PURPOSES_EXPRESS_CONSENT, - Arrays.asList(0, 1, 2, 6, 7, 8, 12, 13, 14, 18, 19, 20)); + Arrays.asList(1, 2, 6, 7, 8, 12, 13, 14, 18, 19, 20)); tcfCaV1.setFieldValue( TcfCaV1Field.PUB_PURPOSES_IMPLIED_CONSENT, Arrays.asList(3, 4, 5, 9, 10, 11, 15, 16, 17, 21, 22, 23)); tcfCaV1.setFieldValue(TcfCaV1Field.NUM_CUSTOM_PURPOSES, 3); tcfCaV1.setFieldValue(TcfCaV1Field.CUSTOM_PURPOSES_EXPRESS_CONSENT, Set.of(1)); - tcfCaV1.setFieldValue(TcfCaV1Field.CUSTOM_PURPOSES_IMPLIED_CONSENT, Set.of(0, 2)); + tcfCaV1.setFieldValue(TcfCaV1Field.CUSTOM_PURPOSES_IMPLIED_CONSENT, Set.of(2)); tcfCaV1.setFieldValue( TcfCaV1Field.CREATED,