From 0ffdeca2b5b00de9f1618c16f511c4264755e902 Mon Sep 17 00:00:00 2001 From: Thomas LAY Date: Fri, 3 Apr 2020 16:38:16 +0200 Subject: [PATCH] Encode and decode TCString --- bits.go | 63 +++++++ decode.go | 203 +++++++++++++++++++++ decode_test.go | 164 +++++++++++++++++ encode_test.go | 335 +++++++++++++++++++++++++++++++++++ encoder.go | 105 +++++++++++ segment_allowed_vendors.go | 65 +++++++ segment_core_string.go | 190 ++++++++++++++++++++ segment_disclosed_vendors.go | 65 +++++++ segment_publisher_tc.go | 54 ++++++ tcdata.go | 45 +++++ 10 files changed, 1289 insertions(+) create mode 100644 bits.go create mode 100644 decode.go create mode 100644 decode_test.go create mode 100644 encode_test.go create mode 100644 encoder.go create mode 100644 segment_allowed_vendors.go create mode 100644 segment_core_string.go create mode 100644 segment_disclosed_vendors.go create mode 100644 segment_publisher_tc.go create mode 100644 tcdata.go diff --git a/bits.go b/bits.go new file mode 100644 index 0000000..920b69d --- /dev/null +++ b/bits.go @@ -0,0 +1,63 @@ +package iabtcf + +type Bits struct { + position uint + bytes []byte +} + +var ( + bytePows = []byte{128, 64, 32, 16, 8, 4, 2, 1} +) + +func NewBits(bytes []byte) *Bits { + return &Bits{bytes: bytes, position: 0} +} + +func (b *Bits) ReadBool() bool { + byteIndex := b.position / 8 + bitIndex := b.position % 8 + b.position++ + + return (b.bytes[byteIndex] & bytePows[bitIndex]) != 0 +} + +func (b *Bits) WriteBool(v bool) { + byteIndex := b.position / 8 + shift := (byteIndex+1)*8 - b.position - 1 + b.position++ + + if v { + b.bytes[byteIndex] |= 1 << uint(shift) + } else { + b.bytes[byteIndex] &^= 1 << uint(shift) + } +} + +func (b *Bits) ReadInt(n uint) int { + v := 0 + for i, shift := uint(0), n-1; i < n; i++ { + if b.ReadBool() { + v += 1 << uint(shift) + } + shift-- + } + + return v +} + +func (b *Bits) WriteInt(v int, n uint) { + b.WriteNumber(int64(v), n) +} + +func (b *Bits) WriteNumber(v int64, n uint) { + startOffset := int(b.position) + for i := int(n) - 1; i >= 0; i-- { + index := startOffset + i + byteIndex := index / 8 + shift := (byteIndex+1)*8 - index - 1 + b.bytes[byteIndex] |= byte(v%2) << uint(shift) + v /= 2 + } + + b.position += n +} diff --git a/decode.go b/decode.go new file mode 100644 index 0000000..b62b8ff --- /dev/null +++ b/decode.go @@ -0,0 +1,203 @@ +package iabtcf + +import ( + "encoding/base64" + "fmt" + "strings" +) + +func DecodeSegmentType(s string) int { + b, err := base64.RawURLEncoding.DecodeString(s) + if err != nil { + return 0 + } + + var e = NewTCEncoder(b) + return e.ReadInt(3) +} + +func Decode(s string) (t *TCData, err error) { + t = &TCData{} + for _, v := range strings.Split(s, ".") { + segmentType := DecodeSegmentType(v) + if segmentType == 1 { + segment, err := DecodeDisclosedVendors(v) + if err == nil { + t.DisclosedVendors = segment + } + } else if segmentType == 2 { + segment, err := DecodeAllowedVendors(v) + if err == nil { + t.AllowedVendors = segment + } + } else if segmentType == 3 { + segment, err := DecodePubllisherTC(v) + if err == nil { + t.PublisherTC = segment + } + } else { + segment, err := DecodeCoreString(v) + if err == nil { + t.CoreString = segment + } + } + } + + if t.CoreString == nil { + return nil, fmt.Errorf("invalid TC string") + } + + return t, nil +} + +func DecodeCoreString(s string) (c *CoreString, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("%v", r) + } + }() + + b, err := base64.RawURLEncoding.DecodeString(s) + if err != nil { + return nil, err + } + + var e = NewTCEncoder(b) + + c = &CoreString{} + c.Version = e.ReadInt(6) + c.Created = e.ReadTime() + c.LastUpdated = e.ReadTime() + c.CmpId = e.ReadInt(12) + c.CmpVersion = e.ReadInt(12) + c.ConsentScreen = e.ReadInt(6) + c.ConsentLanguage = e.ReadIsoCode() + c.VendorListVersion = e.ReadInt(12) + c.TcfPolicyVersion = e.ReadInt(6) + c.IsServiceSpecific = e.ReadBool() + c.UseNonStandardStacks = e.ReadBool() + c.SpecialFeatureOptIns = e.ReadBitField(12) + c.PurposesConsent = e.ReadBitField(24) + c.PurposesLITransparency = e.ReadBitField(24) + c.PurposeOneTreatment = e.ReadBool() + c.PublisherCC = e.ReadIsoCode() + + c.MaxVendorId = e.ReadInt(16) + c.IsRangeEncoding = e.ReadBool() + if c.IsRangeEncoding { + c.NumEntries = e.ReadInt(12) + c.RangeEntries = e.ReadRangeEntries(uint(c.NumEntries)) + } else { + c.VendorsConsent = e.ReadBitField(uint(c.MaxVendorId)) + } + + c.MaxVendorIdLI = e.ReadInt(16) + c.IsRangeEncodingLI = e.ReadBool() + if c.IsRangeEncodingLI { + c.NumEntriesLI = e.ReadInt(12) + c.RangeEntriesLI = e.ReadRangeEntries(uint(c.NumEntriesLI)) + } else { + c.VendorsLITransparency = e.ReadBitField(uint(c.MaxVendorIdLI)) + } + + c.NumPubRestrictions = e.ReadInt(12) + c.PubRestrictions = e.ReadPubRestrictions(uint(c.NumPubRestrictions)) + + return c, nil +} + +func DecodeDisclosedVendors(s string) (d *DisclosedVendors, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("%v", r) + } + }() + + b, err := base64.RawURLEncoding.DecodeString(s) + if err != nil { + return nil, err + } + + var e = NewTCEncoder(b) + + d = &DisclosedVendors{} + d.SegmentType = e.ReadInt(3) + d.MaxVendorId = e.ReadInt(16) + d.IsRangeEncoding = e.ReadBool() + if d.IsRangeEncoding { + d.NumEntries = e.ReadInt(12) + d.RangeEntries = e.ReadRangeEntries(uint(d.NumEntries)) + } else { + d.DisclosedVendors = e.ReadBitField(uint(d.MaxVendorId)) + } + + if d.SegmentType != 1 { + err = fmt.Errorf("disclosed vendors segment type must be 1") + return nil, err + } + + return d, nil +} + +func DecodeAllowedVendors(s string) (a *AllowedVendors, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("%v", r) + } + }() + + b, err := base64.RawURLEncoding.DecodeString(s) + if err != nil { + return nil, err + } + + var e = NewTCEncoder(b) + + a = &AllowedVendors{} + a.SegmentType = e.ReadInt(3) + a.MaxVendorId = e.ReadInt(16) + a.IsRangeEncoding = e.ReadBool() + if a.IsRangeEncoding { + a.NumEntries = e.ReadInt(12) + a.RangeEntries = e.ReadRangeEntries(uint(a.NumEntries)) + } else { + a.AllowedVendors = e.ReadBitField(uint(a.MaxVendorId)) + } + + if a.SegmentType != 2 { + err = fmt.Errorf("allowed vendors segment type must be 2") + return nil, err + } + + return a, nil +} + +func DecodePubllisherTC(s string) (p *PublisherTC, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("%v", r) + } + }() + + b, err := base64.RawURLEncoding.DecodeString(s) + if err != nil { + return nil, err + } + + var e = NewTCEncoder(b) + + p = &PublisherTC{} + p.SegmentType = e.ReadInt(3) + p.PubPurposesConsent = e.ReadBitField(24) + p.PubPurposesLITransparency = e.ReadBitField(24) + p.NumCustomPurposes = e.ReadInt(6) + p.CustomPurposesConsent = e.ReadBitField(uint(p.NumCustomPurposes)) + p.CustomPurposesLITransparency = e.ReadBitField(uint(p.NumCustomPurposes)) + + if p.SegmentType != 3 { + err = fmt.Errorf("allowed vendors segment type must be 3") + return nil, err + } + + return p, nil +} diff --git a/decode_test.go b/decode_test.go new file mode 100644 index 0000000..0f1c295 --- /dev/null +++ b/decode_test.go @@ -0,0 +1,164 @@ +package iabtcf + +import ( + "testing" +) + +func TestDecode(t *testing.T) { + str := "COxR03kOxR1CqBcABCENAgCMAP_AAH_AAAqIF3EXySoGY2thI2YVFxBEIYwfJxyigMgChgQIsSwNQIeFLBoGLiAAHBGYJAQAGBAEEACBAQIkHGBMCQAAgAgBiRCMQEGMCzNIBIBAggEbY0FACCVmHkHSmZCY7064O__QLuIJEFQMAkSBAIACLECIQwAQDiAAAYAlAAABAhIaAAgIWBQEeAAAACAwAAgAAABBAAACAAQAAICIAAABAAAgAiAQAAAAGgIQAACBABACRIAAAEANCAAgiCEAQg4EAo4AAA.IF3EXySoGY2tho2YVFzBEIYwfJxyigMgShgQIsS0NQIeFLBoGPiAAHBGYJAQAGBAkkACBAQIsHGBMCQABgAgRiRCMQEGMDzNIBIBAggkbY0FACCVmnkHS3ZCY70-6u__QA.elAAAAAAAWA" + + data, err := Decode(str) + if err != nil { + t.Errorf("TC String should be decoded without error: %s", err) + return + } + + result := data.ToTCString() + if result == "" { + t.Errorf("Encode() should be produce a string") + return + } + + if result != str { + t.Errorf("Encode() should produce the same string: in = %s, out = %s", str, result) + } +} + +func TestDecodeInvalid(t *testing.T) { + str := "IF3EXySoGY2tho2YVFzBEIYwfJxyigMgShgQIsS0NQIeFLBoGPiAAHBGYJAQAGBAkkACBAQIsHGBMCQABgAgRiRCMQEGMDzNIBIBAggkbY0FACCVmnkHS3ZCY70-6u__QA.elAAAAAAAWA" + + _, err := Decode(str) + if err == nil { + t.Errorf("TC String should not be decoded: %s", err) + return + } +} + +func TestDecodeCoreString(t *testing.T) { + str := "COxR03kOxR1CqBcABCENAgCMAP_AAH_AAAqIF3EXySoGY2thI2YVFxBEIYwfJxyigMgChgQIsSwNQIeFLBoGLiAAHBGYJAQAGBAEEACBAQIkHGBMCQAAgAgBiRCMQEGMCzNIBIBAggEbY0FACCVmHkHSmZCY7064O__QLuIJEFQMAkSBAIACLECIQwAQDiAAAYAlAAABAhIaAAgIWBQEeAAAACAwAAgAAABBAAACAAQAAICIAAABAAAgAiAQAAAAGgIQAACBABACRIAAAEANCAAgiCEAQg4EAo4AAA" + + if DecodeSegmentType(str) != 0 { + t.Errorf("Segment type should be 0") + return + } + + segment, err := DecodeCoreString(str) + if err != nil { + t.Errorf("Segment should be decoded without error: %s", err) + return + } + + result := segment.Encode() + if result == "" { + t.Errorf("Encode() should be produce a string") + return + } + + if result != str { + t.Errorf("Encode() should produce the same string: in = %s, out = %s", str, result) + } +} + +func TestDecodeDisclosedVendors(t *testing.T) { + str := "IF3EXySoGY2tho2YVFzBEIYwfJxyigMgShgQIsS0NQIeFLBoGPiAAHBGYJAQAGBAkkACBAQIsHGBMCQABgAgRiRCMQEGMDzNIBIBAggkbY0FACCVmnkHS3ZCY70-6u__QA" + + if DecodeSegmentType(str) != 1 { + t.Errorf("Segment type should be 1") + return + } + + segment, err := DecodeDisclosedVendors(str) + if err != nil { + t.Errorf("Segment should be decoded without error: %s", err) + return + } + + if segment.IsVendorDisclosed(1) { + t.Errorf("Vendor 1 should not be disclosed") + return + } + + if !segment.IsVendorDisclosed(9) { + t.Errorf("Vendor 9 should be disclosed") + return + } + + result := segment.Encode() + if result == "" { + t.Errorf("Encode() should be produce a string") + return + } + + if result != str { + t.Errorf("Encode() should produce the same string: in = %s, out = %s", str, result) + } +} + +func TestDecodeAllowedVendors(t *testing.T) { + str := "QF3QAgABAA1A" + + if DecodeSegmentType(str) != 2 { + t.Errorf("Segment type should be 2") + return + } + + segment, err := DecodeAllowedVendors(str) + if err != nil { + t.Errorf("Segment should be decoded without error: %s", err) + return + } + + if segment.IsVendorAllowed(10) { + t.Errorf("Vendor 10 should not be disclosed") + return + } + + if !segment.IsVendorAllowed(53) { + t.Errorf("Vendor 53 should be disclosed") + return + } + + result := segment.Encode() + if result == "" { + t.Errorf("Encode() should be produce a string") + return + } + + if result != str { + t.Errorf("Encode() should produce the same string: in = %s, out = %s", str, result) + } +} + +func TestDecodePublisherTC(t *testing.T) { + str := "elAAAAAAAWA" + + if DecodeSegmentType(str) != 3 { + t.Errorf("Segment type should be 3") + return + } + + segment, err := DecodePubllisherTC(str) + if err != nil { + t.Errorf("Segment should be decoded without error: %s", err) + return + } + + if !segment.IsPurposeAllowed(1) { + t.Errorf("Purpose 1 should be allowed") + return + } + + if segment.NumCustomPurposes != 2 { + t.Errorf("NumCustomPurposes should be 2") + } + + result := segment.Encode() + if result == "" { + t.Errorf("Encode() should be produce a string") + return + } + + if result != str { + t.Errorf("Encode() should produce the same string: in = %s, out = %s", str, result) + } +} diff --git a/encode_test.go b/encode_test.go new file mode 100644 index 0000000..e81e849 --- /dev/null +++ b/encode_test.go @@ -0,0 +1,335 @@ +package iabtcf + +import ( + "testing" + "time" +) + +func TestEncode(t *testing.T) { + str := "COxSKBCOxSKCCBcABCENAgCMAPzAAEPAAAqIDaQBQAMgAgABqAR0A2gDaQAwAMgAgANoAAA.IDaQBQAMgAgABqAR0A2g.QF3QAgABAA1A.eEAAAAAAAUA" + data := &TCData{ + CoreString: &CoreString{ + Version: 2, + Created: timeFromDeciSeconds(15859228738), + LastUpdated: timeFromDeciSeconds(15859228802), + CmpId: 92, + CmpVersion: 1, + ConsentScreen: 2, + ConsentLanguage: "EN", + VendorListVersion: 32, + TcfPolicyVersion: 2, + IsServiceSpecific: false, + UseNonStandardStacks: false, + SpecialFeatureOptIns: map[int]bool{ + 1: true, + 2: true, + }, + PurposesConsent: map[int]bool{ + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 9: true, + 10: true, + }, + PurposesLITransparency: map[int]bool{ + 2: true, + 7: true, + 8: true, + 9: true, + 10: true, + }, + PurposeOneTreatment: false, + PublisherCC: "FR", + MaxVendorId: 436, + IsRangeEncoding: true, + VendorsConsent: map[int]bool{}, + NumEntries: 5, + RangeEntries: []*RangeEntry{ + { + StartVendorID: 25, + EndVendorID: 25, + }, + { + StartVendorID: 32, + EndVendorID: 32, + }, + { + StartVendorID: 53, + EndVendorID: 53, + }, + { + StartVendorID: 285, + EndVendorID: 285, + }, + { + StartVendorID: 436, + EndVendorID: 436, + }, + }, + MaxVendorIdLI: 436, + IsRangeEncodingLI: true, + VendorsLITransparency: map[int]bool{}, + NumEntriesLI: 3, + RangeEntriesLI: []*RangeEntry{ + { + StartVendorID: 25, + EndVendorID: 25, + }, + { + StartVendorID: 32, + EndVendorID: 32, + }, + { + StartVendorID: 436, + EndVendorID: 436, + }, + }, + NumPubRestrictions: 0, + }, + DisclosedVendors: &DisclosedVendors{ + SegmentType: 1, + MaxVendorId: 436, + IsRangeEncoding: true, + NumEntries: 5, + RangeEntries: []*RangeEntry{ + { + StartVendorID: 25, + EndVendorID: 25, + }, + { + StartVendorID: 32, + EndVendorID: 32, + }, + { + StartVendorID: 53, + EndVendorID: 53, + }, + { + StartVendorID: 285, + EndVendorID: 285, + }, + { + StartVendorID: 436, + EndVendorID: 436, + }, + }, + }, + AllowedVendors: &AllowedVendors{ + SegmentType: 2, + MaxVendorId: 750, + IsRangeEncoding: true, + NumEntries: 2, + RangeEntries: []*RangeEntry{ + { + StartVendorID: 2, + EndVendorID: 2, + }, + { + StartVendorID: 53, + EndVendorID: 53, + }, + }, + }, + PublisherTC: &PublisherTC{ + SegmentType: 3, + PubPurposesConsent: map[int]bool{ + 1: true, + 2: true, + 7: true, + }, + PubPurposesLITransparency: map[int]bool{}, + NumCustomPurposes: 2, + CustomPurposesConsent: map[int]bool{ + 1: true, + }, + CustomPurposesLITransparency: map[int]bool{}, + }, + } + + result := data.ToTCString() + if result != str { + t.Errorf("Encode() should produce the same string: in = %s, out = %s", str, result) + } + +} +func TestEncodeCoreString(t *testing.T) { + str := "COxSKBCOxSKCCBcABCENAgCMAPzAAEPAAAqIDaQBQAMgAgABqAR0A2gDaQAwAMgAgANoAAA" + segment := &CoreString{ + Version: 2, + Created: timeFromDeciSeconds(15859228738), + LastUpdated: timeFromDeciSeconds(15859228802), + CmpId: 92, + CmpVersion: 1, + ConsentScreen: 2, + ConsentLanguage: "EN", + VendorListVersion: 32, + TcfPolicyVersion: 2, + IsServiceSpecific: false, + UseNonStandardStacks: false, + SpecialFeatureOptIns: map[int]bool{ + 1: true, + 2: true, + }, + PurposesConsent: map[int]bool{ + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 9: true, + 10: true, + }, + PurposesLITransparency: map[int]bool{ + 2: true, + 7: true, + 8: true, + 9: true, + 10: true, + }, + PurposeOneTreatment: false, + PublisherCC: "FR", + MaxVendorId: 436, + IsRangeEncoding: true, + VendorsConsent: map[int]bool{}, + NumEntries: 5, + RangeEntries: []*RangeEntry{ + { + StartVendorID: 25, + EndVendorID: 25, + }, + { + StartVendorID: 32, + EndVendorID: 32, + }, + { + StartVendorID: 53, + EndVendorID: 53, + }, + { + StartVendorID: 285, + EndVendorID: 285, + }, + { + StartVendorID: 436, + EndVendorID: 436, + }, + }, + MaxVendorIdLI: 436, + IsRangeEncodingLI: true, + VendorsLITransparency: map[int]bool{}, + NumEntriesLI: 3, + RangeEntriesLI: []*RangeEntry{ + { + StartVendorID: 25, + EndVendorID: 25, + }, + { + StartVendorID: 32, + EndVendorID: 32, + }, + { + StartVendorID: 436, + EndVendorID: 436, + }, + }, + NumPubRestrictions: 0, + } + + result := segment.Encode() + if result != str { + t.Errorf("Encode() should produce the same string: in = %s, out = %s", str, result) + } +} + +func TestEncodeDisclosedVendors(t *testing.T) { + str := "IDaQBQAMgAgABqAR0A2g" + segment := &DisclosedVendors{ + SegmentType: 1, + MaxVendorId: 436, + IsRangeEncoding: true, + NumEntries: 5, + RangeEntries: []*RangeEntry{ + { + StartVendorID: 25, + EndVendorID: 25, + }, + { + StartVendorID: 32, + EndVendorID: 32, + }, + { + StartVendorID: 53, + EndVendorID: 53, + }, + { + StartVendorID: 285, + EndVendorID: 285, + }, + { + StartVendorID: 436, + EndVendorID: 436, + }, + }, + } + + result := segment.Encode() + if result != str { + t.Errorf("Encode() should produce the same string: in = %s, out = %s", str, result) + } +} + +func TestEncodeAllowedVendors(t *testing.T) { + str := "QF3QAgABAA1A" + segment := &AllowedVendors{ + SegmentType: 2, + MaxVendorId: 750, + IsRangeEncoding: true, + NumEntries: 2, + RangeEntries: []*RangeEntry{ + { + StartVendorID: 2, + EndVendorID: 2, + }, + { + StartVendorID: 53, + EndVendorID: 53, + }, + }, + } + + result := segment.Encode() + if result != str { + t.Errorf("Encode() should produce the same string: in = %s, out = %s", str, result) + } +} + +func TestEncodePublisherTC(t *testing.T) { + str := "eEAAAAAAAUA" + segment := &PublisherTC{ + SegmentType: 3, + PubPurposesConsent: map[int]bool{ + 1: true, + 2: true, + 7: true, + }, + PubPurposesLITransparency: map[int]bool{}, + NumCustomPurposes: 2, + CustomPurposesConsent: map[int]bool{ + 1: true, + }, + CustomPurposesLITransparency: map[int]bool{}, + } + + result := segment.Encode() + if result != str { + t.Errorf("Encode() should produce the same string: in = %s, out = %s", str, result) + } +} + +func timeFromDeciSeconds(deciseconds int64) time.Time { + return time.Unix(deciseconds/10, (deciseconds%10)*int64(time.Millisecond*100)).UTC() +} diff --git a/encoder.go b/encoder.go new file mode 100644 index 0000000..b3afe6f --- /dev/null +++ b/encoder.go @@ -0,0 +1,105 @@ +package iabtcf + +import ( + "time" +) + +const ( + decisecondsPerSecond = 10 + nanosecondsPerDecisecond = int64(time.Millisecond * 100) +) + +type TCEncoder struct { + *Bits +} + +func NewTCEncoder(src []byte) *TCEncoder { + return &TCEncoder{NewBits(src)} +} + +func (r *TCEncoder) ReadTime() time.Time { + var ds = int64(r.ReadInt(36)) + return time.Unix(ds/decisecondsPerSecond, (ds%decisecondsPerSecond)*nanosecondsPerDecisecond).UTC() +} + +func (r *TCEncoder) WriteTime(v time.Time) { + r.WriteNumber(v.UnixNano()/nanosecondsPerDecisecond, 36) +} + +func (r *TCEncoder) ReadIsoCode() string { + var buf = make([]byte, 0, 2) + for i := uint(0); i < 2; i++ { + buf = append(buf, byte(r.ReadInt(6))+'A') + } + return string(buf) +} + +func (r *TCEncoder) WriteIsoCode(v string) { + for _, char := range v { + r.WriteInt(int(byte(char)-'A'), 6) + } +} + +func (r *TCEncoder) ReadBitField(n uint) map[int]bool { + var m = make(map[int]bool) + for i := uint(0); i < n; i++ { + if r.ReadBool() { + m[int(i)+1] = true + } + } + return m +} + +func (r *TCEncoder) WriteRangeEntries(entries []*RangeEntry) { + for _, entry := range entries { + if entry.EndVendorID > entry.StartVendorID { + r.WriteBool(true) + r.WriteInt(entry.StartVendorID, 16) + r.WriteInt(entry.EndVendorID, 16) + } else { + r.WriteBool(false) + r.WriteInt(entry.StartVendorID, 16) + } + } +} + +func (r *TCEncoder) ReadRangeEntries(n uint) []*RangeEntry { + var ret = make([]*RangeEntry, 0, n) + for i := uint(0); i < n; i++ { + var isRange = r.ReadBool() + var start, end int + start = r.ReadInt(16) + if isRange { + end = r.ReadInt(16) + } else { + end = start + } + ret = append(ret, &RangeEntry{StartVendorID: start, EndVendorID: end}) + } + return ret +} + +func (r *TCEncoder) WritePubRestrictions(entries []*PubRestriction) { + for _, entry := range entries { + r.WriteInt(entry.PurposeId, 6) + r.WriteInt(entry.RestrictionType, 2) + r.WriteInt(len(entry.RangeEntries), 12) + r.WriteRangeEntries(entry.RangeEntries) + } +} + +func (r *TCEncoder) ReadPubRestrictions(n uint) []*PubRestriction { + var ret = make([]*PubRestriction, 0, n) + for i := uint(0); i < n; i++ { + var purposeId = r.ReadInt(6) + var restrictionType = r.ReadInt(2) + var numEntries = r.ReadInt(12) + var rangeEntries = r.ReadRangeEntries(uint(numEntries)) + ret = append(ret, &PubRestriction{PurposeId: purposeId, + RestrictionType: restrictionType, + NumEntries: numEntries, + RangeEntries: rangeEntries, + }) + } + return ret +} diff --git a/segment_allowed_vendors.go b/segment_allowed_vendors.go new file mode 100644 index 0000000..8fef18f --- /dev/null +++ b/segment_allowed_vendors.go @@ -0,0 +1,65 @@ +package iabtcf + +import ( + "encoding/base64" +) + +type AllowedVendors struct { + SegmentType int + MaxVendorId int + IsRangeEncoding bool + AllowedVendors map[int]bool + NumEntries int + RangeEntries []*RangeEntry +} + +func (a *AllowedVendors) IsVendorAllowed(id int) bool { + if a.IsRangeEncoding { + for _, entry := range a.RangeEntries { + if entry.StartVendorID <= id && id <= entry.EndVendorID { + return true + } + } + return false + } + + return a.AllowedVendors[id] +} + +func (a *AllowedVendors) Encode() string { + bitSize := 20 + + if a.IsRangeEncoding { + bitSize += 12 + entriesSize := len(a.RangeEntries) + for _, entry := range a.RangeEntries { + if entry.EndVendorID > entry.StartVendorID { + entriesSize += 16 * 2 + } else { + entriesSize += 16 + } + } + bitSize += entriesSize + } else { + bitSize += a.MaxVendorId + } + + var e = NewTCEncoder(make([]byte, bitSize/8)) + if bitSize%8 != 0 { + e = NewTCEncoder(make([]byte, bitSize/8+1)) + } + + e.WriteInt(a.SegmentType, 3) + e.WriteInt(a.MaxVendorId, 16) + e.WriteBool(a.IsRangeEncoding) + if a.IsRangeEncoding { + e.WriteInt(len(a.RangeEntries), 12) + e.WriteRangeEntries(a.RangeEntries) + } else { + for i := 0; i < a.MaxVendorId; i++ { + e.WriteBool(a.IsVendorAllowed(i + 1)) + } + } + + return base64.RawURLEncoding.EncodeToString(e.bytes) +} diff --git a/segment_core_string.go b/segment_core_string.go new file mode 100644 index 0000000..f9888f5 --- /dev/null +++ b/segment_core_string.go @@ -0,0 +1,190 @@ +package iabtcf + +import ( + "encoding/base64" + "time" +) + +type CoreString struct { + Version int + Created time.Time + LastUpdated time.Time + CmpId int + CmpVersion int + ConsentScreen int + ConsentLanguage string + VendorListVersion int + TcfPolicyVersion int + IsServiceSpecific bool + UseNonStandardStacks bool + SpecialFeatureOptIns map[int]bool + PurposesConsent map[int]bool + PurposesLITransparency map[int]bool + PurposeOneTreatment bool + PublisherCC string + MaxVendorId int + IsRangeEncoding bool + VendorsConsent map[int]bool + NumEntries int + RangeEntries []*RangeEntry + MaxVendorIdLI int + IsRangeEncodingLI bool + VendorsLITransparency map[int]bool + NumEntriesLI int + RangeEntriesLI []*RangeEntry + NumPubRestrictions int + PubRestrictions []*PubRestriction +} + +type PubRestriction struct { + PurposeId int + RestrictionType int + NumEntries int + RangeEntries []*RangeEntry +} + +type RangeEntry struct { + StartVendorID int + EndVendorID int +} + +func (c *CoreString) IsSpecialFeatureAllowed(id int) bool { + return c.SpecialFeatureOptIns[id] +} + +func (c *CoreString) IsPurposeAllowed(id int) bool { + return c.PurposesConsent[id] +} + +func (c *CoreString) IsPurposeLIAllowed(id int) bool { + return c.PurposesLITransparency[id] +} + +func (c *CoreString) IsVendorAllowed(id int) bool { + if c.IsRangeEncoding { + for _, entry := range c.RangeEntries { + if entry.StartVendorID <= id && id <= entry.EndVendorID { + return true + } + } + return false + } + + return c.VendorsConsent[id] +} + +func (c *CoreString) IsVendorLIAllowed(id int) bool { + if c.IsRangeEncodingLI { + for _, entry := range c.RangeEntriesLI { + if entry.StartVendorID <= id && id <= entry.EndVendorID { + return true + } + } + return false + } + + return c.VendorsLITransparency[id] +} + +func (c *CoreString) Encode() string { + bitSize := 230 + + if c.IsRangeEncoding { + bitSize += 12 + entriesSize := len(c.RangeEntries) + for _, entry := range c.RangeEntries { + if entry.EndVendorID > entry.StartVendorID { + entriesSize += 16 * 2 + } else { + entriesSize += 16 + } + } + bitSize += +entriesSize + } else { + bitSize += c.MaxVendorId + } + + bitSize += 16 + if c.IsRangeEncodingLI { + bitSize += 12 + entriesSize := len(c.RangeEntriesLI) + for _, entry := range c.RangeEntriesLI { + if entry.EndVendorID > entry.StartVendorID { + entriesSize += 16 * 2 + } else { + entriesSize += 16 + } + } + bitSize += entriesSize + } else { + bitSize += c.MaxVendorIdLI + } + + bitSize += 12 + for _, res := range c.PubRestrictions { + entriesSize := 20 + for _, entry := range res.RangeEntries { + if entry.EndVendorID > entry.StartVendorID { + entriesSize += 16 * 2 + } else { + entriesSize += 16 + } + } + bitSize += entriesSize + } + + var e = NewTCEncoder(make([]byte, bitSize/8)) + if bitSize%8 != 0 { + e = NewTCEncoder(make([]byte, bitSize/8+1)) + } + + e.WriteInt(c.Version, 6) + e.WriteTime(c.Created) + e.WriteTime(c.LastUpdated) + e.WriteInt(c.CmpId, 12) + e.WriteInt(c.CmpVersion, 12) + e.WriteInt(c.ConsentScreen, 6) + e.WriteIsoCode(c.ConsentLanguage) + e.WriteInt(c.VendorListVersion, 12) + e.WriteInt(c.TcfPolicyVersion, 6) + e.WriteBool(c.IsServiceSpecific) + e.WriteBool(c.UseNonStandardStacks) + for i := 0; i < 12; i++ { + e.WriteBool(c.IsSpecialFeatureAllowed(i + 1)) + } + for i := 0; i < 24; i++ { + e.WriteBool(c.IsPurposeAllowed(i + 1)) + } + for i := 0; i < 24; i++ { + e.WriteBool(c.IsPurposeLIAllowed(i + 1)) + } + e.WriteBool(c.PurposeOneTreatment) + e.WriteIsoCode(c.PublisherCC) + + e.WriteInt(c.MaxVendorId, 16) + e.WriteBool(c.IsRangeEncoding) + if c.IsRangeEncoding { + e.WriteInt(len(c.RangeEntries), 12) + e.WriteRangeEntries(c.RangeEntries) + } else { + for i := 0; i < c.MaxVendorId; i++ { + e.WriteBool(c.IsVendorAllowed(i + 1)) + } + } + + e.WriteInt(c.MaxVendorIdLI, 16) + e.WriteBool(c.IsRangeEncodingLI) + if c.IsRangeEncodingLI { + e.WriteInt(len(c.RangeEntriesLI), 12) + e.WriteRangeEntries(c.RangeEntriesLI) + } else { + for i := 0; i < c.MaxVendorIdLI; i++ { + e.WriteBool(c.IsVendorLIAllowed(i + 1)) + } + } + + e.WriteInt(len(c.PubRestrictions), 12) + e.WritePubRestrictions(c.PubRestrictions) + + return base64.RawURLEncoding.EncodeToString(e.bytes) +} diff --git a/segment_disclosed_vendors.go b/segment_disclosed_vendors.go new file mode 100644 index 0000000..3d8d0f4 --- /dev/null +++ b/segment_disclosed_vendors.go @@ -0,0 +1,65 @@ +package iabtcf + +import ( + "encoding/base64" +) + +type DisclosedVendors struct { + SegmentType int + MaxVendorId int + IsRangeEncoding bool + DisclosedVendors map[int]bool + NumEntries int + RangeEntries []*RangeEntry +} + +func (d *DisclosedVendors) IsVendorDisclosed(id int) bool { + if d.IsRangeEncoding { + for _, entry := range d.RangeEntries { + if entry.StartVendorID <= id && id <= entry.EndVendorID { + return true + } + } + return false + } + + return d.DisclosedVendors[id] +} + +func (d *DisclosedVendors) Encode() string { + bitSize := 20 + + if d.IsRangeEncoding { + bitSize += 12 + entriesSize := len(d.RangeEntries) + for _, entry := range d.RangeEntries { + if entry.EndVendorID > entry.StartVendorID { + entriesSize += 16 * 2 + } else { + entriesSize += 16 + } + } + bitSize += entriesSize + } else { + bitSize += d.MaxVendorId + } + + var e = NewTCEncoder(make([]byte, bitSize/8)) + if bitSize%8 != 0 { + e = NewTCEncoder(make([]byte, bitSize/8+1)) + } + + e.WriteInt(d.SegmentType, 3) + e.WriteInt(d.MaxVendorId, 16) + e.WriteBool(d.IsRangeEncoding) + if d.IsRangeEncoding { + e.WriteInt(len(d.RangeEntries), 12) + e.WriteRangeEntries(d.RangeEntries) + } else { + for i := 0; i < d.MaxVendorId; i++ { + e.WriteBool(d.IsVendorDisclosed(i + 1)) + } + } + + return base64.RawURLEncoding.EncodeToString(e.bytes) +} diff --git a/segment_publisher_tc.go b/segment_publisher_tc.go new file mode 100644 index 0000000..a42a161 --- /dev/null +++ b/segment_publisher_tc.go @@ -0,0 +1,54 @@ +package iabtcf + +import "encoding/base64" + +type PublisherTC struct { + SegmentType int + PubPurposesConsent map[int]bool + PubPurposesLITransparency map[int]bool + NumCustomPurposes int + CustomPurposesConsent map[int]bool + CustomPurposesLITransparency map[int]bool +} + +func (p *PublisherTC) IsPurposeAllowed(id int) bool { + return p.PubPurposesConsent[id] +} + +func (p *PublisherTC) IsPurposeLIAllowed(id int) bool { + return p.PubPurposesLITransparency[id] +} + +func (p *PublisherTC) IsCustomPurposeAllowed(id int) bool { + return p.CustomPurposesConsent[id] +} + +func (p *PublisherTC) IsCustomPurposeLIAllowed(id int) bool { + return p.CustomPurposesLITransparency[id] +} + +func (p *PublisherTC) Encode() string { + bitSize := 57 + (p.NumCustomPurposes * 2) + + var e = NewTCEncoder(make([]byte, bitSize/8)) + if bitSize%8 != 0 { + e = NewTCEncoder(make([]byte, bitSize/8+1)) + } + + e.WriteInt(p.SegmentType, 3) + for i := 0; i < 24; i++ { + e.WriteBool(p.IsPurposeAllowed(i + 1)) + } + for i := 0; i < 24; i++ { + e.WriteBool(p.IsPurposeLIAllowed(i + 1)) + } + e.WriteInt(p.NumCustomPurposes, 6) + for i := 0; i < p.NumCustomPurposes; i++ { + e.WriteBool(p.IsCustomPurposeAllowed(i + 1)) + } + for i := 0; i < p.NumCustomPurposes; i++ { + e.WriteBool(p.IsCustomPurposeLIAllowed(i + 1)) + } + + return base64.RawURLEncoding.EncodeToString(e.bytes) +} diff --git a/tcdata.go b/tcdata.go new file mode 100644 index 0000000..8e86cd7 --- /dev/null +++ b/tcdata.go @@ -0,0 +1,45 @@ +package iabtcf + +import "strings" + +type TCData struct { + CoreString *CoreString + DisclosedVendors *DisclosedVendors + AllowedVendors *AllowedVendors + PublisherTC *PublisherTC +} + +func (t *TCData) IsPurposeAllowed(id int) bool { + return t.CoreString.IsPurposeAllowed(id) +} + +func (t *TCData) IsPurposeLIAllowed(id int) bool { + return t.CoreString.IsPurposeLIAllowed(id) +} + +func (t *TCData) IsVendorAllowed(id int) bool { + return t.CoreString.IsVendorAllowed(id) +} + +func (t *TCData) IsVendorLIAllowed(id int) bool { + return t.CoreString.IsVendorLIAllowed(id) +} + +func (t *TCData) ToTCString() string { + var segments []string + + if t.CoreString != nil { + segments = append(segments, t.CoreString.Encode()) + } + if t.DisclosedVendors != nil { + segments = append(segments, t.DisclosedVendors.Encode()) + } + if t.AllowedVendors != nil { + segments = append(segments, t.AllowedVendors.Encode()) + } + if t.PublisherTC != nil { + segments = append(segments, t.PublisherTC.Encode()) + } + + return strings.Join(segments, ".") +}