| File: | blib/lib/Sub/Protected.pm |
| Coverage: | 98.6% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package Sub::Protected; | |||||
| 2 | ||||||
| 3 | # Minimum Perl version: 5.10 (the // operator; several dependencies need it). | |||||
| 4 | # Loading after CHECK (require, string eval) also needs 5.14: see LIMITATIONS. | |||||
| 5 | 13 13 | 1082958 20 | use 5.010; | |||
| 6 | 13 13 13 | 19 8 102 | use strict; | |||
| 7 | 13 13 13 | 43 13 251 | use warnings; | |||
| 8 | ||||||
| 9 | 13 13 13 | 17 12 286 | use Carp qw(croak); | |||
| 10 | 13 13 13 | 2981 22967 29 | use Attribute::Handlers; | |||
| 11 | 13 13 13 | 2207 14921 316 | use Readonly; | |||
| 12 | 13 13 13 | 1784 132471 386 | use Params::Get qw(get_params); | |||
| 13 | 13 13 13 | 3175 111988 394 | use Params::Validate::Strict 0.33 qw(validate_strict); | |||
| 14 | 13 13 13 | 1650 2838 1572 | use Return::Set qw(set_return); | |||
| 15 | ||||||
| 16 | our $VERSION = '0.03'; | |||||
| 17 | ||||||
| 18 | # Public bypass flag. Set to a true value to disable all access checks. | |||||
| 19 | # Use C<local $Sub::Protected::BYPASS = 1> in test code; see BYPASS section. | |||||
| 20 | our $BYPASS = 0; | |||||
| 21 | ||||||
| 22 | # Module-level configuration hash. | |||||
| 23 | # Can be modified directly or injected via Object::Configure. | |||||
| 24 | our %config = ( | |||||
| 25 | # When true, access checks are skipped if $ENV{HARNESS_ACTIVE} is set. | |||||
| 26 | # Set to 0 to test protection behaviour from within a test harness. | |||||
| 27 | harness_bypass => 1, | |||||
| 28 | ); | |||||
| 29 | ||||||
| 30 | # Self-referential constant: the name of this package. | |||||
| 31 | # Used to identify our own frames in the call-stack walk. | |||||
| 32 | Readonly::Scalar my $SELF => __PACKAGE__; | |||||
| 33 | ||||||
| 34 | # Validation schema for a single Perl sub name passed to import(). | |||||
| 35 | Readonly::Scalar my $SUB_NAME_SCHEMA => { | |||||
| 36 | name => { | |||||
| 37 | type => 'string', | |||||
| 38 | regex => qr/\A[_a-zA-Z]\w*\z/, | |||||
| 39 | } | |||||
| 40 | }; | |||||
| 41 | ||||||
| 42 | # Pending (owner_pkg, sub_name) pairs to be wrapped at CHECK time. | |||||
| 43 | # Populated by import(); consumed and cleared by the CHECK block. | |||||
| 44 | my @_pending; | |||||
| 45 | ||||||
| 46 | # Set to 1 when the CHECK block fires. Import() uses this to decide | |||||
| 47 | # whether to schedule wrapping (pre-CHECK) or wrap immediately (post-CHECK). | |||||
| 48 | # If this module is itself loaded after CHECK (run-time require, plugin | |||||
| 49 | # loader, string eval) our CHECK block never runs, so start from the global | |||||
| 50 | # phase. ${^GLOBAL_PHASE} exists from Perl 5.14; on older Perls it is undef | |||||
| 51 | # and the flag starts at 0, so the module must be loaded at compile time. | |||||
| 52 | my $_post_check = (defined(${^GLOBAL_PHASE}) && ${^GLOBAL_PHASE} ne 'START') ? 1 : 0; | |||||
| 53 | ||||||
| 54 | # ------------------------------------------------------------------- | |||||
| 55 | # ATTRIBUTE HANDLER | |||||
| 56 | # ------------------------------------------------------------------- | |||||
| 57 | ||||||
| 58 | # Another module may already own UNIVERSAL::Protected (a :Protected attribute | |||||
| 59 | # of its own). Redefining it would silently change that module's behaviour, | |||||
| 60 | # so say so loudly. This runs before the sub below is compiled. | |||||
| 61 | BEGIN { | |||||
| 62 | 13 | 579 | Carp::carp(__PACKAGE__ . ": UNIVERSAL::Protected is already defined; another module's :Protected attribute will be replaced") | |||
| 63 | if defined &UNIVERSAL::Protected; | |||||
| 64 | } | |||||
| 65 | ||||||
| 66 | # Install the :Protected attribute in UNIVERSAL so every package can use it | |||||
| 67 | # the moment this module is loaded, with no per-package setup needed. | |||||
| 68 | # Attribute::Handlers calls this sub at CHECK phase for each decorated symbol. | |||||
| 69 | # It is also called at BEGIN phase: before CHECK that call does nothing (the | |||||
| 70 | # CHECK-phase call does the work), but after CHECK the CHECK-phase call never | |||||
| 71 | # happens, so the BEGIN-phase call wraps the sub instead. The sub is still | |||||
| 72 | # being compiled at that point and may have no name yet, so the wrapping is | |||||
| 73 | # deferred to the end of the scope (normally the file) being compiled. | |||||
| 74 | # on_scope_end() works here because a BEGIN-phase handler runs while the | |||||
| 75 | # user's file is being compiled; it does NOT work from a CHECK-phase callback. | |||||
| 76 | # The unused parameters ($attr, $data) are required by the protocol. | |||||
| 77 | sub UNIVERSAL::Protected : ATTR(CODE,BEGIN,CHECK) { | |||||
| 78 | 97 | 15252 | my ($package, $symbol, $referent, $attr, $data, $phase) = @_; | |||
| 79 | ||||||
| 80 | 97 | 87 | if($phase eq 'BEGIN') { | |||
| 81 | 48 | 56 | return unless($_post_check); # CHECK will do it | |||
| 82 | 2 | 6 | require B::Hooks::EndOfScope; | |||
| 83 | 2 | 3 | require Sub::Identify; | |||
| 84 | B::Hooks::EndOfScope::on_scope_end(sub { | |||||
| 85 | 2 | 849 | my ($pkg, $name) = Sub::Identify::get_code_info($referent); | |||
| 86 | 13 13 13 | 28 10 548 | no strict 'refs'; | |||
| 87 | 2 2 | 4 5 | UNIVERSAL::Protected($pkg, \*{"${pkg}::$name"}, $referent, $attr, $data, 'CHECK'); | |||
| 88 | 2 | 9 | }); | |||
| 89 | 2 | 13 | return; | |||
| 90 | } | |||||
| 91 | ||||||
| 92 | 49 49 | 26 29 | my $sub_name = *{$symbol}{NAME}; | |||
| 93 | 13 13 13 | 27 10 407 | no warnings 'redefine'; | |||
| 94 | 49 49 | 43 40 | *{$symbol} = _wrap($package, $sub_name, $referent); # function call, not method call | |||
| 95 | 49 | 39 | return; | |||
| 96 | 13 13 13 | 25 27 242 | } | |||
| 97 | ||||||
| 98 | # ------------------------------------------------------------------- | |||||
| 99 | # PUBLIC INTERFACE | |||||
| 100 | # ------------------------------------------------------------------- | |||||
| 101 | ||||||
| 102 - 398 | =head1 NAME
Sub::Protected - Enforce protected subroutine access (Java/C++ semantics)
=head1 VERSION
0.03
=head1 SYNOPSIS
package Foo;
use Sub::Protected; # enables the :Protected attribute
sub new { bless {}, shift }
# Attribute form (preferred: protection lives next to the definition)
sub _helper :Protected {
...
}
sub public_method {
my $self = shift;
$self->_helper; # OK -- same package
}
# ----------------------------------------------------------------
package Bar;
use Sub::Protected qw(_other _private); # declarative form
sub _other { 'other' }
sub _private { 'private' }
=head1 DESCRIPTION
Enforces Java/C++-style "protected" access at runtime: a subroutine
decorated with C<:Protected> (or named in C<use Sub::Protected qw(...)>)
may only be called from within its defining package or from a subclass of
that package. Any other caller causes a C<Carp::croak> with a descriptive
message.
=head2 Two usage forms
=over 4
=item Attribute form (preferred)
sub _helper :Protected { ... }
The C<:Protected> attribute is registered in C<UNIVERSAL> via
L<Attribute::Handlers> when C<Sub::Protected> is loaded, so every package
has access to it without any further C<use> or inheritance. The sub is
wrapped at C<CHECK> time, or, if the module is loaded after C<CHECK> (Perl
5.14 or later), at the end of the file being compiled. This form is preferred because the protection
declaration sits next to the definition and wrapping happens at compile time
(making pre-wrap raw-coderef captures impossible).
=item Declarative form
use Sub::Protected qw(_helper _other);
Each named sub is looked up in the caller's stash and wrapped at C<CHECK>
time (or immediately if the module is loaded at runtime via C<require>).
All named subs must be defined before C<CHECK> fires -- i.e. they must be
compile-time named subs in the same file, not generated at runtime.
=back
=head2 Bypass for testing
Either condition alone (OR logic) disables all access checks:
=over 4
=item * C<$Sub::Protected::BYPASS> set to a true value. Use C<local> in tests.
=item * C<$ENV{HARNESS_ACTIVE}> set (the convention used by L<Test::Harness>/prove).
=back
C<$Sub::Protected::BYPASS> is the recommended form for new test code;
it is explicit and does not depend on the test runner.
C<HARNESS_ACTIVE> is a zero-config convenience.
The HARNESS_ACTIVE bypass can be disabled by setting:
$Sub::Protected::config{harness_bypass} = 0;
=head2 Configuration
The module exposes C<%Sub::Protected::config> for runtime configuration:
=over 4
=item C<harness_bypass> (default: 1)
When true, access checks are skipped whenever C<$ENV{HARNESS_ACTIVE}> is
set. Set to 0 to test protection behaviour from within a test harness.
=back
The hash is compatible with L<Object::Configure> for dependency-injection
scenarios.
=head2 Error message format
_helper() is a protected method of Foo and cannot be called from Bar
The error is raised with L<Carp/croak>, so it reports the file and line of
the offending call, not a line inside Sub::Protected.
=head2 How access is decided
=over 4
=item * Only the immediate caller counts
The decision is made on the package of the code that called the protected
sub. If C<Bar::run> calls C<Foo::public>, which calls C<Foo::_helper>, the
call is allowed: the immediate caller is C<Foo>. If C<Foo::public> instead
calls a C<Bar> method that calls C<Foo::_helper>, it is blocked, however
deep the chain.
=item * Subclasses are allowed
A caller whose package C<isa> the owner is allowed, so calls through
C<SUPER::>, C<< $self->can('_helper') >> and ordinary inherited method
calls all work from a subclass.
=item * The wrapper is invisible
The wrapper hands over with C<goto &sub>, so inside the protected sub
C<caller()> reports the real caller, not Sub::Protected. Arguments
(including aliasing of C<@_>), calling context (list, scalar or void) and
return values, including false ones, pass through unchanged.
=back
=head1 PUBLIC INTERFACE
=head2 import
use Sub::Protected; # attribute form -- no arguments
use Sub::Protected qw(_a _b _c); # declarative form
=head3 Purpose
Called automatically by C<use Sub::Protected>.
With B<no arguments>: does nothing beyond making the C<:Protected> attribute
globally available (which happens when the module is first loaded).
With B<one or more sub names>: registers those subs in the calling
package for wrapping at C<CHECK> time. If C<CHECK> has already fired
(e.g. the module was loaded via runtime C<require>, a plugin loader or a
string C<eval>), wrapping is applied immediately.
Each named sub must be defined before C<CHECK> fires (for pre-CHECK loads)
or before C<import> is called (for post-CHECK loads), so a module loaded at
run time should call C<< Sub::Protected->import(...) >> at the end of the
file, after its subs are defined.
Run-time loading needs Perl 5.14 or later. On older Perls the module must
be loaded at compile time (C<use>, or C<require> inside C<BEGIN>).
=head3 Arguments
=over 4
=item C<$class> (positional, required)
The name of the importing class. Set automatically by the C<use> mechanism.
Must be a non-empty string.
=item C<@subs> (positional, optional)
Zero or more sub names to protect in the calling package. Each must be a
valid Perl identifier: matching C</\A[_a-zA-Z]\w*\z/>.
These calls are equivalent:
Sub::Protected->import(qw(_a _b));
Sub::Protected->import([ qw(_a _b) ]);
Sub::Protected->import({ subs => [ qw(_a _b) ] });
A plain list is always a list of names, so C<qw(subs _x)> protects both
C<subs> and C<_x>. A single arrayref or hashref is normalised by
L<Params::Get>.
=back
=head3 Returns
C<$class> (the importing class name). The return value is ignored by the
C<use> mechanism; it is provided for optional method chaining at the class
level.
=head3 Side effects
=over 4
=item *
Each supplied sub name is appended to an internal pending list (if pre-CHECK)
or wrapped immediately (if post-CHECK).
=item *
The pending list is consumed and cleared when the CHECK block fires.
=item *
C<$@>, C<$!> and C<$_> are left unchanged, as are any pending C<alarm()>
timers.
=back
=head3 Example
package Foo;
use Sub::Protected qw(_helper _init);
sub _helper { ... } # will be protected
sub _init { ... } # will be protected
=head3 API SPECIFICATION
=head4 Input
# Params::Get::get_params / Params::Validate::Strict schema
{
# class is the implicit first argument, set by Perl's 'use' mechanism
subs => {
type => 'array',
required => 0,
each => {
type => 'string',
regex => qr/\A[_a-zA-Z]\w*\z/,
},
},
}
=head4 Output
# Return::Set schema
{
type => 'string',
desc => 'The importing class name ($class), for optional chaining.',
}
=head3 MESSAGES
The following table lists every error or warning this method can produce.
Message Meaning
---------------------------------------- -------------------------------------
"Sub::Protected->import: 'NAME' is not a A sub name passed to import() failed
valid Perl identifier" the identifier regex. Use a name
matching /\A[_a-zA-Z]\w*\z/.
NAME is shown as '' when the
name was undef or a reference.
"Sub::Protected: PKG::NAME is not defined" The named sub was not found in the
package stash at wrap time. For
pre-CHECK loads, ensure the sub is
a compile-time named sub. For
post-CHECK/runtime loads, ensure
the sub is defined before import().
Calling a protected sub from outside its package or subclasses croaks
with the message shown in L</Error message format>.
=encoding utf8
=head3 FORMAL SPECIFICATION
ValidName â { n : seq CHAR | n matches /\A[_a-zA-Z]\w*\z/ }
ââ Import ââââââââââââââââââââââââââââââââââââââââââ
â ÎRegistry
â class? : Package ; caller? : Package
â names? : seq SubName ; postCheck : ð¹
â result! : Package
ââââââââââââââââââââââââââââââââââââââââââââââââââââ
â â n â ran names? ⢠n â ValidName
â postCheck â (â n â ran names? ⢠defined(caller?, n))
â postCheck â protectedâ² = protected ⪠{ n : ran names? ⢠(caller?, n) }
â ¬postCheck â pendingâ² = pending â ⨠n : names? ⢠(caller?, n) â©
â result! = class?
ââââââââââââââââââââââââââââââââââââââââââââââââââââ
-- Failure cases (croak, Registry unchanged):
-- â n â ran names? ⢠n â ValidName
-- â "Sub::Protected->import: 'n' is not a valid Perl identifier"
-- postCheck â§ â n â ran names? ⢠¬defined(caller?, n)
-- â "Sub::Protected: caller?::n is not defined"
=cut | |||||
| 399 | ||||||
| 400 | sub import { | |||||
| 401 | 97 | 280083 | my ($class, @subs) = @_; | |||
| 402 | ||||||
| 403 | # No sub names: the :Protected attribute is always active via UNIVERSAL. | |||||
| 404 | 97 | 137 | return _return_class($class) unless @subs; | |||
| 405 | ||||||
| 406 | # A plain list is always a list of names. Only a single arrayref or | |||||
| 407 | # hashref goes through Params::Get: passing a plain list to it would read | |||||
| 408 | # qw(subs _x) as subs => '_x', silently leaving a sub called subs open. | |||||
| 409 | 50 | 44 | my @names = @subs; | |||
| 410 | 50 | 146 | if (@subs == 1 && ref($subs[0]) && ref($subs[0]) ne 'CODE') { | |||
| 411 | 8 | 15 | my $args = get_params('subs', \@subs); | |||
| 412 | @names = ref($args->{subs}) eq 'ARRAY' | |||||
| 413 | 5 | 7 | ? @{$args->{subs}} | |||
| 414 | 8 | 139 | : ($args->{subs}); | |||
| 415 | } | |||||
| 416 | ||||||
| 417 | # Validate each name against the schema; validate_strict croaks on failure. | |||||
| 418 | # Params::Validate::Strict silently accepts undef for type => 'string', so | |||||
| 419 | # we coerce undef and references to '' so the regex check fires for them. | |||||
| 420 | # local $@ so a successful eval does not clear the caller's $@. | |||||
| 421 | 50 | 42 | for my $sub_name (@names) { | |||
| 422 | 58 | 93 | my $check = (defined $sub_name && !ref $sub_name) ? $sub_name : q{}; | |||
| 423 | 58 | 31 | my $valid = do { | |||
| 424 | 58 | 36 | local $@; | |||
| 425 | 58 58 37 | 33 121 5891 | eval { validate_strict(schema => $SUB_NAME_SCHEMA, input => { name => $check }); 1 }; | |||
| 426 | }; | |||||
| 427 | 58 | 13591 | croak "$SELF->import: '$check' is not a valid Perl identifier" | |||
| 428 | unless $valid; | |||||
| 429 | } | |||||
| 430 | ||||||
| 431 | # Schedule or immediately apply wrapping depending on compilation phase. | |||||
| 432 | 29 | 31 | my $owner_pkg = caller; | |||
| 433 | 29 | 248 | if ($_post_check) { | |||
| 434 | # Module was loaded at runtime (past CHECK): wrap immediately. | |||||
| 435 | 20 | 24 | _process_one($owner_pkg, $_) for @names; | |||
| 436 | } else { | |||||
| 437 | # Pre-CHECK: register for the CHECK block to process. | |||||
| 438 | 9 | 13 | push @_pending, [ $owner_pkg, $_ ] for @names; | |||
| 439 | } | |||||
| 440 | ||||||
| 441 | 24 | 27 | return _return_class($class); | |||
| 442 | } | |||||
| 443 | ||||||
| 444 | # ------------------------------------------------------------------- | |||||
| 445 | # CHECK-TIME PROCESSING | |||||
| 446 | # ------------------------------------------------------------------- | |||||
| 447 | ||||||
| 448 | # Process all pending declarative wraps registered during import(). | |||||
| 449 | # After processing, set the post_check flag so runtime imports wrap directly. | |||||
| 450 | # The bare block silences "Too late to run CHECK block" when this module is | |||||
| 451 | # loaded after CHECK; $_post_check is already 1 in that case. | |||||
| 452 | { | |||||
| 453 | 13 13 13 | 4163 13 1113 | no warnings 'void'; # "Too late to run CHECK block" | |||
| 454 | CHECK { | |||||
| 455 | 12 | 10599 | $_post_check = 1; | |||
| 456 | ||||||
| 457 | # Wrap every pending (package, sub) pair. | |||||
| 458 | 12 | 24 | _process_one(@$_) for @_pending; | |||
| 459 | 12 | 25 | @_pending = (); | |||
| 460 | } | |||||
| 461 | } | |||||
| 462 | ||||||
| 463 | # ------------------------------------------------------------------- | |||||
| 464 | # PRIVATE SUBROUTINES | |||||
| 465 | # ------------------------------------------------------------------- | |||||
| 466 | ||||||
| 467 | # _return_class | |||||
| 468 | # Purpose: Validate and return import()'s return value. | |||||
| 469 | # Entry: ($class) -- the importing class name. | |||||
| 470 | # Exit: Returns $class, checked against the documented string schema. | |||||
| 471 | # Notes: Return::Set::set_return clears $@ even when it succeeds, and | |||||
| 472 | # import() promises to leave $@ alone, so it is localised here. | |||||
| 473 | sub _return_class { | |||||
| 474 | 71 | 51 | my ($class) = @_; | |||
| 475 | ||||||
| 476 | 71 | 70 | local $@; | |||
| 477 | 71 | 102 | return set_return($class, { type => 'string' }); | |||
| 478 | } | |||||
| 479 | ||||||
| 480 | # _process_one | |||||
| 481 | # Purpose: Look up a named sub in a package's stash and wrap it. | |||||
| 482 | # Called from the CHECK block and from import() (post-CHECK). | |||||
| 483 | # Entry: ($owner_pkg, $sub_name) -- both non-empty strings; | |||||
| 484 | # $owner_pkg::$sub_name must be defined in the stash. | |||||
| 485 | # Exit: The stash entry is replaced with the wrapper closure. | |||||
| 486 | # Croaks if the sub is not defined. | |||||
| 487 | # Side effects: Modifies the caller package's stash (redefines the named glob). | |||||
| 488 | # Notes: Uses 'no warnings redefine' because the whole point is replacement. | |||||
| 489 | sub _process_one { | |||||
| 490 | 45 | 12008 | my ($owner_pkg, $sub_name) = @_; | |||
| 491 | ||||||
| 492 | # Guard: only Sub::Protected code should call this. | |||||
| 493 | _assert_private_caller('_process_one') | |||||
| 494 | 45 | 123 | unless $BYPASS || ($config{harness_bypass} && $ENV{HARNESS_ACTIVE}); | |||
| 495 | ||||||
| 496 | 13 13 13 | 39 17 492 | no strict 'refs'; | |||
| 497 | ||||||
| 498 | # Abort clearly when the sub was never defined. | |||||
| 499 | croak "$SELF: ${owner_pkg}::${sub_name} is not defined" | |||||
| 500 | 41 41 | 30 125 | unless defined &{"${owner_pkg}::${sub_name}"}; | |||
| 501 | ||||||
| 502 | 34 34 | 30 34 | my $code = \&{"${owner_pkg}::${sub_name}"}; | |||
| 503 | 13 13 13 | 36 11 2584 | no warnings 'redefine'; | |||
| 504 | 34 34 | 32 48 | *{"${owner_pkg}::${sub_name}"} = _wrap($owner_pkg, $sub_name, $code); | |||
| 505 | 34 | 36 | return; | |||
| 506 | } | |||||
| 507 | ||||||
| 508 | # _wrap | |||||
| 509 | # Purpose: Construct the protection wrapper closure around a coderef. | |||||
| 510 | # Entry: ($owner_pkg, $sub_name, $code) -- all defined; $code is a CODE ref. | |||||
| 511 | # Exit: Returns a new anonymous CODE ref (the wrapper closure). | |||||
| 512 | # At call time: calls _check_access (may croak), then $code. | |||||
| 513 | # Notes: 'goto &$code' is a deliberate, necessary exception to the | |||||
| 514 | # "avoid goto" guideline. It replaces the wrapper's stack frame | |||||
| 515 | # with $code's frame so that caller() inside the protected body | |||||
| 516 | # sees the real caller, not 'Sub::Protected'. Replacing with | |||||
| 517 | # $code->(@_) would make caller(0) return 'Sub::Protected' inside | |||||
| 518 | # the protected sub, breaking any code there that inspects its | |||||
| 519 | # caller. Do NOT change this without updating t/caller_integrity.t. | |||||
| 520 | sub _wrap { | |||||
| 521 | 96 | 18169 | my ($owner_pkg, $sub_name, $code) = @_; | |||
| 522 | ||||||
| 523 | # Guard: only Sub::Protected code (and its subclasses) should call _wrap. | |||||
| 524 | _assert_private_caller('_wrap') | |||||
| 525 | 96 | 194 | unless $BYPASS || ($config{harness_bypass} && $ENV{HARNESS_ACTIVE}); | |||
| 526 | ||||||
| 527 | # Return the wrapper. The goto is load-bearing -- see Notes above. | |||||
| 528 | return sub { | |||||
| 529 | 191 | 777407 | Sub::Protected::_check_access($owner_pkg, $sub_name); | |||
| 530 | 115 | 171 | goto &$code; ## no critic (ControlStructures::ProhibitGoto) | |||
| 531 | 91 | 152 | }; | |||
| 532 | } | |||||
| 533 | ||||||
| 534 | # _check_access | |||||
| 535 | # Purpose: Enforce the protected-access invariant at call time. | |||||
| 536 | # Walk the call stack (skipping Sub::Protected frames) to find | |||||
| 537 | # the first external frame, then allow or croak based on the | |||||
| 538 | # owner/isa relationship. | |||||
| 539 | # Entry: ($owner_pkg, $sub_name) -- both defined strings. | |||||
| 540 | # The current stack has the wrapper closure at the top (frame 0), | |||||
| 541 | # followed by the real caller. | |||||
| 542 | # Exit: Returns normally (undef) if access is permitted. | |||||
| 543 | # Croaks with a descriptive message if access is denied. | |||||
| 544 | # Notes: Frame index starts at 0 (= the wrapper closure that called us). | |||||
| 545 | # The walk increments until a non-Sub::Protected package appears. | |||||
| 546 | # This approach is robust against SUPER:: chains, can() delegation, | |||||
| 547 | # and XS frames that may insert extra frames. | |||||
| 548 | sub _check_access { | |||||
| 549 | 201 | 10898 | my ($owner_pkg, $sub_name) = @_; | |||
| 550 | ||||||
| 551 | # Short-circuit on bypass (OR logic: either condition alone is sufficient). | |||||
| 552 | 201 | 207 | return if $BYPASS; | |||
| 553 | 186 | 362 | return if $config{harness_bypass} && $ENV{HARNESS_ACTIVE}; | |||
| 554 | ||||||
| 555 | # Walk the stack to find the real (first non-SP) caller. | |||||
| 556 | 179 | 118 | my $frame = 0; | |||
| 557 | 179 | 89 | while (1) { | |||
| 558 | 352 | 315 | my $pkg = (caller($frame))[0]; | |||
| 559 | ||||||
| 560 | # No package found: reached the top of the stack. | |||||
| 561 | 352 | 4627 | if (!defined $pkg) { | |||
| 562 | 1 | 5 | croak "${sub_name}() is a protected method of ${owner_pkg}" | |||
| 563 | . ' and cannot be called outside any package context'; | |||||
| 564 | } | |||||
| 565 | ||||||
| 566 | # Skip Sub::Protected's own frames (wrapper closure, _check_access). | |||||
| 567 | 351 | 298 | if ($pkg eq $SELF) { | |||
| 568 | 173 | 96 | $frame++; | |||
| 569 | 173 | 125 | next; | |||
| 570 | } | |||||
| 571 | ||||||
| 572 | # Found the real caller: allow if owner or subclass, else croak. | |||||
| 573 | 178 | 443 | return if $pkg eq $owner_pkg || $pkg->isa($owner_pkg); | |||
| 574 | ||||||
| 575 | 79 | 572 | croak "${sub_name}() is a protected method of ${owner_pkg}" | |||
| 576 | . " and cannot be called from ${pkg}"; | |||||
| 577 | } | |||||
| 578 | } | |||||
| 579 | ||||||
| 580 | # _assert_private_caller | |||||
| 581 | # Purpose: Croak if the guarded private method was called from outside | |||||
| 582 | # Sub::Protected (or a subclass). Used to enforce visibility. | |||||
| 583 | # Entry: ($method_name) -- name of the guarded method, used in the message. | |||||
| 584 | # Must be called directly (not nested deeper) inside the guarded method. | |||||
| 585 | # Exit: Returns normally if the caller is Sub::Protected or a subclass. | |||||
| 586 | # Croaks with a descriptive message otherwise. | |||||
| 587 | # Side effects: Calls Carp::croak on failure. | |||||
| 588 | # Notes: caller() semantics from inside this sub: | |||||
| 589 | # caller(0) -- the package in which the call TO this sub was made. | |||||
| 590 | # That is the guarded method's package = Sub::Protected. | |||||
| 591 | # caller(1) -- the package in which the call TO the guarded method | |||||
| 592 | # was made. That is the actual external caller to check. | |||||
| 593 | sub _assert_private_caller { | |||||
| 594 | 29 | 7952 | my ($method_name) = @_; | |||
| 595 | ||||||
| 596 | # caller(1): the package that invoked the guarded method directly. | |||||
| 597 | 29 | 31 | my $caller = (caller(1))[0] // q{}; | |||
| 598 | 29 14 | 390 42 | return if $caller eq $SELF || eval { $caller->isa($SELF) }; | |||
| 599 | ||||||
| 600 | 12 | 112 | croak "${method_name}() is a private method of $SELF" | |||
| 601 | . " and cannot be called from ${caller}"; | |||||
| 602 | } | |||||
| 603 | ||||||
| 604 | 1; | |||||
| 605 | ||||||