From 53479f38c14c116d17c149bd7e912da4d49cfdf3 Mon Sep 17 00:00:00 2001 From: Jeff Shaw Date: Thu, 26 Feb 2026 14:31:18 -0500 Subject: [PATCH 1/2] Make TcfEuV2's IsServiceSpecific not a condition for encoding VendorsAllowed or VendorsDisclosed. TcfEuV2#encodeSection() now uses the same criteria as iabtcf-java's TCStringEncoder to determine if a segment should be encoded. TCF 2.3 makes VendorsDisclosed required, and so its encoding should not be conditional on IsServiceSpecific. I made additional changes to reduce the amount of whitespace. TcfEuV2#encodeSection() no longer creates an intermediate collection. --- .../com/iab/gpp/encoder/section/TcfEuV2.java | 132 ++++++++++-------- .../com/iab/gpp/encoder/GppModelTest.java | 28 ++-- .../iab/gpp/encoder/section/TcfEuV2Test.java | 54 +++++-- 3 files changed, 131 insertions(+), 83 deletions(-) diff --git a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/section/TcfEuV2.java b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/section/TcfEuV2.java index 1f7f2b1e..3db5802a 100644 --- a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/section/TcfEuV2.java +++ b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/section/TcfEuV2.java @@ -1,11 +1,8 @@ package com.iab.gpp.encoder.section; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.List; import com.iab.gpp.encoder.datatype.RangeEntry; import com.iab.gpp.encoder.error.DecodingException; +import com.iab.gpp.encoder.error.EncodingException; import com.iab.gpp.encoder.error.InvalidFieldException; import com.iab.gpp.encoder.field.TcfEuV2Field; import com.iab.gpp.encoder.segment.EncodableSegment; @@ -13,6 +10,13 @@ import com.iab.gpp.encoder.segment.TcfEuV2PublisherPurposesSegment; import com.iab.gpp.encoder.segment.TcfEuV2VendorsAllowedSegment; import com.iab.gpp.encoder.segment.TcfEuV2VendorsDisclosedSegment; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; public class TcfEuV2 extends AbstractLazilyEncodableSection { @@ -53,73 +57,85 @@ protected List initializeSegments() { segments.add(new TcfEuV2VendorsDisclosedSegment()); return segments; } - + @Override - public List decodeSection(String encodedString) { - List segments = initializeSegments(); - - if(encodedString != null && !encodedString.isEmpty()) { - String[] encodedSegments = encodedString.split("\\."); - for (int i = 0; i < encodedSegments.length; i++) { - - /** - * The first 3 bits contain the segment id. Rather than decode the entire string, just check the first character. - * - * A-H = '000' = 0 - * I-P = '001' = 1 - * Q-X = '010' = 2 - * Y-Z,a-f = '011' = 3 - * - * Note that there is no segment id field for the core segment. Instead the first 6 bits are reserved - * for the encoding version which only coincidentally works here because the version value is less than 8. - */ - - String encodedSegment = encodedSegments[i]; - if(!encodedSegment.isEmpty()) { - char firstChar = encodedSegment.charAt(0); - - // unfortunately, the segment ordering doesn't match the segment ids - if(firstChar >= 'A' && firstChar <= 'H') { - segments.get(0).decode(encodedSegments[i]); - } else if(firstChar >= 'I' && firstChar <= 'P') { - segments.get(3).decode(encodedSegments[i]); - } else if(firstChar >= 'Q' && firstChar <= 'X') { - segments.get(2).decode(encodedSegments[i]); - } else if((firstChar >= 'Y' && firstChar <= 'Z') || (firstChar >= 'a' && firstChar <= 'f')) { - segments.get(1).decode(encodedSegments[i]); - } else { - throw new DecodingException("Invalid segment '" + encodedSegment + "'"); - } - } + public List decodeSection(final String encodedString) { + final List segments = initializeSegments(); + + if (encodedString == null || encodedString.isEmpty()) { + return segments; + } + + final String[] encodedSegments = encodedString.split("\\."); + + for (final String encodedSegment: encodedSegments) { + /** + * The first 3 bits contain the segment id. Rather than decode the entire string, just check the first character. + * + * A-H = '000' = 0 + * I-P = '001' = 1 + * Q-X = '010' = 2 + * Y-Z,a-f = '011' = 3 + * + * Note that there is no segment id field for the core segment. Instead the first 6 bits are reserved + * for the encoding version which only coincidentally works here because the version value is less than 8. + */ + if (encodedSegment.isEmpty()) { + continue; + } + + final char firstChar = encodedSegment.charAt(0); + + // unfortunately, the segment ordering doesn't match the segment ids + if(firstChar >= 'A' && firstChar <= 'H') { + segments.get(0).decode(encodedSegment); + } else if(firstChar >= 'I' && firstChar <= 'P') { + segments.get(3).decode(encodedSegment); + } else if(firstChar >= 'Q' && firstChar <= 'X') { + segments.get(2).decode(encodedSegment); + } else if((firstChar >= 'Y' && firstChar <= 'Z') || (firstChar >= 'a' && firstChar <= 'f')) { + segments.get(1).decode(encodedSegment); + } else { + throw new DecodingException("Invalid segment '" + encodedSegment + "'"); } } - + return segments; } @Override public String encodeSection(List segments) { - List encodedSegments = new ArrayList<>(); - if (segments.size() >= 1) { - encodedSegments.add(segments.get(0).encode()); - - Boolean isServiceSpecific = (Boolean) this.getFieldValue(TcfEuV2Field.IS_SERVICE_SPECIFIC); - if (isServiceSpecific) { - if (segments.size() >= 2) { - encodedSegments.add(segments.get(1).encode()); - } + final StringJoiner encodedSegments = new StringJoiner("."); + + /* + Only encode a non-core section if it doesn't have default values. + This is intended to match https://github.com/InteractiveAdvertisingBureau/iabtcf-java/blob/master/iabtcf-encoder/src/main/java/com/iabtcf/encoder/TCStringEncoder.java. + */ + for (final EncodableSegment segment: segments) { + final boolean encode; + + if (segment instanceof TcfEuV2CoreSegment) { + encode = true; + } else if (segment instanceof TcfEuV2PublisherPurposesSegment) { + final List publisherConsents = getPublisherConsents(); + final List publisherLegitimateInterests = getPublisherLegitimateInterests(); + encode = (publisherConsents != null && publisherConsents.contains(true)) + || (publisherLegitimateInterests != null && publisherLegitimateInterests.contains(true)) + || !Objects.equals(getNumCustomPurposes(), 0); + } else if (segment instanceof TcfEuV2VendorsAllowedSegment) { + encode = !Objects.equals(getVendorsAllowed(), Collections.emptyList()); + } else if (segment instanceof TcfEuV2VendorsDisclosedSegment) { + encode = !Objects.equals(getVendorsDisclosed(), Collections.emptyList()); } else { - if (segments.size() >= 2) { - encodedSegments.add(segments.get(2).encode()); + throw new EncodingException(String.format("Unknown segment type '%s' for section %s.", segment.getClass().getName(), NAME)); + } - if (segments.size() >= 3) { - encodedSegments.add(segments.get(3).encode()); - } - } + if (encode) { + encodedSegments.add(segment.encode()); } } - return String.join(".", encodedSegments); + return encodedSegments.toString(); } @Override 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 bb5f2336..69968bf3 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 @@ -133,7 +133,7 @@ public void testEncodeDefaultAll() { String gppString = gppModel.encode(); Assertions.assertEquals( - "DBACOYs~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAAAA.QAAA.IAAA~BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA~1---~BAAAAAAAAABA.QA~BAAAAABA.QA~BAAAABA~BAAAAEA.QA~BAAAAAQA~BAAAAAEA.QA~BAAAAABA~BAAAAABA.QA~BAAAAAABAA.QA~BAAAAAQA.QA~BAAAAAABAA.QA~BAAAAAQA.QA~BAAAAAQA.QA~BAAAAABA.QA~BAAAAAAAQA.QA~BAAAAAQA.QA~BAAAAAQA.QA", + "DBACOYs~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAAAA~BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA~1---~BAAAAAAAAABA.QA~BAAAAABA.QA~BAAAABA~BAAAAEA.QA~BAAAAAQA~BAAAAAEA.QA~BAAAAABA~BAAAAABA.QA~BAAAAAABAA.QA~BAAAAAQA.QA~BAAAAAABAA.QA~BAAAAAQA.QA~BAAAAAQA.QA~BAAAAABA.QA~BAAAAAAAQA.QA~BAAAAAQA.QA~BAAAAAQA.QA", gppString); } @@ -226,7 +226,7 @@ public void testEncodeTcfEuV2() { String gppString = gppModel.encode(); - Assertions.assertEquals("DBABMA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA.QAAA.IAAA", gppString); + Assertions.assertEquals("DBABMA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA", gppString); Assertions.assertEquals(2, gppString.split("~").length); @@ -275,7 +275,7 @@ public void testEncodeUspV1AndTcfEuV2() { Assertions.assertEquals(false, gppModel.hasSection(TcfCaV1.NAME)); String gppString = gppModel.encode(); - Assertions.assertEquals("DBACNYA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA.QAAA.IAAA~1YNN", gppString); + Assertions.assertEquals("DBACNYA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA~1YNN", gppString); Assertions.assertEquals(3, gppString.split("~").length); @@ -373,7 +373,7 @@ public void testEncodeUspV1AndTcfEuV2AndTcfCaV1() { String gppString = gppModel.encode(); Assertions.assertEquals( - "DBACOeA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA.QAAA.IAAA~BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBgABABAAABAB4AACACAAA.fHHHA4444ao~1YNN", + "DBACOeA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA~BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBgABABAAABAB4AACACAAA.fHHHA4444ao~1YNN", gppString); Assertions.assertEquals(4, gppString.split("~").length); @@ -406,7 +406,7 @@ public void testDecodeDefaults() { @Test public void testDecodeDefaultsAll() { String gppString = - "DBACOYs~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAAAA.QAAA.IAAA~BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA~1---~BAAAAAAAAABA.QA~BAAAAABA.QA~BAAAABA~BAAAAEA.QA~BAAAAAQA~BAAAAAEA.QA~BAAAAABA~BAAAAABA.QA~BAAAAAABAA.QA~BAAAAAQA.QA~BAAAAAABAA.QA~BAAAAAQA.QA~BAAAAAQA.QA~BAAAAABA.QA~BAAAAAAAQA.QA~BAAAAAQA.QA~BAAAAAABAA.QA"; + "DBACOYs~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAAAA~BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA~1---~BAAAAAAAAABA.QA~BAAAAABA.QA~BAAAABA~BAAAAEA.QA~BAAAAAQA~BAAAAAEA.QA~BAAAAABA~BAAAAABA.QA~BAAAAAABAA.QA~BAAAAAQA.QA~BAAAAAABAA.QA~BAAAAAQA.QA~BAAAAAQA.QA~BAAAAABA.QA~BAAAAAAAQA.QA~BAAAAAQA.QA~BAAAAAABAA.QA"; GppModel gppModel = new GppModel(gppString); Assertions.assertEquals(true, gppModel.hasSection(TcfEuV2.NAME)); @@ -456,7 +456,7 @@ public void testDecodeUspv1() { @Test public void testDecodeTcfEuV2() { - String gppString = "DBABMA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA.QAAA.IAAA"; + String gppString = "DBABMA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA"; GppModel gppModel = new GppModel(gppString); Assertions.assertEquals(Arrays.asList(2), gppModel.getSectionIds()); @@ -499,7 +499,7 @@ public void testDecodeTcfEuV2() { @Test public void testDecodeUspv1AndTcfEuV2() { - String gppString = "DBACNYA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA.QAAA.IAAA~1YNN"; + String gppString = "DBACNYA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA~1YNN"; GppModel gppModel = new GppModel(gppString); Assertions.assertEquals(Arrays.asList(2, 6), gppModel.getSectionIds()); @@ -564,7 +564,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~BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBgABABAAABAB4AACACAAA.fHHHA4444ao~1YNN"; GppModel gppModel = new GppModel(gppString); Assertions.assertEquals(Arrays.asList(2, 5, 6), gppModel.getSectionIds()); @@ -650,7 +650,7 @@ public void testEncode1() { gppModel.setFieldValue(TcfEuV2.NAME, TcfEuV2Field.CREATED, utcDateTime); gppModel.setFieldValue(TcfEuV2.NAME, TcfEuV2Field.LAST_UPDATED, utcDateTime); - Assertions.assertEquals("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAOAAAABAAAAA.QAAA.IAAA", gppModel.encode()); + Assertions.assertEquals("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAOAAAABAAAAA", gppModel.encode()); } @Test @@ -661,7 +661,7 @@ public void testEncode2() { gppModel.setFieldValue(TcfEuV2.NAME, TcfEuV2Field.CREATED, utcDateTime); gppModel.setFieldValue(TcfEuV2.NAME, TcfEuV2Field.LAST_UPDATED, utcDateTime); - Assertions.assertEquals("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAOwAQAOgAAAA.QAAA.IAAA", gppModel.encode()); + Assertions.assertEquals("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAOwAQAOgAAAA", gppModel.encode()); } @Test @@ -672,25 +672,25 @@ public void testEncode3() { gppModel.setFieldValue(TcfEuV2.NAME, TcfEuV2Field.CREATED, utcDateTime); gppModel.setFieldValue(TcfEuV2.NAME, TcfEuV2Field.LAST_UPDATED, utcDateTime); - Assertions.assertEquals("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAFpQAwAAgCtAWkAAAAAAA.QAAA.IAAA", + Assertions.assertEquals("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAFpQAwAAgCtAWkAAAAAAA", gppModel.encode()); } @Test public void testDecode1() { - GppModel gppModel = new GppModel("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAOAAAABAAAAA.QAAA.IAAA"); + GppModel gppModel = new GppModel("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAOAAAABAAAAA"); Assertions.assertEquals(Arrays.asList(28), gppModel.getFieldValue(TcfEuV2.NAME, TcfEuV2Field.VENDOR_CONSENTS)); } @Test public void testDecode2() { - GppModel gppModel = new GppModel("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAOwAQAOgAAAA.QAAA.IAAA"); + GppModel gppModel = new GppModel("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAOwAQAOgAAAA"); Assertions.assertEquals(Arrays.asList(29), gppModel.getFieldValue(TcfEuV2.NAME, TcfEuV2Field.VENDOR_CONSENTS)); } @Test public void testDecode3() { - GppModel gppModel = new GppModel("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAFpQAwAAgCtAWkAAAAAAA.QAAA.IAAA"); + GppModel gppModel = new GppModel("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAFpQAwAAgCtAWkAAAAAAA"); Assertions.assertEquals(Arrays.asList(1, 173, 722), gppModel.getFieldValue(TcfEuV2.NAME, TcfEuV2Field.VENDOR_CONSENTS)); } diff --git a/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/section/TcfEuV2Test.java b/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/section/TcfEuV2Test.java index 945f7468..65224182 100644 --- a/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/section/TcfEuV2Test.java +++ b/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/section/TcfEuV2Test.java @@ -1,23 +1,43 @@ package com.iab.gpp.encoder.section; +import com.iab.gpp.encoder.datatype.RangeEntry; +import com.iab.gpp.encoder.error.DecodingException; +import com.iab.gpp.encoder.field.TcfEuV2Field; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.Arrays; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import com.iab.gpp.encoder.datatype.RangeEntry; -import com.iab.gpp.encoder.error.DecodingException; -import com.iab.gpp.encoder.field.TcfEuV2Field; public class TcfEuV2Test { + static final Set FIELD_NAMES = + new TcfEuV2().initializeSegments().stream() + .flatMap(s -> s.getFieldNames().stream()) + .collect(Collectors.toSet()); + + static void assertEqualFields(final TcfEuV2 expected, TcfEuV2 actual) { + for (String fieldName: FIELD_NAMES) { + Assertions.assertEquals(expected.getFieldValue(fieldName), actual.getFieldValue(fieldName)); + } + } + @Test public void testEncode1() { TcfEuV2 tcfEuV2 = new TcfEuV2(); - tcfEuV2.setFieldValue(TcfEuV2Field.CREATED, ZonedDateTime.of(2022, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC"))); - tcfEuV2.setFieldValue(TcfEuV2Field.LAST_UPDATED, ZonedDateTime.of(2022, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC"))); - Assertions.assertEquals("CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAAAA.QAAA.IAAA", tcfEuV2.encode()); + final ZonedDateTime time = ZonedDateTime.of(2022, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC")); + tcfEuV2.setFieldValue(TcfEuV2Field.CREATED, time); + tcfEuV2.setFieldValue(TcfEuV2Field.LAST_UPDATED, time); + + final String encoded = tcfEuV2.encode(); + Assertions.assertEquals("CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAAAA", encoded); + + final TcfEuV2 decoded = new TcfEuV2(encoded); + + assertEqualFields(tcfEuV2, decoded); } @Test @@ -40,12 +60,24 @@ public void testEncode2() { Assertions.assertEquals(Arrays.asList(), tcfEuV2.getFieldValue(TcfEuV2Field.PUBLISHER_CUSTOM_CONSENTS)); Assertions.assertEquals(Arrays.asList(), tcfEuV2.getFieldValue(TcfEuV2Field.PUBLISHER_CUSTOM_LEGITIMATE_INTERESTS)); - Assertions.assertEquals("CPSG_8APSG_8AAAAAAENAACgAAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAAA", tcfEuV2.encode()); + final String encoded = tcfEuV2.encode(); + Assertions.assertEquals("CPSG_8APSG_8AAAAAAENAACgAAAAAAAAAAAAAAAAAAAA", encoded); + + final TcfEuV2 decoded = new TcfEuV2(encoded); + + assertEqualFields(tcfEuV2, decoded); + } + + @Test + public void testAlwaysEncodeCoreSegment() { + final TcfEuV2 tcfEuV2 = new TcfEuV2(); + final char firstChar = tcfEuV2.encode().charAt(0); + Assertions.assertTrue(firstChar >= 'A' && firstChar <= 'H'); } @Test public void testDecode1() { - TcfEuV2 tcfEuV2 = new TcfEuV2("CAAAAAAAAAAAAAAAAAENAACAAAAAAAAAAAAAAAAAAAAA.QAAA.IAAA"); + TcfEuV2 tcfEuV2 = new TcfEuV2("CAAAAAAAAAAAAAAAAAENAACAAAAAAAAAAAAAAAAAAAAA"); Assertions.assertEquals(2, tcfEuV2.getVersion()); Assertions.assertEquals(ZonedDateTime.of(1970, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC")), tcfEuV2.getCreated()); @@ -98,7 +130,7 @@ public void testDecode1() { @Test public void testDecode2() { - TcfEuV2 tcfEuV2 = new TcfEuV2("CPSG_8APSG_8AAAAAAENAACgAAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAAA"); + TcfEuV2 tcfEuV2 = new TcfEuV2("CPSG_8APSG_8AAAAAAENAACgAAAAAAAAAAAAAAAAAAAA"); Assertions.assertEquals(2, tcfEuV2.getVersion()); Assertions.assertEquals(ZonedDateTime.of(2022, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC")), tcfEuV2.getCreated()); @@ -153,7 +185,7 @@ public void testDecode2() { @Test public void testDecode3() { TcfEuV2 tcfEuV2 = new TcfEuV2( - "CPcqBNJPcqBNJNwAAAENAwCAAAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA.QGLtV_T9fb2vj-_Z99_tkeYwf95y3p-wzhheMs-8NyZeH_B4Wv2MyvBX4JiQKGRgksjLBAQdtHGlcTQgBwIlViTLMYk2MjzNKJrJEilsbO2dYGD9Pn8HT3ZCY70-vv__7v3ff_3g.IGLtV_T9fb2vj-_Z99_tkeYwf95y3p-wzhheMs-8NyZeH_B4Wv2MyvBX4JiQKGRgksjLBAQdtHGlcTQgBwIlViTLMYk2MjzNKJrJEilsbO2dYGD9Pn8HT3ZCY70-vv__7v3ff_3g"); + "CPcqBNJPcqBNJNwAAAENAwCAAAAAAAAAAAAAAAAAAAAA.QGLtV_T9fb2vj-_Z99_tkeYwf95y3p-wzhheMs-8NyZeH_B4Wv2MyvBX4JiQKGRgksjLBAQdtHGlcTQgBwIlViTLMYk2MjzNKJrJEilsbO2dYGD9Pn8HT3ZCY70-vv__7v3ff_3g.IGLtV_T9fb2vj-_Z99_tkeYwf95y3p-wzhheMs-8NyZeH_B4Wv2MyvBX4JiQKGRgksjLBAQdtHGlcTQgBwIlViTLMYk2MjzNKJrJEilsbO2dYGD9Pn8HT3ZCY70-vv__7v3ff_3g"); Assertions.assertEquals(2, tcfEuV2.getFieldValue("Version")); @@ -328,7 +360,7 @@ public void testDecode5() { @Test public void testDecode6() { - TcfEuV2 tcfEuV2 = new TcfEuV2("COv_eg6Ov_eg6AOADBENAaCgAP_AAH_AACiQAVEUQQoAIQAqIoghAAQgAA.YAAAAAAAAAAAAAAAAAA"); + TcfEuV2 tcfEuV2 = new TcfEuV2("COv_eg6Ov_eg6AOADBENAaCgAP_AAH_AACiQAVEUQQoAIQAqIoghAAQgAA"); Assertions.assertEquals(2, tcfEuV2.getFieldValue("Version")); Assertions.assertEquals(14, tcfEuV2.getFieldValue("CmpId")); From 90429752f8e945513584003b02b70b162449f59e Mon Sep 17 00:00:00 2001 From: Jeff Shaw Date: Thu, 26 Feb 2026 15:24:36 -0500 Subject: [PATCH 2/2] Make VendorsDisclosed always appear in the TC string per TCF 2.3. --- .../com/iab/gpp/encoder/section/TcfEuV2.java | 11 ++++++++--- .../java/com/iab/gpp/encoder/GppModelTest.java | 18 +++++++++--------- .../iab/gpp/encoder/section/TcfEuV2Test.java | 4 ++-- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/section/TcfEuV2.java b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/section/TcfEuV2.java index 3db5802a..455df78d 100644 --- a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/section/TcfEuV2.java +++ b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/section/TcfEuV2.java @@ -114,7 +114,14 @@ public String encodeSection(List segments) { for (final EncodableSegment segment: segments) { final boolean encode; - if (segment instanceof TcfEuV2CoreSegment) { + if ( + segment instanceof TcfEuV2CoreSegment + || segment instanceof TcfEuV2VendorsDisclosedSegment + ) { + /* + TcfEuV2VendorsDisclosedSegment is required in the TC string as of TCF 2.3. + https://iabeurope.eu/all-you-need-to-know-about-the-transition-to-tcf-v2-3/ + */ encode = true; } else if (segment instanceof TcfEuV2PublisherPurposesSegment) { final List publisherConsents = getPublisherConsents(); @@ -124,8 +131,6 @@ public String encodeSection(List segments) { || !Objects.equals(getNumCustomPurposes(), 0); } else if (segment instanceof TcfEuV2VendorsAllowedSegment) { encode = !Objects.equals(getVendorsAllowed(), Collections.emptyList()); - } else if (segment instanceof TcfEuV2VendorsDisclosedSegment) { - encode = !Objects.equals(getVendorsDisclosed(), Collections.emptyList()); } else { throw new EncodingException(String.format("Unknown segment type '%s' for section %s.", segment.getClass().getName(), NAME)); } 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 69968bf3..4b135f21 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 @@ -133,7 +133,7 @@ public void testEncodeDefaultAll() { String gppString = gppModel.encode(); Assertions.assertEquals( - "DBACOYs~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAAAA~BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA~1---~BAAAAAAAAABA.QA~BAAAAABA.QA~BAAAABA~BAAAAEA.QA~BAAAAAQA~BAAAAAEA.QA~BAAAAABA~BAAAAABA.QA~BAAAAAABAA.QA~BAAAAAQA.QA~BAAAAAABAA.QA~BAAAAAQA.QA~BAAAAAQA.QA~BAAAAABA.QA~BAAAAAAAQA.QA~BAAAAAQA.QA~BAAAAAQA.QA", + "DBACOYs~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAAAA.IAAA~BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA~1---~BAAAAAAAAABA.QA~BAAAAABA.QA~BAAAABA~BAAAAEA.QA~BAAAAAQA~BAAAAAEA.QA~BAAAAABA~BAAAAABA.QA~BAAAAAABAA.QA~BAAAAAQA.QA~BAAAAAABAA.QA~BAAAAAQA.QA~BAAAAAQA.QA~BAAAAABA.QA~BAAAAAAAQA.QA~BAAAAAQA.QA~BAAAAAQA.QA", gppString); } @@ -226,7 +226,7 @@ public void testEncodeTcfEuV2() { String gppString = gppModel.encode(); - Assertions.assertEquals("DBABMA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA", gppString); + Assertions.assertEquals("DBABMA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA.IAAA", gppString); Assertions.assertEquals(2, gppString.split("~").length); @@ -275,7 +275,7 @@ public void testEncodeUspV1AndTcfEuV2() { Assertions.assertEquals(false, gppModel.hasSection(TcfCaV1.NAME)); String gppString = gppModel.encode(); - Assertions.assertEquals("DBACNYA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA~1YNN", gppString); + Assertions.assertEquals("DBACNYA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA.IAAA~1YNN", gppString); Assertions.assertEquals(3, gppString.split("~").length); @@ -373,7 +373,7 @@ public void testEncodeUspV1AndTcfEuV2AndTcfCaV1() { String gppString = gppModel.encode(); Assertions.assertEquals( - "DBACOeA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA~BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBgABABAAABAB4AACACAAA.fHHHA4444ao~1YNN", + "DBACOeA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA.IAAA~BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBgABABAAABAB4AACACAAA.fHHHA4444ao~1YNN", gppString); Assertions.assertEquals(4, gppString.split("~").length); @@ -650,7 +650,7 @@ public void testEncode1() { gppModel.setFieldValue(TcfEuV2.NAME, TcfEuV2Field.CREATED, utcDateTime); gppModel.setFieldValue(TcfEuV2.NAME, TcfEuV2Field.LAST_UPDATED, utcDateTime); - Assertions.assertEquals("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAOAAAABAAAAA", gppModel.encode()); + Assertions.assertEquals("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAOAAAABAAAAA.IAAA", gppModel.encode()); } @Test @@ -661,7 +661,7 @@ public void testEncode2() { gppModel.setFieldValue(TcfEuV2.NAME, TcfEuV2Field.CREATED, utcDateTime); gppModel.setFieldValue(TcfEuV2.NAME, TcfEuV2Field.LAST_UPDATED, utcDateTime); - Assertions.assertEquals("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAOwAQAOgAAAA", gppModel.encode()); + Assertions.assertEquals("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAOwAQAOgAAAA.IAAA", gppModel.encode()); } @Test @@ -672,19 +672,19 @@ public void testEncode3() { gppModel.setFieldValue(TcfEuV2.NAME, TcfEuV2Field.CREATED, utcDateTime); gppModel.setFieldValue(TcfEuV2.NAME, TcfEuV2Field.LAST_UPDATED, utcDateTime); - Assertions.assertEquals("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAFpQAwAAgCtAWkAAAAAAA", + Assertions.assertEquals("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAFpQAwAAgCtAWkAAAAAAA.IAAA", gppModel.encode()); } @Test public void testDecode1() { - GppModel gppModel = new GppModel("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAOAAAABAAAAA"); + GppModel gppModel = new GppModel("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAOAAAABAAAAA.IAAA"); Assertions.assertEquals(Arrays.asList(28), gppModel.getFieldValue(TcfEuV2.NAME, TcfEuV2Field.VENDOR_CONSENTS)); } @Test public void testDecode2() { - GppModel gppModel = new GppModel("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAOwAQAOgAAAA"); + GppModel gppModel = new GppModel("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAOwAQAOgAAAA.IAAA"); Assertions.assertEquals(Arrays.asList(29), gppModel.getFieldValue(TcfEuV2.NAME, TcfEuV2Field.VENDOR_CONSENTS)); } diff --git a/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/section/TcfEuV2Test.java b/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/section/TcfEuV2Test.java index 65224182..49e94a6a 100644 --- a/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/section/TcfEuV2Test.java +++ b/iabgpp-encoder/src/test/java/com/iab/gpp/encoder/section/TcfEuV2Test.java @@ -33,7 +33,7 @@ public void testEncode1() { tcfEuV2.setFieldValue(TcfEuV2Field.LAST_UPDATED, time); final String encoded = tcfEuV2.encode(); - Assertions.assertEquals("CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAAAA", encoded); + Assertions.assertEquals("CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAAAA.IAAA", encoded); final TcfEuV2 decoded = new TcfEuV2(encoded); @@ -61,7 +61,7 @@ public void testEncode2() { Assertions.assertEquals(Arrays.asList(), tcfEuV2.getFieldValue(TcfEuV2Field.PUBLISHER_CUSTOM_LEGITIMATE_INTERESTS)); final String encoded = tcfEuV2.encode(); - Assertions.assertEquals("CPSG_8APSG_8AAAAAAENAACgAAAAAAAAAAAAAAAAAAAA", encoded); + Assertions.assertEquals("CPSG_8APSG_8AAAAAAENAACgAAAAAAAAAAAAAAAAAAAA.IAAA", encoded); final TcfEuV2 decoded = new TcfEuV2(encoded);