File Coverage

File:blib/lib/Sub/Protected.pm
Coverage:98.6%

linestmtbrancondsubtimecode
1package 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
16our $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.
20our $BYPASS = 0;
21
22# Module-level configuration hash.
23# Can be modified directly or injected via Object::Configure.
24our %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.
32Readonly::Scalar my $SELF => __PACKAGE__;
33
34# Validation schema for a single Perl sub name passed to import().
35Readonly::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.
44my @_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.
52my $_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.
61BEGIN {
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.
77sub 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
400sub 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.
473sub _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.
489sub _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.
520sub _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.
548sub _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.
593sub _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
6041;
605