Encode and decode TCString

This commit is contained in:
Thomas LAY
2020-04-03 16:38:16 +02:00
parent 288ce8050b
commit 0ffdeca2b5
10 changed files with 1289 additions and 0 deletions

63
bits.go Normal file
View File

@@ -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
}

203
decode.go Normal file
View File

@@ -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
}

164
decode_test.go Normal file
View File

@@ -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)
}
}

335
encode_test.go Normal file
View File

@@ -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()
}

105
encoder.go Normal file
View File

@@ -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
}

View File

@@ -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)
}

190
segment_core_string.go Normal file
View File

@@ -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)
}

View File

@@ -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)
}

54
segment_publisher_tc.go Normal file
View File

@@ -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)
}

45
tcdata.go Normal file
View File

@@ -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, ".")
}