From 12c26db0c60c2f58007a4aa047f036d936f10924 Mon Sep 17 00:00:00 2001 From: Chad Huff Date: Sun, 14 Jun 2026 15:22:42 -0600 Subject: [PATCH 1/2] Fix TcfCaV1 (Canada) OptimizedRange encoding to use Fibonacci Per the GPP Consent String Specification, the Canada section encodes several fields as OptimizedRange / N-ArrayOfRanges, both of which use Fibonacci coding. They were using fixed-integer encoders. - VendorExpressConsent, VendorImpliedConsent, DisclosedVendors (OptimizedRange): EncodableOptimizedFixedRange -> EncodableOptimizedFibonacciRange (new), backed by a new OptimizedFibonacciRangeEncoder. - PubRestrictions (N-ArrayOfRanges(6,2), whose ids are an OptimizedRange): EncodableArrayOfFixedIntegerRanges -> EncodableArrayOfOptimizedFibonacciRanges (new). The fixed variant is left untouched for TCF EU. Output is byte-identical to the master-based fix (PR #105): updated test vectors match (e.g. vendor section ...BhADVqxGAD0AILVgAA, PubRestrictions ...CCgAS7o). This PR is the encoding fix only; backwards-compatible decoding of pre-fix (fixed-range) strings follows in a separate PR stacked on this one. mvn test: 344 tests, 0 failures; spotless:check clean. Co-Authored-By: Claude Opus 4.8 --- ...odableArrayOfOptimizedFibonacciRanges.java | 76 +++++++++++++++++++ .../EncodableOptimizedFibonacciRange.java | 43 +++++++++++ .../OptimizedFibonacciRangeEncoder.java | 44 +++++++++++ .../iab/gpp/encoder/field/TcfCaV1Field.java | 12 +-- .../com/iab/gpp/encoder/GppModelTest.java | 4 +- .../iab/gpp/encoder/section/TcfCaV1Test.java | 10 +-- 6 files changed, 175 insertions(+), 14 deletions(-) create mode 100644 iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/EncodableArrayOfOptimizedFibonacciRanges.java create mode 100644 iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/EncodableOptimizedFibonacciRange.java create mode 100644 iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/encoder/OptimizedFibonacciRangeEncoder.java diff --git a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/EncodableArrayOfOptimizedFibonacciRanges.java b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/EncodableArrayOfOptimizedFibonacciRanges.java new file mode 100644 index 0000000..385a01f --- /dev/null +++ b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/EncodableArrayOfOptimizedFibonacciRanges.java @@ -0,0 +1,76 @@ +package com.iab.gpp.encoder.datatype; + +import com.iab.gpp.encoder.bitstring.BitString; +import com.iab.gpp.encoder.datatype.encoder.OptimizedFibonacciRangeEncoder; +import com.iab.gpp.encoder.field.FieldKey; +import com.iab.gpp.encoder.segment.EncodableSegment; +import java.util.List; + +/** + * Encodes an {@code N-ArrayOfRanges(X,Y)} field where each record's ids are encoded as an {@code + * OptimizedRange} (Fibonacci coded), per the GPP Consent String Specification. Counterpart to + * {@link EncodableArrayOfFixedIntegerRanges}, which encodes ids using fixed-integer ranges for + * downward compatibility (e.g. TCF EU). + */ +public final class EncodableArrayOfOptimizedFibonacciRanges & FieldKey> + extends AbstractDirtyableBitStringDataType> { + + private final int keyBitStringLength; + private final int typeBitStringLength; + + public EncodableArrayOfOptimizedFibonacciRanges( + String name, int keyBitStringLength, int typeBitStringLength) { + super(name, null); + this.keyBitStringLength = keyBitStringLength; + this.typeBitStringLength = typeBitStringLength; + } + + @Override + public String toString() { + return name + "=N-ArrayOfRanges(" + keyBitStringLength + "," + typeBitStringLength + ")"; + } + + @Override + protected DirtyableList initialize() { + return new DirtyableList<>(); + } + + @Override + protected boolean isPresent(DirtyableList value) { + return !value.isEmpty(); + } + + @Override + protected void encode( + BitString sb, DirtyableList entries, EncodableSegment segment) { + sb.writeInt(entries.size(), 12); + for (RangeEntry entry : entries) { + sb.writeInt(entry.getKey(), keyBitStringLength); + sb.writeInt(entry.getType(), typeBitStringLength); + OptimizedFibonacciRangeEncoder.encode(sb, entry.getIds()); + } + } + + @Override + protected DirtyableList decode(BitString reader, EncodableSegment segment) { + int size = reader.readInt(12); + DirtyableList value = initialize(); + for (int i = 0; i < size; i++) { + int key = reader.readInt(keyBitStringLength); + int type = reader.readInt(typeBitStringLength); + IntegerSet ids = OptimizedFibonacciRangeEncoder.decode(reader); + RangeEntry entry = new RangeEntry(key, type, ids); + value.add(entry); + } + return value; + } + + @SuppressWarnings("unchecked") + @Override + protected DirtyableList processValue( + DirtyableList oldValue, Object newValue) { + oldValue.clear(); + oldValue.addAll((List) newValue); + return oldValue; + } +} diff --git a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/EncodableOptimizedFibonacciRange.java b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/EncodableOptimizedFibonacciRange.java new file mode 100644 index 0000000..b99c6ad --- /dev/null +++ b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/EncodableOptimizedFibonacciRange.java @@ -0,0 +1,43 @@ +package com.iab.gpp.encoder.datatype; + +import com.iab.gpp.encoder.bitstring.BitString; +import com.iab.gpp.encoder.datatype.encoder.OptimizedFibonacciRangeEncoder; +import com.iab.gpp.encoder.field.FieldKey; +import com.iab.gpp.encoder.segment.EncodableSegment; +import java.util.Collection; + +public final class EncodableOptimizedFibonacciRange & FieldKey> + extends AbstractDirtyableBitStringDataType { + + public EncodableOptimizedFibonacciRange(String name) { + super(name, null); + } + + @Override + protected IntegerSet initialize() { + return new IntegerSet(); + } + + @Override + protected boolean isPresent(IntegerSet value) { + return !value.isEmpty(); + } + + @Override + protected void encode(BitString builder, IntegerSet value, EncodableSegment segment) { + OptimizedFibonacciRangeEncoder.encode(builder, value); + } + + @Override + protected IntegerSet decode(BitString reader, EncodableSegment segment) { + return OptimizedFibonacciRangeEncoder.decode(reader); + } + + @SuppressWarnings("unchecked") + @Override + protected IntegerSet processValue(IntegerSet oldValue, Object newValue) { + oldValue.clear(); + oldValue.addAll((Collection) newValue); + return oldValue; + } +} diff --git a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/encoder/OptimizedFibonacciRangeEncoder.java b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/encoder/OptimizedFibonacciRangeEncoder.java new file mode 100644 index 0000000..9c61123 --- /dev/null +++ b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/encoder/OptimizedFibonacciRangeEncoder.java @@ -0,0 +1,44 @@ +package com.iab.gpp.encoder.datatype.encoder; + +import com.iab.gpp.encoder.bitstring.BitString; +import com.iab.gpp.encoder.datatype.IntegerSet; +import com.iab.gpp.encoder.error.DecodingException; +import com.iab.gpp.encoder.error.EncodingException; + +/** + * Encodes an {@code OptimizedRange} that uses Fibonacci coding for the range representation, per + * the GPP Consent String Specification. Counterpart to {@link OptimizedFixedRangeEncoder}, which + * uses fixed-integer ranges for downward compatibility. + */ +public class OptimizedFibonacciRangeEncoder { + + public static void encode(BitString builder, IntegerSet value) throws EncodingException { + // TODO: encoding the range before choosing the shortest is inefficient. There is probably a way + // to identify in advance which will be shorter based on the array length and values + BitString rangeBitString = new BitString(); + int max = FibonacciIntegerRangeEncoder.encode(rangeBitString, value); + int rangeLength = rangeBitString.length(); + int bitFieldLength = max; + + if (rangeLength <= bitFieldLength) { + builder.writeInt(max, 16); + builder.writeBoolean(true); + builder.write(rangeBitString); + } else { + builder.writeInt(max, 16); + builder.writeBoolean(false); + for (int i = 0; i < max; i++) { + builder.writeBoolean(value.containsInt(i + 1)); + } + } + } + + public static IntegerSet decode(BitString reader) throws DecodingException { + int size = reader.readInt(16); + if (reader.readBoolean()) { + return FibonacciIntegerRangeEncoder.decode(reader); + } else { + return reader.readIntegerSet(size); + } + } +} diff --git a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/field/TcfCaV1Field.java b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/field/TcfCaV1Field.java index 6f264b7..d663a4f 100644 --- a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/field/TcfCaV1Field.java +++ b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/field/TcfCaV1Field.java @@ -1,14 +1,14 @@ package com.iab.gpp.encoder.field; import com.iab.gpp.encoder.datatype.DataType; -import com.iab.gpp.encoder.datatype.EncodableArrayOfFixedIntegerRanges; +import com.iab.gpp.encoder.datatype.EncodableArrayOfOptimizedFibonacciRanges; import com.iab.gpp.encoder.datatype.EncodableBoolean; import com.iab.gpp.encoder.datatype.EncodableDatetime; import com.iab.gpp.encoder.datatype.EncodableFixedBitfield; import com.iab.gpp.encoder.datatype.EncodableFixedInteger; import com.iab.gpp.encoder.datatype.EncodableFixedString; import com.iab.gpp.encoder.datatype.EncodableFlexibleBitfield; -import com.iab.gpp.encoder.datatype.EncodableOptimizedFixedRange; +import com.iab.gpp.encoder.datatype.EncodableOptimizedFibonacciRange; import com.iab.gpp.encoder.section.TcfCaV1; public enum TcfCaV1Field implements FieldKey { @@ -25,9 +25,9 @@ public enum TcfCaV1Field implements FieldKey { SPECIAL_FEATURE_EXPRESS_CONSENT(new EncodableFixedBitfield<>("SpecialFeatureExpressConsent", 12)), PURPOSES_EXPRESS_CONSENT(new EncodableFixedBitfield<>("PurposesExpressConsent", 24)), PURPOSES_IMPLIED_CONSENT(new EncodableFixedBitfield<>("PurposesImpliedConsent", 24)), - VENDOR_EXPRESS_CONSENT(new EncodableOptimizedFixedRange<>("VendorExpressConsent")), - VENDOR_IMPLIED_CONSENT(new EncodableOptimizedFixedRange<>("VendorImpliedConsent")), - PUB_RESTRICTIONS(new EncodableArrayOfFixedIntegerRanges<>("PubRestrictions", 6, 2)), + VENDOR_EXPRESS_CONSENT(new EncodableOptimizedFibonacciRange<>("VendorExpressConsent")), + VENDOR_IMPLIED_CONSENT(new EncodableOptimizedFibonacciRange<>("VendorImpliedConsent")), + PUB_RESTRICTIONS(new EncodableArrayOfOptimizedFibonacciRanges<>("PubRestrictions", 6, 2)), PUB_PURPOSES_SEGMENT_TYPE(new EncodableFixedInteger<>("PubPurposesSegmentType", 3, 3)), PUB_PURPOSES_EXPRESS_CONSENT(new EncodableFixedBitfield<>("PubPurposesExpressConsent", 24)), @@ -41,7 +41,7 @@ public enum TcfCaV1Field implements FieldKey { "CustomPurposesImpliedConsent", TcfCaV1Field.NUM_CUSTOM_PURPOSES)), DISCLOSED_VENDORS_SEGMENT_TYPE(new EncodableFixedInteger<>("DisclosedVendorsSegmentType", 3, 1)), - DISCLOSED_VENDORS(new EncodableOptimizedFixedRange<>("DisclosedVendors")); + DISCLOSED_VENDORS(new EncodableOptimizedFibonacciRange<>("DisclosedVendors")); private final DataType type; 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..9c82670 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_8AAyACAENGdCgf_gfgAfgfgBhADVqxGAD0AILVgAA.fHHHA4444ao~1YNN", gppString); Assertions.assertEquals(4, gppString.split("~").length); @@ -616,7 +616,7 @@ public void testDecodeUspv1AndTcfEuV2() { @Test public void testDecodeUspv1AndTcfEuV2AndTcfCaV1() { String gppString = - "DBACOeA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA.QAAA.IAAA~BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBgABABAAABAB4AACACAAA.fHHHA4444ao~1YNN"; + "DBACOeA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA.QAAA.IAAA~BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBhADVqxGAD0AILVgAA.fHHHA4444ao~1YNN"; GppModel gppModel = new GppModel(gppString); Assertions.assertEquals(Set.of(2, 5, 6), gppModel.getSectionIds()); 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..45fc9b5 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_8AAyACAENGdCgf_gfgAfgfgBhADVqxGAD0AILVgAA.fHHHA4444ao", tcfCaV1.encode()); } @Test @@ -103,8 +103,7 @@ public void testEncode4() throws EncodingException, InvalidFieldException { TcfCaV1Field.LAST_UPDATED, ZonedDateTime.of(2022, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC")).toInstant()); Assertions.assertEquals( - "BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAACCgBwABAAOAAoADgAJA.YAAAAAAAAAA", - tcfCaV1.encode()); + "BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAACCgAS7o.YAAAAAAAAAA", tcfCaV1.encode()); } @Test @@ -143,7 +142,7 @@ public void testDecode1() { @Test public void testDecode2() { TcfCaV1 tcfCaV1 = - new TcfCaV1("BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBgABABAAABAB4AACACAAA.fHHHA4444ao"); + new TcfCaV1("BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBhADVqxGAD0AILVgAA.fHHHA4444ao"); Assertions.assertEquals(50, tcfCaV1.getCmpId()); Assertions.assertEquals(2, tcfCaV1.getCmpVersion()); @@ -186,8 +185,7 @@ public void testDecode3() throws DecodingException { @Test public void testDecode4() throws DecodingException { - TcfCaV1 tcfCaV1 = - new TcfCaV1("BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAACCgBwABAAOAAoADgAJA.YAAAAAAAAAA"); + TcfCaV1 tcfCaV1 = new TcfCaV1("BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAACCgAS7o.YAAAAAAAAAA"); List pubRestictions = tcfCaV1.getPubRestrictions(); Assertions.assertEquals(1, pubRestictions.size()); From db7f26300d4af76c94ffe41dc1f2bf1b6123f921 Mon Sep 17 00:00:00 2001 From: Chad Huff Date: Sun, 14 Jun 2026 15:30:14 -0600 Subject: [PATCH 2/2] Decode legacy fixed-range TcfCaV1 strings (backwards compatibility) Strings produced by the previous encoder used fixed-integer ranges for the Canada OptimizedRange / N-ArrayOfRanges fields. Decode them by trying the spec-compliant (Fibonacci) interpretation first and, when the consumed bits do not re-encode to it, re-reading them under the legacy interpretation: - OptimizedFibonacciRangeEncoder.decode (VendorExpressConsent, VendorImpliedConsent, DisclosedVendors): in the range branch, fall back from a Fibonacci range to a fixed-integer range. - EncodableArrayOfOptimizedFibonacciRanges.decode (PubRestrictions): fall back from OptimizedRange ids to a plain fixed-integer range (the previous EncodableArrayOfFixedIntegerRanges layout). Adds BitString get/setReadIndex to mark/reset the read cursor for the round-trip check. Empty / bitfield-form data is unambiguous and unaffected. Tests decode real pre-fix strings: a fixed-range PubRestrictions string and a full real-world string whose vendor lists use fixed-integer ranges. Stacked on the encoding-fix PR; review/merge that first. mvn test: 346 tests, 0 failures; spotless:check clean. Co-Authored-By: Claude Opus 4.8 --- .../iab/gpp/encoder/bitstring/BitString.java | 10 ++++++ ...odableArrayOfOptimizedFibonacciRanges.java | 30 ++++++++++++++-- .../OptimizedFibonacciRangeEncoder.java | 19 +++++++++- .../iab/gpp/encoder/section/TcfCaV1Test.java | 36 +++++++++++++++++++ 4 files changed, 91 insertions(+), 4 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..a304bef 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 @@ -65,6 +65,16 @@ public boolean hasRemaining() { return readIndex < writeIndex; } + // Used to mark/reset the read cursor when an encoding is ambiguous and may need to be re-read + // under a different interpretation (e.g. legacy fixed vs Fibonacci OptimizedRange). + public int getReadIndex() { + return readIndex; + } + + public void setReadIndex(int readIndex) { + this.readIndex = readIndex; + } + public void writeEmpty(int length) { this.writeIndex += length; } diff --git a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/EncodableArrayOfOptimizedFibonacciRanges.java b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/EncodableArrayOfOptimizedFibonacciRanges.java index 385a01f..e40f517 100644 --- a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/EncodableArrayOfOptimizedFibonacciRanges.java +++ b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/EncodableArrayOfOptimizedFibonacciRanges.java @@ -1,6 +1,7 @@ package com.iab.gpp.encoder.datatype; import com.iab.gpp.encoder.bitstring.BitString; +import com.iab.gpp.encoder.datatype.encoder.FixedIntegerRangeEncoder; import com.iab.gpp.encoder.datatype.encoder.OptimizedFibonacciRangeEncoder; import com.iab.gpp.encoder.field.FieldKey; import com.iab.gpp.encoder.segment.EncodableSegment; @@ -53,14 +54,37 @@ protected void encode( @Override protected DirtyableList decode(BitString reader, EncodableSegment segment) { + // ids are an OptimizedRange in the current spec, but strings produced by the previous encoder + // used a fixed-integer range for ids. Decode the current way and, if the consumed bits do not + // re-encode to the same thing, re-read using the legacy fixed-integer range. + int mark = reader.getReadIndex(); + try { + DirtyableList value = decodeEntries(reader, true); + BitString consumed = new BitString(); + consumed.write(reader, mark, reader.getReadIndex()); + BitString reEncoded = new BitString(); + encode(reEncoded, value, segment); + if (reEncoded.toString().equals(consumed.toString())) { + return value; + } + } catch (RuntimeException e) { + // not the current format; fall back to the legacy fixed-integer range ids + } + reader.setReadIndex(mark); + return decodeEntries(reader, false); + } + + private DirtyableList decodeEntries(BitString reader, boolean optimized) { int size = reader.readInt(12); DirtyableList value = initialize(); for (int i = 0; i < size; i++) { int key = reader.readInt(keyBitStringLength); int type = reader.readInt(typeBitStringLength); - IntegerSet ids = OptimizedFibonacciRangeEncoder.decode(reader); - RangeEntry entry = new RangeEntry(key, type, ids); - value.add(entry); + IntegerSet ids = + optimized + ? OptimizedFibonacciRangeEncoder.decode(reader) + : FixedIntegerRangeEncoder.decode(reader); + value.add(new RangeEntry(key, type, ids)); } return value; } diff --git a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/encoder/OptimizedFibonacciRangeEncoder.java b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/encoder/OptimizedFibonacciRangeEncoder.java index 9c61123..0c0cc77 100644 --- a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/encoder/OptimizedFibonacciRangeEncoder.java +++ b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/encoder/OptimizedFibonacciRangeEncoder.java @@ -36,7 +36,24 @@ public static void encode(BitString builder, IntegerSet value) throws EncodingEx public static IntegerSet decode(BitString reader) throws DecodingException { int size = reader.readInt(16); if (reader.readBoolean()) { - return FibonacciIntegerRangeEncoder.decode(reader); + // Range form. It is Fibonacci-coded in the current spec, but strings produced by the previous + // encoder used a fixed-integer range here. Decode as Fibonacci and, if the consumed bits do + // not re-encode to that same Fibonacci range, re-read them as a fixed-integer range. + int mark = reader.getReadIndex(); + try { + IntegerSet value = FibonacciIntegerRangeEncoder.decode(reader); + BitString consumed = new BitString(); + consumed.write(reader, mark, reader.getReadIndex()); + BitString reEncoded = new BitString(); + FibonacciIntegerRangeEncoder.encode(reEncoded, value); + if (reEncoded.toString().equals(consumed.toString())) { + return value; + } + } catch (RuntimeException e) { + // not a valid Fibonacci range; fall back to the legacy fixed-integer range + } + reader.setReadIndex(mark); + return FixedIntegerRangeEncoder.decode(reader); } else { return reader.readIntegerSet(size); } 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 45fc9b5..98bbcfa 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 @@ -194,6 +194,42 @@ public void testDecode4() throws DecodingException { Assertions.assertEquals(Set.of(1, 2, 3, 5, 6, 7, 9), pubRestictions.get(0).getIds()); } + @Test + public void testDecodeLegacyFixedRangePubRestrictions() throws DecodingException { + // String produced by the previous encoder, which encoded PubRestrictions ids as a plain + // fixed-integer range. The backwards-compatible decoder must still read it. + TcfCaV1 tcfCaV1 = + new TcfCaV1("BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAACCgBwABAAOAAoADgAJA.YAAAAAAAAAA"); + + List pubRestrictions = tcfCaV1.getPubRestrictions(); + Assertions.assertEquals(1, pubRestrictions.size()); + Assertions.assertEquals(1, pubRestrictions.get(0).getKey()); + Assertions.assertEquals(1, pubRestrictions.get(0).getType()); + Assertions.assertEquals(Set.of(1, 2, 3, 5, 6, 7, 9), pubRestrictions.get(0).getIds()); + } + + @Test + public void testDecodeLegacyFixedRangeVendors() throws DecodingException { + // A real string produced by the previous encoder, which encoded the vendor OptimizedRange + // fields using fixed-integer ranges. The backwards-compatible decoder must still read it. + TcfCaV1 tcfCaV1 = + new TcfCaV1( + "BQliWsAQliWsAPoABAELC9CoAKgAAJIAAApNAOABUAC0AGgAQwAlgBQAC6AG0AO4AfgBBATAAnMBSYEwYFgAXQBOwC3ALgAc4A7gCAAEmAJ2AT8AxQBmgDOgGfANeAcQA6oCJgEngJyAT-Ao8BUQCpQFvALhAXQAvcBf4DMAGggNNAbUA3EBxoDlgHiAPNAfIBAQCEgEbgI_gSlgmACYIAA.YAAAAAAAAAA"); + + Assertions.assertEquals(1000, tcfCaV1.getCmpId()); + Assertions.assertEquals("EL", tcfCaV1.getConsentLanguage()); + Assertions.assertEquals(189, tcfCaV1.getVendorListVersion()); + Assertions.assertEquals( + Set.of(42, 45, 52, 67, 75, 80, 93, 109, 119, 126, 130, 1216, 1254, 1318), + tcfCaV1.getVendorExpressConsent()); + Assertions.assertEquals( + Set.of( + 93, 157, 183, 184, 231, 238, 256, 294, 315, 319, 394, 410, 413, 415, 431, 452, 469, 550, + 591, 626, 639, 655, 674, 677, 734, 737, 744, 759, 767, 816, 833, 845, 874, 881, 909, + 918, 964, 973, 996, 1028, 1060, 1134, 1151, 1189, 1216, 1217), + tcfCaV1.getVendorImpliedConsent()); + } + @Test() public void testDecodeGarbage1() { Assertions.assertThrows(