File Coverage

File:blib/lib/Sub/Private.pm
Coverage:100.0%

linestmtbrancondsubtimecode
1package 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
29our $VERSION = '0.05';
30
31# ---------------------------------------------------------------------------
32# Mode-name constants.  Using Readonly prevents accidental overwriting.
33# ---------------------------------------------------------------------------
34
35Readonly::Scalar my $MODE_NAMESPACE => 'namespace';
36Readonly::Scalar my $MODE_ENFORCE   => 'enforce';
37
38# Config-key constants -- avoids bare magic strings in %config lookups.
39Readonly::Scalar my $KEY_MODE           => 'mode';
40Readonly::Scalar my $KEY_HARNESS_BYPASS => 'harness_bypass';
41
42# Self-referential constant: the canonical name of this package.
43Readonly::Scalar my $SELF => __PACKAGE__;
44
45# Validation schema for a single Perl sub name passed to import().
46Readonly::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.
136our $BYPASS = 0;
137
138# Module configuration.  //= preserves any value a caller set in a BEGIN
139# block before this module body runs.
140our %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.
146my @_pending;
147
148# Set to 1 once the CHECK block fires so import() can wrap immediately.
149my $_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.
158sub 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
283sub 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.
323CHECK {
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.
338sub _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.
352sub _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.
384sub _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.
408sub _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.
445sub _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
4581;
459