diff --git a/modules/cmpapi/src/encoder/datatype/EncodableArrayOfOptimizedFibonacciRanges.ts b/modules/cmpapi/src/encoder/datatype/EncodableArrayOfOptimizedFibonacciRanges.ts new file mode 100644 index 0000000..5cdab50 --- /dev/null +++ b/modules/cmpapi/src/encoder/datatype/EncodableArrayOfOptimizedFibonacciRanges.ts @@ -0,0 +1,101 @@ +import { DecodingError } from "../error/DecodingError.js"; +import { EncodingError } from "../error/EncodingError.js"; +import { StringUtil } from "../util/StringUtil.js"; +import { AbstractEncodableBitStringDataType } from "./AbstractEncodableBitStringDataType.js"; +import { EncodableOptimizedFibonacciRange } from "./EncodableOptimizedFibonacciRange.js"; +import { RangeEntry } from "./RangeEntry.js"; +import { SubstringError } from "./SubstringError.js"; +import { FixedIntegerEncoder } from "./encoder/FixedIntegerEncoder.js"; + +/** + * Encodes an N-ArrayOfRanges(X,Y) field where each record's ids are encoded as an OptimizedRange + * (Fibonacci coded), per the GPP Consent String Specification. This is the counterpart to + * EncodableArrayOfFixedIntegerRanges, which encodes ids using fixed-integer ranges for downward + * compatibility (e.g. TCF EU). + */ +export class EncodableArrayOfOptimizedFibonacciRanges extends AbstractEncodableBitStringDataType { + private keyBitStringLength: number; + private typeBitStringLength: number; + + constructor( + keyBitStringLength: number, + typeBitStringLength: number, + value?: RangeEntry[], + hardFailIfMissing: boolean = true + ) { + super(hardFailIfMissing); + this.keyBitStringLength = keyBitStringLength; + this.typeBitStringLength = typeBitStringLength; + this.setValue(value); + } + + public encode(): string { + try { + let entries: RangeEntry[] = this.value; + let sb = ""; + sb += FixedIntegerEncoder.encode(entries.length, 12); + for (let i = 0; i < entries.length; i++) { + let entry: RangeEntry = entries[i]; + sb += FixedIntegerEncoder.encode(entry.getKey(), this.keyBitStringLength); + sb += FixedIntegerEncoder.encode(entry.getType(), this.typeBitStringLength); + sb += new EncodableOptimizedFibonacciRange(entry.getIds()).encode(); + } + return sb; + } catch (e) { + throw new EncodingError(e); + } + } + + public decode(bitString: string) { + try { + let entries: RangeEntry[] = []; + let size = FixedIntegerEncoder.decode(StringUtil.substring(bitString, 0, 12)); + let index = 12; + for (let i = 0; i < size; i++) { + let key = FixedIntegerEncoder.decode(StringUtil.substring(bitString, index, index + this.keyBitStringLength)); + index += this.keyBitStringLength; + + let type = FixedIntegerEncoder.decode(StringUtil.substring(bitString, index, index + this.typeBitStringLength)); + index += this.typeBitStringLength; + + let range = new EncodableOptimizedFibonacciRange([]); + let substring = range.substring(bitString, index); + range.decode(substring); + let ids = range.getValue(); + index += substring.length; + + entries.push(new RangeEntry(key, type, ids)); + } + this.value = entries; + } catch (e) { + throw new DecodingError(e); + } + } + + public substring(bitString: string, fromIndex: number): string { + try { + let sb = ""; + sb += StringUtil.substring(bitString, fromIndex, fromIndex + 12); + + let size = FixedIntegerEncoder.decode(sb.toString()); + + let index = fromIndex + sb.length; + for (let i = 0; i < size; i++) { + let keySubstring = StringUtil.substring(bitString, index, index + this.keyBitStringLength); + index += keySubstring.length; + sb += keySubstring; + + let typeSubstring = StringUtil.substring(bitString, index, index + this.typeBitStringLength); + index += typeSubstring.length; + sb += typeSubstring; + + let rangeSubstring = new EncodableOptimizedFibonacciRange([]).substring(bitString, index); + index += rangeSubstring.length; + sb += rangeSubstring; + } + return sb; + } catch (e) { + throw new SubstringError(e); + } + } +} diff --git a/modules/cmpapi/src/encoder/datatype/index.ts b/modules/cmpapi/src/encoder/datatype/index.ts index 28b05c1..5557573 100644 --- a/modules/cmpapi/src/encoder/datatype/index.ts +++ b/modules/cmpapi/src/encoder/datatype/index.ts @@ -3,6 +3,7 @@ export * from "./validate/index.js"; export * from "./AbstractEncodableBitStringDataType.js"; export * from "./DataType.js"; export * from "./EncodableArrayOfFixedIntegerRanges.js"; +export * from "./EncodableArrayOfOptimizedFibonacciRanges.js"; export * from "./EncodableBoolean.js"; export * from "./EncodableDataType.js"; export * from "./EncodableDatetime.js"; diff --git a/modules/cmpapi/src/encoder/segment/TcfCaV1CoreSegment.ts b/modules/cmpapi/src/encoder/segment/TcfCaV1CoreSegment.ts index 826ad22..9324e59 100644 --- a/modules/cmpapi/src/encoder/segment/TcfCaV1CoreSegment.ts +++ b/modules/cmpapi/src/encoder/segment/TcfCaV1CoreSegment.ts @@ -2,11 +2,13 @@ import { AbstractBase64UrlEncoder } from "../base64/AbstractBase64UrlEncoder.js" import { CompressedBase64UrlEncoder } from "../base64/CompressedBase64UrlEncoder.js"; import { BitStringEncoder } from "../bitstring/BitStringEncoder.js"; import { EncodableArrayOfFixedIntegerRanges } from "../datatype/EncodableArrayOfFixedIntegerRanges.js"; +import { EncodableArrayOfOptimizedFibonacciRanges } from "../datatype/EncodableArrayOfOptimizedFibonacciRanges.js"; import { EncodableBoolean } from "../datatype/EncodableBoolean.js"; import { EncodableDatetime } from "../datatype/EncodableDatetime.js"; import { EncodableFixedBitfield } from "../datatype/EncodableFixedBitfield.js"; import { EncodableFixedInteger } from "../datatype/EncodableFixedInteger.js"; import { EncodableFixedString } from "../datatype/EncodableFixedString.js"; +import { EncodableOptimizedFibonacciRange } from "../datatype/EncodableOptimizedFibonacciRange.js"; import { EncodableOptimizedFixedRange } from "../datatype/EncodableOptimizedFixedRange.js"; import { DecodingError } from "../error/DecodingError.js"; import { EncodableBitStringFields } from "../field/EncodableBitStringFields.js"; @@ -33,6 +35,16 @@ export class TcfCaV1CoreSegment extends AbstractLazilyEncodableSegment { - private base64UrlEncoder: AbstractBase64UrlEncoder = TraditionalBase64UrlEncoder.getInstance(); + private base64UrlEncoder: AbstractBase64UrlEncoder = CompressedBase64UrlEncoder.getInstance(); private bitStringEncoder: BitStringEncoder = BitStringEncoder.getInstance(); constructor(encodedString?: string) { @@ -27,9 +28,23 @@ export class TcfCaV1DisclosedVendorsSegment extends AbstractLazilyEncodableSegme // overriden protected initializeFields(): EncodableBitStringFields { + return this.buildFields(false); + } + + /** + * Builds the disclosed-vendors field set. When legacy is true the OptimizedRange field uses the + * pre-fix fixed-integer encoder; otherwise it uses the spec-compliant Fibonacci encoder. The + * legacy field set is only used to decode strings produced by the older encoder (see + * decodeSegment). + */ + private buildFields(legacy: boolean): EncodableBitStringFields { let fields: EncodableBitStringFields = new EncodableBitStringFields(); fields.put(TcfCaV1Field.DISCLOSED_VENDORS_SEGMENT_TYPE.toString(), new EncodableFixedInteger(3, 1)); - fields.put(TcfCaV1Field.DISCLOSED_VENDORS.toString(), new EncodableOptimizedFixedRange([])); + if (legacy) { + fields.put(TcfCaV1Field.DISCLOSED_VENDORS.toString(), new EncodableOptimizedFixedRange([])); + } else { + fields.put(TcfCaV1Field.DISCLOSED_VENDORS.toString(), new EncodableOptimizedFibonacciRange([])); + } return fields; } @@ -47,9 +62,44 @@ export class TcfCaV1DisclosedVendorsSegment extends AbstractLazilyEncodableSegme } try { let bitString: string = this.base64UrlEncoder.decode(encodedString); + + // Prefer the spec-compliant (Fibonacci OptimizedRange) interpretation, falling back to the + // legacy (fixed-range) interpretation used by the pre-fix encoder. Re-encoding always + // migrates to the spec-compliant format because the values decode into the Fibonacci datatype. + if (this.tryDecode(bitString, fields, false)) { + return; + } + if (this.tryDecode(bitString, fields, true)) { + return; + } + this.bitStringEncoder.decode(bitString, this.getFieldNames(), fields); } catch (e) { - throw new DecodingError("Unable to decode HeaderV1CoreSegment '" + encodedString + "'"); + throw new DecodingError("Unable to decode TcfCaV1DisclosedVendorsSegment '" + encodedString + "'"); + } + } + + /** + * Attempts to decode bitString using either the current or legacy field set and verifies the + * result by re-encoding it: if the re-encoded bits are a prefix of the decoded bits (the tail + * being base64 padding), the interpretation produced the string. On success the decoded values + * are copied into targetFields (which always use the current encoders) so that any subsequent + * re-encode emits the spec-compliant format. + */ + private tryDecode(bitString: string, targetFields: EncodableBitStringFields, legacy: boolean): boolean { + try { + let candidate = this.buildFields(legacy); + this.bitStringEncoder.decode(bitString, this.getFieldNames(), candidate); + let reEncoded = this.bitStringEncoder.encode(candidate, this.getFieldNames()); + if (bitString.startsWith(reEncoded)) { + for (let fieldName of this.getFieldNames()) { + targetFields.get(fieldName).setValue(candidate.get(fieldName).getValue()); + } + return true; + } + } catch (e) { + // This interpretation does not apply; the caller will try the next one. } + return false; } } diff --git a/modules/cmpapi/test/GppModel.test.ts b/modules/cmpapi/test/GppModel.test.ts index 33a4e59..a1cfd22 100644 --- a/modules/cmpapi/test/GppModel.test.ts +++ b/modules/cmpapi/test/GppModel.test.ts @@ -394,7 +394,7 @@ describe("manifest.GppModel", (): void => { let gppString = gppModel.encode(); expect(gppString).to.eql( - "DBACOeA~CPSG_8APSG_8ANwAAAENAwCgAAAAAAAAAAAAAAAAAAAA.IAAA.YAAAAAAAAAAA~BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBgABABAAABAB4AACACAAA.fHHHA4444ao~1YNN" + "DBACOeA~CPSG_8APSG_8ANwAAAENAwCgAAAAAAAAAAAAAAAAAAAA.IAAA.YAAAAAAAAAAA~BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBhADVqxGAD0AILVgAA.fHHHA4444ao~1YNN" ); expect(gppString.split("~").length).to.eql(4); @@ -905,12 +905,12 @@ describe("manifest.GppModel", (): void => { it("should fail to decode missing sections", (): void => { let gppModel = new GppModel( - "DBACOeA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA.QAAA.IAAA~BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBgABABAAABAB4AACACAAA.fHHHA4444ao" + "DBACOeA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA.QAAA.IAAA~BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBhADVqxGAD0AILVgAA.fHHHA4444ao" ); expect(function () { gppModel.getHeader(); }).to.throw( - "Unable to decode 'DBACOeA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA.QAAA.IAAA~BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBgABABAAABAB4AACACAAA.fHHHA4444ao'. The number of sections does not match the number of sections defined in the header." + "Unable to decode 'DBACOeA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA.QAAA.IAAA~BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBhADVqxGAD0AILVgAA.fHHHA4444ao'. The number of sections does not match the number of sections defined in the header." ); }); diff --git a/modules/cmpapi/test/encoder/section/TcfCaV1.test.ts b/modules/cmpapi/test/encoder/section/TcfCaV1.test.ts index 3af2e4a..3cf2877 100644 --- a/modules/cmpapi/test/encoder/section/TcfCaV1.test.ts +++ b/modules/cmpapi/test/encoder/section/TcfCaV1.test.ts @@ -11,7 +11,7 @@ describe("manifest.section.TcfCaV1", (): void => { expect(tcfCaV1.encode()).to.eql("BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA"); }); - it("should encode to BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBgABABAAABAB4AACACAAA.fHHHA4444ao", (): void => { + it("should encode to BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBhADVqxGAD0AILVgAA.fHHHA4444ao", (): void => { let tcfCaV1 = new TcfCaV1(); tcfCaV1.setFieldValue(TcfCaV1Field.CMP_ID, 50); @@ -145,23 +145,23 @@ describe("manifest.section.TcfCaV1", (): void => { tcfCaV1.setFieldValue(TcfCaV1Field.CREATED, new Date("2022-01-01T00:00:00Z")); tcfCaV1.setFieldValue(TcfCaV1Field.LAST_UPDATED, new Date("2022-01-01T00:00:00Z")); - expect(tcfCaV1.encode()).to.eql("BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBgABABAAABAB4AACACAAA.fHHHA4444ao"); + expect(tcfCaV1.encode()).to.eql("BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBhADVqxGAD0AILVgAA.fHHHA4444ao"); }); - it("should encode to BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA.IAGO5wAA", (): void => { + it("should encode to BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA.IAGO5w", (): void => { let tcfCaV1 = new TcfCaV1(); tcfCaV1.setFieldValue(TcfCaV1Field.DISCLOSED_VENDORS, [1, 2, 3, 5, 6, 7, 10, 11, 12]); tcfCaV1.setFieldValue(TcfCaV1Field.CREATED, new Date("2022-01-01T00:00:00Z")); tcfCaV1.setFieldValue(TcfCaV1Field.LAST_UPDATED, new Date("2022-01-01T00:00:00Z")); - expect(tcfCaV1.encode()).to.eql("BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA.IAGO5wAA"); + expect(tcfCaV1.encode()).to.eql("BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA.IAGO5w"); }); - it("should encode to BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAACCgBwABAAOAAoADgAJA.YAAAAAAAAAA", (): void => { + it("should encode to BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAACCgAS7o.YAAAAAAAAAA", (): void => { let tcfCaV1 = new TcfCaV1(); tcfCaV1.setFieldValue(TcfCaV1Field.PUB_RESTRICTIONS, [new RangeEntry(1, 1, [1, 2, 3, 5, 6, 7, 9])]); tcfCaV1.setFieldValue(TcfCaV1Field.CREATED, new Date("2022-01-01T00:00:00Z")); tcfCaV1.setFieldValue(TcfCaV1Field.LAST_UPDATED, new Date("2022-01-01T00:00:00Z")); - expect(tcfCaV1.encode()).to.eql("BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAACCgBwABAAOAAoADgAJA.YAAAAAAAAAA"); + expect(tcfCaV1.encode()).to.eql("BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAACCgAS7o.YAAAAAAAAAA"); }); it("should decode BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA", (): void => { @@ -301,8 +301,8 @@ describe("manifest.section.TcfCaV1", (): void => { expect(tcfCaV1.getFieldValue(TcfCaV1Field.PUB_PURPOSES_SEGMENT_TYPE)).to.eql(3); }); - it("should decode BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBgABABAAABAB4AACACAAA.fHHHA4444ao", (): void => { - let tcfCaV1 = new TcfCaV1("BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBgABABAAABAB4AACACAAA.fHHHA4444ao"); + it("should decode BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBhADVqxGAD0AILVgAA.fHHHA4444ao", (): void => { + let tcfCaV1 = new TcfCaV1("BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBhADVqxGAD0AILVgAA.fHHHA4444ao"); expect(tcfCaV1.getFieldValue(TcfCaV1Field.CMP_ID)).to.eql(50); expect(tcfCaV1.getFieldValue(TcfCaV1Field.CMP_VERSION)).to.eql(2); @@ -438,8 +438,8 @@ describe("manifest.section.TcfCaV1", (): void => { expect(tcfCaV1.getFieldValue(TcfCaV1Field.PUB_PURPOSES_SEGMENT_TYPE)).to.eql(3); }); - it("should decode BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAACCgBwABAAOAAoADgAJA.YAAAAAAAAAA", (): void => { - let tcfCaV1 = new TcfCaV1("BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAACCgBwABAAOAAoADgAJA.YAAAAAAAAAA"); + it("should decode BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAACCgAS7o.YAAAAAAAAAA", (): void => { + let tcfCaV1 = new TcfCaV1("BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAACCgAS7o.YAAAAAAAAAA"); expect(tcfCaV1.getFieldValue(TcfCaV1Field.PUB_RESTRICTIONS).length).to.eql(1); expect(tcfCaV1.getFieldValue(TcfCaV1Field.PUB_RESTRICTIONS)[0].key).to.eql(1); @@ -447,6 +447,87 @@ describe("manifest.section.TcfCaV1", (): void => { expect(tcfCaV1.getFieldValue(TcfCaV1Field.PUB_RESTRICTIONS)[0].ids).to.eql([1, 2, 3, 5, 6, 7, 9]); }); + it("should round-trip vendor/disclosed ranges via the Fibonacci range path", (): void => { + // Sparse, high vendor IDs force the OptimizedRange to choose the (Fibonacci) range + // representation over a bitfield, exercising the Fibonacci range encode/decode path. + let tcfCaV1 = new TcfCaV1(); + tcfCaV1.setFieldValue(TcfCaV1Field.VENDOR_EXPRESS_CONSENT, [1, 100, 200]); + tcfCaV1.setFieldValue(TcfCaV1Field.VENDOR_IMPLIED_CONSENT, [50, 51, 52, 999]); + tcfCaV1.setFieldValue(TcfCaV1Field.DISCLOSED_VENDORS, [2, 250, 600]); + tcfCaV1.setFieldValue(TcfCaV1Field.PUB_RESTRICTIONS, [ + new RangeEntry(1, 0, [5, 100, 101, 102, 800]), + new RangeEntry(2, 2, [3, 500]), + ]); + + let decoded = new TcfCaV1(tcfCaV1.encode()); + expect(decoded.getFieldValue(TcfCaV1Field.VENDOR_EXPRESS_CONSENT)).to.eql([1, 100, 200]); + expect(decoded.getFieldValue(TcfCaV1Field.VENDOR_IMPLIED_CONSENT)).to.eql([50, 51, 52, 999]); + expect(decoded.getFieldValue(TcfCaV1Field.DISCLOSED_VENDORS)).to.eql([2, 250, 600]); + let decodedPubRestrictions = decoded.getFieldValue(TcfCaV1Field.PUB_RESTRICTIONS); + expect(decodedPubRestrictions.length).to.eql(2); + expect(decodedPubRestrictions[0].key).to.eql(1); + expect(decodedPubRestrictions[0].type).to.eql(0); + expect(decodedPubRestrictions[0].ids).to.eql([5, 100, 101, 102, 800]); + expect(decodedPubRestrictions[1].key).to.eql(2); + expect(decodedPubRestrictions[1].type).to.eql(2); + expect(decodedPubRestrictions[1].ids).to.eql([3, 500]); + }); + + it("should decode a legacy fixed-range vendor string (backwards compatibility)", (): void => { + // String produced by the pre-fix encoder, which used fixed-integer ranges for the + // VendorExpressConsent / VendorImpliedConsent OptimizedRange fields. + let tcfCaV1 = new TcfCaV1("BPSG_8APSG_8AAyACAENGdCgf_gfgAfgfgBgABABAAABAB4AACACAAA.fHHHA4444ao"); + expect(tcfCaV1.getFieldValue(TcfCaV1Field.VENDOR_EXPRESS_CONSENT)).to.eql([12, 24, 48]); + expect(tcfCaV1.getFieldValue(TcfCaV1Field.VENDOR_IMPLIED_CONSENT)).to.eql([18, 30]); + }); + + it("should decode a legacy fixed-range PubRestrictions string (backwards compatibility)", (): void => { + let tcfCaV1 = new TcfCaV1("BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAACCgBwABAAOAAoADgAJA.YAAAAAAAAAA"); + let pubRestrictions = tcfCaV1.getFieldValue(TcfCaV1Field.PUB_RESTRICTIONS); + expect(pubRestrictions.length).to.eql(1); + expect(pubRestrictions[0].key).to.eql(1); + expect(pubRestrictions[0].type).to.eql(1); + expect(pubRestrictions[0].ids).to.eql([1, 2, 3, 5, 6, 7, 9]); + }); + + it("should decode a legacy traditional-base64 DisclosedVendors string (backwards compatibility)", (): void => { + let tcfCaV1 = new TcfCaV1("BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA.IAGO5wAA"); + expect(tcfCaV1.getFieldValue(TcfCaV1Field.DISCLOSED_VENDORS)).to.eql([1, 2, 3, 5, 6, 7, 10, 11, 12]); + }); + + it("should decode a legacy string and re-encode it in the spec-compliant Fibonacci form", (): void => { + // A real TcfCaV1 string produced by the pre-fix encoder (fixed-integer OptimizedRange). The + // backwards-compatible decoder reads it, and re-encoding emits the spec-compliant Fibonacci form. + let legacy = + "BQliWsAQliWsAPoABAELC9CoAKgAAJIAAApNAOABUAC0AGgAQwAlgBQAC6AG0AO4AfgBBATAAnMBSYEwYFgAXQBOwC3ALgAc4A7gCAAEmAJ2AT8AxQBmgDOgGfANeAcQA6oCJgEngJyAT-Ao8BUQCpQFvALhAXQAvcBf4DMAGggNNAbUA3EBxoDlgHiAPNAfIBAQCEgEbgI_gSlgmACYIAA.YAAAAAAAAAA"; + let tcfCaV1 = new TcfCaV1(legacy); + + expect(tcfCaV1.getFieldValue(TcfCaV1Field.CMP_ID)).to.eql(1000); + expect(tcfCaV1.getFieldValue(TcfCaV1Field.CMP_VERSION)).to.eql(1); + expect(tcfCaV1.getFieldValue(TcfCaV1Field.CONSENT_LANGUAGE)).to.eql("EL"); + expect(tcfCaV1.getFieldValue(TcfCaV1Field.VENDOR_LIST_VERSION)).to.eql(189); + expect(tcfCaV1.getFieldValue(TcfCaV1Field.USE_NON_STANDARD_STACKS)).to.eql(true); + expect(tcfCaV1.getFieldValue(TcfCaV1Field.VENDOR_EXPRESS_CONSENT)).to.eql([ + 42, 45, 52, 67, 75, 80, 93, 109, 119, 126, 130, 1216, 1254, 1318, + ]); + expect(tcfCaV1.getFieldValue(TcfCaV1Field.VENDOR_IMPLIED_CONSENT)).to.eql([ + 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, + ]); + + // Touching the timestamps (preserving their values) marks the core segment dirty, and + // setIsDirty(true) marks the section dirty (the same mechanism GppModel uses), so encode() + // re-emits the string in the spec-compliant Fibonacci form. + tcfCaV1.setFieldValue(TcfCaV1Field.CREATED, tcfCaV1.getFieldValue(TcfCaV1Field.CREATED)); + tcfCaV1.setFieldValue(TcfCaV1Field.LAST_UPDATED, tcfCaV1.getFieldValue(TcfCaV1Field.LAST_UPDATED)); + tcfCaV1.setIsDirty(true); + + expect(tcfCaV1.encode()).to.eql( + "BQliWsAQliWsAPoABAELC9CoAKgAAJIAAApNAOBMZZGDDAxMmWskIahojBMGBYoGiOJ4FlhahgNZUxMZiYDUwllGgYGJpYyBjLIwZFqasFllNGqaMhisVpTU1DyeAAA.YAAAAAAAAAA" + ); + }); + it("should throw Error on garbage 1", (): void => { expect(function () { new TcfCaV1("A").getFieldValue(TcfCaV1Field.USE_NON_STANDARD_STACKS);