Why this matters
GDPR::IAB::TCFv2::Validator currently calls
GDPR::IAB::TCFv2->Parse($input) unconditionally
(lib/GDPR/IAB/TCFv2/Validator.pm, around line 99). That means:
- Correctness gap. When the caller sets
strict_legal_basis => 1,
the validator asserts the stricter spec semantics on the consent
bits, but the parser still happily accepts a v1 string or a v2.3
string missing the Disclosed Vendors segment. A malformed input can
reach the legal-basis check and produce a misleading verdict
instead of being rejected up front.
- Performance gap.
strict_legal_basis checks a specific vendor's
permissions across multiple purposes. For range-encoded TC strings,
each is_vendor_consent_allowed / is_vendor_legitimate_interest_allowed
call walks the range list O(N). Parse(..., prefetch => [ $vendor_id ])
pre-walks the ranges once and caches the answer — see
lib/GDPR/IAB/TCFv2.pm's Parse() and
lib/GDPR/IAB/TCFv2/RangeSection.pm.
Goal
Have validate / validate_all build their parsed object with
strict => 1, prefetch => [ $vendor_id ] whenever appropriate, so a
single Validator instance gives both the spec-tight rejection
and the O(1) hot-vendor lookup users implicitly expect from
strict_legal_basis.
This is only possible if vendor_id, strict_legal_basis, and
cmp_validator are known at construction time. Today they can be
overridden per call via %overrides, which makes the parse-time
decision ambiguous: which vendor do you prefetch? which strictness do
you parse under? So this issue also narrows the per-call override
surface to the three purpose-id lists.
Scope
Narrow validate / validate_all overrides
- Allowed
%overrides keys become: consent_purpose_ids,
legitimate_interest_purpose_ids, flexible_purpose_ids.
- Drop per-call support for:
vendor_id, strict_legal_basis,
verify_disclosed_vendors, min_tcf_policy_version, cmp_validator.
These become constructor-only.
- Unrecognized keys must
croak with an explicit message — silent
no-op would let existing callers lose their override on upgrade.
Couple strict_legal_basis to parser flags
- When the input is a raw
$tc_string, parse with
strict => $self->{strict_legal_basis} ? 1 : 0, prefetch => [ $self->{vendor_id} ].
- When the input is a pre-parsed
GDPR::IAB::TCFv2 object, the
strict/prefetch decision was the caller's; the validator cannot
retro-apply it. Document this contract in the POD.
Tests (t/06-validator.t, t/14-cmp-validator.t)
- Regression: a non-strict-rejected TC string (e.g. a
version != 2
fixture) passed with strict_legal_basis true must produce a
parse-time croak, not a silent validation pass.
- Range-encoded fixture to confirm prefetch is honored (assertion
shape TBD — likely just a sanity check that the call succeeds and
the parsed object's vendor cache is populated).
- New regression:
$validator->validate($tc, vendor_id => $other)
must croak. Replaces existing per-call vendor_id tests
(t/06-validator.t lines 63, 74, 120).
- Same migration for the per-call
strict_legal_basis test
(t/06-validator.t:260) and per-call cmp_validator test
(t/14-cmp-validator.t:156).
Documentation
lib/GDPR/IAB/TCFv2/Validator.pm:
- Update the
strict_legal_basis POD entry to spell out the
coupling (strict parse + prefetch).
- Update the
validate / validate_all POD to reflect the
narrowed override surface.
- Document the pre-parsed-object escape hatch for "parse once,
validate many vendors" — that pattern moves out of the override
machinery and into the documented $tc_string_or_object API.
CHANGELOG.md / release notes: add a MIGRATING section
covering the breaking change.
Out of scope
- Adding new constructor knobs to control parse flags directly —
the coupling is structural, not an extra surface.
- Auto-detecting whether a pre-parsed object was constructed with
strict / prefetch. Possible future enhancement under an
opt-in flag, but not part of this issue.
Open questions (TBD)
- Pre-parsed object guard rails. Should the validator
warn (carp) when it sees a pre-parsed object built without
strict/prefetch while strict_legal_basis is set? Default off,
opt-in flag, or leave silent?
prefetch shape. Parse accepts prefetch => [ ids... ];
single-vendor case is [ $vendor_id ]. Confirm the array form is
the only supported shape today (vs. a bare scalar) before
committing to it in the validator.
- Breaking-change vehicle. Ship this in the next minor or hold
for a major bump? The narrowed override surface is unambiguously
breaking; the strict+prefetch coupling on its own is not. The
tracked memory and roadmap currently assume both ship together
under a major bump.
Definition of done
validate / validate_all accept only the three purpose-id keys
as overrides; everything else croaks.
- The raw-string path parses with the coupled flags; the
pre-parsed-object path is documented as caller-owned.
t/06-validator.t and t/14-cmp-validator.t cover the new
croak-on-unknown-key behavior and the strict+prefetch regression.
lib/GDPR/IAB/TCFv2/Validator.pm POD reflects the new contract.
CHANGELOG.md carries a MIGRATING note.
References
- Roadmap entry:
TODO.pod, "Phase 2 follow-up -- couple Validator
strict_legal_basis to strict parse + vendor prefetch, narrow
per-call overrides to purpose lists"
- Current code:
lib/GDPR/IAB/TCFv2/Validator.pm, especially _run_validation (the unconditional Parse at ~line 99) and the
per-call override resolution that follows it.
- Related:
lib/GDPR/IAB/TCFv2.pm Parse() (strict and prefetch
named arguments) and lib/GDPR/IAB/TCFv2/RangeSection.pm (where
the prefetch cache lives).
Why this matters
GDPR::IAB::TCFv2::Validatorcurrently callsGDPR::IAB::TCFv2->Parse($input)unconditionally(
lib/GDPR/IAB/TCFv2/Validator.pm, around line 99). That means:strict_legal_basis => 1,the validator asserts the stricter spec semantics on the consent
bits, but the parser still happily accepts a v1 string or a v2.3
string missing the Disclosed Vendors segment. A malformed input can
reach the legal-basis check and produce a misleading verdict
instead of being rejected up front.
strict_legal_basischecks a specific vendor'spermissions across multiple purposes. For range-encoded TC strings,
each
is_vendor_consent_allowed/is_vendor_legitimate_interest_allowedcall walks the range list O(N).
Parse(..., prefetch => [ $vendor_id ])pre-walks the ranges once and caches the answer — see
lib/GDPR/IAB/TCFv2.pm'sParse()andlib/GDPR/IAB/TCFv2/RangeSection.pm.Goal
Have
validate/validate_allbuild their parsed object withstrict => 1, prefetch => [ $vendor_id ]whenever appropriate, so asingle
Validatorinstance gives both the spec-tight rejectionand the O(1) hot-vendor lookup users implicitly expect from
strict_legal_basis.This is only possible if
vendor_id,strict_legal_basis, andcmp_validatorare known at construction time. Today they can beoverridden per call via
%overrides, which makes the parse-timedecision ambiguous: which vendor do you prefetch? which strictness do
you parse under? So this issue also narrows the per-call override
surface to the three purpose-id lists.
Scope
Narrow
validate/validate_alloverrides%overrideskeys become:consent_purpose_ids,legitimate_interest_purpose_ids,flexible_purpose_ids.vendor_id,strict_legal_basis,verify_disclosed_vendors,min_tcf_policy_version,cmp_validator.These become constructor-only.
croakwith an explicit message — silentno-op would let existing callers lose their override on upgrade.
Couple
strict_legal_basisto parser flags$tc_string, parse withstrict => $self->{strict_legal_basis} ? 1 : 0, prefetch => [ $self->{vendor_id} ].GDPR::IAB::TCFv2object, thestrict/prefetch decision was the caller's; the validator cannot
retro-apply it. Document this contract in the POD.
Tests (
t/06-validator.t,t/14-cmp-validator.t)version != 2fixture) passed with
strict_legal_basistrue must produce aparse-time
croak, not a silent validation pass.shape TBD — likely just a sanity check that the call succeeds and
the parsed object's vendor cache is populated).
$validator->validate($tc, vendor_id => $other)must
croak. Replaces existing per-callvendor_idtests(
t/06-validator.tlines 63, 74, 120).strict_legal_basistest(
t/06-validator.t:260) and per-callcmp_validatortest(
t/14-cmp-validator.t:156).Documentation
lib/GDPR/IAB/TCFv2/Validator.pm:strict_legal_basisPOD entry to spell out thecoupling (strict parse + prefetch).
validate/validate_allPOD to reflect thenarrowed override surface.
validate many vendors" — that pattern moves out of the override
machinery and into the documented
$tc_string_or_objectAPI.CHANGELOG.md/ release notes: add a MIGRATING sectioncovering the breaking change.
Out of scope
the coupling is structural, not an extra surface.
strict/prefetch. Possible future enhancement under anopt-in flag, but not part of this issue.
Open questions (TBD)
warn (
carp) when it sees a pre-parsed object built withoutstrict/prefetch while
strict_legal_basisis set? Default off,opt-in flag, or leave silent?
prefetchshape.Parseacceptsprefetch => [ ids... ];single-vendor case is
[ $vendor_id ]. Confirm the array form isthe only supported shape today (vs. a bare scalar) before
committing to it in the validator.
for a major bump? The narrowed override surface is unambiguously
breaking; the strict+prefetch coupling on its own is not. The
tracked memory and roadmap currently assume both ship together
under a major bump.
Definition of done
validate/validate_allaccept only the three purpose-id keysas overrides; everything else
croaks.pre-parsed-object path is documented as caller-owned.
t/06-validator.tandt/14-cmp-validator.tcover the newcroak-on-unknown-key behavior and the strict+prefetch regression.
lib/GDPR/IAB/TCFv2/Validator.pmPOD reflects the new contract.CHANGELOG.mdcarries a MIGRATING note.References
TODO.pod, "Phase 2 follow-up -- couple Validatorstrict_legal_basisto strict parse + vendor prefetch, narrowper-call overrides to purpose lists"
lib/GDPR/IAB/TCFv2/Validator.pm, especially_run_validation(the unconditionalParseat ~line 99) and theper-call override resolution that follows it.
lib/GDPR/IAB/TCFv2.pmParse()(strictandprefetchnamed arguments) and
lib/GDPR/IAB/TCFv2/RangeSection.pm(wherethe prefetch cache lives).