Skip to content

Commit 753a8ed

Browse files
authored
OPT-1479: Add parsing of disclosedVendors block (#6)
1 parent 8852bd3 commit 753a8ed

File tree

8 files changed

+333
-57
lines changed

8 files changed

+333
-57
lines changed

bits.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ func (bits Bits) ToBitString() string {
141141
return result
142142
}
143143

144+
// ReadBitNumber reads bit number as bool and checks boundaries
145+
func (b *Bits) ReadBitNumber(number, offset, maxNbbBits int) bool {
146+
if b == nil || number < 1 || number > maxNbbBits {
147+
return false
148+
}
149+
return b.ReadBoolField(offset + number - 1)
150+
}
151+
144152
// //////////////////////////////////////////////////
145153
// bit string helper
146154

disclosed_vendors.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package iabtcf
2+
3+
// HasDisclosedVendorsBlock returns true if there is at least one disclosedVendors block in the consent string.
4+
func (c *LazyConsent) HasDisclosedVendorsBlock() bool {
5+
6+
for _, block := range c.Extras {
7+
blockType := block.ReadIntField(0, 3)
8+
if blockType == 1 {
9+
return true
10+
}
11+
}
12+
return false
13+
14+
}
15+
16+
// IsVendorDisclosed examines all of the disclosedVendor blocks and returns true if the given vendor ID is found
17+
// in any of them. Returns false if there are no disclosed vendor blocks in the consent string.
18+
//
19+
// See also https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20Consent%20string%20and%20vendor%20list%20formats%20v2.md#disclosed-vendors
20+
func (c *LazyConsent) IsVendorDisclosed(vendorID int) bool {
21+
22+
if vendorID <= 0 {
23+
return false
24+
}
25+
26+
for _, block := range c.Extras {
27+
blockType := block.ReadIntField(0, 3)
28+
if blockType != 1 {
29+
// not a disclosedVendor block
30+
continue
31+
}
32+
33+
maxVendorID := block.ReadIntField(3, 16)
34+
if vendorID > maxVendorID {
35+
continue
36+
}
37+
38+
isRangeEncoding := block.ReadBoolField(19)
39+
if isRangeEncoding {
40+
// range encoding
41+
numEntries := block.ReadIntField(20, 12)
42+
offset := 32
43+
for range numEntries {
44+
isaRange := block.ReadBoolField(offset)
45+
offset++
46+
startID := block.ReadIntField(offset, 16)
47+
offset += 16
48+
if vendorID == startID {
49+
// found vendor ID
50+
return true
51+
}
52+
if isaRange {
53+
endID := block.ReadIntField(offset, 16)
54+
offset += 16
55+
if vendorID > startID && vendorID <= endID {
56+
// vendor ID is in a range
57+
return true
58+
}
59+
}
60+
}
61+
} else {
62+
// Bit field encoding: bit fields start at offset 20, and vendor ID starts at 1
63+
if block.ReadBoolField(19 + vendorID) {
64+
// found vendor ID
65+
return true
66+
}
67+
}
68+
}
69+
70+
return false
71+
}

disclosed_vendors_test.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
package iabtcf
2+
3+
import (
4+
"encoding/base64"
5+
"strconv"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestDisclosedVendors(t *testing.T) {
11+
12+
tests := []struct {
13+
block string
14+
hasDisclosedVendorsBlock bool
15+
vendorID int
16+
hasVendorID bool
17+
}{
18+
{ // case 0
19+
block: "0",
20+
hasDisclosedVendorsBlock: false,
21+
vendorID: 123,
22+
hasVendorID: false,
23+
},
24+
{ // case 1
25+
block: sprintb(1, 3) + sprintb(0, 16),
26+
hasDisclosedVendorsBlock: true,
27+
vendorID: 123,
28+
hasVendorID: false,
29+
},
30+
{ // case 2
31+
block: sprintb(1, 3) + sprintb(256, 16),
32+
hasDisclosedVendorsBlock: true,
33+
vendorID: 123,
34+
hasVendorID: false,
35+
},
36+
{ // case 3
37+
block: sprintb(1, 3) + sprintb(256, 16) + "0",
38+
hasDisclosedVendorsBlock: true,
39+
vendorID: 123,
40+
hasVendorID: false,
41+
},
42+
{ // case 4
43+
block: sprintb(1, 3) + sprintb(256, 16) + "0" + strings.Repeat("0", 123),
44+
hasDisclosedVendorsBlock: true,
45+
vendorID: 123,
46+
hasVendorID: false,
47+
},
48+
{ // case 5
49+
block: sprintb(1, 3) + sprintb(256, 16) + "0" + strings.Repeat("0", 122) + "1",
50+
hasDisclosedVendorsBlock: true,
51+
vendorID: 123,
52+
hasVendorID: true,
53+
},
54+
{ // case 6
55+
block: sprintb(1, 3) + sprintb(256, 16) + "1" + sprintb(0, 12),
56+
hasDisclosedVendorsBlock: true,
57+
vendorID: 123,
58+
hasVendorID: false,
59+
},
60+
{ // case 7
61+
block: sprintb(1, 3) + sprintb(256, 16) + "1" + sprintb(1, 12) + "0" + sprintb(123, 16),
62+
hasDisclosedVendorsBlock: true,
63+
vendorID: 123,
64+
hasVendorID: true,
65+
},
66+
{ // case 8
67+
block: sprintb(1, 3) + sprintb(256, 16) + "1" + sprintb(1, 12) + "0" + sprintb(124, 16),
68+
hasDisclosedVendorsBlock: true,
69+
vendorID: 123,
70+
hasVendorID: false,
71+
},
72+
{ // case 9
73+
block: sprintb(1, 3) + sprintb(256, 16) + "1" + sprintb(1, 12) + "1" + sprintb(120, 16) + sprintb(127, 16),
74+
hasDisclosedVendorsBlock: true,
75+
vendorID: 123,
76+
hasVendorID: true,
77+
},
78+
{ // case 10
79+
block: sprintb(1, 3) + sprintb(256, 16) + "1" + sprintb(1, 12) + "1" + sprintb(130, 16) + sprintb(137, 16),
80+
hasDisclosedVendorsBlock: true,
81+
vendorID: 123,
82+
hasVendorID: false,
83+
},
84+
{ // case 11
85+
block: sprintb(1, 3) + sprintb(256, 16) + "1" + sprintb(2, 12) + "1" + sprintb(130, 16) + sprintb(137, 16) + "0" + sprintb(123, 16),
86+
hasDisclosedVendorsBlock: true,
87+
vendorID: 123,
88+
hasVendorID: true,
89+
},
90+
{ // case 12
91+
block: sprintb(1, 3) + sprintb(256, 16) + "1" + sprintb(2, 12) + "0" + sprintb(124, 16) + "1" + sprintb(120, 16) + sprintb(123, 16),
92+
hasDisclosedVendorsBlock: true,
93+
vendorID: 123,
94+
hasVendorID: true,
95+
},
96+
}
97+
98+
dummyCoreString := base64.RawURLEncoding.EncodeToString(sscanb("000010" + strings.Repeat("0", 224)))
99+
100+
for i, tt := range tests {
101+
consent := dummyCoreString + "." + base64.RawURLEncoding.EncodeToString(sscanb(tt.block))
102+
lc, err := LazyParseCoreString(consent)
103+
if err != nil {
104+
t.Error(err)
105+
continue
106+
}
107+
if want, got := tt.hasDisclosedVendorsBlock, lc.HasDisclosedVendorsBlock(); want != got {
108+
t.Errorf("case %d: has disclosed vendor block: want %v, got %v", i, want, got)
109+
}
110+
if want, got := tt.hasVendorID, lc.IsVendorDisclosed(tt.vendorID); want != got {
111+
t.Errorf("case %d: includes vendor %d: want %v, got %v", i, tt.vendorID, want, got)
112+
}
113+
}
114+
}
115+
116+
// sprintb returns a binary string representation of the number given. The string is guarateed to be exactly bits in
117+
// length. In case of overflow, high order bits are truncated. Bits must be ≤ 63.
118+
func sprintb(number, bits int) string {
119+
x := strconv.FormatInt(int64(number), 2)
120+
switch {
121+
case len(x) > bits:
122+
return x[len(x)-bits:]
123+
case len(x) == bits:
124+
return x
125+
default:
126+
return strings.Repeat("0", bits-len(x)) + x
127+
}
128+
}
129+
130+
func TestSprintb(t *testing.T) {
131+
tests := []struct {
132+
number int
133+
bits int
134+
want string
135+
}{
136+
{5, 4, "0101"}, // 5 in 4 bits: "0101"
137+
{5, 3, "101"}, // 5 in 3 bits: "101"
138+
{5, 2, "01"}, // 5 in 2 bits: "01" (truncated)
139+
{0, 4, "0000"}, // 0 in 4 bits: "0000"
140+
{15, 4, "1111"}, // 15 in 4 bits: "1111"
141+
{16, 4, "0000"}, // 16 in 4 bits: "0000" (truncated)
142+
{1, 1, "1"}, // 1 in 1 bit: "1"
143+
{2, 4, "0010"}, // 2 in 4 bits: "0010"
144+
{255, 8, "11111111"}, // 255 in 8 bits: "11111111"
145+
{256, 8, "00000000"}, // 256 in 8 bits: "00000000" (truncated)
146+
}
147+
for _, tt := range tests {
148+
got := sprintb(tt.number, tt.bits)
149+
if got != tt.want {
150+
t.Errorf("sprintb(%d, %d) = '%s', want '%s'", tt.number, tt.bits, got, tt.want)
151+
}
152+
}
153+
}
154+
155+
// sscanb converts a string of 1s and 0s to a big-endian byte array. That is the first digit in the string is
156+
// mapped to bit 7 of the byte in position 0. Right zero-padding is added as needed.
157+
func sscanb(binary string) []byte {
158+
159+
n := len(binary)
160+
if n%8 != 0 {
161+
binary += strings.Repeat("0", 8-n%8)
162+
n = len(binary)
163+
}
164+
out := make([]byte, n/8)
165+
for i := 0; i < n; i += 8 {
166+
var b byte
167+
for j := range 8 {
168+
if binary[i+j] == '1' {
169+
b |= 1 << (7 - j)
170+
}
171+
}
172+
out[i/8] = b
173+
}
174+
return out
175+
}
176+
177+
func TestSscanb(t *testing.T) {
178+
tests := []struct {
179+
input string
180+
want []byte
181+
}{
182+
{"", []byte{}},
183+
{"1", []byte{128}},
184+
{"0001", []byte{16}},
185+
{"10101", []byte{168}},
186+
{"11111111", []byte{255}},
187+
{"00000000", []byte{0}},
188+
{"1010101010101", []byte{170, 168}},
189+
{"1010101010101011", []byte{170, 171}},
190+
{"10101010101010110", []byte{170, 171, 0}},
191+
{"10101010101010111", []byte{170, 171, 128}},
192+
}
193+
for _, tt := range tests {
194+
got := sscanb(tt.input)
195+
if len(got) != len(tt.want) {
196+
t.Errorf("sscanb(%q) length = %d, want %d", tt.input, len(got), len(tt.want))
197+
continue
198+
}
199+
for i := range got {
200+
if got[i] != tt.want[i] {
201+
t.Errorf("sscanb(%q)[%d] = %d, want %d", tt.input, i, got[i], tt.want[i])
202+
}
203+
}
204+
}
205+
}

go.mod

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
module github.com/travelaudience/go-iabtcf
22

3-
go 1.13
3+
go 1.26
44

55
require (
6-
github.com/google/go-cmp v0.4.0
7-
github.com/pkg/errors v0.9.1
86
github.com/rupertchen/go-bits v0.2.0
9-
github.com/stretchr/testify v1.9.0 // indirect
7+
github.com/stretchr/testify v1.9.0
8+
)
9+
10+
require (
11+
github.com/davecgh/go-spew v1.1.1 // indirect
12+
github.com/pkg/errors v0.9.1 // indirect
13+
github.com/pmezard/go-difflib v1.0.0 // indirect
14+
gopkg.in/yaml.v3 v3.0.1 // indirect
1015
)

go.sum

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,14 @@
1-
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
21
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
32
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4-
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
5-
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
63
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
74
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
85
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
96
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
107
github.com/rupertchen/go-bits v0.2.0 h1:B5+B70H4vWgwMppvo3wiYtwgN1j9m2nD9DJnnMqGcbQ=
118
github.com/rupertchen/go-bits v0.2.0/go.mod h1:V1n1fOC+mPsmLRcRQ5Esgi7CMdsPNeWNz4nVGm+DMJc=
12-
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
13-
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
14-
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
15-
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
16-
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
17-
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
18-
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
199
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
2010
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
21-
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
11+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
2212
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
23-
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
2413
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
2514
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)