| File: | blib/lib/Sub/Private.pm |
| Coverage: | 99.6% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package Sub::Private; | |||||
| 2 | ||||||
| 3 | # Minimum Perl version: 5.14 (${^GLOBAL_PHASE}, needed to protect subs in | |||||
| 4 | # packages loaded after CHECK) | |||||
| 5 | 15 15 | 733151 19 | use 5.014; | |||
| 6 | 15 15 15 | 16 10 90 | use strict; | |||
| 7 | 15 15 15 | 17 5 237 | use warnings; | |||
| 8 | ||||||
| 9 | 15 15 15 | 2987 21555 28 | use Attribute::Handlers; | |||
| 10 | 15 15 15 | 2908 57838 122 | use B::Hooks::EndOfScope qw(); | |||
| 11 | 15 15 15 | 50 6 240 | use Carp qw(croak carp); | |||
| 12 | 15 15 15 | 1720 12187 324 | use Readonly; | |||
| 13 | 15 15 15 | 4784 152488 406 | use Params::Validate::Strict 0.33 qw(validate_strict); | |||
| 14 | 15 15 15 | 2215 175709 334 | use Return::Set qw(set_return); | |||
| 15 | 15 15 15 | 2550 6202 336 | use Sub::Identify qw(get_code_info); | |||
| 16 | ||||||
| 17 | # namespace::clean is used as a class method only; import nothing. | |||||
| 18 | 15 15 15 | 2658 25248 1605 | use namespace::clean qw(); | |||
| 19 | ||||||
| 20 - 28 | =head1 NAME Sub::Private - Private subroutines and methods =head1 VERSION Version 0.07 =cut | |||||
| 29 | ||||||
| 30 | our $VERSION = '0.07'; | |||||
| 31 | ||||||
| 32 | # --------------------------------------------------------------------------- | |||||
| 33 | # Mode-name constants. Using Readonly prevents accidental overwriting. | |||||
| 34 | # --------------------------------------------------------------------------- | |||||
| 35 | ||||||
| 36 | Readonly::Scalar my $MODE_NAMESPACE => 'namespace'; | |||||
| 37 | Readonly::Scalar my $MODE_ENFORCE => 'enforce'; | |||||
| 38 | ||||||
| 39 | # Config-key constants -- avoids bare magic strings in %config lookups. | |||||
| 40 | Readonly::Scalar my $KEY_MODE => 'mode'; | |||||
| 41 | Readonly::Scalar my $KEY_HARNESS_BYPASS => 'harness_bypass'; | |||||
| 42 | ||||||
| 43 | # The phase name the attribute handler passes to itself when it finishes, | |||||
| 44 | # at the end of the enclosing scope, a sub compiled after CHECK. | |||||
| 45 | Readonly::Scalar my $PHASE_SCOPE_END => 'SCOPE_END'; | |||||
| 46 | ||||||
| 47 | # Self-referential constant: the canonical name of this package. | |||||
| 48 | Readonly::Scalar my $SELF => __PACKAGE__; | |||||
| 49 | ||||||
| 50 | # Validation schema for a single Perl sub name passed to import(). | |||||
| 51 | Readonly::Scalar my $SUB_NAME_SCHEMA => { | |||||
| 52 | name => { | |||||
| 53 | type => 'string', | |||||
| 54 | regex => qr/\A[_a-zA-Z]\w*\z/, | |||||
| 55 | } | |||||
| 56 | }; | |||||
| 57 | ||||||
| 58 - 178 | =head1 SYNOPSIS
package Foo;
use Sub::Private;
sub foo { return 42 }
sub bar :Private {
return foo() + 1;
}
sub baz {
return bar() + 1;
}
=head1 DESCRIPTION
Enforces strictly private access on subroutines. A subroutine decorated
with C<:Private> (or named in C<use Sub::Private qw(...)> when in enforce
mode) may only be called from within its defining package. Subclasses do
not inherit access: private means I<this package only>.
=head2 Two enforcement modes
=over 4
=item C<namespace> mode (default, backward-compatible)
Removes the subroutine from the package symbol table using
L<namespace::clean>. Direct (non-method) function calls compiled before
cleanup still work because Perl optimises them to direct opcode references.
OO method dispatch (C<$self->name>) does not work for private subs in this
mode because method lookup uses the symbol table at runtime.
This is the default mode and is backward-compatible with all existing code.
=item C<enforce> mode (OO-safe, opt-in)
Replaces the subroutine with a wrapper closure that checks C<caller> at
call time and either delegates (owner package) or croaks (anyone else).
Works correctly with OO dispatch (C<$self->_helper>).
Enable it for your package only, in the C<use> line:
package MyClass;
use Sub::Private { mode => 'enforce' };
sub _helper :Private { ... }
This is the recommended form for modules. Other packages keep the mode
they chose, or the global C<$Sub::Private::config{mode}>.
The older form sets the mode for every package in the program, and must
stay set until C<CHECK>, so it changes how other modules' C<:Private>
subs are handled. Use it only in a program, not in a module:
BEGIN { $Sub::Private::config{mode} = 'enforce' }
package MyClass;
use Sub::Private;
sub _helper :Private { ... }
=back
=head2 Behaviour of wrapped subs (enforce mode)
=over 4
=item * The wrapper hands over with C<goto &sub>, so it does not appear
on the call stack: C<caller> inside a private sub sees its real caller.
=item * Arguments, C<@_> aliasing and calling context (list, scalar or
void) are passed through unchanged.
=item * C<$_> is left untouched, whether the call is allowed or blocked.
=item * A blocked call croaks; it does not fall through to C<AUTOLOAD>.
=item * Works with L<Moo> and L<Moose> classes; use the declarative form
after C<use Moo> or C<use Moose>.
=back
=head2 Loading at run time
Packages that use C<Sub::Private> may be loaded at run time (C<require>,
a plugin loader, a string C<eval>), and C<Sub::Private> itself may be
loaded for the first time that way. The private subs are then wrapped
(or removed, in C<namespace> mode) when the enclosing scope, normally the
rest of the file, has been compiled.
=head2 Bypass for testing
Either condition alone (OR logic) disables all access checks in enforce
mode:
=over 4
=item * C<$Sub::Private::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::Private::BYPASS> is the recommended form for new test code.
The C<HARNESS_ACTIVE> bypass can be disabled:
$Sub::Private::config{harness_bypass} = 0;
=head2 Configuration
use Sub::Private { mode => 'enforce' }; -- this package only (recommended)
$Sub::Private::config{mode} -- 'namespace' (default) or 'enforce',
for packages that chose no mode
$Sub::Private::config{harness_bypass} -- 1 (default); set to 0 to test enforcement
=head2 Error message format (enforce mode)
bar() is a private subroutine of Foo and cannot be called from Bar
=cut | |||||
| 179 | ||||||
| 180 | # Public bypass flag. Use C<local $Sub::Private::BYPASS = 1> in test code. | |||||
| 181 | our $BYPASS = 0; | |||||
| 182 | ||||||
| 183 | # Module configuration. //= preserves any value a caller set in a BEGIN | |||||
| 184 | # block before this module body runs. | |||||
| 185 | our %config; | |||||
| 186 | $config{$KEY_MODE} //= $MODE_NAMESPACE; | |||||
| 187 | $config{$KEY_HARNESS_BYPASS} //= 1; | |||||
| 188 | ||||||
| 189 | # Pending (owner_pkg, sub_name) pairs to be wrapped at CHECK time. | |||||
| 190 | # Populated by import(); consumed and cleared by the CHECK block. | |||||
| 191 | my @_pending; | |||||
| 192 | ||||||
| 193 | # Modes chosen per package with "use Sub::Private { mode => ... }", keyed by | |||||
| 194 | # package name. A package not listed uses $config{mode}. | |||||
| 195 | my %_package_mode; | |||||
| 196 | ||||||
| 197 | # True once CHECK has run, so wrapping must happen without it. Set by our | |||||
| 198 | # own CHECK block, or here when this module is first loaded after CHECK | |||||
| 199 | # (run-time require, plugin loader, string eval). | |||||
| 200 | my $_post_check = (${^GLOBAL_PHASE} ne 'START') ? 1 : 0; | |||||
| 201 | ||||||
| 202 | # ------------------------------------------------------------------- | |||||
| 203 | # ATTRIBUTE HANDLER | |||||
| 204 | # ------------------------------------------------------------------- | |||||
| 205 | ||||||
| 206 | # Install :Private in UNIVERSAL so every package can use it after a | |||||
| 207 | # single "use Sub::Private", with no per-package setup required. | |||||
| 208 | # ATTR(CODE,CHECK) fires at CHECK time, after all subs are compiled. | |||||
| 209 | # The BEGIN phase covers code compiled after CHECK, when the CHECK-phase | |||||
| 210 | # call never comes. | |||||
| 211 | sub UNIVERSAL::Private :ATTR(CODE,BEGIN,CHECK) { | |||||
| 212 | 138 | 7798 | my ($package, $symbol, $referent, $attr, $data, $phase) = @_; | |||
| 213 | ||||||
| 214 | 138 | 153 | if ($phase eq 'BEGIN') { | |||
| 215 | # Before CHECK, the CHECK-phase call does the work. | |||||
| 216 | 68 | 106 | return unless $_post_check; | |||
| 217 | ||||||
| 218 | # After CHECK the sub is still being compiled and may have no name | |||||
| 219 | # yet, so finish the job when the enclosing scope is compiled. | |||||
| 220 | # on_scope_end works here because the user's code is compiling. | |||||
| 221 | B::Hooks::EndOfScope::on_scope_end(sub { | |||||
| 222 | 5 | 305 | my ($pkg, $name) = get_code_info($referent); | |||
| 223 | 15 15 15 | 32 12 892 | no strict 'refs'; ## no critic (ProhibitNoStrict) | |||
| 224 | 5 5 | 5 6 | UNIVERSAL::Private($pkg, \*{"${pkg}::$name"}, $referent, $attr, $data, $PHASE_SCOPE_END); | |||
| 225 | 5 | 17 | }); | |||
| 226 | 5 | 34 | return; | |||
| 227 | } | |||||
| 228 | ||||||
| 229 | # Once its own CHECK block has run, Attribute::Handlers also makes the | |||||
| 230 | # CHECK-phase call straight away for code compiled later, before the sub | |||||
| 231 | # is named or installed ($symbol is then 'ANON'). Acting on it removed | |||||
| 232 | # the half-compiled sub in namespace mode, and installed a wrapper as | |||||
| 233 | # main::ANON in enforce mode. The BEGIN-phase call above deals with | |||||
| 234 | # that code, so only the real CHECK phase is used here. | |||||
| 235 | 70 | 126 | return if $phase eq 'CHECK' && ${^GLOBAL_PHASE} ne 'CHECK'; | |||
| 236 | ||||||
| 237 | 68 68 | 28 35 | my $sub_name = *{$symbol}{NAME}; | |||
| 238 | ||||||
| 239 | # The mode chosen by the sub's own package, or else the global one. | |||||
| 240 | 68 | 48 | my $mode = _mode_for($package); | |||
| 241 | ||||||
| 242 | # Reject unrecognised mode values early rather than silently misbehaving. | |||||
| 243 | 68 | 48 | _assert_known_mode($mode); | |||
| 244 | ||||||
| 245 | 68 | 40 | if ($mode eq $MODE_ENFORCE) { | |||
| 246 | # Enforce mode: replace the stash entry with an access-checking wrapper. | |||||
| 247 | 15 15 15 | 21 6 588 | no warnings 'redefine'; | |||
| 248 | 60 60 | 29 38 | *{$symbol} = _wrap($package, $sub_name, $referent); | |||
| 249 | } else { | |||||
| 250 | # Namespace mode: remove the sub from the stash entirely. | |||||
| 251 | # on_scope_end does NOT work from a CHECK-phase callback, so we call | |||||
| 252 | # clean_subroutines() directly here. | |||||
| 253 | 8 | 21 | namespace::clean->clean_subroutines( get_code_info($referent) ); | |||
| 254 | } | |||||
| 255 | 68 | 341 | return; | |||
| 256 | 15 15 15 | 29 21 353 | } | |||
| 257 | ||||||
| 258 | # ------------------------------------------------------------------- | |||||
| 259 | # PUBLIC INTERFACE | |||||
| 260 | # ------------------------------------------------------------------- | |||||
| 261 | ||||||
| 262 - 418 | =head1 PUBLIC INTERFACE
=head2 import
use Sub::Private; # attribute form -- no arguments
use Sub::Private qw(_a _b _c); # declarative form (enforce mode only)
use Sub::Private { mode => 'enforce' }; # mode for this package
use Sub::Private { mode => 'enforce' }, qw(_a _b); # both
=head3 Purpose
Called automatically by C<use Sub::Private>.
With B<no arguments>: makes the C<:Private> attribute globally available
via C<UNIVERSAL>. No other action is taken.
With B<a hash reference first>: sets options for the calling package
only. The one option is C<mode>, C<'namespace'> or C<'enforce'>. It
decides what happens to that package's C<:Private> subs and whether it
may use the declarative form, whatever C<$Sub::Private::config{mode}>
says. The global mode is not changed, so other packages are not
affected. The mode applies to subs in the package that made the C<use>
call, so put the C<use> line inside the package.
With B<one or more sub names>: registers those named subs in the calling
package for access-enforcement wrapping at C<CHECK> time. If C<CHECK>
has already fired (for example the package is loaded with C<require> at
run time), the subs are wrapped as soon as the enclosing scope (normally
the rest of the file) has been compiled, so the C<use> line can still
come before the subs it names. A direct C<< Sub::Private->import(...) >>
call at run time wraps immediately. Requires the package's mode (see
above), or else C<$Sub::Private::config{mode}>, to equal C<'enforce'>;
croaks otherwise.
=head3 Arguments
=over 4
=item C<\%options> (optional)
A hash reference, before any sub names. The only key allowed is
C<mode>, whose value must be C<'namespace'> or C<'enforce'>.
=item C<@subs> (optional)
Zero or more Perl sub names. Each must be a defined, non-reference scalar
matching C</\A[_a-zA-Z]\w*\z/>. C<undef>, references, empty strings, and
names starting with a digit or containing hyphens are all rejected.
=back
=head3 Returns
The class name (C<'Sub::Private'>) as a plain string in all cases.
=head3 Side effects
=over 4
=item * With C<\%options>: records the mode for the calling package. A
later call from the same package replaces it.
=item * Pre-CHECK: appends C<[$owner_pkg, $sub_name]> pairs to the
internal C<@_pending> list.
=item * Post-CHECK, during compilation: installs wrapper closures in the
calling package's stash when the enclosing scope has been compiled.
=item * Post-CHECK, at run time: installs wrapper closures directly in the
calling package's stash.
=back
=head3 Example
BEGIN { $Sub::Private::config{mode} = 'enforce' }
package MyClass;
use Sub::Private qw(_helper _init);
sub new { bless {}, shift }
sub _helper { ... } # wrapped at CHECK time
sub _init { ... } # wrapped at CHECK time
sub run { my $s = shift; $s->_helper; $s->_init }
Enforce mode for one package, leaving every other package alone:
package MyClass;
use Sub::Private { mode => 'enforce' };
sub new { bless {}, shift }
sub _helper :Private { ... } # wrapped at CHECK time
sub run { (shift)->_helper }
=head3 API SPECIFICATION
=head4 Input
# No-argument form: always valid.
Sub::Private->import();
# Options, first in the list (optional):
{
options => {
type => 'hashref',
optional => 1,
schema => {
mode => {
type => 'string',
memberof => [ 'namespace', 'enforce' ],
optional => 1,
},
},
}
}
# Declarative form (enforce mode only):
{
subs => {
type => 'array',
optional => 1,
element => {
type => 'string',
regex => qr/\A[_a-zA-Z]\w*\z/,
},
}
}
=head4 Output
{ type => 'string' } # returns the class name 'Sub::Private'
=head3 MESSAGES
Message Meaning / Action
--------------------------------------------------- -----------------------------------------------
"Sub::Private->import: declarative form requires use Sub::Private qw(...) was called while
mode => 'enforce'" the package's mode is not 'enforce'. Use
use Sub::Private { mode => 'enforce' },
qw(...) instead.
"Sub::Private->import: unknown option 'KEY'" The options hash has a key other than mode.
"Sub::Private: unknown mode 'MODE' -- use The mode option, or $config{mode}, is not
'namespace' or 'enforce'" one of the two modes.
"Sub::Private->import: 'NAME' is not a valid The sub name failed the identifier regex.
Perl identifier" Check for typos, hyphens, leading digits,
undef, or reference values in the import list.
"Sub::Private: PKG::NAME is not defined" The named sub was not found in the stash at
wrap time. Define the sub before import()
runs, before CHECK fires, or (after CHECK)
before the end of the enclosing scope.
After CHECK, the location reported is the
"use Sub::Private" line.
=cut | |||||
| 419 | ||||||
| 420 | sub import { | |||||
| 421 | 152 | 248246 | my ($class, @subs) = @_; | |||
| 422 | 152 | 128 | my ($owner_pkg, $file, $line) = caller; | |||
| 423 | ||||||
| 424 | # A leading hashref holds options for the calling package only. | |||||
| 425 | 152 | 1229 | _set_package_options($owner_pkg, shift @subs) if ref($subs[0]) eq 'HASH'; | |||
| 426 | ||||||
| 427 | # No sub names: the :Private attribute is always available via UNIVERSAL. | |||||
| 428 | 148 | 182 | return set_return($class, { type => 'string' }) unless @subs; | |||
| 429 | ||||||
| 430 | # Declarative form is only meaningful in enforce mode. | |||||
| 431 | 71 | 73 | croak "$SELF->import: declarative form requires mode => '$MODE_ENFORCE'" | |||
| 432 | if _mode_for($owner_pkg) ne $MODE_ENFORCE; | |||||
| 433 | ||||||
| 434 | # Validate every name before touching the stash (fail-fast, all-or-nothing). | |||||
| 435 | 63 | 54 | for my $sub_name (@subs) { | |||
| 436 | # Coerce invalid types (undef, ref) to empty string before schema check. | |||||
| 437 | 76 | 115 | my $check = (defined $sub_name && !ref $sub_name) ? $sub_name : q{}; | |||
| 438 | 76 | 40 | eval { | |||
| 439 | 76 | 96 | validate_strict( | |||
| 440 | schema => $SUB_NAME_SCHEMA, | |||||
| 441 | input => { name => $check }, | |||||
| 442 | ); | |||||
| 443 | }; | |||||
| 444 | 76 | 19850 | croak "$SELF->import: '$check' is not a valid Perl identifier" if $@; | |||
| 445 | } | |||||
| 446 | ||||||
| 447 | # Schedule or immediately apply wrapping depending on compile phase. | |||||
| 448 | 31 | 30 | if ($_post_check) { | |||
| 449 | 21 | 34 | if (defined $^S) { | |||
| 450 | # Called at run time: the subs already exist, wrap them now. | |||||
| 451 | 18 | 32 | _process_one($owner_pkg, $_) for @subs; | |||
| 452 | } else { | |||||
| 453 | # "use" after CHECK (e.g. a run-time require): the subs below the | |||||
| 454 | # "use" line are not compiled yet, so wrap them when the | |||||
| 455 | # enclosing scope has been compiled. | |||||
| 456 | B::Hooks::EndOfScope::on_scope_end(sub { | |||||
| 457 | # Report errors at the "use" line, not inside the hook. | |||||
| 458 | 3 3 2 | 186 4 2 | eval { _process_one($owner_pkg, $_) for @subs; 1 } or do { | |||
| 459 | 1 | 74 | (my $err = $@) =~ s/ at .+? line \d+\.?\n\z//s; | |||
| 460 | 1 | 10 | die "$err at $file line $line.\n"; | |||
| 461 | }; | |||||
| 462 | 3 | 8 | }); | |||
| 463 | } | |||||
| 464 | } else { | |||||
| 465 | 10 | 19 | push @_pending, [ $owner_pkg, $_ ] for @subs; | |||
| 466 | } | |||||
| 467 | ||||||
| 468 | 26 | 79 | return set_return($class, { type => 'string' }); | |||
| 469 | } | |||||
| 470 | ||||||
| 471 | # ------------------------------------------------------------------- | |||||
| 472 | # CHECK-TIME PROCESSING | |||||
| 473 | # ------------------------------------------------------------------- | |||||
| 474 | ||||||
| 475 | # Process all declarative wraps queued during import(). | |||||
| 476 | # After this fires, $_post_check=1 so future import() calls wrap without it. | |||||
| 477 | # When this module is loaded after CHECK the block cannot run, which is | |||||
| 478 | # expected; silence "Too late to run CHECK block". | |||||
| 479 | { | |||||
| 480 | 15 15 15 | 5197 59 1907 | no warnings 'void'; | |||
| 481 | CHECK { | |||||
| 482 | 14 | 14527 | $_post_check = 1; | |||
| 483 | 14 | 36 | _process_one(@$_) for @_pending; | |||
| 484 | 14 | 31 | @_pending = (); | |||
| 485 | } | |||||
| 486 | } | |||||
| 487 | ||||||
| 488 | # ------------------------------------------------------------------- | |||||
| 489 | # PRIVATE SUBROUTINES | |||||
| 490 | # ------------------------------------------------------------------- | |||||
| 491 | ||||||
| 492 | # _assert_known_mode | |||||
| 493 | # Purpose : Validate that $config{mode} is a recognised string. | |||||
| 494 | # Entry : $mode -- the value to validate | |||||
| 495 | # Exit status : Returns normally for 'namespace' or 'enforce'; croaks | |||||
| 496 | # with a descriptive message for any other value. | |||||
| 497 | sub _assert_known_mode { | |||||
| 498 | 90 | 10676 | my ($mode) = @_; | |||
| 499 | 90 | 114 | return if $mode eq $MODE_NAMESPACE || $mode eq $MODE_ENFORCE; | |||
| 500 | 11 | 44 | croak "$SELF: unknown mode '$mode'" | |||
| 501 | . " -- use '$MODE_NAMESPACE' or '$MODE_ENFORCE'"; | |||||
| 502 | } | |||||
| 503 | ||||||
| 504 | # _mode_for | |||||
| 505 | # Purpose : Find the mode that applies to the subs of a package. | |||||
| 506 | # Entry : $pkg -- a package name | |||||
| 507 | # Exit status : Returns the mode the package chose with | |||||
| 508 | # "use Sub::Private { mode => ... }", or else $config{mode}. | |||||
| 509 | # Side effects : None. | |||||
| 510 | sub _mode_for { | |||||
| 511 | 139 | 78 | my ($pkg) = @_; | |||
| 512 | ||||||
| 513 | 139 | 262 | return $_package_mode{$pkg} // $config{$KEY_MODE}; | |||
| 514 | } | |||||
| 515 | ||||||
| 516 | # _set_package_options | |||||
| 517 | # Purpose : Record the options a package gave in its "use" line. | |||||
| 518 | # Entry : $pkg -- the package that called import() | |||||
| 519 | # $options -- hashref; the only key allowed is "mode" | |||||
| 520 | # Exit status : Returns normally; croaks on an unknown key or mode. | |||||
| 521 | # Side effects : Sets the package's entry in %_package_mode. The global | |||||
| 522 | # $config{mode} is not touched, so other packages are not | |||||
| 523 | # affected. | |||||
| 524 | sub _set_package_options { | |||||
| 525 | 13 | 8 | my ($pkg, $options) = @_; | |||
| 526 | ||||||
| 527 | # Check every key before changing anything (all-or-nothing). | |||||
| 528 | 13 13 | 5 18 | for my $key (sort keys %{$options}) { | |||
| 529 | 11 | 16 | croak "$SELF->import: unknown option '$key'" if $key ne $KEY_MODE; | |||
| 530 | } | |||||
| 531 | 11 | 13 | if (exists $options->{$KEY_MODE}) { | |||
| 532 | 9 | 9 | my $mode = $options->{$KEY_MODE} // q{}; | |||
| 533 | 9 | 9 | _assert_known_mode($mode); | |||
| 534 | 7 | 2 | $_package_mode{$pkg} = $mode; | |||
| 535 | } | |||||
| 536 | 9 | 11 | return; | |||
| 537 | } | |||||
| 538 | ||||||
| 539 | # _process_one | |||||
| 540 | # Purpose : Look up a named sub in a package stash and install a wrapper. | |||||
| 541 | # Entry : $owner_pkg -- the package that declared the sub | |||||
| 542 | # $sub_name -- the unqualified sub name to wrap | |||||
| 543 | # Exit status : Returns normally; the stash entry is replaced with a wrapper. | |||||
| 544 | # Side effects : Modifies the package stash for $owner_pkg. | |||||
| 545 | # Notes : Guarded by _assert_private_caller -- external calls croak. | |||||
| 546 | sub _process_one { | |||||
| 547 | 48 | 5961 | my ($owner_pkg, $sub_name) = @_; | |||
| 548 | ||||||
| 549 | # Guard: only Sub::Private itself may call this. | |||||
| 550 | _assert_private_caller('_process_one') | |||||
| 551 | 48 | 159 | unless $BYPASS || ($config{$KEY_HARNESS_BYPASS} && $ENV{HARNESS_ACTIVE}); | |||
| 552 | ||||||
| 553 | 15 15 15 | 20 11 435 | no strict 'refs'; ## no critic (ProhibitNoStrict) | |||
| 554 | ||||||
| 555 | # Ensure the target sub exists in the stash before wrapping. | |||||
| 556 | croak "$SELF: ${owner_pkg}::${sub_name} is not defined" | |||||
| 557 | 45 45 | 22 145 | unless defined &{"${owner_pkg}::${sub_name}"}; | |||
| 558 | ||||||
| 559 | 37 37 | 20 45 | my $code = \&{"${owner_pkg}::${sub_name}"}; | |||
| 560 | ||||||
| 561 | # Replace the stash entry with the enforcement wrapper. | |||||
| 562 | 15 15 15 | 41 11 2347 | no warnings 'redefine'; | |||
| 563 | 37 37 | 41 50 | *{"${owner_pkg}::${sub_name}"} = _wrap($owner_pkg, $sub_name, $code); | |||
| 564 | 37 | 44 | return; | |||
| 565 | } | |||||
| 566 | ||||||
| 567 | # _wrap | |||||
| 568 | # Purpose : Build an enforcement wrapper closure around a coderef. | |||||
| 569 | # Entry : $owner_pkg -- the package that owns the private sub | |||||
| 570 | # $sub_name -- the unqualified sub name (for error messages) | |||||
| 571 | # $code -- the original coderef to delegate to | |||||
| 572 | # Exit status : Returns a new coderef that enforces the private-access rule. | |||||
| 573 | # Side effects : none (variables captured by closure) | |||||
| 574 | # Notes : goto &$code is used rather than $code->(@_) so that caller() | |||||
| 575 | # inside the private sub sees the real caller, not Sub::Private. | |||||
| 576 | # This is load-bearing: removing it breaks tests that inspect | |||||
| 577 | # caller() inside a private sub. Guarded by _assert_private_caller. | |||||
| 578 | sub _wrap { | |||||
| 579 | 109 | 11475 | my ($owner_pkg, $sub_name, $code) = @_; | |||
| 580 | ||||||
| 581 | # Guard: only Sub::Private itself may call this. | |||||
| 582 | _assert_private_caller('_wrap') | |||||
| 583 | 109 | 189 | unless $BYPASS || ($config{$KEY_HARNESS_BYPASS} && $ENV{HARNESS_ACTIVE}); | |||
| 584 | ||||||
| 585 | # Capture the three args in the closure; the wrapper has no mutable state. | |||||
| 586 | return sub { | |||||
| 587 | 197 | 558629 | Sub::Private::_check_access($owner_pkg, $sub_name); | |||
| 588 | 109 | 134 | goto &$code; ## no critic (ControlStructures::ProhibitGoto) | |||
| 589 | 103 | 139 | }; | |||
| 590 | } | |||||
| 591 | ||||||
| 592 | # _check_access | |||||
| 593 | # Purpose : Enforce the private-access invariant at call time. | |||||
| 594 | # Entry : $owner_pkg -- the package that owns the private sub | |||||
| 595 | # $sub_name -- unqualified sub name (for error messages) | |||||
| 596 | # Exit status : Returns normally if the immediate non-Sub::Private caller is | |||||
| 597 | # the owner package. Croaks if any other package is found first. | |||||
| 598 | # Notes : Unlike Sub::Protected there is NO ->isa check. Private means | |||||
| 599 | # the owner package ONLY; subclasses are blocked. | |||||
| 600 | # The stack walk skips Sub::Private frames so the wrapper is | |||||
| 601 | # transparent to the check. | |||||
| 602 | sub _check_access { | |||||
| 603 | 208 | 68431 | my ($owner_pkg, $sub_name) = @_; | |||
| 604 | ||||||
| 605 | # Fast bypass paths: either condition alone disables all checks (OR logic). | |||||
| 606 | 208 | 176 | return if $BYPASS; | |||
| 607 | 195 | 317 | return if $config{$KEY_HARNESS_BYPASS} && $ENV{HARNESS_ACTIVE}; | |||
| 608 | ||||||
| 609 | # Walk the call stack, skipping Sub::Private wrapper frames. | |||||
| 610 | 190 | 97 | my $frame = 0; | |||
| 611 | 190 | 89 | while (1) { | |||
| 612 | 373 | 271 | my $pkg = (caller($frame))[0]; | |||
| 613 | ||||||
| 614 | # Reached the bottom of the stack with no valid caller found. | |||||
| 615 | 373 | 3456 | if (!defined $pkg) { | |||
| 616 | 1 | 9 | croak "${sub_name}() is a private subroutine of ${owner_pkg}" | |||
| 617 | . ' and cannot be called from outside any package'; | |||||
| 618 | } | |||||
| 619 | ||||||
| 620 | # Skip any Sub::Private frames (e.g., the wrapper closure itself). | |||||
| 621 | 372 | 256 | $frame++, next if $pkg eq $SELF; | |||
| 622 | ||||||
| 623 | # The first non-Sub::Private caller must be the owner; everyone else | |||||
| 624 | # is blocked -- no isa allowance, unlike Sub::Protected. | |||||
| 625 | 189 | 162 | return if $pkg eq $owner_pkg; | |||
| 626 | 94 | 358 | croak "${sub_name}() is a private subroutine of ${owner_pkg}" | |||
| 627 | . " and cannot be called from ${pkg}"; | |||||
| 628 | } | |||||
| 629 | } | |||||
| 630 | ||||||
| 631 | # _assert_private_caller | |||||
| 632 | # Purpose : Croak if a guarded private method was called from outside | |||||
| 633 | # Sub::Private. | |||||
| 634 | # Entry : $method_name -- the guarded method name (for error messages) | |||||
| 635 | # Exit status : Returns normally if caller(1) is Sub::Private; croaks | |||||
| 636 | # otherwise with a descriptive message. | |||||
| 637 | # Notes : caller(1) is the package that called the guarded method, | |||||
| 638 | # which in turn called this function. | |||||
| 639 | sub _assert_private_caller { | |||||
| 640 | 31 | 3450 | my ($method_name) = @_; | |||
| 641 | ||||||
| 642 | # caller(1): the package one frame above the guarded method. | |||||
| 643 | 31 | 24 | my $caller = (caller(1))[0] // q{}; | |||
| 644 | ||||||
| 645 | # Only calls originating within Sub::Private itself are permitted. | |||||
| 646 | 31 | 259 | return if $caller eq $SELF; | |||
| 647 | ||||||
| 648 | 13 | 41 | croak "${method_name}() is a private method of $SELF" | |||
| 649 | . " and cannot be called from ${caller}"; | |||||
| 650 | } | |||||
| 651 | ||||||
| 652 | 1; | |||||
| 653 | ||||||