Skip to content

Commit

Permalink
Add range section cache (#26)
Browse files Browse the repository at this point in the history
* add initial code

* reorg code

* rename test

* add unit tests

* add pod

* update readme
  • Loading branch information
peczenyj committed Dec 17, 2023
1 parent f63f7b4 commit 78fdb32
Show file tree
Hide file tree
Showing 5 changed files with 648 additions and 498 deletions.
25 changes: 21 additions & 4 deletions README.pod
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,33 @@ or
date_format => '%Y%m%d', # yyymmdd
},
strict => 1,
prefetch => 284,
);

Parse may receive an optional hash of parameters: C<strict> (boolean) and C<json> (hashref with the following properties):
Parse may receive an optional hash with the following parameters:

=over

=item *

On C<strict> mode we will validate if the version of the consent string is the version 2 (or die with an exception).

The C<strict> mode is disabled by default.

=item *

The C<prefetch> option receives one (as scalar) or more (as arrayref) vendor ids.

This is useful when parsing a range based consent string, since we need to visit all ranges to find a particular id.

=item *

C<json> is hashref with the following properties used to customize the json format:

=over

=item *

C<verbose> changes the json encoding. By default we omit some false values such as C<vendor_consents> to create
a compact json representation. With C<verbose> we will present everything. See L</TO_JSON> for more details.

Expand Down Expand Up @@ -150,9 +169,7 @@ except if the option C<use_epoch> is true.

=back

On C<strict> mode we will validate if the version of the consent string is the version 2 (or die with an exception).

The C<strict> mode is disabled by default.
=back

=head1 METHODS

Expand Down
33 changes: 29 additions & 4 deletions lib/GDPR/IAB/TCFv2.pm
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ sub Parse {
$options{json}->{date_format} ||= DATE_FORMAT_ISO_8601;
$options{json}->{boolean_values} ||= [ _json_false(), _json_true() ];

if ( exists $opts{prefetch} ) {
my $prefetch = $opts{prefetch};

$prefetch = [$prefetch] if ref($prefetch) ne ref( [] );

$options{prefetch} = $prefetch;
}

my $self = {
core_data => $segments->{core_data},
publisher_tc_data => $segments->{publisher_tc},
Expand Down Expand Up @@ -740,14 +748,33 @@ or
date_format => '%Y%m%d', # yyymmdd
},
strict => 1,
prefetch => 284,
);
Parse may receive an optional hash of parameters: C<strict> (boolean) and C<json> (hashref with the following properties):
Parse may receive an optional hash with the following parameters:
=over
=item *
On C<strict> mode we will validate if the version of the consent string is the version 2 (or die with an exception).
The C<strict> mode is disabled by default.
=item *
The C<prefetch> option receives one (as scalar) or more (as arrayref) vendor ids.
This is useful when parsing a range based consent string, since we need to visit all ranges to find a particular id.
=item *
C<json> is hashref with the following properties used to customize the json format:
=over
=item *
C<verbose> changes the json encoding. By default we omit some false values such as C<vendor_consents> to create
a compact json representation. With C<verbose> we will present everything. See L</TO_JSON> for more details.
Expand Down Expand Up @@ -776,9 +803,7 @@ except if the option C<use_epoch> is true.
=back
On C<strict> mode we will validate if the version of the consent string is the version 2 (or die with an exception).
The C<strict> mode is disabled by default.
=back
=head1 METHODS
Expand Down
83 changes: 55 additions & 28 deletions lib/GDPR/IAB/TCFv2/RangeSection.pm
Original file line number Diff line number Diff line change
Expand Up @@ -29,41 +29,58 @@ sub Parse {
"a BitField for vendor consent strings using RangeSections require at least 31 bytes. Got $data_size"
if $data_size < 31;

my ( $num_entries, $next_offset ) = get_uint12( $data, $offset );
my %prefetch;
my %cache;

my @ranges;
if ( exists $options->{prefetch} ) {
my $vendor_ids = $options->{prefetch};

foreach my $i ( 1 .. $num_entries ) {
my $range;
( $range, $next_offset ) = _parse_range(
$data,
$data_size,
$next_offset,
$max_id,
$options,
);

push @ranges, $range;
foreach my $vendor_id ( @{$vendor_ids} ) {
$prefetch{$vendor_id} = 1;
$cache{$vendor_id} = 0;
}
}

my $self = {
ranges => \@ranges,
ranges => [],
cache => \%cache,
max_id => $max_id,
options => $options,
};

bless $self, $klass;

my $next_offset = $self->_parse( $data, $data_size, $offset, \%prefetch );

return ( $self, $next_offset );
}

sub _parse {
my ( $self, $data, $data_size, $offset, $prefetch ) = @_;

my ( $num_entries, $next_offset ) = get_uint12( $data, $offset );

foreach my $i ( 1 .. $num_entries ) {
$next_offset = $self->_parse_range(
$data,
$data_size,
$next_offset,
$prefetch,
);
}

return $next_offset;
}

sub _parse_range {
my ( $data, $data_size, $offset, $max_id, $options ) = @_;
my ( $self, $data, $data_size, $offset, $prefetch ) = @_;

croak
"bit $offset was suppose to start a new range entry, but the consent string was only $data_size bytes long"
if $data_size <= $offset / 8;

my $max_id = $self->{max_id};

# If the first bit is set, it's a Range of IDs
my ( $is_range, $next_offset ) = is_set $data, $offset;
if ($is_range) {
Expand All @@ -82,8 +99,14 @@ sub _parse_range {

croak "start $start can't be bigger than end $end" if $start > $end;

return [ $start, $end ],
$next_offset;
push @{ $self->{ranges} }, [ $start, $end ];

foreach my $vendor_id ( keys %{$prefetch} ) {
$self->{cache}->{$vendor_id} = delete( $prefetch->{$vendor_id} )
if $start <= $vendor_id && $vendor_id <= $end;
}

return $next_offset;
}

my $vendor_id;
Expand All @@ -94,7 +117,12 @@ sub _parse_range {
"bit $offset range entry exclusion vendor $vendor_id, but only vendors [1, $max_id] are valid"
if 1 > $vendor_id || $vendor_id > $max_id;

return [ $vendor_id, $vendor_id ], $next_offset;
push @{ $self->{ranges} }, [ $vendor_id, $vendor_id ];

$self->{cache}->{$vendor_id} = delete( $prefetch->{$vendor_id} )
if exists $prefetch->{$vendor_id};

return $next_offset;
}

sub max_id {
Expand All @@ -109,6 +137,8 @@ sub contains {
croak "invalid vendor id $id: must be positive integer bigger than 0"
if $id < 1;

return $self->{cache}->{$id} if exists $self->{cache}->{$id};

return if $id > $self->{max_id};

foreach my $range ( @{ $self->{ranges} } ) {
Expand All @@ -132,15 +162,7 @@ sub all {
sub TO_JSON {
my $self = shift;

if ( !!$self->{options}->{json}->{compact} ) {
my @vendors;

foreach my $range ( @{ $self->{ranges} } ) {
push @vendors, $range->[0] .. $range->[1];
}

return \@vendors;
}
return $self->all if !!$self->{options}->{json}->{compact};

my ( $false, $true ) = @{ $self->{options}->{json}->{boolean_values} };

Expand Down Expand Up @@ -174,13 +196,14 @@ GDPR::IAB::TCFv2::RangeSection - Transparency & Consent String version 2 range s
data_size => length($data),
offset => 230, # offset for vendor ranges
max_id => $max_id_consent,
prefetch => 284, # will cache the result of vendor id 284
);
say "range section contains id 284" if $range_section->contains(284);
=head1 CONSTRUCTOR
Constructor C<Parse> receives an hash of 5 parameters:
Constructor C<Parse> receives an hash parameters:
=over
Expand All @@ -204,6 +227,10 @@ Key C<max_id> is the max id (used to validate the ranges if all data is between
Key C<options> is the L<GDPR::IAB::TCFv2> options (includes the C<json> field to modify the L</TO_JSON> method output.
=item *
Key C<prefetch> is an optional arrayref of vendor ids to populate the result as cache.
=back
Will die if any parameter is missing.
Expand Down
Loading

0 comments on commit 78fdb32

Please sign in to comment.