| File: | blib/lib/Sub/Private.pm |
| Coverage: | 100.0% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package Sub::Private; | |||||
| 2 | ||||||
| 3 | # Minimum Perl version: 5.8 (Attribute::Handlers became core in 5.8) | |||||
| 4 | 12 12 | 792430 18 | use 5.008; | |||
| 5 | 12 12 12 | 17 8 116 | use strict; | |||
| 6 | 12 12 12 | 14 11 244 | use warnings; | |||
| 7 | 12 12 12 | 2208 71921 27 | use autodie qw(:all); | |||
| 8 | ||||||
| 9 | 12 12 12 | 91353 21262 30 | use Attribute::Handlers; | |||
| 10 | 12 12 12 | 205 8 269 | use Carp qw(croak carp); | |||
| 11 | 12 12 12 | 1734 11389 303 | use Readonly; | |||
| 12 | 12 12 12 | 3941 284704 439 | use Params::Validate::Strict 0.33 qw(validate_strict); | |||
| 13 | 12 12 12 | 2431 3371 257 | use Return::Set qw(set_return); | |||
| 14 | 12 12 12 | 2143 5535 307 | use Sub::Identify qw(get_code_info); | |||
| 15 | ||||||
| 16 | # namespace::clean is used as a class method only; import nothing. | |||||
| 17 | 12 12 12 | 2404 82039 1704 | use namespace::clean qw(); | |||
| 18 | ||||||
| 19 - 27 | =head1 NAME Sub::Private - Private subroutines and methods =head1 VERSION Version 0.05 =cut | |||||
| 28 | ||||||
| 29 | our $VERSION = '0.05'; | |||||
| 30 | ||||||
| 31 | # --------------------------------------------------------------------------- | |||||
| 32 | # Mode-name constants. Using Readonly prevents accidental overwriting. | |||||
| 33 | # --------------------------------------------------------------------------- | |||||
| 34 | ||||||
| 35 | Readonly::Scalar my $MODE_NAMESPACE => 'namespace'; | |||||
| 36 | Readonly::Scalar my $MODE_ENFORCE => 'enforce'; | |||||
| 37 | ||||||
| 38 | # Config-key constants -- avoids bare magic strings in %config lookups. | |||||
| 39 | Readonly::Scalar my $KEY_MODE => 'mode'; | |||||
| 40 | Readonly::Scalar my $KEY_HARNESS_BYPASS => 'harness_bypass'; | |||||
| 41 | ||||||
| 42 | # Self-referential constant: the canonical name of this package. | |||||
| 43 | Readonly::Scalar my $SELF => __PACKAGE__; | |||||
| 44 | ||||||
| 45 | # Validation schema for a single Perl sub name passed to import(). | |||||
| 46 | Readonly::Scalar my $SUB_NAME_SCHEMA => { | |||||
| 47 | name => { | |||||
| 48 | type => 'string', | |||||
| 49 | regex => qr/\A[_a-zA-Z]\w*\z/, | |||||
| 50 | } | |||||
| 51 | }; | |||||
| 52 | ||||||
| 53 - 133 | =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 before declaring your first private sub:
BEGIN { $Sub::Private::config{mode} = 'enforce' }
package MyClass;
use Sub::Private;
sub _helper :Private { ... }
=back
=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
$Sub::Private::config{mode} -- 'namespace' (default) or 'enforce'
$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 | |||||
| 134 | ||||||
| 135 | # Public bypass flag. Use C<local $Sub::Private::BYPASS = 1> in test code. | |||||
| 136 | our $BYPASS = 0; | |||||
| 137 | ||||||
| 138 | # Module configuration. //= preserves any value a caller set in a BEGIN | |||||
| 139 | # block before this module body runs. | |||||
| 140 | our %config; | |||||
| 141 | $config{$KEY_MODE} //= $MODE_NAMESPACE; | |||||
| 142 | $config{$KEY_HARNESS_BYPASS} //= 1; | |||||
| 143 | ||||||
| 144 | # Pending (owner_pkg, sub_name) pairs to be wrapped at CHECK time. | |||||
| 145 | # Populated by import(); consumed and cleared by the CHECK block. | |||||
| 146 | my @_pending; | |||||
| 147 | ||||||
| 148 | # Set to 1 once the CHECK block fires so import() can wrap immediately. | |||||
| 149 | my $_post_check = 0; | |||||
| 150 | ||||||
| 151 | # ------------------------------------------------------------------- | |||||
| 152 | # ATTRIBUTE HANDLER | |||||
| 153 | # ------------------------------------------------------------------- | |||||
| 154 | ||||||
| 155 | # Install :Private in UNIVERSAL so every package can use it after a | |||||
| 156 | # single "use Sub::Private", with no per-package setup required. | |||||
| 157 | # ATTR(CODE,CHECK) fires at CHECK time, after all subs are compiled. | |||||
| 158 | sub UNIVERSAL::Private :ATTR(CODE,CHECK) { | |||||
| 159 | 56 | 1719 | my ($package, $symbol, $referent, $attr, $data) = @_; | |||
| 160 | 56 56 | 34 31 | my $sub_name = *{$symbol}{NAME}; | |||
| 161 | ||||||
| 162 | # Reject unrecognised mode values early rather than silently misbehaving. | |||||
| 163 | 56 | 62 | _assert_known_mode($config{$KEY_MODE}); | |||
| 164 | ||||||
| 165 | 56 | 71 | if ($config{$KEY_MODE} eq $MODE_ENFORCE) { | |||
| 166 | # Enforce mode: replace the stash entry with an access-checking wrapper. | |||||
| 167 | 12 12 12 | 40 12 634 | no warnings 'redefine'; | |||
| 168 | 54 54 | 38 68 | *{$symbol} = _wrap($package, $sub_name, $referent); | |||
| 169 | } else { | |||||
| 170 | # Namespace mode: remove the sub from the stash entirely. | |||||
| 171 | # on_scope_end does NOT work from a CHECK-phase callback, so we call | |||||
| 172 | # clean_subroutines() directly here. | |||||
| 173 | 2 | 8 | namespace::clean->clean_subroutines( get_code_info($referent) ); | |||
| 174 | } | |||||
| 175 | 56 | 143 | return; | |||
| 176 | 12 12 12 | 22 10 358 | } | |||
| 177 | ||||||
| 178 | # ------------------------------------------------------------------- | |||||
| 179 | # PUBLIC INTERFACE | |||||
| 180 | # ------------------------------------------------------------------- | |||||
| 181 | ||||||
| 182 - 281 | =head1 PUBLIC INTERFACE
=head2 import
use Sub::Private; # attribute form -- no arguments
use Sub::Private qw(_a _b _c); # declarative form (enforce mode only)
=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<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 (e.g., when calling from a test), wrapping is applied
immediately. Requires C<$Sub::Private::config{mode}> to equal
C<'enforce'>; croaks otherwise.
=head3 Arguments
=over 4
=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 * Pre-CHECK: appends C<[$owner_pkg, $sub_name]> pairs to the
internal C<@_pending> list.
=item * Post-CHECK: 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 }
=head3 API specification
=head4 Input
# No-argument form: always valid.
Sub::Private->import();
# 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'" $config{mode} is not 'enforce'. Set
$config{mode} = 'enforce' in a BEGIN block
before "use Sub::Private".
"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, or before CHECK fires.
=cut | |||||
| 282 | ||||||
| 283 | sub import { | |||||
| 284 | 127 | 566355 | my ($class, @subs) = @_; | |||
| 285 | ||||||
| 286 | # No sub names: the :Private attribute is always available via UNIVERSAL. | |||||
| 287 | 127 | 262 | return set_return($class, { type => 'string' }) unless @subs; | |||
| 288 | ||||||
| 289 | # Declarative form is only meaningful in enforce mode. | |||||
| 290 | croak "$SELF->import: declarative form requires mode => '$MODE_ENFORCE'" | |||||
| 291 | 63 | 174 | if $config{$KEY_MODE} ne $MODE_ENFORCE; | |||
| 292 | ||||||
| 293 | # Validate every name before touching the stash (fail-fast, all-or-nothing). | |||||
| 294 | 57 | 62 | for my $sub_name (@subs) { | |||
| 295 | # Coerce invalid types (undef, ref) to empty string before schema check. | |||||
| 296 | 68 | 148 | my $check = (defined $sub_name && !ref $sub_name) ? $sub_name : q{}; | |||
| 297 | 68 | 54 | eval { | |||
| 298 | 68 | 134 | validate_strict( | |||
| 299 | schema => $SUB_NAME_SCHEMA, | |||||
| 300 | input => { name => $check }, | |||||
| 301 | ); | |||||
| 302 | }; | |||||
| 303 | 68 | 87671 | croak "$SELF->import: '$check' is not a valid Perl identifier" if $@; | |||
| 304 | } | |||||
| 305 | ||||||
| 306 | # Schedule or immediately apply wrapping depending on compile phase. | |||||
| 307 | 26 | 38 | my $owner_pkg = caller; | |||
| 308 | 26 | 212 | if ($_post_check) { | |||
| 309 | 17 | 36 | _process_one($owner_pkg, $_) for @subs; | |||
| 310 | } else { | |||||
| 311 | 9 | 15 | push @_pending, [ $owner_pkg, $_ ] for @subs; | |||
| 312 | } | |||||
| 313 | ||||||
| 314 | 21 | 49 | return set_return($class, { type => 'string' }); | |||
| 315 | } | |||||
| 316 | ||||||
| 317 | # ------------------------------------------------------------------- | |||||
| 318 | # CHECK-TIME PROCESSING | |||||
| 319 | # ------------------------------------------------------------------- | |||||
| 320 | ||||||
| 321 | # Process all declarative wraps queued during import(). | |||||
| 322 | # After this fires, $_post_check=1 so future import() calls wrap immediately. | |||||
| 323 | CHECK { | |||||
| 324 | 12 | 16076 | $_post_check = 1; | |||
| 325 | 12 | 24 | _process_one(@$_) for @_pending; | |||
| 326 | 12 | 37 | @_pending = (); | |||
| 327 | } | |||||
| 328 | ||||||
| 329 | # ------------------------------------------------------------------- | |||||
| 330 | # PRIVATE SUBROUTINES | |||||
| 331 | # ------------------------------------------------------------------- | |||||
| 332 | ||||||
| 333 | # _assert_known_mode | |||||
| 334 | # Purpose : Validate that $config{mode} is a recognised string. | |||||
| 335 | # Entry : $mode -- the value to validate | |||||
| 336 | # Exit status : Returns normally for 'namespace' or 'enforce'; croaks | |||||
| 337 | # with a descriptive message for any other value. | |||||
| 338 | sub _assert_known_mode { | |||||
| 339 | 69 | 22870 | my ($mode) = @_; | |||
| 340 | 69 | 127 | return if $mode eq $MODE_NAMESPACE || $mode eq $MODE_ENFORCE; | |||
| 341 | 9 | 39 | croak "$SELF: unknown mode '$mode'" | |||
| 342 | . " -- use '$MODE_NAMESPACE' or '$MODE_ENFORCE'"; | |||||
| 343 | } | |||||
| 344 | ||||||
| 345 | # _process_one | |||||
| 346 | # Purpose : Look up a named sub in a package stash and install a wrapper. | |||||
| 347 | # Entry : $owner_pkg -- the package that declared the sub | |||||
| 348 | # $sub_name -- the unqualified sub name to wrap | |||||
| 349 | # Exit status : Returns normally; the stash entry is replaced with a wrapper. | |||||
| 350 | # Side effects : Modifies the package stash for $owner_pkg. | |||||
| 351 | # Notes : Guarded by _assert_private_caller -- external calls croak. | |||||
| 352 | sub _process_one { | |||||
| 353 | 42 | 14041 | my ($owner_pkg, $sub_name) = @_; | |||
| 354 | ||||||
| 355 | # Guard: only Sub::Private itself may call this. | |||||
| 356 | _assert_private_caller('_process_one') | |||||
| 357 | 42 | 126 | unless $BYPASS || ($config{$KEY_HARNESS_BYPASS} && $ENV{HARNESS_ACTIVE}); | |||
| 358 | ||||||
| 359 | 12 12 12 | 4583 19 429 | no strict 'refs'; | |||
| 360 | ||||||
| 361 | # Ensure the target sub exists in the stash before wrapping. | |||||
| 362 | croak "$SELF: ${owner_pkg}::${sub_name} is not defined" | |||||
| 363 | 39 39 | 44 132 | unless defined &{"${owner_pkg}::${sub_name}"}; | |||
| 364 | ||||||
| 365 | 32 32 | 23 53 | my $code = \&{"${owner_pkg}::${sub_name}"}; | |||
| 366 | ||||||
| 367 | # Replace the stash entry with the enforcement wrapper. | |||||
| 368 | 12 12 12 | 32 17 2383 | no warnings 'redefine'; | |||
| 369 | 32 32 | 42 56 | *{"${owner_pkg}::${sub_name}"} = _wrap($owner_pkg, $sub_name, $code); | |||
| 370 | 32 | 41 | return; | |||
| 371 | } | |||||
| 372 | ||||||
| 373 | # _wrap | |||||
| 374 | # Purpose : Build an enforcement wrapper closure around a coderef. | |||||
| 375 | # Entry : $owner_pkg -- the package that owns the private sub | |||||
| 376 | # $sub_name -- the unqualified sub name (for error messages) | |||||
| 377 | # $code -- the original coderef to delegate to | |||||
| 378 | # Exit status : Returns a new coderef that enforces the private-access rule. | |||||
| 379 | # Side effects : none (variables captured by closure) | |||||
| 380 | # Notes : goto &$code is used rather than $code->(@_) so that caller() | |||||
| 381 | # inside the private sub sees the real caller, not Sub::Private. | |||||
| 382 | # This is load-bearing: removing it breaks tests that inspect | |||||
| 383 | # caller() inside a private sub. Guarded by _assert_private_caller. | |||||
| 384 | sub _wrap { | |||||
| 385 | 98 | 21490 | my ($owner_pkg, $sub_name, $code) = @_; | |||
| 386 | ||||||
| 387 | # Guard: only Sub::Private itself may call this. | |||||
| 388 | _assert_private_caller('_wrap') | |||||
| 389 | 98 | 224 | unless $BYPASS || ($config{$KEY_HARNESS_BYPASS} && $ENV{HARNESS_ACTIVE}); | |||
| 390 | ||||||
| 391 | # Capture the three args in the closure; the wrapper has no mutable state. | |||||
| 392 | return sub { | |||||
| 393 | 178 | 591095 | Sub::Private::_check_access($owner_pkg, $sub_name); | |||
| 394 | 98 | 154 | goto &$code; ## no critic (ControlStructures::ProhibitGoto) | |||
| 395 | 92 | 144 | }; | |||
| 396 | } | |||||
| 397 | ||||||
| 398 | # _check_access | |||||
| 399 | # Purpose : Enforce the private-access invariant at call time. | |||||
| 400 | # Entry : $owner_pkg -- the package that owns the private sub | |||||
| 401 | # $sub_name -- unqualified sub name (for error messages) | |||||
| 402 | # Exit status : Returns normally if the immediate non-Sub::Private caller is | |||||
| 403 | # the owner package. Croaks if any other package is found first. | |||||
| 404 | # Notes : Unlike Sub::Protected there is NO ->isa check. Private means | |||||
| 405 | # the owner package ONLY; subclasses are blocked. | |||||
| 406 | # The stack walk skips Sub::Private frames so the wrapper is | |||||
| 407 | # transparent to the check. | |||||
| 408 | sub _check_access { | |||||
| 409 | 189 | 91362 | my ($owner_pkg, $sub_name) = @_; | |||
| 410 | ||||||
| 411 | # Fast bypass paths: either condition alone disables all checks (OR logic). | |||||
| 412 | 189 | 203 | return if $BYPASS; | |||
| 413 | 176 | 359 | return if $config{$KEY_HARNESS_BYPASS} && $ENV{HARNESS_ACTIVE}; | |||
| 414 | ||||||
| 415 | # Walk the call stack, skipping Sub::Private wrapper frames. | |||||
| 416 | 171 | 130 | my $frame = 0; | |||
| 417 | 171 | 130 | while (1) { | |||
| 418 | 335 | 301 | my $pkg = (caller($frame))[0]; | |||
| 419 | ||||||
| 420 | # Reached the bottom of the stack with no valid caller found. | |||||
| 421 | 335 | 3911 | if (!defined $pkg) { | |||
| 422 | 1 | 9 | croak "${sub_name}() is a private subroutine of ${owner_pkg}" | |||
| 423 | . ' and cannot be called from outside any package'; | |||||
| 424 | } | |||||
| 425 | ||||||
| 426 | # Skip any Sub::Private frames (e.g., the wrapper closure itself). | |||||
| 427 | 334 | 306 | $frame++, next if $pkg eq $SELF; | |||
| 428 | ||||||
| 429 | # The first non-Sub::Private caller must be the owner; everyone else | |||||
| 430 | # is blocked -- no isa allowance, unlike Sub::Protected. | |||||
| 431 | 170 | 176 | return if $pkg eq $owner_pkg; | |||
| 432 | 86 | 393 | croak "${sub_name}() is a private subroutine of ${owner_pkg}" | |||
| 433 | . " and cannot be called from ${pkg}"; | |||||
| 434 | } | |||||
| 435 | } | |||||
| 436 | ||||||
| 437 | # _assert_private_caller | |||||
| 438 | # Purpose : Croak if a guarded private method was called from outside | |||||
| 439 | # Sub::Private. | |||||
| 440 | # Entry : $method_name -- the guarded method name (for error messages) | |||||
| 441 | # Exit status : Returns normally if caller(1) is Sub::Private; croaks | |||||
| 442 | # otherwise with a descriptive message. | |||||
| 443 | # Notes : caller(1) is the package that called the guarded method, | |||||
| 444 | # which in turn called this function. | |||||
| 445 | sub _assert_private_caller { | |||||
| 446 | 19 | 7155 | my ($method_name) = @_; | |||
| 447 | ||||||
| 448 | # caller(1): the package one frame above the guarded method. | |||||
| 449 | 19 | 24 | my $caller = (caller(1))[0] // q{}; | |||
| 450 | ||||||
| 451 | # Only calls originating within Sub::Private itself are permitted. | |||||
| 452 | 19 | 273 | return if $caller eq $SELF; | |||
| 453 | ||||||
| 454 | 13 | 56 | croak "${method_name}() is a private method of $SELF" | |||
| 455 | . " and cannot be called from ${caller}"; | |||||
| 456 | } | |||||
| 457 | ||||||
| 458 | 1; | |||||
| 459 | ||||||