X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2F_Util.pm;h=7d4a4075c662444a5563015a2dab80f8aa5bcef5;hb=d6c13bfdf6656317fedbf7e9deeb450cf42efb5b;hp=1c9c4b067cf50c31df68277325aa408942f15b27;hpb=8eac247d3f1150abf238ae6a119860cdc8cf02b9;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/_Util.pm b/lib/DBIx/Class/_Util.pm index 1c9c4b0..7d4a407 100644 --- a/lib/DBIx/Class/_Util.pm +++ b/lib/DBIx/Class/_Util.pm @@ -6,7 +6,10 @@ use DBIx::Class::StartupCheck; # load es early as we can, usually a noop use warnings; use strict; -my $mro_recursor_stack; +# For the love of everything that is crab-like: DO NOT reach into this +# The entire thing is really fragile and should not be screwed with +# unless absolutely and unavoidably necessary +our $__describe_class_query_cache; BEGIN { package # hide from pause @@ -31,6 +34,8 @@ BEGIN { HAS_ITHREADS => $Config{useithreads} ? 1 : 0, + TAINT_MODE => 0 + ${^TAINT}, # tri-state: 0, 1, -1 + UNSTABLE_DOLLARAT => ( PERL_VERSION < 5.013002 ) ? 1 : 0, ( map @@ -44,6 +49,8 @@ BEGIN { DBIC_SHUFFLE_UNORDERED_RESULTSETS DBIC_ASSERT_NO_INTERNAL_WANTARRAY DBIC_ASSERT_NO_INTERNAL_INDIRECT_CALLS + DBIC_ASSERT_NO_ERRONEOUS_METAINSTANCE_USE + DBIC_ASSERT_NO_FAILING_SANITY_CHECKS DBIC_STRESSTEST_UTF8_UPGRADE_GENERATED_COLLAPSER_SOURCE DBIC_STRESSTEST_COLUMN_INFO_UNAWARE_STORAGE ) @@ -68,55 +75,67 @@ BEGIN { require Digest::MD5; require Math::BigInt; + my $cur_class; + no strict 'refs'; + # the non-assign-unless-there-is-a-hash is deliberate - ( $mro_recursor_stack->{cache} || {} )->{$_[0]}{gen} ||= ( + ( $__describe_class_query_cache->{'!internal!'} || {} )->{$_[0]}{gen} ||= ( Math::BigInt->new( '0x' . ( Digest::MD5::md5_hex( join "\0", map { - ( $mro_recursor_stack->{cache} || {} )->{$_}{methlist} ||= do { + ( $__describe_class_query_cache->{'!internal!'} || {} )->{$_}{methlist} ||= ( - my $class = $_; - no strict 'refs'; + $cur_class = $_ + + and # RV to be hashed up and turned into a number join "\0", ( - $class, + $cur_class, map {( # stringification should be sufficient, ignore names/refaddr entirely $_, - attributes::get( $_ ), + do { + my @attrs; + local $@; + local $SIG{__DIE__} if $SIG{__DIE__}; + # attributes::get may throw on blessed-false crefs :/ + eval { @attrs = attributes::get( $_ ); 1 } + or warn "Unable to determine attributes of coderef $_ due to the following error: $@"; + @attrs; + }, )} map {( # skip dummy C::C3 helper crefs - ! ( ( $Class::C3::MRO{$class} || {} )->{methods}{$_} ) + ! ( ( $Class::C3::MRO{$cur_class} || {} )->{methods}{$_} ) and ( - ref(\ "${class}::"->{$_} ) ne 'GLOB' + ref(\ "${cur_class}::"->{$_} ) ne 'GLOB' or - defined( *{ "${class}::"->{$_} }{CODE} ) + defined( *{ "${cur_class}::"->{$_} }{CODE} ) ) ) - ? ( \&{"${class}::$_"} ) + ? ( \&{"${cur_class}::$_"} ) : () } - keys %{ "${class}::" } - ); - } + keys %{ "${cur_class}::" } + ) + ) } ( @{ - ( $mro_recursor_stack->{cache} || {} )->{$_[0]}{linear_isa} + ( $__describe_class_query_cache->{'!internal!'} || {} )->{$_[0]}{linear_isa} ||= mro::get_linear_isa($_[0]) }, (( - ( $mro_recursor_stack->{cache} || {} )->{$_[0]}{is_universal} + ( $__describe_class_query_cache->{'!internal!'} || {} )->{$_[0]}{is_universal} ||= mro::is_universal($_[0]) ) ? () : @{ - ( $mro_recursor_stack->{cache} || {} )->{UNIVERSAL}{linear_isa} + ( $__describe_class_query_cache->{'!internal!'} || {} )->{UNIVERSAL}{linear_isa} ||= mro::get_linear_isa("UNIVERSAL") } ), @@ -136,13 +155,11 @@ BEGIN { # to be outfits with *COPY PASTED* pieces of lib/DBIx/Class/Storage/* # in their production codebases. There is no point in breaking these # if whatever they used actually continues to work - my $warned; my $sigh = sub { - - require Carp; - my $cluck = "The @{[ (caller(1))[3] ]} constant is no more - adjust your code" . Carp::longmess(); - - warn $cluck unless $warned->{$cluck}++; + DBIx::Class::_Util::emit_loud_diag( + skip_frames => 1, + msg => "The @{[ (caller(1))[3] ]} constant is no more - adjust your code" + ); 0; }; @@ -160,10 +177,17 @@ use B (); use Carp 'croak'; use Storable 'nfreeze'; use Scalar::Util qw(weaken blessed reftype refaddr); -use Sub::Quote qw(qsub); use Sub::Name (); use attributes (); +# Usually versions are not specified anywhere aside the Makefile.PL +# (writing them out in-code is extremely obnoxious) +# However without a recent enough Moo the quote_sub override fails +# in very puzzling and hard to detect ways: so add a version check +# just this once +use Sub::Quote qw(qsub); +BEGIN { Sub::Quote->VERSION('2.002002') } + # Already correctly prototyped: perlbrew exec perl -MStorable -e 'warn prototype \&Storable::dclone' BEGIN { *deep_clone = \&Storable::dclone } @@ -171,8 +195,8 @@ use base 'Exporter'; our @EXPORT_OK = qw( sigwarn_silencer modver_gt_or_eq modver_gt_or_eq_and_lt fail_on_internal_wantarray fail_on_internal_call - refdesc refcount hrefaddr set_subname describe_class_methods - scope_guard detected_reinvoked_destructor + refdesc refcount hrefaddr set_subname get_subname describe_class_methods + scope_guard detected_reinvoked_destructor emit_loud_diag true false is_exception dbic_internal_try visit_namespaces quote_sub qsub perlstring serialize deep_clone dump_value uniq @@ -310,7 +334,14 @@ sub visit_namespaces { $visited_count; } -# FIXME In another life switch this to a polyfill like the one in namespace::clean +# FIXME In another life switch these to a polyfill like the ones in namespace::clean +sub get_subname ($) { + my $gv = B::svref_2object( $_[0] )->GV; + wantarray + ? ( $gv->STASH->NAME, $gv->NAME ) + : ( join '::', $gv->STASH->NAME, $gv->NAME ) + ; +} sub set_subname ($$) { # fully qualify name @@ -321,7 +352,19 @@ sub set_subname ($$) { } sub serialize ($) { + # stable hash order local $Storable::canonical = 1; + + # explicitly false - there is nothing sensible that can come out of + # an attempt at CODE serialization + local $Storable::Deparse; + + # take no chances + local $Storable::forgive_me; + + # FIXME + # A number of codepaths *expect* this to be Storable.pm-based so that + # the STORABLE_freeze hooks in the metadata subtree get executed properly nfreeze($_[0]); } @@ -357,9 +400,20 @@ sub dump_value ($) { ->Deparse(1) ; - $d->Sparseseen(1) if modver_gt_or_eq ( - 'Data::Dumper', '2.136' - ); + # FIXME - this is kinda ridiculous - there ought to be a + # Data::Dumper->new_with_defaults or somesuch... + # + if( modver_gt_or_eq ( 'Data::Dumper', '2.136' ) ) { + $d->Sparseseen(1); + + if( modver_gt_or_eq ( 'Data::Dumper', '2.153' ) ) { + $d->Maxrecurse(1000); + + if( modver_gt_or_eq ( 'Data::Dumper', '2.160' ) ) { + $d->Trailingcomma(1); + } + } + } $d; } @@ -370,6 +424,64 @@ sub dump_value ($) { $dump_str; } +my $seen_loud_screams; +sub emit_loud_diag { + my $args = { ref $_[0] eq 'HASH' ? %{$_[0]} : @_ }; + + unless ( defined $args->{msg} and length $args->{msg} ) { + emit_loud_diag( + msg => "No 'msg' value supplied to emit_loud_diag()" + ); + exit 70; + } + + my $msg = "\n" . join( ': ', + ( $0 eq '-e' ? () : $0 ), + $args->{msg} + ); + + # when we die - we usually want to keep doing it + $args->{emit_dups} = !!$args->{confess} + unless exists $args->{emit_dups}; + + local $Carp::CarpLevel = + ( $args->{skip_frames} || 0 ) + + + $Carp::CarpLevel + + + # hide our own frame + 1 + ; + + my $longmess = Carp::longmess(); + + # different object references will thwart deduplication without this + ( my $key = "${msg}\n${longmess}" ) =~ s/\b0x[0-9a-f]+\b/0x.../gi; + + return $seen_loud_screams->{$key} if + $seen_loud_screams->{$key}++ + and + ! $args->{emit_dups} + ; + + $msg .= $longmess + unless $msg =~ /\n\z/; + + print STDERR "$msg\n" + or + print STDOUT "\n!!!STDERR ISN'T WRITABLE!!!:$msg\n"; + + return $seen_loud_screams->{$key} + unless $args->{confess}; + + # increment *again*, because... Carp. + $Carp::CarpLevel++; + + # not $msg - Carp will reapply the longmess on its own + Carp::confess($args->{msg}); +} + + ### ### This is *NOT* boolean.pm - deliberately not using a singleton ### @@ -405,8 +517,9 @@ sub scope_guard (&) { 1; } or - Carp::cluck( - "Execution of scope guard $_[0] resulted in the non-trappable exception:\n\n$@" + DBIx::Class::_Util::emit_loud_diag( + emit_dups => 1, + msg => "Execution of scope guard $_[0] resulted in the non-trappable exception:\n\n$@\n " ); } } @@ -471,18 +584,16 @@ sub is_exception ($) { and length( my $class = ref $e ) ) { - carp_unique( sprintf( - "Objects of external exception class '%s' stringify to '' (the " + carp_unique( + "Objects of external exception class '$class' stringify to '' (the " . 'empty string), implementing the so called null-object-pattern. ' . 'Given Perl\'s "globally cooperative" exception handling using this ' . 'class of exceptions is extremely dangerous, as it may (and often ' . 'does) result in silent discarding of errors. DBIx::Class tries to ' . 'work around this as much as possible, but other parts of your ' . 'software stack may not be even aware of the problem. Please submit ' - . 'a bugreport against the distribution containing %s', - - ($class) x 2, - )); + . "a bugreport against the distribution containing '$class'", + ); $not_blank = 1; } @@ -595,10 +706,10 @@ sub is_exception ($) { for keys %$destruction_registry; if (! length ref $_[0]) { - printf STDERR '%s() expects a blessed reference %s', - (caller(0))[3], - Carp::longmess, - ; + emit_loud_diag( + emit_dups => 1, + msg => (caller(0))[3] . '() expects a blessed reference' + ); return undef; # don't know wtf to do } elsif (! defined $destruction_registry->{ my $addr = refaddr($_[0]) } ) { @@ -606,7 +717,7 @@ sub is_exception ($) { return 0; } else { - carp_unique ( sprintf ( + emit_loud_diag( msg => sprintf ( 'Preventing *MULTIPLE* DESTROY() invocations on %s - an *EXTREMELY ' . 'DANGEROUS* condition which is *ALMOST CERTAINLY GLOBAL* within your ' . 'application, affecting *ALL* classes without active protection against ' @@ -635,11 +746,10 @@ sub modver_gt_or_eq ($$) { croak "Nonsensical minimum version supplied" if ! defined $ver or $ver !~ $ver_rx; - no strict 'refs'; - my $ver_cache = ${"${mod}::__DBIC_MODULE_VERSION_CHECKS__"} ||= ( $mod->VERSION - ? {} - : croak "$mod does not seem to provide a version (perhaps it never loaded)" - ); + my $ver_cache = do { + no strict 'refs'; + ${"${mod}::__DBIC_MODULE_VERSION_CHECKS__"} ||= {} + }; ! defined $ver_cache->{$ver} and @@ -648,6 +758,18 @@ sub modver_gt_or_eq ($$) { local $SIG{__WARN__} = sigwarn_silencer( qr/\Qisn't numeric in subroutine entry/ ) if SPURIOUS_VERSION_CHECK_WARNINGS; + # prevent captures by potential __WARN__ hooks or the like: + # there is nothing of value that can be happening here, and + # leaving a hook in-place can only serve to fail some test + local $SIG{__WARN__} if ( + ! SPURIOUS_VERSION_CHECK_WARNINGS + and + $SIG{__WARN__} + ); + + croak "$mod does not seem to provide a version (perhaps it never loaded)" + unless $mod->VERSION; + local $SIG{__DIE__} if $SIG{__DIE__}; local $@; eval { $mod->VERSION($ver) } ? 1 : 0; @@ -670,9 +792,6 @@ sub modver_gt_or_eq_and_lt ($$$) { } { - # FIXME - should be a private my(), but I'm too uncertain whether - # all bases are covered - our $describe_class_query_cache; sub describe_class_methods { my $args = ( @@ -686,12 +805,22 @@ sub modver_gt_or_eq_and_lt ($$$) { croak "Expecting a class name either as the sole argument or a 'class' option" if not defined $class or $class !~ $module_name_rx; + croak( + "The supplied 'class' argument is tainted: this is *extremely* " + . 'dangerous, fix your code ASAP!!! ( for more details read through ' + . 'https://is.gd/perl_mro_taint_wtf )' + ) if ( + DBIx::Class::_ENV_::TAINT_MODE + and + Scalar::Util::tainted($class) + ); + $requested_mro ||= mro::get_mro($class); # mro::set_mro() does not bump pkg_gen - WHAT THE FUCK?! my $query_cache_key = "$class|$requested_mro"; - my $stack_cache_key = + my $internal_cache_key = ( mro::get_mro($class) eq $requested_mro ) ? $class : $query_cache_key @@ -705,44 +834,42 @@ sub modver_gt_or_eq_and_lt ($$$) { # we use the cache for linear_isa lookups on new MRO as well - it adds # a *tiny* speedup, and simplifies the code a lot # - local $mro_recursor_stack->{cache} = {} - unless $mro_recursor_stack->{cache}; + local $__describe_class_query_cache->{'!internal!'} = {} + unless $__describe_class_query_cache->{'!internal!'}; my $my_gen = 0; $my_gen += get_real_pkg_gen($_) for ( my @full_ISA = ( @{ - $mro_recursor_stack->{cache}{$stack_cache_key}{linear_isa} + $__describe_class_query_cache->{'!internal!'}{$internal_cache_key}{linear_isa} ||= mro::get_linear_isa($class, $requested_mro) }, (( - $mro_recursor_stack->{cache}{$class}{is_universal} + $__describe_class_query_cache->{'!internal!'}{$class}{is_universal} ||= mro::is_universal($class) ) ? () : @{ - $mro_recursor_stack->{cache}{UNIVERSAL}{linear_isa} + $__describe_class_query_cache->{'!internal!'}{UNIVERSAL}{linear_isa} ||= mro::get_linear_isa("UNIVERSAL") }), )); - my $slot = $describe_class_query_cache->{$query_cache_key} ||= {}; + my $slot = $__describe_class_query_cache->{$query_cache_key} ||= {}; unless ( ($slot->{cumulative_gen}||0) == $my_gen ) { - # remove ourselves from ISA - shift @full_ISA; - # reset %$slot = ( class => $class, - isa => [ - @{ $mro_recursor_stack->{cache}{$stack_cache_key}{linear_isa} } - [ 1 .. $#{$mro_recursor_stack->{cache}{$stack_cache_key}{linear_isa}} ] + isa => { map { $_ => 1 } @full_ISA }, + linear_isa => [ + @{ $__describe_class_query_cache->{'!internal!'}{$internal_cache_key}{linear_isa} } + [ 1 .. $#{$__describe_class_query_cache->{'!internal!'}{$internal_cache_key}{linear_isa}} ] ], mro => { type => $requested_mro, @@ -751,6 +878,9 @@ sub modver_gt_or_eq_and_lt ($$$) { cumulative_gen => $my_gen, ); + # remove ourselves from ISA + shift @full_ISA; + # ensure the cache is populated for the parents, code below can then # efficiently operate over the query_cache directly describe_class_methods($_) for reverse @full_ISA; @@ -783,7 +913,7 @@ sub modver_gt_or_eq_and_lt ($$$) { # what describe_class_methods for @full_ISA produced above ( map { values %{ - $describe_class_query_cache->{$_}{methods_defined_in_class} || {} + $__describe_class_query_cache->{$_}{methods_defined_in_class} || {} } } map { "$_|" . mro::get_mro($_) } reverse @full_ISA ), # our own non-cleaned subs + their attributes @@ -805,9 +935,15 @@ sub modver_gt_or_eq_and_lt ($$$) { ) ? { via_class => $class, name => $_, - attributes => { - map { $_ => 1 } attributes::get( \&{"${class}::${_}"} ) - }, + attributes => { map { $_ => 1 } do { + my @attrs; + local $@; + local $SIG{__DIE__} if $SIG{__DIE__}; + # attributes::get may throw on blessed-false crefs :/ + eval { @attrs = attributes::get( \&{"${class}::${_}"} ); 1 } + or warn "Unable to determine attributes of the \\&${class}::$_ method due to following error: $@"; + @attrs; + } }, } : () } keys %{"${class}::"} ) @@ -821,7 +957,7 @@ sub modver_gt_or_eq_and_lt ($$$) { if ( ! DBIx::Class::_ENV_::OLD_MRO and - ${^TAINT} + DBIx::Class::_ENV_::TAINT_MODE ) { $slot->{cumulative_gen} = 0; @@ -940,9 +1076,10 @@ sub fail_on_internal_call { { package DB; $fr = [ CORE::caller(1) ]; - $argdesc = ref $DB::args[0] - ? DBIx::Class::_Util::refdesc($DB::args[0]) - : ( $DB::args[0] . '' ) + $argdesc = + ( not defined $DB::args[0] ) ? 'UNAVAILABLE' + : ( length ref $DB::args[0] ) ? DBIx::Class::_Util::refdesc($DB::args[0]) + : $DB::args[0] . '' ; }; @@ -963,8 +1100,44 @@ sub fail_on_internal_call { : $fr ; + + die "\nMethod $fr->[3] is not marked with the 'DBIC_method_is_indirect_sugar' attribute\n\n" unless ( + + # unlikely but who knows... + ! @$fr + + or + + # This is a weird-ass double-purpose method, only one branch of which is marked + # as an illegal indirect call + # Hence the 'indirect' attribute makes no sense + # FIXME - likely need to mark this in some other manner + $fr->[3] eq 'DBIx::Class::ResultSet::new' + + or + + # RsrcProxy stuff is special and not attr-annotated on purpose + # Yet it is marked (correctly) as fail_on_internal_call(), as DBIC + # itself should not call these methods as first-entry + $fr->[3] =~ /^DBIx::Class::ResultSourceProxy::[^:]+$/ + + or + + # FIXME - there is likely a more fine-graned way to escape "foreign" + # callers, based on annotations... (albeit a slower one) + # For the time being just skip in a dumb way + $fr->[3] !~ /^DBIx::Class|^DBICx::|^DBICTest::/ + + or + + grep + { $_ eq 'DBIC_method_is_indirect_sugar' } + do { no strict 'refs'; attributes::get( \&{ $fr->[3] }) } + ); + + if ( - $argdesc + defined $fr->[0] and $check_fr->[0] =~ /^(?:DBIx::Class|DBICx::)/ and @@ -981,4 +1154,59 @@ sub fail_on_internal_call { } } +if (DBIx::Class::_ENV_::ASSERT_NO_ERRONEOUS_METAINSTANCE_USE) { + + no warnings 'redefine'; + + my $next_bless = defined(&CORE::GLOBAL::bless) + ? \&CORE::GLOBAL::bless + : sub { CORE::bless($_[0], $_[1]) } + ; + + *CORE::GLOBAL::bless = sub { + my $class = (@_ > 1) ? $_[1] : CORE::caller(); + + # allow for reblessing (role application) + return $next_bless->( $_[0], $class ) + if defined blessed $_[0]; + + my $obj = $next_bless->( $_[0], $class ); + + my $calling_sub = (CORE::caller(1))[3] || ''; + + ( + # before 5.18 ->isa() will choke on the "0" package + # which we test for in several obscure cases, sigh... + !( DBIx::Class::_ENV_::PERL_VERSION < 5.018 ) + or + $class + ) + and + ( + ( + $calling_sub !~ /^ (?: + DBIx::Class::Schema::clone + | + DBIx::Class::DB::setup_schema_instance + )/x + and + $class->isa("DBIx::Class::Schema") + ) + or + ( + $calling_sub ne 'DBIx::Class::ResultSource::new' + and + $class->isa("DBIx::Class::ResultSource") + ) + ) + and + local $Carp::CarpLevel = $Carp::CarpLevel + 1 + and + Carp::confess("Improper instantiation of '$obj': you *MUST* call the corresponding constructor"); + + + $obj; + }; +} + 1;