commit ac16e136c6ee5040252ad87690e18f843cf9e50e Author: Mario Fetka Date: Fri Sep 15 15:16:18 2017 +0200 Imported Upstream version 0.18 diff --git a/Changes b/Changes new file mode 100644 index 0000000..e1cca50 --- /dev/null +++ b/Changes @@ -0,0 +1,55 @@ +0.18 Sep 2 2009 +* fixed the source code repos in the POD. now we use the following + git repos: + http://github.com/agentzh/cookiexs/ + +0.17 Sep 2 2009 +* fixed a parser bug when variables take empty values like "foo=", + reported and fixed by Colin Keith (rt.cpan.org #49302) + +0.16 Sep 12 2008 +* Fixed an issue in POD. + +0.15 Sep 11 2008 +* Some minor optimization (";" is never actually used to separate multiple values for a single key). +* Added Filter::Util::Call to the dependency list. + +0.14 Sep 10 2008 +* Carefully reviewed the old implementation and gave it a massive rewrite. +* Fixed the issue in #39120 +* Fixed a lot of incompatibilities with the latest CGI::Cookie. + +0.13 Arg 4 2008 +* Fixed the module name in the test suite; we now use CGI::Cookie::XS, rather than Cookie::XS. + +0.12 Arg 4 2008 +* Fixed a typo in the POD. + +0.11 Aug 4 2008 +* Renamed the module to CGI::Cookie::XS. + +0.10 Aug 4 2008 +* Put a notice saying that Cookie::XS is deprecated; one should use CGI::Cookie::XS instead. + +0.09 Mar 24 2008 +* Enabled the tests in 99-pod-coverage.t. + +0.08 Mar 20 2008 +* Fixed the foo=ba=r bug reported by RT #34238. +* Fixed a stack overflow when input cookies are too big. +* Added a lot of stuff to the POD documentation. +* Cleaned up the XS code a bit. + +0.07 Mar 6 2008 +* Fixed "const char*" in XS.xs to make it parsable by perl 5.6.x + +0.06 Mar 3 2008 +* Fixed the duplicate BUGS section in the POD. +* Fixed the version number in META.yml. + +0.05 Mar 3 2008 +* Fixed a lot of typos in the POD. + +0.04 Mar 3 2008 +* initial CPAN release + diff --git a/INLINE.h b/INLINE.h new file mode 100644 index 0000000..68e465c --- /dev/null +++ b/INLINE.h @@ -0,0 +1,26 @@ +#define Inline_Stack_Vars dXSARGS +#define Inline_Stack_Items items +#define Inline_Stack_Item(x) ST(x) +#define Inline_Stack_Reset sp = mark +#define Inline_Stack_Push(x) XPUSHs(x) +#define Inline_Stack_Done PUTBACK +#define Inline_Stack_Return(x) XSRETURN(x) +#define Inline_Stack_Void XSRETURN(0) + +#define INLINE_STACK_VARS Inline_Stack_Vars +#define INLINE_STACK_ITEMS Inline_Stack_Items +#define INLINE_STACK_ITEM(x) Inline_Stack_Item(x) +#define INLINE_STACK_RESET Inline_Stack_Reset +#define INLINE_STACK_PUSH(x) Inline_Stack_Push(x) +#define INLINE_STACK_DONE Inline_Stack_Done +#define INLINE_STACK_RETURN(x) Inline_Stack_Return(x) +#define INLINE_STACK_VOID Inline_Stack_Void + +#define inline_stack_vars Inline_Stack_Vars +#define inline_stack_items Inline_Stack_Items +#define inline_stack_item(x) Inline_Stack_Item(x) +#define inline_stack_reset Inline_Stack_Reset +#define inline_stack_push(x) Inline_Stack_Push(x) +#define inline_stack_done Inline_Stack_Done +#define inline_stack_return(x) Inline_Stack_Return(x) +#define inline_stack_void Inline_Stack_Void diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..73bc12c --- /dev/null +++ b/MANIFEST @@ -0,0 +1,35 @@ +Changes +inc/Module/AutoInstall.pm +inc/Module/Install.pm +inc/Module/Install/AutoInstall.pm +inc/Module/Install/Base.pm +inc/Module/Install/Can.pm +inc/Module/Install/Compiler.pm +inc/Module/Install/Fetch.pm +inc/Module/Install/Include.pm +inc/Module/Install/Makefile.pm +inc/Module/Install/Metadata.pm +inc/Module/Install/Win32.pm +inc/Module/Install/WriteAll.pm +inc/Module/Install/TestBase.pm +inc/Spiffy.pm +inc/Test/Base.pm +inc/Test/Base/Filter.pm +inc/Test/Builder.pm +inc/Test/Builder/Module.pm +inc/Test/More.pm +INLINE.h +lib/CGI/Cookie/XS.pm +Makefile.PL +MANIFEST This list of files +MANIFEST.SKIP +META.yml +README +t/TestCookie.pm +t/01-sanity.t +t/02-overflow.t +t/03-bug.t +t/99-pod-coverage.t +t/99-pod.t +util.c +XS.xs diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP new file mode 100644 index 0000000..a777379 --- /dev/null +++ b/MANIFEST.SKIP @@ -0,0 +1,14 @@ +^smerge +\.bak$ +^Makefile$ +\.swp$ +\.tar\.gz$ +^blib/ +^pm_to_blib$ +~$ +^util\.o$ +^XS\.bs$ +^XS\.c$ +^XS\.o$ +^obsolete +\.git diff --git a/META.yml b/META.yml new file mode 100644 index 0000000..6439615 --- /dev/null +++ b/META.yml @@ -0,0 +1,22 @@ +--- +abstract: 'HTTP Cookie parser in pure C' +author: + - 'Agent Zhang ' +distribution_type: module +generated_by: 'Module::Install version 0.77' +license: perl +meta-spec: + url: http://module-build.sourceforge.net/META-spec-v1.4.html + version: 1.4 +name: CGI-Cookie-XS +no_index: + directory: + - inc + - t +requires: + Filter::Util::Call: 0 + perl: 5.6.1 +resources: + license: http://dev.perl.org/licenses/ + repository: http://github.com/agentzh/cookiexs/tree/master +version: 0.18 diff --git a/Makefile.PL b/Makefile.PL new file mode 100644 index 0000000..70b69f0 --- /dev/null +++ b/Makefile.PL @@ -0,0 +1,23 @@ +use strict; +use lib '.'; +use inc::Module::Install; + +name ('CGI-Cookie-XS'); +license ('perl'); +author ('Agent Zhang '); +perl_version ('5.006001'); +all_from ('lib/CGI/Cookie/XS.pm'); +repository ('http://github.com/agentzh/cookiexs/tree/master'); + +cc_inc_paths '.'; +cc_files (glob("*.c"), (-e 'XS.c' ? () : 'XS.c')); +cc_optimize_flags '-g3'; +can_cc or die "This module requires a C compiler"; + +#build_requires ('Test::More'); +requires ('Filter::Util::Call'); +use_test_base(); + +auto_install(); +WriteAll(); + diff --git a/README b/README new file mode 100644 index 0000000..11103be --- /dev/null +++ b/README @@ -0,0 +1,127 @@ +NAME + CGI::Cookie::XS - HTTP Cookie parser in pure C + +VERSION + This document describes CGI::Cookie::XS 0.18 released on September 2, + 2009. + +SYNOPSIS + use CGI::Cookie::XS; + + my $raw_cookie = 'foo=a%20phrase;weird; bar=yes%2C%20a%20phrase; baz=%5Ewibble&leiyh; qux=%27'; + my $res = CGI::Cookie::XS->parse($raw_cookie); + # $res is something like: + # { + # 'bar' => [ + # 'yes, a phrase' + # ], + # 'baz' => [ + # '^wibble', + # 'leiyh' + # ], + # 'foo' => [ + # 'a phrase' + # ], + # 'qux' => [ + # '\'' + # ] + # }; + + # or directly read raw cookies from the CGI environments: + $res = CGI::Cookie::XS->fetch; + +DESCRIPTION + This module implements a very simple parser for cookies used in HTTP + applications. We've found CGI::Simple::Cookie and CGI::Cookie rather + slow according to the profiling results for our OpenResty project, hence + the rewrite in C. + + This library is still in beta stage and the API is still in flux. We're + just following the "release early, releaes often" guideline. So please + check back often ;) + + Special effort has been made to ensure this module works in the same way + as the latest CGI::Cookie (i.e., the pure Perl implementation). If you + find it doesn't, please let us know. + +METHODS + We currently provide 2 static methods, "parse" and "fetch". They work + mostly the same way as those methods found in CGI::Cookie and + CGI::Simple::Cookie but with the exception that our version returns + plain Perl data structures rather than hashes of Perl objects (due to + performance considerations). + + We'll implement some cookie dump methods in the near future. + + "$ref = CGI::Cookie::XS->parse($raw_cookie)" + Parses $raw_cookie and returns the reference of a hash of arrays. + The keys of the hash are cookie variables' names while the values of + the hash are lists of cookie variable's values. + + There is a length limit on the $raw_cookie. If $raw_cookie is longer + than 4 KB (i.e. 4 * 1024 bytes, excluding the trailing '\0'), the + overflowing part will be truncated. + + Also note that, "fetch" does not assume any encoding on the cookie + values. It just decodes the encoded entries verbatim and treat them + as plain "binary" stuff. + + "$ref = CGI::Cookie::XS->fetch()" + Reads the raw cookie from the "HTTP_COOKIE" and "COOKIE" + environments (which are usually set by HTTP servers like lighttd or + apache) and then parses the value using the "parse" method and + finally returns the results. + +TODO + * Removing trailing spaces in cookie values. + +SOURCE CONTROL + For the very latest version of this module, check out the source from + the Git repos below: + + + + There is anonymous access to all. If you'd like a commit bit, please let + us know. :) + +BUGS + There must be some serious bugs lurking somewhere. We haven't done + comprehensive testing for our code yet. It's a TODO. + + Please report bugs or send wish-list to + . + +SEE ALSO + CGI::Cookie, CGI::Cookie::Simple. + +AUTHOR + yuting "" + agentzh "" + +COPYRIGHT + Copyright (c) 2008 by Yahoo! China EEEE Works, Alibaba Inc. + + Copyright (c) 2009 by Taobao Inc., Alibaba Group. + +License + The "MIT" License + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/XS.xs b/XS.xs new file mode 100644 index 0000000..8cefab7 --- /dev/null +++ b/XS.xs @@ -0,0 +1,192 @@ +#include "EXTERN.h" +#include "perl.h" +#include "XSUB.h" +#include "INLINE.h" + +#define DDD(x) + +#ifndef DDD +#define DDD(x) fprintf(stderr, "%s\n", x); +#endif + +#define COOKIE_LEN_LIMIT 1024 * 4 +#ifndef NULL +#define NULL (void*)0 +#endif + +#ifndef TRUE +#define TRUE 1 +#endif + +#ifndef FALSE +#define FALSE 0 +#endif + +#ifndef BOOL +#define BOOL short int +#endif + +//static char *encode_hex_str(const char*, char **); +extern char** XS_unpack_charPtrPtr(SV* arg); +extern void XS_pack_charPtrPtr( SV* arg, char** array, int count); + +char Buffer[COOKIE_LEN_LIMIT]; + +static int _decode_hex_str(const char*, char **); + +SV* _parse_cookie(char* cs) { + int i, value_flag; + char* p; /* moving first for look-ahead */ + char* q; /* moving slower for tracking values */ + char* decode; + AV *array = NULL; + HV *hash = NULL; + BOOL parsing_value = FALSE; + + decode = (char *) malloc (COOKIE_LEN_LIMIT * sizeof(decode)); + if (decode == NULL) { + croak("CGI::Cookie::XS::parse - Failed to malloc"); + } + strncpy(Buffer, cs, COOKIE_LEN_LIMIT); + Buffer[COOKIE_LEN_LIMIT-1] = '\0'; + hash = newHV(); + + + p = Buffer; + DDD("before loop"); + while (*p == ' ' || *p == '\t') p++; // remove leading spaces + q = p; + while (*p) { + //DDD("in loop"); + if (*p == '=' && !parsing_value ){ + array = newAV(); + *p = '\0'; + + // Only move on if not the end of the cookie value + if (*(p+1) != ';' && *(p+1) != ',' && *(p+1) != '\0') + p++; + + _decode_hex_str(q, &decode); + q = p; + hv_store( + hash, decode, strlen(decode), newRV_noinc((SV *)array), 0 + ); + //array = NULL; + parsing_value = TRUE; + } else if (*p == ';' || *p == ',') { + *p = '\0'; + p++; + while (*p == ' ') + p++; + _decode_hex_str(q, &decode); + q = p; + if (*decode != '\0' && parsing_value && array != NULL) + av_push(array, newSVpvf("%s", decode)); + parsing_value = FALSE; + } else if (*p == '&') { // find a second value + *p = 0; p++; + _decode_hex_str(q, &decode); + q = p; + if (parsing_value && array != NULL) + av_push(array, newSVpvf("%s", decode)); + } + p++; + } + DDD("before decode"); + if (*q != '\0' && parsing_value) { + _decode_hex_str(q, &decode); + DDD("before push array"); + if (array != NULL) + av_push(array, newSVpvf("%s", decode)); + DDD("after push array"); + } + if (decode) free(decode); + DDD("before return"); + return newRV_noinc((SV *) hash); +} + +char *encode_hex_str(const char *str, char **out_buf) +{ + static const char *verbatim = "-_.*"; + static const char *hex = "0123456789ABCDEF"; + char *newstr = *out_buf; + char *c; + + if (!str && !newstr) + return NULL; + + for (c = newstr; *str; str++) + if ((isalnum(*str) && !(*str & 0x80)) || strchr(verbatim, *str)) + *c++ = *str; + else if (*str == ' ') + *c++ = '+'; + else if (*str == '\n') { + *c++ = '%'; + *c++ = '0'; + *c++ = 'D'; + *c++ = '%'; + *c++ = '0'; + *c++ = 'A'; + } else { + *c++ = '%'; + *c++ = hex[(*str >> 4) & 15]; + *c++ = hex[*str & 15]; + } + *c = 0; + return newstr; +} + +static int decode_hex_octet(const char *s) +{ + int hex_value; + char *tail, hex[3]; + + if (s && (hex[0] = s[0]) && (hex[1] = s[1])) { + hex[2] = 0; + hex_value = strtol(hex, &tail, 16); + if (tail - hex == 2) + return hex_value; + } + return -1; +} + + +int _decode_hex_str (const char *str, char **out) +{ + char *dest = *out; + int i, val; + + memset(dest, 0, COOKIE_LEN_LIMIT); + + if (!str && ! dest) + return 0; + + // most cases won't have hex octets + if (!strchr(str, '%')){ + strcpy(dest, str); + return 1; + } + + + for (i = 0; str[i]; i++) { + *dest++ = (str[i] == '%' && (val = decode_hex_octet(str+i+1)) >= 0) ? + i+=2, val : str[i]; + } + return 1; +} + + +MODULE = CGI::Cookie::XS PACKAGE = CGI::Cookie::XS + +PROTOTYPES: DISABLE + + +SV * +_parse_cookie (cs) + char * cs + +int +_decode_hex_str (str, out) + char * str + char ** out + diff --git a/inc/Module/AutoInstall.pm b/inc/Module/AutoInstall.pm new file mode 100644 index 0000000..7efc552 --- /dev/null +++ b/inc/Module/AutoInstall.pm @@ -0,0 +1,768 @@ +#line 1 +package Module::AutoInstall; + +use strict; +use Cwd (); +use ExtUtils::MakeMaker (); + +use vars qw{$VERSION}; +BEGIN { + $VERSION = '1.03'; +} + +# special map on pre-defined feature sets +my %FeatureMap = ( + '' => 'Core Features', # XXX: deprecated + '-core' => 'Core Features', +); + +# various lexical flags +my ( @Missing, @Existing, %DisabledTests, $UnderCPAN, $HasCPANPLUS ); +my ( $Config, $CheckOnly, $SkipInstall, $AcceptDefault, $TestOnly ); +my ( $PostambleActions, $PostambleUsed ); + +# See if it's a testing or non-interactive session +_accept_default( $ENV{AUTOMATED_TESTING} or ! -t STDIN ); +_init(); + +sub _accept_default { + $AcceptDefault = shift; +} + +sub missing_modules { + return @Missing; +} + +sub do_install { + __PACKAGE__->install( + [ + $Config + ? ( UNIVERSAL::isa( $Config, 'HASH' ) ? %{$Config} : @{$Config} ) + : () + ], + @Missing, + ); +} + +# initialize various flags, and/or perform install +sub _init { + foreach my $arg ( + @ARGV, + split( + /[\s\t]+/, + $ENV{PERL_AUTOINSTALL} || $ENV{PERL_EXTUTILS_AUTOINSTALL} || '' + ) + ) + { + if ( $arg =~ /^--config=(.*)$/ ) { + $Config = [ split( ',', $1 ) ]; + } + elsif ( $arg =~ /^--installdeps=(.*)$/ ) { + __PACKAGE__->install( $Config, @Missing = split( /,/, $1 ) ); + exit 0; + } + elsif ( $arg =~ /^--default(?:deps)?$/ ) { + $AcceptDefault = 1; + } + elsif ( $arg =~ /^--check(?:deps)?$/ ) { + $CheckOnly = 1; + } + elsif ( $arg =~ /^--skip(?:deps)?$/ ) { + $SkipInstall = 1; + } + elsif ( $arg =~ /^--test(?:only)?$/ ) { + $TestOnly = 1; + } + } +} + +# overrides MakeMaker's prompt() to automatically accept the default choice +sub _prompt { + goto &ExtUtils::MakeMaker::prompt unless $AcceptDefault; + + my ( $prompt, $default ) = @_; + my $y = ( $default =~ /^[Yy]/ ); + + print $prompt, ' [', ( $y ? 'Y' : 'y' ), '/', ( $y ? 'n' : 'N' ), '] '; + print "$default\n"; + return $default; +} + +# the workhorse +sub import { + my $class = shift; + my @args = @_ or return; + my $core_all; + + print "*** $class version " . $class->VERSION . "\n"; + print "*** Checking for Perl dependencies...\n"; + + my $cwd = Cwd::cwd(); + + $Config = []; + + my $maxlen = length( + ( + sort { length($b) <=> length($a) } + grep { /^[^\-]/ } + map { + ref($_) + ? ( ( ref($_) eq 'HASH' ) ? keys(%$_) : @{$_} ) + : '' + } + map { +{@args}->{$_} } + grep { /^[^\-]/ or /^-core$/i } keys %{ +{@args} } + )[0] + ); + + while ( my ( $feature, $modules ) = splice( @args, 0, 2 ) ) { + my ( @required, @tests, @skiptests ); + my $default = 1; + my $conflict = 0; + + if ( $feature =~ m/^-(\w+)$/ ) { + my $option = lc($1); + + # check for a newer version of myself + _update_to( $modules, @_ ) and return if $option eq 'version'; + + # sets CPAN configuration options + $Config = $modules if $option eq 'config'; + + # promote every features to core status + $core_all = ( $modules =~ /^all$/i ) and next + if $option eq 'core'; + + next unless $option eq 'core'; + } + + print "[" . ( $FeatureMap{ lc($feature) } || $feature ) . "]\n"; + + $modules = [ %{$modules} ] if UNIVERSAL::isa( $modules, 'HASH' ); + + unshift @$modules, -default => &{ shift(@$modules) } + if ( ref( $modules->[0] ) eq 'CODE' ); # XXX: bugward combatability + + while ( my ( $mod, $arg ) = splice( @$modules, 0, 2 ) ) { + if ( $mod =~ m/^-(\w+)$/ ) { + my $option = lc($1); + + $default = $arg if ( $option eq 'default' ); + $conflict = $arg if ( $option eq 'conflict' ); + @tests = @{$arg} if ( $option eq 'tests' ); + @skiptests = @{$arg} if ( $option eq 'skiptests' ); + + next; + } + + printf( "- %-${maxlen}s ...", $mod ); + + if ( $arg and $arg =~ /^\D/ ) { + unshift @$modules, $arg; + $arg = 0; + } + + # XXX: check for conflicts and uninstalls(!) them. + if ( + defined( my $cur = _version_check( _load($mod), $arg ||= 0 ) ) ) + { + print "loaded. ($cur" . ( $arg ? " >= $arg" : '' ) . ")\n"; + push @Existing, $mod => $arg; + $DisabledTests{$_} = 1 for map { glob($_) } @skiptests; + } + else { + print "missing." . ( $arg ? " (would need $arg)" : '' ) . "\n"; + push @required, $mod => $arg; + } + } + + next unless @required; + + my $mandatory = ( $feature eq '-core' or $core_all ); + + if ( + !$SkipInstall + and ( + $CheckOnly + or _prompt( + qq{==> Auto-install the } + . ( @required / 2 ) + . ( $mandatory ? ' mandatory' : ' optional' ) + . qq{ module(s) from CPAN?}, + $default ? 'y' : 'n', + ) =~ /^[Yy]/ + ) + ) + { + push( @Missing, @required ); + $DisabledTests{$_} = 1 for map { glob($_) } @skiptests; + } + + elsif ( !$SkipInstall + and $default + and $mandatory + and + _prompt( qq{==> The module(s) are mandatory! Really skip?}, 'n', ) + =~ /^[Nn]/ ) + { + push( @Missing, @required ); + $DisabledTests{$_} = 1 for map { glob($_) } @skiptests; + } + + else { + $DisabledTests{$_} = 1 for map { glob($_) } @tests; + } + } + + $UnderCPAN = _check_lock(); # check for $UnderCPAN + + if ( @Missing and not( $CheckOnly or $UnderCPAN ) ) { + require Config; + print +"*** Dependencies will be installed the next time you type '$Config::Config{make}'.\n"; + + # make an educated guess of whether we'll need root permission. + print " (You may need to do that as the 'root' user.)\n" + if eval '$>'; + } + print "*** $class configuration finished.\n"; + + chdir $cwd; + + # import to main:: + no strict 'refs'; + *{'main::WriteMakefile'} = \&Write if caller(0) eq 'main'; +} + +# Check to see if we are currently running under CPAN.pm and/or CPANPLUS; +# if we are, then we simply let it taking care of our dependencies +sub _check_lock { + return unless @Missing; + + if ($ENV{PERL5_CPANPLUS_IS_RUNNING}) { + print <<'END_MESSAGE'; + +*** Since we're running under CPANPLUS, I'll just let it take care + of the dependency's installation later. +END_MESSAGE + return 1; + } + + _load_cpan(); + + # Find the CPAN lock-file + my $lock = MM->catfile( $CPAN::Config->{cpan_home}, ".lock" ); + return unless -f $lock; + + # Check the lock + local *LOCK; + return unless open(LOCK, $lock); + + if ( + ( $^O eq 'MSWin32' ? _under_cpan() : == getppid() ) + and ( $CPAN::Config->{prerequisites_policy} || '' ) ne 'ignore' + ) { + print <<'END_MESSAGE'; + +*** Since we're running under CPAN, I'll just let it take care + of the dependency's installation later. +END_MESSAGE + return 1; + } + + close LOCK; + return; +} + +sub install { + my $class = shift; + + my $i; # used below to strip leading '-' from config keys + my @config = ( map { s/^-// if ++$i; $_ } @{ +shift } ); + + my ( @modules, @installed ); + while ( my ( $pkg, $ver ) = splice( @_, 0, 2 ) ) { + + # grep out those already installed + if ( defined( _version_check( _load($pkg), $ver ) ) ) { + push @installed, $pkg; + } + else { + push @modules, $pkg, $ver; + } + } + + return @installed unless @modules; # nothing to do + return @installed if _check_lock(); # defer to the CPAN shell + + print "*** Installing dependencies...\n"; + + return unless _connected_to('cpan.org'); + + my %args = @config; + my %failed; + local *FAILED; + if ( $args{do_once} and open( FAILED, '.#autoinstall.failed' ) ) { + while () { chomp; $failed{$_}++ } + close FAILED; + + my @newmod; + while ( my ( $k, $v ) = splice( @modules, 0, 2 ) ) { + push @newmod, ( $k => $v ) unless $failed{$k}; + } + @modules = @newmod; + } + + if ( _has_cpanplus() ) { + _install_cpanplus( \@modules, \@config ); + } else { + _install_cpan( \@modules, \@config ); + } + + print "*** $class installation finished.\n"; + + # see if we have successfully installed them + while ( my ( $pkg, $ver ) = splice( @modules, 0, 2 ) ) { + if ( defined( _version_check( _load($pkg), $ver ) ) ) { + push @installed, $pkg; + } + elsif ( $args{do_once} and open( FAILED, '>> .#autoinstall.failed' ) ) { + print FAILED "$pkg\n"; + } + } + + close FAILED if $args{do_once}; + + return @installed; +} + +sub _install_cpanplus { + my @modules = @{ +shift }; + my @config = _cpanplus_config( @{ +shift } ); + my $installed = 0; + + require CPANPLUS::Backend; + my $cp = CPANPLUS::Backend->new; + my $conf = $cp->configure_object; + + return unless $conf->can('conf') # 0.05x+ with "sudo" support + or _can_write($conf->_get_build('base')); # 0.04x + + # if we're root, set UNINST=1 to avoid trouble unless user asked for it. + my $makeflags = $conf->get_conf('makeflags') || ''; + if ( UNIVERSAL::isa( $makeflags, 'HASH' ) ) { + # 0.03+ uses a hashref here + $makeflags->{UNINST} = 1 unless exists $makeflags->{UNINST}; + + } else { + # 0.02 and below uses a scalar + $makeflags = join( ' ', split( ' ', $makeflags ), 'UNINST=1' ) + if ( $makeflags !~ /\bUNINST\b/ and eval qq{ $> eq '0' } ); + + } + $conf->set_conf( makeflags => $makeflags ); + $conf->set_conf( prereqs => 1 ); + + + + while ( my ( $key, $val ) = splice( @config, 0, 2 ) ) { + $conf->set_conf( $key, $val ); + } + + my $modtree = $cp->module_tree; + while ( my ( $pkg, $ver ) = splice( @modules, 0, 2 ) ) { + print "*** Installing $pkg...\n"; + + MY::preinstall( $pkg, $ver ) or next if defined &MY::preinstall; + + my $success; + my $obj = $modtree->{$pkg}; + + if ( $obj and defined( _version_check( $obj->{version}, $ver ) ) ) { + my $pathname = $pkg; + $pathname =~ s/::/\\W/; + + foreach my $inc ( grep { m/$pathname.pm/i } keys(%INC) ) { + delete $INC{$inc}; + } + + my $rv = $cp->install( modules => [ $obj->{module} ] ); + + if ( $rv and ( $rv->{ $obj->{module} } or $rv->{ok} ) ) { + print "*** $pkg successfully installed.\n"; + $success = 1; + } else { + print "*** $pkg installation cancelled.\n"; + $success = 0; + } + + $installed += $success; + } else { + print << "."; +*** Could not find a version $ver or above for $pkg; skipping. +. + } + + MY::postinstall( $pkg, $ver, $success ) if defined &MY::postinstall; + } + + return $installed; +} + +sub _cpanplus_config { + my @config = (); + while ( @_ ) { + my ($key, $value) = (shift(), shift()); + if ( $key eq 'prerequisites_policy' ) { + if ( $value eq 'follow' ) { + $value = CPANPLUS::Internals::Constants::PREREQ_INSTALL(); + } elsif ( $value eq 'ask' ) { + $value = CPANPLUS::Internals::Constants::PREREQ_ASK(); + } elsif ( $value eq 'ignore' ) { + $value = CPANPLUS::Internals::Constants::PREREQ_IGNORE(); + } else { + die "*** Cannot convert option $key = '$value' to CPANPLUS version.\n"; + } + } else { + die "*** Cannot convert option $key to CPANPLUS version.\n"; + } + } + return @config; +} + +sub _install_cpan { + my @modules = @{ +shift }; + my @config = @{ +shift }; + my $installed = 0; + my %args; + + _load_cpan(); + require Config; + + if (CPAN->VERSION < 1.80) { + # no "sudo" support, probe for writableness + return unless _can_write( MM->catfile( $CPAN::Config->{cpan_home}, 'sources' ) ) + and _can_write( $Config::Config{sitelib} ); + } + + # if we're root, set UNINST=1 to avoid trouble unless user asked for it. + my $makeflags = $CPAN::Config->{make_install_arg} || ''; + $CPAN::Config->{make_install_arg} = + join( ' ', split( ' ', $makeflags ), 'UNINST=1' ) + if ( $makeflags !~ /\bUNINST\b/ and eval qq{ $> eq '0' } ); + + # don't show start-up info + $CPAN::Config->{inhibit_startup_message} = 1; + + # set additional options + while ( my ( $opt, $arg ) = splice( @config, 0, 2 ) ) { + ( $args{$opt} = $arg, next ) + if $opt =~ /^force$/; # pseudo-option + $CPAN::Config->{$opt} = $arg; + } + + local $CPAN::Config->{prerequisites_policy} = 'follow'; + + while ( my ( $pkg, $ver ) = splice( @modules, 0, 2 ) ) { + MY::preinstall( $pkg, $ver ) or next if defined &MY::preinstall; + + print "*** Installing $pkg...\n"; + + my $obj = CPAN::Shell->expand( Module => $pkg ); + my $success = 0; + + if ( $obj and defined( _version_check( $obj->cpan_version, $ver ) ) ) { + my $pathname = $pkg; + $pathname =~ s/::/\\W/; + + foreach my $inc ( grep { m/$pathname.pm/i } keys(%INC) ) { + delete $INC{$inc}; + } + + my $rv = $args{force} ? CPAN::Shell->force( install => $pkg ) + : CPAN::Shell->install($pkg); + $rv ||= eval { + $CPAN::META->instance( 'CPAN::Distribution', $obj->cpan_file, ) + ->{install} + if $CPAN::META; + }; + + if ( $rv eq 'YES' ) { + print "*** $pkg successfully installed.\n"; + $success = 1; + } + else { + print "*** $pkg installation failed.\n"; + $success = 0; + } + + $installed += $success; + } + else { + print << "."; +*** Could not find a version $ver or above for $pkg; skipping. +. + } + + MY::postinstall( $pkg, $ver, $success ) if defined &MY::postinstall; + } + + return $installed; +} + +sub _has_cpanplus { + return ( + $HasCPANPLUS = ( + $INC{'CPANPLUS/Config.pm'} + or _load('CPANPLUS::Shell::Default') + ) + ); +} + +# make guesses on whether we're under the CPAN installation directory +sub _under_cpan { + require Cwd; + require File::Spec; + + my $cwd = File::Spec->canonpath( Cwd::cwd() ); + my $cpan = File::Spec->canonpath( $CPAN::Config->{cpan_home} ); + + return ( index( $cwd, $cpan ) > -1 ); +} + +sub _update_to { + my $class = __PACKAGE__; + my $ver = shift; + + return + if defined( _version_check( _load($class), $ver ) ); # no need to upgrade + + if ( + _prompt( "==> A newer version of $class ($ver) is required. Install?", + 'y' ) =~ /^[Nn]/ + ) + { + die "*** Please install $class $ver manually.\n"; + } + + print << "."; +*** Trying to fetch it from CPAN... +. + + # install ourselves + _load($class) and return $class->import(@_) + if $class->install( [], $class, $ver ); + + print << '.'; exit 1; + +*** Cannot bootstrap myself. :-( Installation terminated. +. +} + +# check if we're connected to some host, using inet_aton +sub _connected_to { + my $site = shift; + + return ( + ( _load('Socket') and Socket::inet_aton($site) ) or _prompt( + qq( +*** Your host cannot resolve the domain name '$site', which + probably means the Internet connections are unavailable. +==> Should we try to install the required module(s) anyway?), 'n' + ) =~ /^[Yy]/ + ); +} + +# check if a directory is writable; may create it on demand +sub _can_write { + my $path = shift; + mkdir( $path, 0755 ) unless -e $path; + + return 1 if -w $path; + + print << "."; +*** You are not allowed to write to the directory '$path'; + the installation may fail due to insufficient permissions. +. + + if ( + eval '$>' and lc(`sudo -V`) =~ /version/ and _prompt( + qq( +==> Should we try to re-execute the autoinstall process with 'sudo'?), + ((-t STDIN) ? 'y' : 'n') + ) =~ /^[Yy]/ + ) + { + + # try to bootstrap ourselves from sudo + print << "."; +*** Trying to re-execute the autoinstall process with 'sudo'... +. + my $missing = join( ',', @Missing ); + my $config = join( ',', + UNIVERSAL::isa( $Config, 'HASH' ) ? %{$Config} : @{$Config} ) + if $Config; + + return + unless system( 'sudo', $^X, $0, "--config=$config", + "--installdeps=$missing" ); + + print << "."; +*** The 'sudo' command exited with error! Resuming... +. + } + + return _prompt( + qq( +==> Should we try to install the required module(s) anyway?), 'n' + ) =~ /^[Yy]/; +} + +# load a module and return the version it reports +sub _load { + my $mod = pop; # class/instance doesn't matter + my $file = $mod; + + $file =~ s|::|/|g; + $file .= '.pm'; + + local $@; + return eval { require $file; $mod->VERSION } || ( $@ ? undef: 0 ); +} + +# Load CPAN.pm and it's configuration +sub _load_cpan { + return if $CPAN::VERSION; + require CPAN; + if ( $CPAN::HandleConfig::VERSION ) { + # Newer versions of CPAN have a HandleConfig module + CPAN::HandleConfig->load; + } else { + # Older versions had the load method in Config directly + CPAN::Config->load; + } +} + +# compare two versions, either use Sort::Versions or plain comparison +sub _version_check { + my ( $cur, $min ) = @_; + return unless defined $cur; + + $cur =~ s/\s+$//; + + # check for version numbers that are not in decimal format + if ( ref($cur) or ref($min) or $cur =~ /v|\..*\./ or $min =~ /v|\..*\./ ) { + if ( ( $version::VERSION or defined( _load('version') )) and + version->can('new') + ) { + + # use version.pm if it is installed. + return ( + ( version->new($cur) >= version->new($min) ) ? $cur : undef ); + } + elsif ( $Sort::Versions::VERSION or defined( _load('Sort::Versions') ) ) + { + + # use Sort::Versions as the sorting algorithm for a.b.c versions + return ( ( Sort::Versions::versioncmp( $cur, $min ) != -1 ) + ? $cur + : undef ); + } + + warn "Cannot reliably compare non-decimal formatted versions.\n" + . "Please install version.pm or Sort::Versions.\n"; + } + + # plain comparison + local $^W = 0; # shuts off 'not numeric' bugs + return ( $cur >= $min ? $cur : undef ); +} + +# nothing; this usage is deprecated. +sub main::PREREQ_PM { return {}; } + +sub _make_args { + my %args = @_; + + $args{PREREQ_PM} = { %{ $args{PREREQ_PM} || {} }, @Existing, @Missing } + if $UnderCPAN or $TestOnly; + + if ( $args{EXE_FILES} and -e 'MANIFEST' ) { + require ExtUtils::Manifest; + my $manifest = ExtUtils::Manifest::maniread('MANIFEST'); + + $args{EXE_FILES} = + [ grep { exists $manifest->{$_} } @{ $args{EXE_FILES} } ]; + } + + $args{test}{TESTS} ||= 't/*.t'; + $args{test}{TESTS} = join( ' ', + grep { !exists( $DisabledTests{$_} ) } + map { glob($_) } split( /\s+/, $args{test}{TESTS} ) ); + + my $missing = join( ',', @Missing ); + my $config = + join( ',', UNIVERSAL::isa( $Config, 'HASH' ) ? %{$Config} : @{$Config} ) + if $Config; + + $PostambleActions = ( + $missing + ? "\$(PERL) $0 --config=$config --installdeps=$missing" + : "\$(NOECHO) \$(NOOP)" + ); + + return %args; +} + +# a wrapper to ExtUtils::MakeMaker::WriteMakefile +sub Write { + require Carp; + Carp::croak "WriteMakefile: Need even number of args" if @_ % 2; + + if ($CheckOnly) { + print << "."; +*** Makefile not written in check-only mode. +. + return; + } + + my %args = _make_args(@_); + + no strict 'refs'; + + $PostambleUsed = 0; + local *MY::postamble = \&postamble unless defined &MY::postamble; + ExtUtils::MakeMaker::WriteMakefile(%args); + + print << "." unless $PostambleUsed; +*** WARNING: Makefile written with customized MY::postamble() without + including contents from Module::AutoInstall::postamble() -- + auto installation features disabled. Please contact the author. +. + + return 1; +} + +sub postamble { + $PostambleUsed = 1; + + return << "."; + +config :: installdeps +\t\$(NOECHO) \$(NOOP) + +checkdeps :: +\t\$(PERL) $0 --checkdeps + +installdeps :: +\t$PostambleActions + +. + +} + +1; + +__END__ + +#line 1003 diff --git a/inc/Module/Install.pm b/inc/Module/Install.pm new file mode 100644 index 0000000..eb449ca --- /dev/null +++ b/inc/Module/Install.pm @@ -0,0 +1,369 @@ +#line 1 +package Module::Install; + +# For any maintainers: +# The load order for Module::Install is a bit magic. +# It goes something like this... +# +# IF ( host has Module::Install installed, creating author mode ) { +# 1. Makefile.PL calls "use inc::Module::Install" +# 2. $INC{inc/Module/Install.pm} set to installed version of inc::Module::Install +# 3. The installed version of inc::Module::Install loads +# 4. inc::Module::Install calls "require Module::Install" +# 5. The ./inc/ version of Module::Install loads +# } ELSE { +# 1. Makefile.PL calls "use inc::Module::Install" +# 2. $INC{inc/Module/Install.pm} set to ./inc/ version of Module::Install +# 3. The ./inc/ version of Module::Install loads +# } + +BEGIN { + require 5.004; +} +use strict 'vars'; + +use vars qw{$VERSION}; +BEGIN { + # All Module::Install core packages now require synchronised versions. + # This will be used to ensure we don't accidentally load old or + # different versions of modules. + # This is not enforced yet, but will be some time in the next few + # releases once we can make sure it won't clash with custom + # Module::Install extensions. + $VERSION = '0.77'; + + *inc::Module::Install::VERSION = *VERSION; + @inc::Module::Install::ISA = __PACKAGE__; + +} + + + + + +# Whether or not inc::Module::Install is actually loaded, the +# $INC{inc/Module/Install.pm} is what will still get set as long as +# the caller loaded module this in the documented manner. +# If not set, the caller may NOT have loaded the bundled version, and thus +# they may not have a MI version that works with the Makefile.PL. This would +# result in false errors or unexpected behaviour. And we don't want that. +my $file = join( '/', 'inc', split /::/, __PACKAGE__ ) . '.pm'; +unless ( $INC{$file} ) { die <<"END_DIE" } + +Please invoke ${\__PACKAGE__} with: + + use inc::${\__PACKAGE__}; + +not: + + use ${\__PACKAGE__}; + +END_DIE + + + + + +# If the script that is loading Module::Install is from the future, +# then make will detect this and cause it to re-run over and over +# again. This is bad. Rather than taking action to touch it (which +# is unreliable on some platforms and requires write permissions) +# for now we should catch this and refuse to run. +if ( -f $0 and (stat($0))[9] > time ) { die <<"END_DIE" } + +Your installer $0 has a modification time in the future. + +This is known to create infinite loops in make. + +Please correct this, then run $0 again. + +END_DIE + + + + + +# Build.PL was formerly supported, but no longer is due to excessive +# difficulty in implementing every single feature twice. +if ( $0 =~ /Build.PL$/i ) { die <<"END_DIE" } + +Module::Install no longer supports Build.PL. + +It was impossible to maintain duel backends, and has been deprecated. + +Please remove all Build.PL files and only use the Makefile.PL installer. + +END_DIE + + + + + +# To save some more typing in Module::Install installers, every... +# use inc::Module::Install +# ...also acts as an implicit use strict. +$^H |= strict::bits(qw(refs subs vars)); + + + + + +use Cwd (); +use File::Find (); +use File::Path (); +use FindBin; + +sub autoload { + my $self = shift; + my $who = $self->_caller; + my $cwd = Cwd::cwd(); + my $sym = "${who}::AUTOLOAD"; + $sym->{$cwd} = sub { + my $pwd = Cwd::cwd(); + if ( my $code = $sym->{$pwd} ) { + # delegate back to parent dirs + goto &$code unless $cwd eq $pwd; + } + $$sym =~ /([^:]+)$/ or die "Cannot autoload $who - $sym"; + unless ( uc($1) eq $1 ) { + unshift @_, ( $self, $1 ); + goto &{$self->can('call')}; + } + }; +} + +sub import { + my $class = shift; + my $self = $class->new(@_); + my $who = $self->_caller; + + unless ( -f $self->{file} ) { + require "$self->{path}/$self->{dispatch}.pm"; + File::Path::mkpath("$self->{prefix}/$self->{author}"); + $self->{admin} = "$self->{name}::$self->{dispatch}"->new( _top => $self ); + $self->{admin}->init; + @_ = ($class, _self => $self); + goto &{"$self->{name}::import"}; + } + + *{"${who}::AUTOLOAD"} = $self->autoload; + $self->preload; + + # Unregister loader and worker packages so subdirs can use them again + delete $INC{"$self->{file}"}; + delete $INC{"$self->{path}.pm"}; + + return 1; +} + +sub preload { + my $self = shift; + unless ( $self->{extensions} ) { + $self->load_extensions( + "$self->{prefix}/$self->{path}", $self + ); + } + + my @exts = @{$self->{extensions}}; + unless ( @exts ) { + my $admin = $self->{admin}; + @exts = $admin->load_all_extensions; + } + + my %seen; + foreach my $obj ( @exts ) { + while (my ($method, $glob) = each %{ref($obj) . '::'}) { + next unless $obj->can($method); + next if $method =~ /^_/; + next if $method eq uc($method); + $seen{$method}++; + } + } + + my $who = $self->_caller; + foreach my $name ( sort keys %seen ) { + *{"${who}::$name"} = sub { + ${"${who}::AUTOLOAD"} = "${who}::$name"; + goto &{"${who}::AUTOLOAD"}; + }; + } +} + +sub new { + my ($class, %args) = @_; + + # ignore the prefix on extension modules built from top level. + my $base_path = Cwd::abs_path($FindBin::Bin); + unless ( Cwd::abs_path(Cwd::cwd()) eq $base_path ) { + delete $args{prefix}; + } + + return $args{_self} if $args{_self}; + + $args{dispatch} ||= 'Admin'; + $args{prefix} ||= 'inc'; + $args{author} ||= ($^O eq 'VMS' ? '_author' : '.author'); + $args{bundle} ||= 'inc/BUNDLES'; + $args{base} ||= $base_path; + $class =~ s/^\Q$args{prefix}\E:://; + $args{name} ||= $class; + $args{version} ||= $class->VERSION; + unless ( $args{path} ) { + $args{path} = $args{name}; + $args{path} =~ s!::!/!g; + } + $args{file} ||= "$args{base}/$args{prefix}/$args{path}.pm"; + $args{wrote} = 0; + + bless( \%args, $class ); +} + +sub call { + my ($self, $method) = @_; + my $obj = $self->load($method) or return; + splice(@_, 0, 2, $obj); + goto &{$obj->can($method)}; +} + +sub load { + my ($self, $method) = @_; + + $self->load_extensions( + "$self->{prefix}/$self->{path}", $self + ) unless $self->{extensions}; + + foreach my $obj (@{$self->{extensions}}) { + return $obj if $obj->can($method); + } + + my $admin = $self->{admin} or die <<"END_DIE"; +The '$method' method does not exist in the '$self->{prefix}' path! +Please remove the '$self->{prefix}' directory and run $0 again to load it. +END_DIE + + my $obj = $admin->load($method, 1); + push @{$self->{extensions}}, $obj; + + $obj; +} + +sub load_extensions { + my ($self, $path, $top) = @_; + + unless ( grep { lc $_ eq lc $self->{prefix} } @INC ) { + unshift @INC, $self->{prefix}; + } + + foreach my $rv ( $self->find_extensions($path) ) { + my ($file, $pkg) = @{$rv}; + next if $self->{pathnames}{$pkg}; + + local $@; + my $new = eval { require $file; $pkg->can('new') }; + unless ( $new ) { + warn $@ if $@; + next; + } + $self->{pathnames}{$pkg} = delete $INC{$file}; + push @{$self->{extensions}}, &{$new}($pkg, _top => $top ); + } + + $self->{extensions} ||= []; +} + +sub find_extensions { + my ($self, $path) = @_; + + my @found; + File::Find::find( sub { + my $file = $File::Find::name; + return unless $file =~ m!^\Q$path\E/(.+)\.pm\Z!is; + my $subpath = $1; + return if lc($subpath) eq lc($self->{dispatch}); + + $file = "$self->{path}/$subpath.pm"; + my $pkg = "$self->{name}::$subpath"; + $pkg =~ s!/!::!g; + + # If we have a mixed-case package name, assume case has been preserved + # correctly. Otherwise, root through the file to locate the case-preserved + # version of the package name. + if ( $subpath eq lc($subpath) || $subpath eq uc($subpath) ) { + my $content = Module::Install::_read($subpath . '.pm'); + my $in_pod = 0; + foreach ( split //, $content ) { + $in_pod = 1 if /^=\w/; + $in_pod = 0 if /^=cut/; + next if ($in_pod || /^=cut/); # skip pod text + next if /^\s*#/; # and comments + if ( m/^\s*package\s+($pkg)\s*;/i ) { + $pkg = $1; + last; + } + } + } + + push @found, [ $file, $pkg ]; + }, $path ) if -d $path; + + @found; +} + + + + + +##################################################################### +# Utility Functions + +sub _caller { + my $depth = 0; + my $call = caller($depth); + while ( $call eq __PACKAGE__ ) { + $depth++; + $call = caller($depth); + } + return $call; +} + +sub _read { + local *FH; + open FH, "< $_[0]" or die "open($_[0]): $!"; + my $str = do { local $/; }; + close FH or die "close($_[0]): $!"; + return $str; +} + +sub _write { + local *FH; + open FH, "> $_[0]" or die "open($_[0]): $!"; + foreach ( 1 .. $#_ ) { print FH $_[$_] or die "print($_[0]): $!" } + close FH or die "close($_[0]): $!"; +} + +# _version is for processing module versions (eg, 1.03_05) not +# Perl versions (eg, 5.8.1). + +sub _version ($) { + my $s = shift || 0; + $s =~ s/^(\d+)\.?//; + my $l = $1 || 0; + my @v = map { $_ . '0' x (3 - length $_) } $s =~ /(\d{1,3})\D?/g; + $l = $l . '.' . join '', @v if @v; + return $l + 0; +} + +# Cloned from Params::Util::_CLASS +sub _CLASS ($) { + ( + defined $_[0] + and + ! ref $_[0] + and + $_[0] =~ m/^[^\W\d]\w*(?:::\w+)*$/s + ) ? $_[0] : undef; +} + +1; + +# Copyright 2008 Adam Kennedy. diff --git a/inc/Module/Install/AutoInstall.pm b/inc/Module/Install/AutoInstall.pm new file mode 100644 index 0000000..8b3bcaa --- /dev/null +++ b/inc/Module/Install/AutoInstall.pm @@ -0,0 +1,61 @@ +#line 1 +package Module::Install::AutoInstall; + +use strict; +use Module::Install::Base; + +use vars qw{$VERSION $ISCORE @ISA}; +BEGIN { + $VERSION = '0.77'; + $ISCORE = 1; + @ISA = qw{Module::Install::Base}; +} + +sub AutoInstall { $_[0] } + +sub run { + my $self = shift; + $self->auto_install_now(@_); +} + +sub write { + my $self = shift; + $self->auto_install(@_); +} + +sub auto_install { + my $self = shift; + return if $self->{done}++; + + # Flatten array of arrays into a single array + my @core = map @$_, map @$_, grep ref, + $self->build_requires, $self->requires; + + my @config = @_; + + # We'll need Module::AutoInstall + $self->include('Module::AutoInstall'); + require Module::AutoInstall; + + Module::AutoInstall->import( + (@config ? (-config => \@config) : ()), + (@core ? (-core => \@core) : ()), + $self->features, + ); + + $self->makemaker_args( Module::AutoInstall::_make_args() ); + + my $class = ref($self); + $self->postamble( + "# --- $class section:\n" . + Module::AutoInstall::postamble() + ); +} + +sub auto_install_now { + my $self = shift; + $self->auto_install(@_); + Module::AutoInstall::do_install(); +} + +1; diff --git a/inc/Module/Install/Base.pm b/inc/Module/Install/Base.pm new file mode 100644 index 0000000..433ebed --- /dev/null +++ b/inc/Module/Install/Base.pm @@ -0,0 +1,72 @@ +#line 1 +package Module::Install::Base; + +$VERSION = '0.77'; + +# Suspend handler for "redefined" warnings +BEGIN { + my $w = $SIG{__WARN__}; + $SIG{__WARN__} = sub { $w }; +} + +### This is the ONLY module that shouldn't have strict on +# use strict; + +#line 41 + +sub new { + my ($class, %args) = @_; + + foreach my $method ( qw(call load) ) { + *{"$class\::$method"} = sub { + shift()->_top->$method(@_); + } unless defined &{"$class\::$method"}; + } + + bless( \%args, $class ); +} + +#line 61 + +sub AUTOLOAD { + my $self = shift; + local $@; + my $autoload = eval { $self->_top->autoload } or return; + goto &$autoload; +} + +#line 76 + +sub _top { $_[0]->{_top} } + +#line 89 + +sub admin { + $_[0]->_top->{admin} or Module::Install::Base::FakeAdmin->new; +} + +#line 101 + +sub is_admin { + $_[0]->admin->VERSION; +} + +sub DESTROY {} + +package Module::Install::Base::FakeAdmin; + +my $Fake; +sub new { $Fake ||= bless(\@_, $_[0]) } + +sub AUTOLOAD {} + +sub DESTROY {} + +# Restore warning handler +BEGIN { + $SIG{__WARN__} = $SIG{__WARN__}->(); +} + +1; + +#line 146 diff --git a/inc/Module/Install/Can.pm b/inc/Module/Install/Can.pm new file mode 100644 index 0000000..9025607 --- /dev/null +++ b/inc/Module/Install/Can.pm @@ -0,0 +1,83 @@ +#line 1 +package Module::Install::Can; + +use strict; +use Module::Install::Base; +use Config (); +### This adds a 5.005 Perl version dependency. +### This is a bug and will be fixed. +use File::Spec (); +use ExtUtils::MakeMaker (); + +use vars qw{$VERSION $ISCORE @ISA}; +BEGIN { + $VERSION = '0.77'; + $ISCORE = 1; + @ISA = qw{Module::Install::Base}; +} + +# check if we can load some module +### Upgrade this to not have to load the module if possible +sub can_use { + my ($self, $mod, $ver) = @_; + $mod =~ s{::|\\}{/}g; + $mod .= '.pm' unless $mod =~ /\.pm$/i; + + my $pkg = $mod; + $pkg =~ s{/}{::}g; + $pkg =~ s{\.pm$}{}i; + + local $@; + eval { require $mod; $pkg->VERSION($ver || 0); 1 }; +} + +# check if we can run some command +sub can_run { + my ($self, $cmd) = @_; + + my $_cmd = $cmd; + return $_cmd if (-x $_cmd or $_cmd = MM->maybe_command($_cmd)); + + for my $dir ((split /$Config::Config{path_sep}/, $ENV{PATH}), '.') { + next if $dir eq ''; + my $abs = File::Spec->catfile($dir, $_[1]); + return $abs if (-x $abs or $abs = MM->maybe_command($abs)); + } + + return; +} + +# can we locate a (the) C compiler +sub can_cc { + my $self = shift; + my @chunks = split(/ /, $Config::Config{cc}) or return; + + # $Config{cc} may contain args; try to find out the program part + while (@chunks) { + return $self->can_run("@chunks") || (pop(@chunks), next); + } + + return; +} + +# Fix Cygwin bug on maybe_command(); +if ( $^O eq 'cygwin' ) { + require ExtUtils::MM_Cygwin; + require ExtUtils::MM_Win32; + if ( ! defined(&ExtUtils::MM_Cygwin::maybe_command) ) { + *ExtUtils::MM_Cygwin::maybe_command = sub { + my ($self, $file) = @_; + if ($file =~ m{^/cygdrive/}i and ExtUtils::MM_Win32->can('maybe_command')) { + ExtUtils::MM_Win32->maybe_command($file); + } else { + ExtUtils::MM_Unix->maybe_command($file); + } + } + } +} + +1; + +__END__ + +#line 158 diff --git a/inc/Module/Install/Compiler.pm b/inc/Module/Install/Compiler.pm new file mode 100644 index 0000000..6b29b23 --- /dev/null +++ b/inc/Module/Install/Compiler.pm @@ -0,0 +1,68 @@ +#line 1 +package Module::Install::Compiler; + +use strict; +use Module::Install::Base; +use File::Basename (); + +use vars qw{$VERSION $ISCORE @ISA}; +BEGIN { + $VERSION = '0.77'; + $ISCORE = 1; + @ISA = qw{Module::Install::Base}; +} + +sub ppport { + my $self = shift; + if ( $self->is_admin ) { + return $self->admin->ppport(@_); + } else { + # Fallback to just a check + my $file = shift || 'ppport.h'; + unless ( -f $file ) { + die "Packaging error, $file is missing"; + } + } +} + +sub cc_files { + require Config; + my $self = shift; + $self->makemaker_args( + OBJECT => join ' ', map { substr($_, 0, -2) . $Config::Config{_o} } @_ + ); +} + +sub cc_inc_paths { + my $self = shift; + $self->makemaker_args( + INC => join ' ', map { "-I$_" } @_ + ); +} + +sub cc_lib_paths { + my $self = shift; + $self->makemaker_args( + LIBS => join ' ', map { "-L$_" } @_ + ); +} + +sub cc_lib_links { + my $self = shift; + $self->makemaker_args( + LIBS => join ' ', $self->makemaker_args->{LIBS}, map { "-l$_" } @_ + ); +} + +sub cc_optimize_flags { + my $self = shift; + $self->makemaker_args( + OPTIMIZE => join ' ', @_ + ); +} + +1; + +__END__ + +#line 123 diff --git a/inc/Module/Install/Fetch.pm b/inc/Module/Install/Fetch.pm new file mode 100644 index 0000000..d66aba5 --- /dev/null +++ b/inc/Module/Install/Fetch.pm @@ -0,0 +1,93 @@ +#line 1 +package Module::Install::Fetch; + +use strict; +use Module::Install::Base; + +use vars qw{$VERSION $ISCORE @ISA}; +BEGIN { + $VERSION = '0.77'; + $ISCORE = 1; + @ISA = qw{Module::Install::Base}; +} + +sub get_file { + my ($self, %args) = @_; + my ($scheme, $host, $path, $file) = + $args{url} =~ m|^(\w+)://([^/]+)(.+)/(.+)| or return; + + if ( $scheme eq 'http' and ! eval { require LWP::Simple; 1 } ) { + $args{url} = $args{ftp_url} + or (warn("LWP support unavailable!\n"), return); + ($scheme, $host, $path, $file) = + $args{url} =~ m|^(\w+)://([^/]+)(.+)/(.+)| or return; + } + + $|++; + print "Fetching '$file' from $host... "; + + unless (eval { require Socket; Socket::inet_aton($host) }) { + warn "'$host' resolve failed!\n"; + return; + } + + return unless $scheme eq 'ftp' or $scheme eq 'http'; + + require Cwd; + my $dir = Cwd::getcwd(); + chdir $args{local_dir} or return if exists $args{local_dir}; + + if (eval { require LWP::Simple; 1 }) { + LWP::Simple::mirror($args{url}, $file); + } + elsif (eval { require Net::FTP; 1 }) { eval { + # use Net::FTP to get past firewall + my $ftp = Net::FTP->new($host, Passive => 1, Timeout => 600); + $ftp->login("anonymous", 'anonymous@example.com'); + $ftp->cwd($path); + $ftp->binary; + $ftp->get($file) or (warn("$!\n"), return); + $ftp->quit; + } } + elsif (my $ftp = $self->can_run('ftp')) { eval { + # no Net::FTP, fallback to ftp.exe + require FileHandle; + my $fh = FileHandle->new; + + local $SIG{CHLD} = 'IGNORE'; + unless ($fh->open("|$ftp -n")) { + warn "Couldn't open ftp: $!\n"; + chdir $dir; return; + } + + my @dialog = split(/\n/, <<"END_FTP"); +open $host +user anonymous anonymous\@example.com +cd $path +binary +get $file $file +quit +END_FTP + foreach (@dialog) { $fh->print("$_\n") } + $fh->close; + } } + else { + warn "No working 'ftp' program available!\n"; + chdir $dir; return; + } + + unless (-f $file) { + warn "Fetching failed: $@\n"; + chdir $dir; return; + } + + return if exists $args{size} and -s $file != $args{size}; + system($args{run}) if exists $args{run}; + unlink($file) if $args{remove}; + + print(((!exists $args{check_for} or -e $args{check_for}) + ? "done!" : "failed! ($!)"), "\n"); + chdir $dir; return !$?; +} + +1; diff --git a/inc/Module/Install/Include.pm b/inc/Module/Install/Include.pm new file mode 100644 index 0000000..0c7dc5a --- /dev/null +++ b/inc/Module/Install/Include.pm @@ -0,0 +1,34 @@ +#line 1 +package Module::Install::Include; + +use strict; +use Module::Install::Base; + +use vars qw{$VERSION $ISCORE @ISA}; +BEGIN { + $VERSION = '0.77'; + $ISCORE = 1; + @ISA = qw{Module::Install::Base}; +} + +sub include { + shift()->admin->include(@_); +} + +sub include_deps { + shift()->admin->include_deps(@_); +} + +sub auto_include { + shift()->admin->auto_include(@_); +} + +sub auto_include_deps { + shift()->admin->auto_include_deps(@_); +} + +sub auto_include_dependent_dists { + shift()->admin->auto_include_dependent_dists(@_); +} + +1; diff --git a/inc/Module/Install/Makefile.pm b/inc/Module/Install/Makefile.pm new file mode 100644 index 0000000..92cd1ef --- /dev/null +++ b/inc/Module/Install/Makefile.pm @@ -0,0 +1,253 @@ +#line 1 +package Module::Install::Makefile; + +use strict 'vars'; +use Module::Install::Base; +use ExtUtils::MakeMaker (); + +use vars qw{$VERSION $ISCORE @ISA}; +BEGIN { + $VERSION = '0.77'; + $ISCORE = 1; + @ISA = qw{Module::Install::Base}; +} + +sub Makefile { $_[0] } + +my %seen = (); + +sub prompt { + shift; + + # Infinite loop protection + my @c = caller(); + if ( ++$seen{"$c[1]|$c[2]|$_[0]"} > 3 ) { + die "Caught an potential prompt infinite loop ($c[1]|$c[2]|$_[0])"; + } + + # In automated testing, always use defaults + if ( $ENV{AUTOMATED_TESTING} and ! $ENV{PERL_MM_USE_DEFAULT} ) { + local $ENV{PERL_MM_USE_DEFAULT} = 1; + goto &ExtUtils::MakeMaker::prompt; + } else { + goto &ExtUtils::MakeMaker::prompt; + } +} + +sub makemaker_args { + my $self = shift; + my $args = ( $self->{makemaker_args} ||= {} ); + %$args = ( %$args, @_ ); + return $args; +} + +# For mm args that take multiple space-seperated args, +# append an argument to the current list. +sub makemaker_append { + my $self = sShift; + my $name = shift; + my $args = $self->makemaker_args; + $args->{name} = defined $args->{$name} + ? join( ' ', $args->{name}, @_ ) + : join( ' ', @_ ); +} + +sub build_subdirs { + my $self = shift; + my $subdirs = $self->makemaker_args->{DIR} ||= []; + for my $subdir (@_) { + push @$subdirs, $subdir; + } +} + +sub clean_files { + my $self = shift; + my $clean = $self->makemaker_args->{clean} ||= {}; + %$clean = ( + %$clean, + FILES => join ' ', grep { length $_ } ($clean->{FILES} || (), @_), + ); +} + +sub realclean_files { + my $self = shift; + my $realclean = $self->makemaker_args->{realclean} ||= {}; + %$realclean = ( + %$realclean, + FILES => join ' ', grep { length $_ } ($realclean->{FILES} || (), @_), + ); +} + +sub libs { + my $self = shift; + my $libs = ref $_[0] ? shift : [ shift ]; + $self->makemaker_args( LIBS => $libs ); +} + +sub inc { + my $self = shift; + $self->makemaker_args( INC => shift ); +} + +my %test_dir = (); + +sub _wanted_t { + /\.t$/ and -f $_ and $test_dir{$File::Find::dir} = 1; +} + +sub tests_recursive { + my $self = shift; + if ( $self->tests ) { + die "tests_recursive will not work if tests are already defined"; + } + my $dir = shift || 't'; + unless ( -d $dir ) { + die "tests_recursive dir '$dir' does not exist"; + } + %test_dir = (); + require File::Find; + File::Find::find( \&_wanted_t, $dir ); + $self->tests( join ' ', map { "$_/*.t" } sort keys %test_dir ); +} + +sub write { + my $self = shift; + die "&Makefile->write() takes no arguments\n" if @_; + + # Make sure we have a new enough + require ExtUtils::MakeMaker; + + # MakeMaker can complain about module versions that include + # an underscore, even though its own version may contain one! + # Hence the funny regexp to get rid of it. See RT #35800 + # for details. + + $self->configure_requires( 'ExtUtils::MakeMaker' => $ExtUtils::MakeMaker::VERSION =~ /^(\d+\.\d+)/ ); + + # Generate the + my $args = $self->makemaker_args; + $args->{DISTNAME} = $self->name; + $args->{NAME} = $self->module_name || $self->name; + $args->{VERSION} = $self->version; + $args->{NAME} =~ s/-/::/g; + if ( $self->tests ) { + $args->{test} = { TESTS => $self->tests }; + } + if ($] >= 5.005) { + $args->{ABSTRACT} = $self->abstract; + $args->{AUTHOR} = $self->author; + } + if ( eval($ExtUtils::MakeMaker::VERSION) >= 6.10 ) { + $args->{NO_META} = 1; + } + if ( eval($ExtUtils::MakeMaker::VERSION) > 6.17 and $self->sign ) { + $args->{SIGN} = 1; + } + unless ( $self->is_admin ) { + delete $args->{SIGN}; + } + + # merge both kinds of requires into prereq_pm + my $prereq = ($args->{PREREQ_PM} ||= {}); + %$prereq = ( %$prereq, + map { @$_ } + map { @$_ } + grep $_, + ($self->configure_requires, $self->build_requires, $self->requires) + ); + + # Remove any reference to perl, PREREQ_PM doesn't support it + delete $args->{PREREQ_PM}->{perl}; + + # merge both kinds of requires into prereq_pm + my $subdirs = ($args->{DIR} ||= []); + if ($self->bundles) { + foreach my $bundle (@{ $self->bundles }) { + my ($file, $dir) = @$bundle; + push @$subdirs, $dir if -d $dir; + delete $prereq->{$file}; + } + } + + if ( my $perl_version = $self->perl_version ) { + eval "use $perl_version; 1" + or die "ERROR: perl: Version $] is installed, " + . "but we need version >= $perl_version"; + } + + $args->{INSTALLDIRS} = $self->installdirs; + + my %args = map { ( $_ => $args->{$_} ) } grep {defined($args->{$_})} keys %$args; + + my $user_preop = delete $args{dist}->{PREOP}; + if (my $preop = $self->admin->preop($user_preop)) { + foreach my $key ( keys %$preop ) { + $args{dist}->{$key} = $preop->{$key}; + } + } + + my $mm = ExtUtils::MakeMaker::WriteMakefile(%args); + $self->fix_up_makefile($mm->{FIRST_MAKEFILE} || 'Makefile'); +} + +sub fix_up_makefile { + my $self = shift; + my $makefile_name = shift; + my $top_class = ref($self->_top) || ''; + my $top_version = $self->_top->VERSION || ''; + + my $preamble = $self->preamble + ? "# Preamble by $top_class $top_version\n" + . $self->preamble + : ''; + my $postamble = "# Postamble by $top_class $top_version\n" + . ($self->postamble || ''); + + local *MAKEFILE; + open MAKEFILE, "< $makefile_name" or die "fix_up_makefile: Couldn't open $makefile_name: $!"; + my $makefile = do { local $/; }; + close MAKEFILE or die $!; + + $makefile =~ s/\b(test_harness\(\$\(TEST_VERBOSE\), )/$1'inc', /; + $makefile =~ s/( -I\$\(INST_ARCHLIB\))/ -Iinc$1/g; + $makefile =~ s/( "-I\$\(INST_LIB\)")/ "-Iinc"$1/g; + $makefile =~ s/^(FULLPERL = .*)/$1 "-Iinc"/m; + $makefile =~ s/^(PERL = .*)/$1 "-Iinc"/m; + + # Module::Install will never be used to build the Core Perl + # Sometimes PERL_LIB and PERL_ARCHLIB get written anyway, which breaks + # PREFIX/PERL5LIB, and thus, install_share. Blank them if they exist + $makefile =~ s/^PERL_LIB = .+/PERL_LIB =/m; + #$makefile =~ s/^PERL_ARCHLIB = .+/PERL_ARCHLIB =/m; + + # Perl 5.005 mentions PERL_LIB explicitly, so we have to remove that as well. + $makefile =~ s/(\"?)-I\$\(PERL_LIB\)\1//g; + + # XXX - This is currently unused; not sure if it breaks other MM-users + # $makefile =~ s/^pm_to_blib\s+:\s+/pm_to_blib :: /mg; + + open MAKEFILE, "> $makefile_name" or die "fix_up_makefile: Couldn't open $makefile_name: $!"; + print MAKEFILE "$preamble$makefile$postamble" or die $!; + close MAKEFILE or die $!; + + 1; +} + +sub preamble { + my ($self, $text) = @_; + $self->{preamble} = $text . $self->{preamble} if defined $text; + $self->{preamble}; +} + +sub postamble { + my ($self, $text) = @_; + $self->{postamble} ||= $self->admin->postamble; + $self->{postamble} .= $text if defined $text; + $self->{postamble} +} + +1; + +__END__ + +#line 379 diff --git a/inc/Module/Install/Metadata.pm b/inc/Module/Install/Metadata.pm new file mode 100644 index 0000000..397fb97 --- /dev/null +++ b/inc/Module/Install/Metadata.pm @@ -0,0 +1,500 @@ +#line 1 +package Module::Install::Metadata; + +use strict 'vars'; +use Module::Install::Base; + +use vars qw{$VERSION $ISCORE @ISA}; +BEGIN { + $VERSION = '0.77'; + $ISCORE = 1; + @ISA = qw{Module::Install::Base}; +} + +my @scalar_keys = qw{ + name + module_name + abstract + author + version + distribution_type + tests + installdirs +}; + +my @tuple_keys = qw{ + configure_requires + build_requires + requires + recommends + bundles + resources +}; + +my @resource_keys = qw{ + homepage + bugtracker + repository +}; + +sub Meta { shift } +sub Meta_ScalarKeys { @scalar_keys } +sub Meta_TupleKeys { @tuple_keys } +sub Meta_ResourceKeys { @resource_keys } + +foreach my $key ( @scalar_keys ) { + *$key = sub { + my $self = shift; + return $self->{values}{$key} if defined wantarray and !@_; + $self->{values}{$key} = shift; + return $self; + }; +} + +foreach my $key ( @resource_keys ) { + *$key = sub { + my $self = shift; + unless ( @_ ) { + return () unless $self->{values}{resources}; + return map { $_->[1] } + grep { $_->[0] eq $key } + @{ $self->{values}{resources} }; + } + return $self->{values}{resources}{$key} unless @_; + my $uri = shift or die( + "Did not provide a value to $key()" + ); + $self->resources( $key => $uri ); + return 1; + }; +} + +sub requires { + my $self = shift; + while ( @_ ) { + my $module = shift or last; + my $version = shift || 0; + push @{ $self->{values}{requires} }, [ $module, $version ]; + } + $self->{values}{requires}; +} + +sub build_requires { + my $self = shift; + while ( @_ ) { + my $module = shift or last; + my $version = shift || 0; + push @{ $self->{values}{build_requires} }, [ $module, $version ]; + } + $self->{values}{build_requires}; +} + +sub configure_requires { + my $self = shift; + while ( @_ ) { + my $module = shift or last; + my $version = shift || 0; + push @{ $self->{values}{configure_requires} }, [ $module, $version ]; + } + $self->{values}{configure_requires}; +} + +sub recommends { + my $self = shift; + while ( @_ ) { + my $module = shift or last; + my $version = shift || 0; + push @{ $self->{values}{recommends} }, [ $module, $version ]; + } + $self->{values}{recommends}; +} + +sub bundles { + my $self = shift; + while ( @_ ) { + my $module = shift or last; + my $version = shift || 0; + push @{ $self->{values}{bundles} }, [ $module, $version ]; + } + $self->{values}{bundles}; +} + +# Resource handling +my %lc_resource = map { $_ => 1 } qw{ + homepage + license + bugtracker + repository +}; + +sub resources { + my $self = shift; + while ( @_ ) { + my $name = shift or last; + my $value = shift or next; + if ( $name eq lc $name and ! $lc_resource{$name} ) { + die("Unsupported reserved lowercase resource '$name'"); + } + $self->{values}{resources} ||= []; + push @{ $self->{values}{resources} }, [ $name, $value ]; + } + $self->{values}{resources}; +} + +# Aliases for build_requires that will have alternative +# meanings in some future version of META.yml. +sub test_requires { shift->build_requires(@_) } +sub install_requires { shift->build_requires(@_) } + +# Aliases for installdirs options +sub install_as_core { $_[0]->installdirs('perl') } +sub install_as_cpan { $_[0]->installdirs('site') } +sub install_as_site { $_[0]->installdirs('site') } +sub install_as_vendor { $_[0]->installdirs('vendor') } + +sub sign { + my $self = shift; + return $self->{values}{sign} if defined wantarray and ! @_; + $self->{values}{sign} = ( @_ ? $_[0] : 1 ); + return $self; +} + +sub dynamic_config { + my $self = shift; + unless ( @_ ) { + warn "You MUST provide an explicit true/false value to dynamic_config\n"; + return $self; + } + $self->{values}{dynamic_config} = $_[0] ? 1 : 0; + return 1; +} + +sub perl_version { + my $self = shift; + return $self->{values}{perl_version} unless @_; + my $version = shift or die( + "Did not provide a value to perl_version()" + ); + + # Convert triple-part versions (eg, 5.6.1 or 5.8.9) to + # numbers (eg, 5.006001 or 5.008009). + + $version =~ s/^(\d+)\.(\d+)\.(\d+)$/sprintf("%d.%03d%03d",$1,$2,$3)/e; + + $version =~ s/_.+$//; + $version = $version + 0; # Numify + unless ( $version >= 5.005 ) { + die "Module::Install only supports 5.005 or newer (use ExtUtils::MakeMaker)\n"; + } + $self->{values}{perl_version} = $version; + return 1; +} + +sub license { + my $self = shift; + return $self->{values}{license} unless @_; + my $license = shift or die( + 'Did not provide a value to license()' + ); + $self->{values}{license} = $license; + + # Automatically fill in license URLs + if ( $license eq 'perl' ) { + $self->resources( license => 'http://dev.perl.org/licenses/' ); + } + + return 1; +} + +sub all_from { + my ( $self, $file ) = @_; + + unless ( defined($file) ) { + my $name = $self->name or die( + "all_from called with no args without setting name() first" + ); + $file = join('/', 'lib', split(/-/, $name)) . '.pm'; + $file =~ s{.*/}{} unless -e $file; + unless ( -e $file ) { + die("all_from cannot find $file from $name"); + } + } + unless ( -f $file ) { + die("The path '$file' does not exist, or is not a file"); + } + + # Some methods pull from POD instead of code. + # If there is a matching .pod, use that instead + my $pod = $file; + $pod =~ s/\.pm$/.pod/i; + $pod = $file unless -e $pod; + + # Pull the different values + $self->name_from($file) unless $self->name; + $self->version_from($file) unless $self->version; + $self->perl_version_from($file) unless $self->perl_version; + $self->author_from($pod) unless $self->author; + $self->license_from($pod) unless $self->license; + $self->abstract_from($pod) unless $self->abstract; + + return 1; +} + +sub provides { + my $self = shift; + my $provides = ( $self->{values}{provides} ||= {} ); + %$provides = (%$provides, @_) if @_; + return $provides; +} + +sub auto_provides { + my $self = shift; + return $self unless $self->is_admin; + unless (-e 'MANIFEST') { + warn "Cannot deduce auto_provides without a MANIFEST, skipping\n"; + return $self; + } + # Avoid spurious warnings as we are not checking manifest here. + local $SIG{__WARN__} = sub {1}; + require ExtUtils::Manifest; + local *ExtUtils::Manifest::manicheck = sub { return }; + + require Module::Build; + my $build = Module::Build->new( + dist_name => $self->name, + dist_version => $self->version, + license => $self->license, + ); + $self->provides( %{ $build->find_dist_packages || {} } ); +} + +sub feature { + my $self = shift; + my $name = shift; + my $features = ( $self->{values}{features} ||= [] ); + my $mods; + + if ( @_ == 1 and ref( $_[0] ) ) { + # The user used ->feature like ->features by passing in the second + # argument as a reference. Accomodate for that. + $mods = $_[0]; + } else { + $mods = \@_; + } + + my $count = 0; + push @$features, ( + $name => [ + map { + ref($_) ? ( ref($_) eq 'HASH' ) ? %$_ : @$_ : $_ + } @$mods + ] + ); + + return @$features; +} + +sub features { + my $self = shift; + while ( my ( $name, $mods ) = splice( @_, 0, 2 ) ) { + $self->feature( $name, @$mods ); + } + return $self->{values}{features} + ? @{ $self->{values}{features} } + : (); +} + +sub no_index { + my $self = shift; + my $type = shift; + push @{ $self->{values}{no_index}{$type} }, @_ if $type; + return $self->{values}{no_index}; +} + +sub read { + my $self = shift; + $self->include_deps( 'YAML::Tiny', 0 ); + + require YAML::Tiny; + my $data = YAML::Tiny::LoadFile('META.yml'); + + # Call methods explicitly in case user has already set some values. + while ( my ( $key, $value ) = each %$data ) { + next unless $self->can($key); + if ( ref $value eq 'HASH' ) { + while ( my ( $module, $version ) = each %$value ) { + $self->can($key)->($self, $module => $version ); + } + } else { + $self->can($key)->($self, $value); + } + } + return $self; +} + +sub write { + my $self = shift; + return $self unless $self->is_admin; + $self->admin->write_meta; + return $self; +} + +sub version_from { + require ExtUtils::MM_Unix; + my ( $self, $file ) = @_; + $self->version( ExtUtils::MM_Unix->parse_version($file) ); +} + +sub abstract_from { + require ExtUtils::MM_Unix; + my ( $self, $file ) = @_; + $self->abstract( + bless( + { DISTNAME => $self->name }, + 'ExtUtils::MM_Unix' + )->parse_abstract($file) + ); +} + +# Add both distribution and module name +sub name_from { + my ($self, $file) = @_; + if ( + Module::Install::_read($file) =~ m/ + ^ \s* + package \s* + ([\w:]+) + \s* ; + /ixms + ) { + my ($name, $module_name) = ($1, $1); + $name =~ s{::}{-}g; + $self->name($name); + unless ( $self->module_name ) { + $self->module_name($module_name); + } + } else { + die("Cannot determine name from $file\n"); + } +} + +sub perl_version_from { + my $self = shift; + if ( + Module::Install::_read($_[0]) =~ m/ + ^ + (?:use|require) \s* + v? + ([\d_\.]+) + \s* ; + /ixms + ) { + my $perl_version = $1; + $perl_version =~ s{_}{}g; + $self->perl_version($perl_version); + } else { + warn "Cannot determine perl version info from $_[0]\n"; + return; + } +} + +sub author_from { + my $self = shift; + my $content = Module::Install::_read($_[0]); + if ($content =~ m/ + =head \d \s+ (?:authors?)\b \s* + ([^\n]*) + | + =head \d \s+ (?:licen[cs]e|licensing|copyright|legal)\b \s* + .*? copyright .*? \d\d\d[\d.]+ \s* (?:\bby\b)? \s* + ([^\n]*) + /ixms) { + my $author = $1 || $2; + $author =~ s{E}{<}g; + $author =~ s{E}{>}g; + $self->author($author); + } else { + warn "Cannot determine author info from $_[0]\n"; + } +} + +sub license_from { + my $self = shift; + if ( + Module::Install::_read($_[0]) =~ m/ + ( + =head \d \s+ + (?:licen[cs]e|licensing|copyright|legal)\b + .*? + ) + (=head\\d.*|=cut.*|) + \z + /ixms ) { + my $license_text = $1; + my @phrases = ( + 'under the same (?:terms|license) as perl itself' => 'perl', 1, + 'GNU general public license' => 'gpl', 1, + 'GNU public license' => 'gpl', 1, + 'GNU lesser general public license' => 'lgpl', 1, + 'GNU lesser public license' => 'lgpl', 1, + 'GNU library general public license' => 'lgpl', 1, + 'GNU library public license' => 'lgpl', 1, + 'BSD license' => 'bsd', 1, + 'Artistic license' => 'artistic', 1, + 'GPL' => 'gpl', 1, + 'LGPL' => 'lgpl', 1, + 'BSD' => 'bsd', 1, + 'Artistic' => 'artistic', 1, + 'MIT' => 'mit', 1, + 'proprietary' => 'proprietary', 0, + ); + while ( my ($pattern, $license, $osi) = splice(@phrases, 0, 3) ) { + $pattern =~ s{\s+}{\\s+}g; + if ( $license_text =~ /\b$pattern\b/i ) { + if ( $osi and $license_text =~ /All rights reserved/i ) { + print "WARNING: 'All rights reserved' in copyright may invalidate Open Source license.\n"; + } + $self->license($license); + return 1; + } + } + } + + warn "Cannot determine license info from $_[0]\n"; + return 'unknown'; +} + +sub bugtracker_from { + my $self = shift; + my $content = Module::Install::_read($_[0]); + my @links = $content =~ m/L\<(http\:\/\/rt\.cpan\.org\/[^>]+)\>/g; + unless ( @links ) { + warn "Cannot determine bugtracker info from $_[0]\n"; + return 0; + } + if ( @links > 1 ) { + warn "Found more than on rt.cpan.org link in $_[0]\n"; + return 0; + } + + # Set the bugtracker + bugtracker( $links[0] ); + return 1; +} + +sub install_script { + my $self = shift; + my $args = $self->makemaker_args; + my $exe = $args->{EXE_FILES} ||= []; + foreach ( @_ ) { + if ( -f $_ ) { + push @$exe, $_; + } elsif ( -d 'script' and -f "script/$_" ) { + push @$exe, "script/$_"; + } else { + die("Cannot find script '$_'"); + } + } +} + +1; diff --git a/inc/Module/Install/TestBase.pm b/inc/Module/Install/TestBase.pm new file mode 100644 index 0000000..b3f12ea --- /dev/null +++ b/inc/Module/Install/TestBase.pm @@ -0,0 +1,29 @@ +#line 1 +package Module::Install::TestBase; +use strict; +use warnings; + +use Module::Install::Base; + +use vars qw($VERSION @ISA); +BEGIN { + $VERSION = '0.11'; + @ISA = 'Module::Install::Base'; +} + +sub use_test_base { + my $self = shift; + $self->include('Test::Base'); + $self->include('Test::Base::Filter'); + $self->include('Spiffy'); + $self->include('Test::More'); + $self->include('Test::Builder'); + $self->include('Test::Builder::Module'); + $self->requires('Filter::Util::Call'); +} + +1; + +=encoding utf8 + +#line 70 diff --git a/inc/Module/Install/Win32.pm b/inc/Module/Install/Win32.pm new file mode 100644 index 0000000..cff76a2 --- /dev/null +++ b/inc/Module/Install/Win32.pm @@ -0,0 +1,64 @@ +#line 1 +package Module::Install::Win32; + +use strict; +use Module::Install::Base; + +use vars qw{$VERSION @ISA $ISCORE}; +BEGIN { + $VERSION = '0.77'; + @ISA = qw{Module::Install::Base}; + $ISCORE = 1; +} + +# determine if the user needs nmake, and download it if needed +sub check_nmake { + my $self = shift; + $self->load('can_run'); + $self->load('get_file'); + + require Config; + return unless ( + $^O eq 'MSWin32' and + $Config::Config{make} and + $Config::Config{make} =~ /^nmake\b/i and + ! $self->can_run('nmake') + ); + + print "The required 'nmake' executable not found, fetching it...\n"; + + require File::Basename; + my $rv = $self->get_file( + url => 'http://download.microsoft.com/download/vc15/Patch/1.52/W95/EN-US/Nmake15.exe', + ftp_url => 'ftp://ftp.microsoft.com/Softlib/MSLFILES/Nmake15.exe', + local_dir => File::Basename::dirname($^X), + size => 51928, + run => 'Nmake15.exe /o > nul', + check_for => 'Nmake.exe', + remove => 1, + ); + + die <<'END_MESSAGE' unless $rv; + +------------------------------------------------------------------------------- + +Since you are using Microsoft Windows, you will need the 'nmake' utility +before installation. It's available at: + + http://download.microsoft.com/download/vc15/Patch/1.52/W95/EN-US/Nmake15.exe + or + ftp://ftp.microsoft.com/Softlib/MSLFILES/Nmake15.exe + +Please download the file manually, save it to a directory in %PATH% (e.g. +C:\WINDOWS\COMMAND\), then launch the MS-DOS command line shell, "cd" to +that directory, and run "Nmake15.exe" from there; that will create the +'nmake.exe' file needed by this module. + +You may then resume the installation process described in README. + +------------------------------------------------------------------------------- +END_MESSAGE + +} + +1; diff --git a/inc/Module/Install/WriteAll.pm b/inc/Module/Install/WriteAll.pm new file mode 100644 index 0000000..f35620f --- /dev/null +++ b/inc/Module/Install/WriteAll.pm @@ -0,0 +1,40 @@ +#line 1 +package Module::Install::WriteAll; + +use strict; +use Module::Install::Base; + +use vars qw{$VERSION @ISA $ISCORE}; +BEGIN { + $VERSION = '0.77'; + @ISA = qw{Module::Install::Base}; + $ISCORE = 1; +} + +sub WriteAll { + my $self = shift; + my %args = ( + meta => 1, + sign => 0, + inline => 0, + check_nmake => 1, + @_, + ); + + $self->sign(1) if $args{sign}; + $self->Meta->write if $args{meta}; + $self->admin->WriteAll(%args) if $self->is_admin; + + $self->check_nmake if $args{check_nmake}; + unless ( $self->makemaker_args->{PL_FILES} ) { + $self->makemaker_args( PL_FILES => {} ); + } + + if ( $args{inline} ) { + $self->Inline->write; + } else { + $self->Makefile->write; + } +} + +1; diff --git a/inc/Spiffy.pm b/inc/Spiffy.pm new file mode 100644 index 0000000..7b10f7a --- /dev/null +++ b/inc/Spiffy.pm @@ -0,0 +1,539 @@ +#line 1 +package Spiffy; +use strict; +use 5.006001; +use warnings; +use Carp; +require Exporter; +our $VERSION = '0.30'; +our @EXPORT = (); +our @EXPORT_BASE = qw(field const stub super); +our @EXPORT_OK = (@EXPORT_BASE, qw(id WWW XXX YYY ZZZ)); +our %EXPORT_TAGS = (XXX => [qw(WWW XXX YYY ZZZ)]); + +my $stack_frame = 0; +my $dump = 'yaml'; +my $bases_map = {}; + +sub WWW; sub XXX; sub YYY; sub ZZZ; + +# This line is here to convince "autouse" into believing we are autousable. +sub can { + ($_[1] eq 'import' and caller()->isa('autouse')) + ? \&Exporter::import # pacify autouse's equality test + : $_[0]->SUPER::can($_[1]) # normal case +} + +# TODO +# +# Exported functions like field and super should be hidden so as not to +# be confused with methods that can be inherited. +# + +sub new { + my $class = shift; + $class = ref($class) || $class; + my $self = bless {}, $class; + while (@_) { + my $method = shift; + $self->$method(shift); + } + return $self; +} + +my $filtered_files = {}; +my $filter_dump = 0; +my $filter_save = 0; +our $filter_result = ''; +sub import { + no strict 'refs'; + no warnings; + my $self_package = shift; + + # XXX Using parse_arguments here might cause confusion, because the + # subclass's boolean_arguments and paired_arguments can conflict, causing + # difficult debugging. Consider using something truly local. + my ($args, @export_list) = do { + local *boolean_arguments = sub { + qw( + -base -Base -mixin -selfless + -XXX -dumper -yaml + -filter_dump -filter_save + ) + }; + local *paired_arguments = sub { qw(-package) }; + $self_package->parse_arguments(@_); + }; + return spiffy_mixin_import(scalar(caller(0)), $self_package, @export_list) + if $args->{-mixin}; + + $filter_dump = 1 if $args->{-filter_dump}; + $filter_save = 1 if $args->{-filter_save}; + $dump = 'yaml' if $args->{-yaml}; + $dump = 'dumper' if $args->{-dumper}; + + local @EXPORT_BASE = @EXPORT_BASE; + + if ($args->{-XXX}) { + push @EXPORT_BASE, @{$EXPORT_TAGS{XXX}} + unless grep /^XXX$/, @EXPORT_BASE; + } + + spiffy_filter() + if ($args->{-selfless} or $args->{-Base}) and + not $filtered_files->{(caller($stack_frame))[1]}++; + + my $caller_package = $args->{-package} || caller($stack_frame); + push @{"$caller_package\::ISA"}, $self_package + if $args->{-Base} or $args->{-base}; + + for my $class (@{all_my_bases($self_package)}) { + next unless $class->isa('Spiffy'); + my @export = grep { + not defined &{"$caller_package\::$_"}; + } ( @{"$class\::EXPORT"}, + ($args->{-Base} or $args->{-base}) + ? @{"$class\::EXPORT_BASE"} : (), + ); + my @export_ok = grep { + not defined &{"$caller_package\::$_"}; + } @{"$class\::EXPORT_OK"}; + + # Avoid calling the expensive Exporter::export + # if there is nothing to do (optimization) + my %exportable = map { ($_, 1) } @export, @export_ok; + next unless keys %exportable; + + my @export_save = @{"$class\::EXPORT"}; + my @export_ok_save = @{"$class\::EXPORT_OK"}; + @{"$class\::EXPORT"} = @export; + @{"$class\::EXPORT_OK"} = @export_ok; + my @list = grep { + (my $v = $_) =~ s/^[\!\:]//; + $exportable{$v} or ${"$class\::EXPORT_TAGS"}{$v}; + } @export_list; + Exporter::export($class, $caller_package, @list); + @{"$class\::EXPORT"} = @export_save; + @{"$class\::EXPORT_OK"} = @export_ok_save; + } +} + +sub spiffy_filter { + require Filter::Util::Call; + my $done = 0; + Filter::Util::Call::filter_add( + sub { + return 0 if $done; + my ($data, $end) = ('', ''); + while (my $status = Filter::Util::Call::filter_read()) { + return $status if $status < 0; + if (/^__(?:END|DATA)__\r?$/) { + $end = $_; + last; + } + $data .= $_; + $_ = ''; + } + $_ = $data; + my @my_subs; + s[^(sub\s+\w+\s+\{)(.*\n)] + [${1}my \$self = shift;$2]gm; + s[^(sub\s+\w+)\s*\(\s*\)(\s+\{.*\n)] + [${1}${2}]gm; + s[^my\s+sub\s+(\w+)(\s+\{)(.*)((?s:.*?\n))\}\n] + [push @my_subs, $1; "\$$1 = sub$2my \$self = shift;$3$4\};\n"]gem; + my $preclare = ''; + if (@my_subs) { + $preclare = join ',', map "\$$_", @my_subs; + $preclare = "my($preclare);"; + } + $_ = "use strict;use warnings;$preclare${_};1;\n$end"; + if ($filter_dump) { print; exit } + if ($filter_save) { $filter_result = $_; $_ = $filter_result; } + $done = 1; + } + ); +} + +sub base { + push @_, -base; + goto &import; +} + +sub all_my_bases { + my $class = shift; + + return $bases_map->{$class} + if defined $bases_map->{$class}; + + my @bases = ($class); + no strict 'refs'; + for my $base_class (@{"${class}::ISA"}) { + push @bases, @{all_my_bases($base_class)}; + } + my $used = {}; + $bases_map->{$class} = [grep {not $used->{$_}++} @bases]; +} + +my %code = ( + sub_start => + "sub {\n", + set_default => + " \$_[0]->{%s} = %s\n unless exists \$_[0]->{%s};\n", + init => + " return \$_[0]->{%s} = do { my \$self = \$_[0]; %s }\n" . + " unless \$#_ > 0 or defined \$_[0]->{%s};\n", + weak_init => + " return do {\n" . + " \$_[0]->{%s} = do { my \$self = \$_[0]; %s };\n" . + " Scalar::Util::weaken(\$_[0]->{%s}) if ref \$_[0]->{%s};\n" . + " \$_[0]->{%s};\n" . + " } unless \$#_ > 0 or defined \$_[0]->{%s};\n", + return_if_get => + " return \$_[0]->{%s} unless \$#_ > 0;\n", + set => + " \$_[0]->{%s} = \$_[1];\n", + weaken => + " Scalar::Util::weaken(\$_[0]->{%s}) if ref \$_[0]->{%s};\n", + sub_end => + " return \$_[0]->{%s};\n}\n", +); + +sub field { + my $package = caller; + my ($args, @values) = do { + no warnings; + local *boolean_arguments = sub { (qw(-weak)) }; + local *paired_arguments = sub { (qw(-package -init)) }; + Spiffy->parse_arguments(@_); + }; + my ($field, $default) = @values; + $package = $args->{-package} if defined $args->{-package}; + die "Cannot have a default for a weakened field ($field)" + if defined $default && $args->{-weak}; + return if defined &{"${package}::$field"}; + require Scalar::Util if $args->{-weak}; + my $default_string = + ( ref($default) eq 'ARRAY' and not @$default ) + ? '[]' + : (ref($default) eq 'HASH' and not keys %$default ) + ? '{}' + : default_as_code($default); + + my $code = $code{sub_start}; + if ($args->{-init}) { + my $fragment = $args->{-weak} ? $code{weak_init} : $code{init}; + $code .= sprintf $fragment, $field, $args->{-init}, ($field) x 4; + } + $code .= sprintf $code{set_default}, $field, $default_string, $field + if defined $default; + $code .= sprintf $code{return_if_get}, $field; + $code .= sprintf $code{set}, $field; + $code .= sprintf $code{weaken}, $field, $field + if $args->{-weak}; + $code .= sprintf $code{sub_end}, $field; + + my $sub = eval $code; + die $@ if $@; + no strict 'refs'; + *{"${package}::$field"} = $sub; + return $code if defined wantarray; +} + +sub default_as_code { + require Data::Dumper; + local $Data::Dumper::Sortkeys = 1; + my $code = Data::Dumper::Dumper(shift); + $code =~ s/^\$VAR1 = //; + $code =~ s/;$//; + return $code; +} + +sub const { + my $package = caller; + my ($args, @values) = do { + no warnings; + local *paired_arguments = sub { (qw(-package)) }; + Spiffy->parse_arguments(@_); + }; + my ($field, $default) = @values; + $package = $args->{-package} if defined $args->{-package}; + no strict 'refs'; + return if defined &{"${package}::$field"}; + *{"${package}::$field"} = sub { $default } +} + +sub stub { + my $package = caller; + my ($args, @values) = do { + no warnings; + local *paired_arguments = sub { (qw(-package)) }; + Spiffy->parse_arguments(@_); + }; + my ($field, $default) = @values; + $package = $args->{-package} if defined $args->{-package}; + no strict 'refs'; + return if defined &{"${package}::$field"}; + *{"${package}::$field"} = + sub { + require Carp; + Carp::confess + "Method $field in package $package must be subclassed"; + } +} + +sub parse_arguments { + my $class = shift; + my ($args, @values) = ({}, ()); + my %booleans = map { ($_, 1) } $class->boolean_arguments; + my %pairs = map { ($_, 1) } $class->paired_arguments; + while (@_) { + my $elem = shift; + if (defined $elem and defined $booleans{$elem}) { + $args->{$elem} = (@_ and $_[0] =~ /^[01]$/) + ? shift + : 1; + } + elsif (defined $elem and defined $pairs{$elem} and @_) { + $args->{$elem} = shift; + } + else { + push @values, $elem; + } + } + return wantarray ? ($args, @values) : $args; +} + +sub boolean_arguments { () } +sub paired_arguments { () } + +# get a unique id for any node +sub id { + if (not ref $_[0]) { + return 'undef' if not defined $_[0]; + \$_[0] =~ /\((\w+)\)$/o or die; + return "$1-S"; + } + require overload; + overload::StrVal($_[0]) =~ /\((\w+)\)$/o or die; + return $1; +} + +#=============================================================================== +# It's super, man. +#=============================================================================== +package DB; +{ + no warnings 'redefine'; + sub super_args { + my @dummy = caller(@_ ? $_[0] : 2); + return @DB::args; + } +} + +package Spiffy; +sub super { + my $method; + my $frame = 1; + while ($method = (caller($frame++))[3]) { + $method =~ s/.*::// and last; + } + my @args = DB::super_args($frame); + @_ = @_ ? ($args[0], @_) : @args; + my $class = ref $_[0] ? ref $_[0] : $_[0]; + my $caller_class = caller; + my $seen = 0; + my @super_classes = reverse grep { + ($seen or $seen = ($_ eq $caller_class)) ? 0 : 1; + } reverse @{all_my_bases($class)}; + for my $super_class (@super_classes) { + no strict 'refs'; + next if $super_class eq $class; + if (defined &{"${super_class}::$method"}) { + ${"$super_class\::AUTOLOAD"} = ${"$class\::AUTOLOAD"} + if $method eq 'AUTOLOAD'; + return &{"${super_class}::$method"}; + } + } + return; +} + +#=============================================================================== +# This code deserves a spanking, because it is being very naughty. +# It is exchanging base.pm's import() for its own, so that people +# can use base.pm with Spiffy modules, without being the wiser. +#=============================================================================== +my $real_base_import; +my $real_mixin_import; + +BEGIN { + require base unless defined $INC{'base.pm'}; + $INC{'mixin.pm'} ||= 'Spiffy/mixin.pm'; + $real_base_import = \&base::import; + $real_mixin_import = \&mixin::import; + no warnings; + *base::import = \&spiffy_base_import; + *mixin::import = \&spiffy_mixin_import; +} + +# my $i = 0; +# while (my $caller = caller($i++)) { +# next unless $caller eq 'base' or $caller eq 'mixin'; +# croak <isa('Spiffy'); + } @base_classes; + my $inheritor = caller(0); + for my $base_class (@base_classes) { + next if $inheritor->isa($base_class); + croak "Can't mix Spiffy and non-Spiffy classes in 'use base'.\n", + "See the documentation of Spiffy.pm for details\n " + unless $base_class->isa('Spiffy'); + $stack_frame = 1; # tell import to use different caller + import($base_class, '-base'); + $stack_frame = 0; + } +} + +sub mixin { + my $self = shift; + my $target_class = ref($self); + spiffy_mixin_import($target_class, @_) +} + +sub spiffy_mixin_import { + my $target_class = shift; + $target_class = caller(0) + if $target_class eq 'mixin'; + my $mixin_class = shift + or die "Nothing to mixin"; + eval "require $mixin_class"; + my @roles = @_; + my $pseudo_class = join '-', $target_class, $mixin_class, @roles; + my %methods = spiffy_mixin_methods($mixin_class, @roles); + no strict 'refs'; + no warnings; + @{"$pseudo_class\::ISA"} = @{"$target_class\::ISA"}; + @{"$target_class\::ISA"} = ($pseudo_class); + for (keys %methods) { + *{"$pseudo_class\::$_"} = $methods{$_}; + } +} + +sub spiffy_mixin_methods { + my $mixin_class = shift; + no strict 'refs'; + my %methods = spiffy_all_methods($mixin_class); + map { + $methods{$_} + ? ($_, \ &{"$methods{$_}\::$_"}) + : ($_, \ &{"$mixin_class\::$_"}) + } @_ + ? (get_roles($mixin_class, @_)) + : (keys %methods); +} + +sub get_roles { + my $mixin_class = shift; + my @roles = @_; + while (grep /^!*:/, @roles) { + @roles = map { + s/!!//g; + /^!:(.*)/ ? do { + my $m = "_role_$1"; + map("!$_", $mixin_class->$m); + } : + /^:(.*)/ ? do { + my $m = "_role_$1"; + ($mixin_class->$m); + } : + ($_) + } @roles; + } + if (@roles and $roles[0] =~ /^!/) { + my %methods = spiffy_all_methods($mixin_class); + unshift @roles, keys(%methods); + } + my %roles; + for (@roles) { + s/!!//g; + delete $roles{$1}, next + if /^!(.*)/; + $roles{$_} = 1; + } + keys %roles; +} + +sub spiffy_all_methods { + no strict 'refs'; + my $class = shift; + return if $class eq 'Spiffy'; + my %methods = map { + ($_, $class) + } grep { + defined &{"$class\::$_"} and not /^_/ + } keys %{"$class\::"}; + my %super_methods; + %super_methods = spiffy_all_methods(${"$class\::ISA"}[0]) + if @{"$class\::ISA"}; + %{{%super_methods, %methods}}; +} + + +# END of naughty code. +#=============================================================================== +# Debugging support +#=============================================================================== +sub spiffy_dump { + no warnings; + if ($dump eq 'dumper') { + require Data::Dumper; + $Data::Dumper::Sortkeys = 1; + $Data::Dumper::Indent = 1; + return Data::Dumper::Dumper(@_); + } + require YAML; + $YAML::UseVersion = 0; + return YAML::Dump(@_) . "...\n"; +} + +sub at_line_number { + my ($file_path, $line_number) = (caller(1))[1,2]; + " at $file_path line $line_number\n"; +} + +sub WWW { + warn spiffy_dump(@_) . at_line_number; + return wantarray ? @_ : $_[0]; +} + +sub XXX { + die spiffy_dump(@_) . at_line_number; +} + +sub YYY { + print spiffy_dump(@_) . at_line_number; + return wantarray ? @_ : $_[0]; +} + +sub ZZZ { + require Carp; + Carp::confess spiffy_dump(@_); +} + +1; + +__END__ + +#line 1066 diff --git a/inc/Test/Base.pm b/inc/Test/Base.pm new file mode 100644 index 0000000..3dcf40a --- /dev/null +++ b/inc/Test/Base.pm @@ -0,0 +1,653 @@ +#line 1 +# TODO: +# +package Test::Base; +use 5.006001; +use Spiffy 0.30 -Base; +use Spiffy ':XXX'; +our $VERSION = '0.55'; + +my @test_more_exports; +BEGIN { + @test_more_exports = qw( + ok isnt like unlike is_deeply cmp_ok + skip todo_skip pass fail + eq_array eq_hash eq_set + plan can_ok isa_ok diag + use_ok + $TODO + ); +} + +use Test::More import => \@test_more_exports; +use Carp; + +our @EXPORT = (@test_more_exports, qw( + is no_diff + + blocks next_block first_block + delimiters spec_file spec_string + filters filters_delay filter_arguments + run run_compare run_is run_is_deeply run_like run_unlike + WWW XXX YYY ZZZ + tie_output no_diag_on_only + + find_my_self default_object + + croak carp cluck confess +)); + +field '_spec_file'; +field '_spec_string'; +field _filters => [qw(norm trim)]; +field _filters_map => {}; +field spec => + -init => '$self->_spec_init'; +field block_list => + -init => '$self->_block_list_init'; +field _next_list => []; +field block_delim => + -init => '$self->block_delim_default'; +field data_delim => + -init => '$self->data_delim_default'; +field _filters_delay => 0; +field _no_diag_on_only => 0; + +field block_delim_default => '==='; +field data_delim_default => '---'; + +my $default_class; +my $default_object; +my $reserved_section_names = {}; + +sub default_object { + $default_object ||= $default_class->new; + return $default_object; +} + +my $import_called = 0; +sub import() { + $import_called = 1; + my $class = (grep /^-base$/i, @_) + ? scalar(caller) + : $_[0]; + if (not defined $default_class) { + $default_class = $class; + } +# else { +# croak "Can't use $class after using $default_class" +# unless $default_class->isa($class); +# } + + unless (grep /^-base$/i, @_) { + my @args; + for (my $ii = 1; $ii <= $#_; ++$ii) { + if ($_[$ii] eq '-package') { + ++$ii; + } else { + push @args, $_[$ii]; + } + } + Test::More->import(import => \@test_more_exports, @args) + if @args; + } + + _strict_warnings(); + goto &Spiffy::import; +} + +# Wrap Test::Builder::plan +my $plan_code = \&Test::Builder::plan; +my $Have_Plan = 0; +{ + no warnings 'redefine'; + *Test::Builder::plan = sub { + $Have_Plan = 1; + goto &$plan_code; + }; +} + +my $DIED = 0; +$SIG{__DIE__} = sub { $DIED = 1; die @_ }; + +sub block_class { $self->find_class('Block') } +sub filter_class { $self->find_class('Filter') } + +sub find_class { + my $suffix = shift; + my $class = ref($self) . "::$suffix"; + return $class if $class->can('new'); + $class = __PACKAGE__ . "::$suffix"; + return $class if $class->can('new'); + eval "require $class"; + return $class if $class->can('new'); + die "Can't find a class for $suffix"; +} + +sub check_late { + if ($self->{block_list}) { + my $caller = (caller(1))[3]; + $caller =~ s/.*:://; + croak "Too late to call $caller()" + } +} + +sub find_my_self() { + my $self = ref($_[0]) eq $default_class + ? splice(@_, 0, 1) + : default_object(); + return $self, @_; +} + +sub blocks() { + (my ($self), @_) = find_my_self(@_); + + croak "Invalid arguments passed to 'blocks'" + if @_ > 1; + croak sprintf("'%s' is invalid argument to blocks()", shift(@_)) + if @_ && $_[0] !~ /^[a-zA-Z]\w*$/; + + my $blocks = $self->block_list; + + my $section_name = shift || ''; + my @blocks = $section_name + ? (grep { exists $_->{$section_name} } @$blocks) + : (@$blocks); + + return scalar(@blocks) unless wantarray; + + return (@blocks) if $self->_filters_delay; + + for my $block (@blocks) { + $block->run_filters + unless $block->is_filtered; + } + + return (@blocks); +} + +sub next_block() { + (my ($self), @_) = find_my_self(@_); + my $list = $self->_next_list; + if (@$list == 0) { + $list = [@{$self->block_list}, undef]; + $self->_next_list($list); + } + my $block = shift @$list; + if (defined $block and not $block->is_filtered) { + $block->run_filters; + } + return $block; +} + +sub first_block() { + (my ($self), @_) = find_my_self(@_); + $self->_next_list([]); + $self->next_block; +} + +sub filters_delay() { + (my ($self), @_) = find_my_self(@_); + $self->_filters_delay(defined $_[0] ? shift : 1); +} + +sub no_diag_on_only() { + (my ($self), @_) = find_my_self(@_); + $self->_no_diag_on_only(defined $_[0] ? shift : 1); +} + +sub delimiters() { + (my ($self), @_) = find_my_self(@_); + $self->check_late; + my ($block_delimiter, $data_delimiter) = @_; + $block_delimiter ||= $self->block_delim_default; + $data_delimiter ||= $self->data_delim_default; + $self->block_delim($block_delimiter); + $self->data_delim($data_delimiter); + return $self; +} + +sub spec_file() { + (my ($self), @_) = find_my_self(@_); + $self->check_late; + $self->_spec_file(shift); + return $self; +} + +sub spec_string() { + (my ($self), @_) = find_my_self(@_); + $self->check_late; + $self->_spec_string(shift); + return $self; +} + +sub filters() { + (my ($self), @_) = find_my_self(@_); + if (ref($_[0]) eq 'HASH') { + $self->_filters_map(shift); + } + else { + my $filters = $self->_filters; + push @$filters, @_; + } + return $self; +} + +sub filter_arguments() { + $Test::Base::Filter::arguments; +} + +sub have_text_diff { + eval { require Text::Diff; 1 } && + $Text::Diff::VERSION >= 0.35 && + $Algorithm::Diff::VERSION >= 1.15; +} + +sub is($$;$) { + (my ($self), @_) = find_my_self(@_); + my ($actual, $expected, $name) = @_; + local $Test::Builder::Level = $Test::Builder::Level + 1; + if ($ENV{TEST_SHOW_NO_DIFFS} or + not defined $actual or + not defined $expected or + $actual eq $expected or + not($self->have_text_diff) or + $expected !~ /\n./s + ) { + Test::More::is($actual, $expected, $name); + } + else { + $name = '' unless defined $name; + ok $actual eq $expected, + $name . "\n" . Text::Diff::diff(\$expected, \$actual); + } +} + +sub run(&;$) { + (my ($self), @_) = find_my_self(@_); + my $callback = shift; + for my $block (@{$self->block_list}) { + $block->run_filters unless $block->is_filtered; + &{$callback}($block); + } +} + +my $name_error = "Can't determine section names"; +sub _section_names { + return @_ if @_ == 2; + my $block = $self->first_block + or croak $name_error; + my @names = grep { + $_ !~ /^(ONLY|LAST|SKIP)$/; + } @{$block->{_section_order}[0] || []}; + croak "$name_error. Need two sections in first block" + unless @names == 2; + return @names; +} + +sub _assert_plan { + plan('no_plan') unless $Have_Plan; +} + +sub END { + run_compare() unless $Have_Plan or $DIED or not $import_called; +} + +sub run_compare() { + (my ($self), @_) = find_my_self(@_); + $self->_assert_plan; + my ($x, $y) = $self->_section_names(@_); + local $Test::Builder::Level = $Test::Builder::Level + 1; + for my $block (@{$self->block_list}) { + next unless exists($block->{$x}) and exists($block->{$y}); + $block->run_filters unless $block->is_filtered; + if (ref $block->$x) { + is_deeply($block->$x, $block->$y, + $block->name ? $block->name : ()); + } + elsif (ref $block->$y eq 'Regexp') { + my $regexp = ref $y ? $y : $block->$y; + like($block->$x, $regexp, $block->name ? $block->name : ()); + } + else { + is($block->$x, $block->$y, $block->name ? $block->name : ()); + } + } +} + +sub run_is() { + (my ($self), @_) = find_my_self(@_); + $self->_assert_plan; + my ($x, $y) = $self->_section_names(@_); + local $Test::Builder::Level = $Test::Builder::Level + 1; + for my $block (@{$self->block_list}) { + next unless exists($block->{$x}) and exists($block->{$y}); + $block->run_filters unless $block->is_filtered; + is($block->$x, $block->$y, + $block->name ? $block->name : () + ); + } +} + +sub run_is_deeply() { + (my ($self), @_) = find_my_self(@_); + $self->_assert_plan; + my ($x, $y) = $self->_section_names(@_); + for my $block (@{$self->block_list}) { + next unless exists($block->{$x}) and exists($block->{$y}); + $block->run_filters unless $block->is_filtered; + is_deeply($block->$x, $block->$y, + $block->name ? $block->name : () + ); + } +} + +sub run_like() { + (my ($self), @_) = find_my_self(@_); + $self->_assert_plan; + my ($x, $y) = $self->_section_names(@_); + for my $block (@{$self->block_list}) { + next unless exists($block->{$x}) and defined($y); + $block->run_filters unless $block->is_filtered; + my $regexp = ref $y ? $y : $block->$y; + like($block->$x, $regexp, + $block->name ? $block->name : () + ); + } +} + +sub run_unlike() { + (my ($self), @_) = find_my_self(@_); + $self->_assert_plan; + my ($x, $y) = $self->_section_names(@_); + for my $block (@{$self->block_list}) { + next unless exists($block->{$x}) and defined($y); + $block->run_filters unless $block->is_filtered; + my $regexp = ref $y ? $y : $block->$y; + unlike($block->$x, $regexp, + $block->name ? $block->name : () + ); + } +} + +sub _pre_eval { + my $spec = shift; + return $spec unless $spec =~ + s/\A\s*<<<(.*?)>>>\s*$//sm; + my $eval_code = $1; + eval "package main; $eval_code"; + croak $@ if $@; + return $spec; +} + +sub _block_list_init { + my $spec = $self->spec; + $spec = $self->_pre_eval($spec); + my $cd = $self->block_delim; + my @hunks = ($spec =~ /^(\Q${cd}\E.*?(?=^\Q${cd}\E|\z))/msg); + my $blocks = $self->_choose_blocks(@hunks); + $self->block_list($blocks); # Need to set early for possible filter use + my $seq = 1; + for my $block (@$blocks) { + $block->blocks_object($self); + $block->seq_num($seq++); + } + return $blocks; +} + +sub _choose_blocks { + my $blocks = []; + for my $hunk (@_) { + my $block = $self->_make_block($hunk); + if (exists $block->{ONLY}) { + diag "I found ONLY: maybe you're debugging?" + unless $self->_no_diag_on_only; + return [$block]; + } + next if exists $block->{SKIP}; + push @$blocks, $block; + if (exists $block->{LAST}) { + return $blocks; + } + } + return $blocks; +} + +sub _check_reserved { + my $id = shift; + croak "'$id' is a reserved name. Use something else.\n" + if $reserved_section_names->{$id} or + $id =~ /^_/; +} + +sub _make_block { + my $hunk = shift; + my $cd = $self->block_delim; + my $dd = $self->data_delim; + my $block = $self->block_class->new; + $hunk =~ s/\A\Q${cd}\E[ \t]*(.*)\s+// or die; + my $name = $1; + my @parts = split /^\Q${dd}\E +\(?(\w+)\)? *(.*)?\n/m, $hunk; + my $description = shift @parts; + $description ||= ''; + unless ($description =~ /\S/) { + $description = $name; + } + $description =~ s/\s*\z//; + $block->set_value(description => $description); + + my $section_map = {}; + my $section_order = []; + while (@parts) { + my ($type, $filters, $value) = splice(@parts, 0, 3); + $self->_check_reserved($type); + $value = '' unless defined $value; + $filters = '' unless defined $filters; + if ($filters =~ /:(\s|\z)/) { + croak "Extra lines not allowed in '$type' section" + if $value =~ /\S/; + ($filters, $value) = split /\s*:(?:\s+|\z)/, $filters, 2; + $value = '' unless defined $value; + $value =~ s/^\s*(.*?)\s*$/$1/; + } + $section_map->{$type} = { + filters => $filters, + }; + push @$section_order, $type; + $block->set_value($type, $value); + } + $block->set_value(name => $name); + $block->set_value(_section_map => $section_map); + $block->set_value(_section_order => $section_order); + return $block; +} + +sub _spec_init { + return $self->_spec_string + if $self->_spec_string; + local $/; + my $spec; + if (my $spec_file = $self->_spec_file) { + open FILE, $spec_file or die $!; + $spec = ; + close FILE; + } + else { + $spec = do { + package main; + no warnings 'once'; + ; + }; + } + return $spec; +} + +sub _strict_warnings() { + require Filter::Util::Call; + my $done = 0; + Filter::Util::Call::filter_add( + sub { + return 0 if $done; + my ($data, $end) = ('', ''); + while (my $status = Filter::Util::Call::filter_read()) { + return $status if $status < 0; + if (/^__(?:END|DATA)__\r?$/) { + $end = $_; + last; + } + $data .= $_; + $_ = ''; + } + $_ = "use strict;use warnings;$data$end"; + $done = 1; + } + ); +} + +sub tie_output() { + my $handle = shift; + die "No buffer to tie" unless @_; + tie $handle, 'Test::Base::Handle', $_[0]; +} + +sub no_diff { + $ENV{TEST_SHOW_NO_DIFFS} = 1; +} + +package Test::Base::Handle; + +sub TIEHANDLE() { + my $class = shift; + bless \ $_[0], $class; +} + +sub PRINT { + $$self .= $_ for @_; +} + +#=============================================================================== +# Test::Base::Block +# +# This is the default class for accessing a Test::Base block object. +#=============================================================================== +package Test::Base::Block; +our @ISA = qw(Spiffy); + +our @EXPORT = qw(block_accessor); + +sub AUTOLOAD { + return; +} + +sub block_accessor() { + my $accessor = shift; + no strict 'refs'; + return if defined &$accessor; + *$accessor = sub { + my $self = shift; + if (@_) { + Carp::croak "Not allowed to set values for '$accessor'"; + } + my @list = @{$self->{$accessor} || []}; + return wantarray + ? (@list) + : $list[0]; + }; +} + +block_accessor 'name'; +block_accessor 'description'; +Spiffy::field 'seq_num'; +Spiffy::field 'is_filtered'; +Spiffy::field 'blocks_object'; +Spiffy::field 'original_values' => {}; + +sub set_value { + no strict 'refs'; + my $accessor = shift; + block_accessor $accessor + unless defined &$accessor; + $self->{$accessor} = [@_]; +} + +sub run_filters { + my $map = $self->_section_map; + my $order = $self->_section_order; + Carp::croak "Attempt to filter a block twice" + if $self->is_filtered; + for my $type (@$order) { + my $filters = $map->{$type}{filters}; + my @value = $self->$type; + $self->original_values->{$type} = $value[0]; + for my $filter ($self->_get_filters($type, $filters)) { + $Test::Base::Filter::arguments = + $filter =~ s/=(.*)$// ? $1 : undef; + my $function = "main::$filter"; + no strict 'refs'; + if (defined &$function) { + local $_ = join '', @value; + my $old = $_; + @value = &$function(@value); + if (not(@value) or + @value == 1 and $value[0] =~ /\A(\d+|)\z/ + ) { + if ($value[0] && $_ eq $old) { + Test::Base::diag("Filters returning numbers are supposed to do munging \$_: your filter '$function' apparently doesn't."); + } + @value = ($_); + } + } + else { + my $filter_object = $self->blocks_object->filter_class->new; + die "Can't find a function or method for '$filter' filter\n" + unless $filter_object->can($filter); + $filter_object->current_block($self); + @value = $filter_object->$filter(@value); + } + # Set the value after each filter since other filters may be + # introspecting. + $self->set_value($type, @value); + } + } + $self->is_filtered(1); +} + +sub _get_filters { + my $type = shift; + my $string = shift || ''; + $string =~ s/\s*(.*?)\s*/$1/; + my @filters = (); + my $map_filters = $self->blocks_object->_filters_map->{$type} || []; + $map_filters = [ $map_filters ] unless ref $map_filters; + my @append = (); + for ( + @{$self->blocks_object->_filters}, + @$map_filters, + split(/\s+/, $string), + ) { + my $filter = $_; + last unless length $filter; + if ($filter =~ s/^-//) { + @filters = grep { $_ ne $filter } @filters; + } + elsif ($filter =~ s/^\+//) { + push @append, $filter; + } + else { + push @filters, $filter; + } + } + return @filters, @append; +} + +{ + %$reserved_section_names = map { + ($_, 1); + } keys(%Test::Base::Block::), qw( new DESTROY ); +} + +__DATA__ + +=encoding utf8 + +#line 1330 diff --git a/inc/Test/Base/Filter.pm b/inc/Test/Base/Filter.pm new file mode 100644 index 0000000..a440ed9 --- /dev/null +++ b/inc/Test/Base/Filter.pm @@ -0,0 +1,344 @@ +#line 1 +#. TODO: +#. + +#=============================================================================== +# This is the default class for handling Test::Base data filtering. +#=============================================================================== +package Test::Base::Filter; +use Spiffy -Base; +use Spiffy ':XXX'; + +field 'current_block'; + +our $arguments; +sub current_arguments { + return undef unless defined $arguments; + my $args = $arguments; + $args =~ s/(\\s)/ /g; + $args =~ s/(\\[a-z])/'"' . $1 . '"'/gee; + return $args; +} + +sub assert_scalar { + return if @_ == 1; + require Carp; + my $filter = (caller(1))[3]; + $filter =~ s/.*:://; + Carp::croak "Input to the '$filter' filter must be a scalar, not a list"; +} + +sub _apply_deepest { + my $method = shift; + return () unless @_; + if (ref $_[0] eq 'ARRAY') { + for my $aref (@_) { + @$aref = $self->_apply_deepest($method, @$aref); + } + return @_; + } + $self->$method(@_); +} + +sub _split_array { + map { + [$self->split($_)]; + } @_; +} + +sub _peel_deepest { + return () unless @_; + if (ref $_[0] eq 'ARRAY') { + if (ref $_[0]->[0] eq 'ARRAY') { + for my $aref (@_) { + @$aref = $self->_peel_deepest(@$aref); + } + return @_; + } + return map { $_->[0] } @_; + } + return @_; +} + +#=============================================================================== +# these filters work on the leaves of nested arrays +#=============================================================================== +sub Join { $self->_peel_deepest($self->_apply_deepest(join => @_)) } +sub Reverse { $self->_apply_deepest(reverse => @_) } +sub Split { $self->_apply_deepest(_split_array => @_) } +sub Sort { $self->_apply_deepest(sort => @_) } + + +sub append { + my $suffix = $self->current_arguments; + map { $_ . $suffix } @_; +} + +sub array { + return [@_]; +} + +sub base64_decode { + $self->assert_scalar(@_); + require MIME::Base64; + MIME::Base64::decode_base64(shift); +} + +sub base64_encode { + $self->assert_scalar(@_); + require MIME::Base64; + MIME::Base64::encode_base64(shift); +} + +sub chomp { + map { CORE::chomp; $_ } @_; +} + +sub chop { + map { CORE::chop; $_ } @_; +} + +sub dumper { + no warnings 'once'; + require Data::Dumper; + local $Data::Dumper::Sortkeys = 1; + local $Data::Dumper::Indent = 1; + local $Data::Dumper::Terse = 1; + Data::Dumper::Dumper(@_); +} + +sub escape { + $self->assert_scalar(@_); + my $text = shift; + $text =~ s/(\\.)/eval "qq{$1}"/ge; + return $text; +} + +sub eval { + $self->assert_scalar(@_); + my @return = CORE::eval(shift); + return $@ if $@; + return @return; +} + +sub eval_all { + $self->assert_scalar(@_); + my $out = ''; + my $err = ''; + Test::Base::tie_output(*STDOUT, $out); + Test::Base::tie_output(*STDERR, $err); + my $return = CORE::eval(shift); + no warnings; + untie *STDOUT; + untie *STDERR; + return $return, $@, $out, $err; +} + +sub eval_stderr { + $self->assert_scalar(@_); + my $output = ''; + Test::Base::tie_output(*STDERR, $output); + CORE::eval(shift); + no warnings; + untie *STDERR; + return $output; +} + +sub eval_stdout { + $self->assert_scalar(@_); + my $output = ''; + Test::Base::tie_output(*STDOUT, $output); + CORE::eval(shift); + no warnings; + untie *STDOUT; + return $output; +} + +sub exec_perl_stdout { + my $tmpfile = "/tmp/test-blocks-$$"; + $self->_write_to($tmpfile, @_); + open my $execution, "$^X $tmpfile 2>&1 |" + or die "Couldn't open subprocess: $!\n"; + local $/; + my $output = <$execution>; + close $execution; + unlink($tmpfile) + or die "Couldn't unlink $tmpfile: $!\n"; + return $output; +} + +sub flatten { + $self->assert_scalar(@_); + my $ref = shift; + if (ref($ref) eq 'HASH') { + return map { + ($_, $ref->{$_}); + } sort keys %$ref; + } + if (ref($ref) eq 'ARRAY') { + return @$ref; + } + die "Can only flatten a hash or array ref"; +} + +sub get_url { + $self->assert_scalar(@_); + my $url = shift; + CORE::chomp($url); + require LWP::Simple; + LWP::Simple::get($url); +} + +sub hash { + return +{ @_ }; +} + +sub head { + my $size = $self->current_arguments || 1; + return splice(@_, 0, $size); +} + +sub join { + my $string = $self->current_arguments; + $string = '' unless defined $string; + CORE::join $string, @_; +} + +sub lines { + $self->assert_scalar(@_); + my $text = shift; + return () unless length $text; + my @lines = ($text =~ /^(.*\n?)/gm); + return @lines; +} + +sub norm { + $self->assert_scalar(@_); + my $text = shift; + $text = '' unless defined $text; + $text =~ s/\015\012/\n/g; + $text =~ s/\r/\n/g; + return $text; +} + +sub prepend { + my $prefix = $self->current_arguments; + map { $prefix . $_ } @_; +} + +sub read_file { + $self->assert_scalar(@_); + my $file = shift; + CORE::chomp $file; + open my $fh, $file + or die "Can't open '$file' for input:\n$!"; + CORE::join '', <$fh>; +} + +sub regexp { + $self->assert_scalar(@_); + my $text = shift; + my $flags = $self->current_arguments; + if ($text =~ /\n.*?\n/s) { + $flags = 'xism' + unless defined $flags; + } + else { + CORE::chomp($text); + } + $flags ||= ''; + my $regexp = eval "qr{$text}$flags"; + die $@ if $@; + return $regexp; +} + +sub reverse { + CORE::reverse(@_); +} + +sub slice { + die "Invalid args for slice" + unless $self->current_arguments =~ /^(\d+)(?:,(\d))?$/; + my ($x, $y) = ($1, $2); + $y = $x if not defined $y; + die "Invalid args for slice" + if $x > $y; + return splice(@_, $x, 1 + $y - $x); +} + +sub sort { + CORE::sort(@_); +} + +sub split { + $self->assert_scalar(@_); + my $separator = $self->current_arguments; + if (defined $separator and $separator =~ s{^/(.*)/$}{$1}) { + my $regexp = $1; + $separator = qr{$regexp}; + } + $separator = qr/\s+/ unless $separator; + CORE::split $separator, shift; +} + +sub strict { + $self->assert_scalar(@_); + <<'...' . shift; +use strict; +use warnings; +... +} + +sub tail { + my $size = $self->current_arguments || 1; + return splice(@_, @_ - $size, $size); +} + +sub trim { + map { + s/\A([ \t]*\n)+//; + s/(?<=\n)\s*\z//g; + $_; + } @_; +} + +sub unchomp { + map { $_ . "\n" } @_; +} + +sub write_file { + my $file = $self->current_arguments + or die "No file specified for write_file filter"; + if ($file =~ /(.*)[\\\/]/) { + my $dir = $1; + if (not -e $dir) { + require File::Path; + File::Path::mkpath($dir) + or die "Can't create $dir"; + } + } + open my $fh, ">$file" + or die "Can't open '$file' for output\n:$!"; + print $fh @_; + close $fh; + return $file; +} + +sub yaml { + $self->assert_scalar(@_); + require YAML; + return YAML::Load(shift); +} + +sub _write_to { + my $filename = shift; + open my $script, ">$filename" + or die "Couldn't open $filename: $!\n"; + print $script @_; + close $script + or die "Couldn't close $filename: $!\n"; +} + +__DATA__ + +#line 639 diff --git a/inc/Test/Builder.pm b/inc/Test/Builder.pm new file mode 100644 index 0000000..af5a9dd --- /dev/null +++ b/inc/Test/Builder.pm @@ -0,0 +1,1175 @@ +#line 1 +package Test::Builder; + +use 5.004; + +# $^C was only introduced in 5.005-ish. We do this to prevent +# use of uninitialized value warnings in older perls. +$^C ||= 0; + +use strict; +use vars qw($VERSION); +$VERSION = '0.72'; +$VERSION = eval $VERSION; # make the alpha version come out as a number + +# Make Test::Builder thread-safe for ithreads. +BEGIN { + use Config; + # Load threads::shared when threads are turned on. + # 5.8.0's threads are so busted we no longer support them. + if( $] >= 5.008001 && $Config{useithreads} && $INC{'threads.pm'}) { + require threads::shared; + + # Hack around YET ANOTHER threads::shared bug. It would + # occassionally forget the contents of the variable when sharing it. + # So we first copy the data, then share, then put our copy back. + *share = sub (\[$@%]) { + my $type = ref $_[0]; + my $data; + + if( $type eq 'HASH' ) { + %$data = %{$_[0]}; + } + elsif( $type eq 'ARRAY' ) { + @$data = @{$_[0]}; + } + elsif( $type eq 'SCALAR' ) { + $$data = ${$_[0]}; + } + else { + die("Unknown type: ".$type); + } + + $_[0] = &threads::shared::share($_[0]); + + if( $type eq 'HASH' ) { + %{$_[0]} = %$data; + } + elsif( $type eq 'ARRAY' ) { + @{$_[0]} = @$data; + } + elsif( $type eq 'SCALAR' ) { + ${$_[0]} = $$data; + } + else { + die("Unknown type: ".$type); + } + + return $_[0]; + }; + } + # 5.8.0's threads::shared is busted when threads are off + # and earlier Perls just don't have that module at all. + else { + *share = sub { return $_[0] }; + *lock = sub { 0 }; + } +} + + +#line 128 + +my $Test = Test::Builder->new; +sub new { + my($class) = shift; + $Test ||= $class->create; + return $Test; +} + + +#line 150 + +sub create { + my $class = shift; + + my $self = bless {}, $class; + $self->reset; + + return $self; +} + +#line 169 + +use vars qw($Level); + +sub reset { + my ($self) = @_; + + # We leave this a global because it has to be localized and localizing + # hash keys is just asking for pain. Also, it was documented. + $Level = 1; + + $self->{Test_Died} = 0; + $self->{Have_Plan} = 0; + $self->{No_Plan} = 0; + $self->{Original_Pid} = $$; + + share($self->{Curr_Test}); + $self->{Curr_Test} = 0; + $self->{Test_Results} = &share([]); + + $self->{Exported_To} = undef; + $self->{Expected_Tests} = 0; + + $self->{Skip_All} = 0; + + $self->{Use_Nums} = 1; + + $self->{No_Header} = 0; + $self->{No_Ending} = 0; + + $self->_dup_stdhandles unless $^C; + + return undef; +} + +#line 221 + +sub exported_to { + my($self, $pack) = @_; + + if( defined $pack ) { + $self->{Exported_To} = $pack; + } + return $self->{Exported_To}; +} + +#line 243 + +sub plan { + my($self, $cmd, $arg) = @_; + + return unless $cmd; + + local $Level = $Level + 1; + + if( $self->{Have_Plan} ) { + $self->croak("You tried to plan twice"); + } + + if( $cmd eq 'no_plan' ) { + $self->no_plan; + } + elsif( $cmd eq 'skip_all' ) { + return $self->skip_all($arg); + } + elsif( $cmd eq 'tests' ) { + if( $arg ) { + local $Level = $Level + 1; + return $self->expected_tests($arg); + } + elsif( !defined $arg ) { + $self->croak("Got an undefined number of tests"); + } + elsif( !$arg ) { + $self->croak("You said to run 0 tests"); + } + } + else { + my @args = grep { defined } ($cmd, $arg); + $self->croak("plan() doesn't understand @args"); + } + + return 1; +} + +#line 290 + +sub expected_tests { + my $self = shift; + my($max) = @_; + + if( @_ ) { + $self->croak("Number of tests must be a positive integer. You gave it '$max'") + unless $max =~ /^\+?\d+$/ and $max > 0; + + $self->{Expected_Tests} = $max; + $self->{Have_Plan} = 1; + + $self->_print("1..$max\n") unless $self->no_header; + } + return $self->{Expected_Tests}; +} + + +#line 315 + +sub no_plan { + my $self = shift; + + $self->{No_Plan} = 1; + $self->{Have_Plan} = 1; +} + +#line 330 + +sub has_plan { + my $self = shift; + + return($self->{Expected_Tests}) if $self->{Expected_Tests}; + return('no_plan') if $self->{No_Plan}; + return(undef); +}; + + +#line 348 + +sub skip_all { + my($self, $reason) = @_; + + my $out = "1..0"; + $out .= " # Skip $reason" if $reason; + $out .= "\n"; + + $self->{Skip_All} = 1; + + $self->_print($out) unless $self->no_header; + exit(0); +} + +#line 382 + +sub ok { + my($self, $test, $name) = @_; + + # $test might contain an object which we don't want to accidentally + # store, so we turn it into a boolean. + $test = $test ? 1 : 0; + + $self->_plan_check; + + lock $self->{Curr_Test}; + $self->{Curr_Test}++; + + # In case $name is a string overloaded object, force it to stringify. + $self->_unoverload_str(\$name); + + $self->diag(<caller; + + my $todo = $self->todo($pack); + $self->_unoverload_str(\$todo); + + my $out; + my $result = &share({}); + + unless( $test ) { + $out .= "not "; + @$result{ 'ok', 'actual_ok' } = ( ( $todo ? 1 : 0 ), 0 ); + } + else { + @$result{ 'ok', 'actual_ok' } = ( 1, $test ); + } + + $out .= "ok"; + $out .= " $self->{Curr_Test}" if $self->use_numbers; + + if( defined $name ) { + $name =~ s|#|\\#|g; # # in a name can confuse Test::Harness. + $out .= " - $name"; + $result->{name} = $name; + } + else { + $result->{name} = ''; + } + + if( $todo ) { + $out .= " # TODO $todo"; + $result->{reason} = $todo; + $result->{type} = 'todo'; + } + else { + $result->{reason} = ''; + $result->{type} = ''; + } + + $self->{Test_Results}[$self->{Curr_Test}-1] = $result; + $out .= "\n"; + + $self->_print($out); + + unless( $test ) { + my $msg = $todo ? "Failed (TODO)" : "Failed"; + $self->_print_diag("\n") if $ENV{HARNESS_ACTIVE}; + + if( defined $name ) { + $self->diag(qq[ $msg test '$name'\n]); + $self->diag(qq[ at $file line $line.\n]); + } + else { + $self->diag(qq[ $msg test at $file line $line.\n]); + } + } + + return $test ? 1 : 0; +} + + +sub _unoverload { + my $self = shift; + my $type = shift; + + $self->_try(sub { require overload } ) || return; + + foreach my $thing (@_) { + if( $self->_is_object($$thing) ) { + if( my $string_meth = overload::Method($$thing, $type) ) { + $$thing = $$thing->$string_meth(); + } + } + } +} + + +sub _is_object { + my($self, $thing) = @_; + + return $self->_try(sub { ref $thing && $thing->isa('UNIVERSAL') }) ? 1 : 0; +} + + +sub _unoverload_str { + my $self = shift; + + $self->_unoverload(q[""], @_); +} + +sub _unoverload_num { + my $self = shift; + + $self->_unoverload('0+', @_); + + for my $val (@_) { + next unless $self->_is_dualvar($$val); + $$val = $$val+0; + } +} + + +# This is a hack to detect a dualvar such as $! +sub _is_dualvar { + my($self, $val) = @_; + + local $^W = 0; + my $numval = $val+0; + return 1 if $numval != 0 and $numval ne $val; +} + + + +#line 530 + +sub is_eq { + my($self, $got, $expect, $name) = @_; + local $Level = $Level + 1; + + $self->_unoverload_str(\$got, \$expect); + + if( !defined $got || !defined $expect ) { + # undef only matches undef and nothing else + my $test = !defined $got && !defined $expect; + + $self->ok($test, $name); + $self->_is_diag($got, 'eq', $expect) unless $test; + return $test; + } + + return $self->cmp_ok($got, 'eq', $expect, $name); +} + +sub is_num { + my($self, $got, $expect, $name) = @_; + local $Level = $Level + 1; + + $self->_unoverload_num(\$got, \$expect); + + if( !defined $got || !defined $expect ) { + # undef only matches undef and nothing else + my $test = !defined $got && !defined $expect; + + $self->ok($test, $name); + $self->_is_diag($got, '==', $expect) unless $test; + return $test; + } + + return $self->cmp_ok($got, '==', $expect, $name); +} + +sub _is_diag { + my($self, $got, $type, $expect) = @_; + + foreach my $val (\$got, \$expect) { + if( defined $$val ) { + if( $type eq 'eq' ) { + # quote and force string context + $$val = "'$$val'" + } + else { + # force numeric context + $self->_unoverload_num($val); + } + } + else { + $$val = 'undef'; + } + } + + return $self->diag(sprintf <ok($test, $name); + $self->_cmp_diag($got, 'ne', $dont_expect) unless $test; + return $test; + } + + return $self->cmp_ok($got, 'ne', $dont_expect, $name); +} + +sub isnt_num { + my($self, $got, $dont_expect, $name) = @_; + local $Level = $Level + 1; + + if( !defined $got || !defined $dont_expect ) { + # undef only matches undef and nothing else + my $test = defined $got || defined $dont_expect; + + $self->ok($test, $name); + $self->_cmp_diag($got, '!=', $dont_expect) unless $test; + return $test; + } + + return $self->cmp_ok($got, '!=', $dont_expect, $name); +} + + +#line 660 + +sub like { + my($self, $this, $regex, $name) = @_; + + local $Level = $Level + 1; + $self->_regex_ok($this, $regex, '=~', $name); +} + +sub unlike { + my($self, $this, $regex, $name) = @_; + + local $Level = $Level + 1; + $self->_regex_ok($this, $regex, '!~', $name); +} + + +#line 685 + + +my %numeric_cmps = map { ($_, 1) } + ("<", "<=", ">", ">=", "==", "!=", "<=>"); + +sub cmp_ok { + my($self, $got, $type, $expect, $name) = @_; + + # Treat overloaded objects as numbers if we're asked to do a + # numeric comparison. + my $unoverload = $numeric_cmps{$type} ? '_unoverload_num' + : '_unoverload_str'; + + $self->$unoverload(\$got, \$expect); + + + my $test; + { + local($@,$!,$SIG{__DIE__}); # isolate eval + + my $code = $self->_caller_context; + + # Yes, it has to look like this or 5.4.5 won't see the #line directive. + # Don't ask me, man, I just work here. + $test = eval " +$code" . "\$got $type \$expect;"; + + } + local $Level = $Level + 1; + my $ok = $self->ok($test, $name); + + unless( $ok ) { + if( $type =~ /^(eq|==)$/ ) { + $self->_is_diag($got, $type, $expect); + } + else { + $self->_cmp_diag($got, $type, $expect); + } + } + return $ok; +} + +sub _cmp_diag { + my($self, $got, $type, $expect) = @_; + + $got = defined $got ? "'$got'" : 'undef'; + $expect = defined $expect ? "'$expect'" : 'undef'; + return $self->diag(sprintf <caller(1); + + my $code = ''; + $code .= "#line $line $file\n" if defined $file and defined $line; + + return $code; +} + +#line 771 + +sub BAIL_OUT { + my($self, $reason) = @_; + + $self->{Bailed_Out} = 1; + $self->_print("Bail out! $reason"); + exit 255; +} + +#line 784 + +*BAILOUT = \&BAIL_OUT; + + +#line 796 + +sub skip { + my($self, $why) = @_; + $why ||= ''; + $self->_unoverload_str(\$why); + + $self->_plan_check; + + lock($self->{Curr_Test}); + $self->{Curr_Test}++; + + $self->{Test_Results}[$self->{Curr_Test}-1] = &share({ + 'ok' => 1, + actual_ok => 1, + name => '', + type => 'skip', + reason => $why, + }); + + my $out = "ok"; + $out .= " $self->{Curr_Test}" if $self->use_numbers; + $out .= " # skip"; + $out .= " $why" if length $why; + $out .= "\n"; + + $self->_print($out); + + return 1; +} + + +#line 838 + +sub todo_skip { + my($self, $why) = @_; + $why ||= ''; + + $self->_plan_check; + + lock($self->{Curr_Test}); + $self->{Curr_Test}++; + + $self->{Test_Results}[$self->{Curr_Test}-1] = &share({ + 'ok' => 1, + actual_ok => 0, + name => '', + type => 'todo_skip', + reason => $why, + }); + + my $out = "not ok"; + $out .= " $self->{Curr_Test}" if $self->use_numbers; + $out .= " # TODO & SKIP $why\n"; + + $self->_print($out); + + return 1; +} + + +#line 916 + + +sub maybe_regex { + my ($self, $regex) = @_; + my $usable_regex = undef; + + return $usable_regex unless defined $regex; + + my($re, $opts); + + # Check for qr/foo/ + if( ref $regex eq 'Regexp' ) { + $usable_regex = $regex; + } + # Check for '/foo/' or 'm,foo,' + elsif( ($re, $opts) = $regex =~ m{^ /(.*)/ (\w*) $ }sx or + (undef, $re, $opts) = $regex =~ m,^ m([^\w\s]) (.+) \1 (\w*) $,sx + ) + { + $usable_regex = length $opts ? "(?$opts)$re" : $re; + } + + return $usable_regex; +}; + +sub _regex_ok { + my($self, $this, $regex, $cmp, $name) = @_; + + my $ok = 0; + my $usable_regex = $self->maybe_regex($regex); + unless (defined $usable_regex) { + $ok = $self->ok( 0, $name ); + $self->diag(" '$regex' doesn't look much like a regex to me."); + return $ok; + } + + { + my $test; + my $code = $self->_caller_context; + + local($@, $!, $SIG{__DIE__}); # isolate eval + + # Yes, it has to look like this or 5.4.5 won't see the #line directive. + # Don't ask me, man, I just work here. + $test = eval " +$code" . q{$test = $this =~ /$usable_regex/ ? 1 : 0}; + + $test = !$test if $cmp eq '!~'; + + local $Level = $Level + 1; + $ok = $self->ok( $test, $name ); + } + + unless( $ok ) { + $this = defined $this ? "'$this'" : 'undef'; + my $match = $cmp eq '=~' ? "doesn't match" : "matches"; + $self->diag(sprintf <() }; + + return wantarray ? ($return, $@) : $return; +} + +#line 1022 + +sub is_fh { + my $self = shift; + my $maybe_fh = shift; + return 0 unless defined $maybe_fh; + + return 1 if ref $maybe_fh eq 'GLOB'; # its a glob ref + return 1 if ref \$maybe_fh eq 'GLOB'; # its a glob + + return eval { $maybe_fh->isa("IO::Handle") } || + # 5.5.4's tied() and can() doesn't like getting undef + eval { (tied($maybe_fh) || '')->can('TIEHANDLE') }; +} + + +#line 1067 + +sub level { + my($self, $level) = @_; + + if( defined $level ) { + $Level = $level; + } + return $Level; +} + + +#line 1100 + +sub use_numbers { + my($self, $use_nums) = @_; + + if( defined $use_nums ) { + $self->{Use_Nums} = $use_nums; + } + return $self->{Use_Nums}; +} + + +#line 1134 + +foreach my $attribute (qw(No_Header No_Ending No_Diag)) { + my $method = lc $attribute; + + my $code = sub { + my($self, $no) = @_; + + if( defined $no ) { + $self->{$attribute} = $no; + } + return $self->{$attribute}; + }; + + no strict 'refs'; + *{__PACKAGE__.'::'.$method} = $code; +} + + +#line 1188 + +sub diag { + my($self, @msgs) = @_; + + return if $self->no_diag; + return unless @msgs; + + # Prevent printing headers when compiling (i.e. -c) + return if $^C; + + # Smash args together like print does. + # Convert undef to 'undef' so its readable. + my $msg = join '', map { defined($_) ? $_ : 'undef' } @msgs; + + # Escape each line with a #. + $msg =~ s/^/# /gm; + + # Stick a newline on the end if it needs it. + $msg .= "\n" unless $msg =~ /\n\Z/; + + local $Level = $Level + 1; + $self->_print_diag($msg); + + return 0; +} + +#line 1225 + +sub _print { + my($self, @msgs) = @_; + + # Prevent printing headers when only compiling. Mostly for when + # tests are deparsed with B::Deparse + return if $^C; + + my $msg = join '', @msgs; + + local($\, $", $,) = (undef, ' ', ''); + my $fh = $self->output; + + # Escape each line after the first with a # so we don't + # confuse Test::Harness. + $msg =~ s/\n(.)/\n# $1/sg; + + # Stick a newline on the end if it needs it. + $msg .= "\n" unless $msg =~ /\n\Z/; + + print $fh $msg; +} + +#line 1259 + +sub _print_diag { + my $self = shift; + + local($\, $", $,) = (undef, ' ', ''); + my $fh = $self->todo ? $self->todo_output : $self->failure_output; + print $fh @_; +} + +#line 1296 + +sub output { + my($self, $fh) = @_; + + if( defined $fh ) { + $self->{Out_FH} = $self->_new_fh($fh); + } + return $self->{Out_FH}; +} + +sub failure_output { + my($self, $fh) = @_; + + if( defined $fh ) { + $self->{Fail_FH} = $self->_new_fh($fh); + } + return $self->{Fail_FH}; +} + +sub todo_output { + my($self, $fh) = @_; + + if( defined $fh ) { + $self->{Todo_FH} = $self->_new_fh($fh); + } + return $self->{Todo_FH}; +} + + +sub _new_fh { + my $self = shift; + my($file_or_fh) = shift; + + my $fh; + if( $self->is_fh($file_or_fh) ) { + $fh = $file_or_fh; + } + else { + $fh = do { local *FH }; + open $fh, ">$file_or_fh" or + $self->croak("Can't open test output log $file_or_fh: $!"); + _autoflush($fh); + } + + return $fh; +} + + +sub _autoflush { + my($fh) = shift; + my $old_fh = select $fh; + $| = 1; + select $old_fh; +} + + +sub _dup_stdhandles { + my $self = shift; + + $self->_open_testhandles; + + # Set everything to unbuffered else plain prints to STDOUT will + # come out in the wrong order from our own prints. + _autoflush(\*TESTOUT); + _autoflush(\*STDOUT); + _autoflush(\*TESTERR); + _autoflush(\*STDERR); + + $self->output(\*TESTOUT); + $self->failure_output(\*TESTERR); + $self->todo_output(\*TESTOUT); +} + + +my $Opened_Testhandles = 0; +sub _open_testhandles { + return if $Opened_Testhandles; + # We dup STDOUT and STDERR so people can change them in their + # test suites while still getting normal test output. + open(TESTOUT, ">&STDOUT") or die "Can't dup STDOUT: $!"; + open(TESTERR, ">&STDERR") or die "Can't dup STDERR: $!"; + $Opened_Testhandles = 1; +} + + +#line 1396 + +sub _message_at_caller { + my $self = shift; + + local $Level = $Level + 1; + my($pack, $file, $line) = $self->caller; + return join("", @_) . " at $file line $line.\n"; +} + +sub carp { + my $self = shift; + warn $self->_message_at_caller(@_); +} + +sub croak { + my $self = shift; + die $self->_message_at_caller(@_); +} + +sub _plan_check { + my $self = shift; + + unless( $self->{Have_Plan} ) { + local $Level = $Level + 2; + $self->croak("You tried to run a test without a plan"); + } +} + +#line 1444 + +sub current_test { + my($self, $num) = @_; + + lock($self->{Curr_Test}); + if( defined $num ) { + unless( $self->{Have_Plan} ) { + $self->croak("Can't change the current test number without a plan!"); + } + + $self->{Curr_Test} = $num; + + # If the test counter is being pushed forward fill in the details. + my $test_results = $self->{Test_Results}; + if( $num > @$test_results ) { + my $start = @$test_results ? @$test_results : 0; + for ($start..$num-1) { + $test_results->[$_] = &share({ + 'ok' => 1, + actual_ok => undef, + reason => 'incrementing test number', + type => 'unknown', + name => undef + }); + } + } + # If backward, wipe history. Its their funeral. + elsif( $num < @$test_results ) { + $#{$test_results} = $num - 1; + } + } + return $self->{Curr_Test}; +} + + +#line 1489 + +sub summary { + my($self) = shift; + + return map { $_->{'ok'} } @{ $self->{Test_Results} }; +} + +#line 1544 + +sub details { + my $self = shift; + return @{ $self->{Test_Results} }; +} + +#line 1569 + +sub todo { + my($self, $pack) = @_; + + $pack = $pack || $self->exported_to || $self->caller($Level); + return 0 unless $pack; + + no strict 'refs'; + return defined ${$pack.'::TODO'} ? ${$pack.'::TODO'} + : 0; +} + +#line 1590 + +sub caller { + my($self, $height) = @_; + $height ||= 0; + + my @caller = CORE::caller($self->level + $height + 1); + return wantarray ? @caller : $caller[0]; +} + +#line 1602 + +#line 1616 + +#'# +sub _sanity_check { + my $self = shift; + + $self->_whoa($self->{Curr_Test} < 0, 'Says here you ran a negative number of tests!'); + $self->_whoa(!$self->{Have_Plan} and $self->{Curr_Test}, + 'Somehow your tests ran without a plan!'); + $self->_whoa($self->{Curr_Test} != @{ $self->{Test_Results} }, + 'Somehow you got a different number of results than tests ran!'); +} + +#line 1637 + +sub _whoa { + my($self, $check, $desc) = @_; + if( $check ) { + local $Level = $Level + 1; + $self->croak(<<"WHOA"); +WHOA! $desc +This should never happen! Please contact the author immediately! +WHOA + } +} + +#line 1659 + +sub _my_exit { + $? = $_[0]; + + return 1; +} + + +#line 1672 + +$SIG{__DIE__} = sub { + # We don't want to muck with death in an eval, but $^S isn't + # totally reliable. 5.005_03 and 5.6.1 both do the wrong thing + # with it. Instead, we use caller. This also means it runs under + # 5.004! + my $in_eval = 0; + for( my $stack = 1; my $sub = (CORE::caller($stack))[3]; $stack++ ) { + $in_eval = 1 if $sub =~ /^\(eval\)/; + } + $Test->{Test_Died} = 1 unless $in_eval; +}; + +sub _ending { + my $self = shift; + + $self->_sanity_check(); + + # Don't bother with an ending if this is a forked copy. Only the parent + # should do the ending. + # Exit if plan() was never called. This is so "require Test::Simple" + # doesn't puke. + # Don't do an ending if we bailed out. + if( ($self->{Original_Pid} != $$) or + (!$self->{Have_Plan} && !$self->{Test_Died}) or + $self->{Bailed_Out} + ) + { + _my_exit($?); + return; + } + + # Figure out if we passed or failed and print helpful messages. + my $test_results = $self->{Test_Results}; + if( @$test_results ) { + # The plan? We have no plan. + if( $self->{No_Plan} ) { + $self->_print("1..$self->{Curr_Test}\n") unless $self->no_header; + $self->{Expected_Tests} = $self->{Curr_Test}; + } + + # Auto-extended arrays and elements which aren't explicitly + # filled in with a shared reference will puke under 5.8.0 + # ithreads. So we have to fill them in by hand. :( + my $empty_result = &share({}); + for my $idx ( 0..$self->{Expected_Tests}-1 ) { + $test_results->[$idx] = $empty_result + unless defined $test_results->[$idx]; + } + + my $num_failed = grep !$_->{'ok'}, + @{$test_results}[0..$self->{Curr_Test}-1]; + + my $num_extra = $self->{Curr_Test} - $self->{Expected_Tests}; + + if( $num_extra < 0 ) { + my $s = $self->{Expected_Tests} == 1 ? '' : 's'; + $self->diag(<<"FAIL"); +Looks like you planned $self->{Expected_Tests} test$s but only ran $self->{Curr_Test}. +FAIL + } + elsif( $num_extra > 0 ) { + my $s = $self->{Expected_Tests} == 1 ? '' : 's'; + $self->diag(<<"FAIL"); +Looks like you planned $self->{Expected_Tests} test$s but ran $num_extra extra. +FAIL + } + + if ( $num_failed ) { + my $num_tests = $self->{Curr_Test}; + my $s = $num_failed == 1 ? '' : 's'; + + my $qualifier = $num_extra == 0 ? '' : ' run'; + + $self->diag(<<"FAIL"); +Looks like you failed $num_failed test$s of $num_tests$qualifier. +FAIL + } + + if( $self->{Test_Died} ) { + $self->diag(<<"FAIL"); +Looks like your test died just after $self->{Curr_Test}. +FAIL + + _my_exit( 255 ) && return; + } + + my $exit_code; + if( $num_failed ) { + $exit_code = $num_failed <= 254 ? $num_failed : 254; + } + elsif( $num_extra != 0 ) { + $exit_code = 255; + } + else { + $exit_code = 0; + } + + _my_exit( $exit_code ) && return; + } + elsif ( $self->{Skip_All} ) { + _my_exit( 0 ) && return; + } + elsif ( $self->{Test_Died} ) { + $self->diag(<<'FAIL'); +Looks like your test died before it could output anything. +FAIL + _my_exit( 255 ) && return; + } + else { + $self->diag("No tests run!\n"); + _my_exit( 255 ) && return; + } +} + +END { + $Test->_ending if defined $Test and !$Test->no_ending; +} + +#line 1847 + +1; diff --git a/inc/Test/Builder/Module.pm b/inc/Test/Builder/Module.pm new file mode 100644 index 0000000..8a394f6 --- /dev/null +++ b/inc/Test/Builder/Module.pm @@ -0,0 +1,82 @@ +#line 1 +package Test::Builder::Module; + +use Test::Builder; + +require Exporter; +@ISA = qw(Exporter); + +$VERSION = '0.72'; + +use strict; + +# 5.004's Exporter doesn't have export_to_level. +my $_export_to_level = sub { + my $pkg = shift; + my $level = shift; + (undef) = shift; # redundant arg + my $callpkg = caller($level); + $pkg->export($callpkg, @_); +}; + + +#line 82 + +sub import { + my($class) = shift; + + my $test = $class->builder; + + my $caller = caller; + + $test->exported_to($caller); + + $class->import_extra(\@_); + my(@imports) = $class->_strip_imports(\@_); + + $test->plan(@_); + + $class->$_export_to_level(1, $class, @imports); +} + + +sub _strip_imports { + my $class = shift; + my $list = shift; + + my @imports = (); + my @other = (); + my $idx = 0; + while( $idx <= $#{$list} ) { + my $item = $list->[$idx]; + + if( defined $item and $item eq 'import' ) { + push @imports, @{$list->[$idx+1]}; + $idx++; + } + else { + push @other, $item; + } + + $idx++; + } + + @$list = @other; + + return @imports; +} + + +#line 144 + +sub import_extra {} + + +#line 175 + +sub builder { + return Test::Builder->new; +} + + +1; diff --git a/inc/Test/More.pm b/inc/Test/More.pm new file mode 100644 index 0000000..2ccdf19 --- /dev/null +++ b/inc/Test/More.pm @@ -0,0 +1,672 @@ +#line 1 +package Test::More; + +use 5.004; + +use strict; + + +# Can't use Carp because it might cause use_ok() to accidentally succeed +# even though the module being used forgot to use Carp. Yes, this +# actually happened. +sub _carp { + my($file, $line) = (caller(1))[1,2]; + warn @_, " at $file line $line\n"; +} + + + +use vars qw($VERSION @ISA @EXPORT %EXPORT_TAGS $TODO); +$VERSION = '0.72'; +$VERSION = eval $VERSION; # make the alpha version come out as a number + +use Test::Builder::Module; +@ISA = qw(Test::Builder::Module); +@EXPORT = qw(ok use_ok require_ok + is isnt like unlike is_deeply + cmp_ok + skip todo todo_skip + pass fail + eq_array eq_hash eq_set + $TODO + plan + can_ok isa_ok + diag + BAIL_OUT + ); + + +#line 157 + +sub plan { + my $tb = Test::More->builder; + + $tb->plan(@_); +} + + +# This implements "use Test::More 'no_diag'" but the behavior is +# deprecated. +sub import_extra { + my $class = shift; + my $list = shift; + + my @other = (); + my $idx = 0; + while( $idx <= $#{$list} ) { + my $item = $list->[$idx]; + + if( defined $item and $item eq 'no_diag' ) { + $class->builder->no_diag(1); + } + else { + push @other, $item; + } + + $idx++; + } + + @$list = @other; +} + + +#line 257 + +sub ok ($;$) { + my($test, $name) = @_; + my $tb = Test::More->builder; + + $tb->ok($test, $name); +} + +#line 324 + +sub is ($$;$) { + my $tb = Test::More->builder; + + $tb->is_eq(@_); +} + +sub isnt ($$;$) { + my $tb = Test::More->builder; + + $tb->isnt_eq(@_); +} + +*isn't = \&isnt; + + +#line 369 + +sub like ($$;$) { + my $tb = Test::More->builder; + + $tb->like(@_); +} + + +#line 385 + +sub unlike ($$;$) { + my $tb = Test::More->builder; + + $tb->unlike(@_); +} + + +#line 425 + +sub cmp_ok($$$;$) { + my $tb = Test::More->builder; + + $tb->cmp_ok(@_); +} + + +#line 461 + +sub can_ok ($@) { + my($proto, @methods) = @_; + my $class = ref $proto || $proto; + my $tb = Test::More->builder; + + unless( $class ) { + my $ok = $tb->ok( 0, "->can(...)" ); + $tb->diag(' can_ok() called with empty class or reference'); + return $ok; + } + + unless( @methods ) { + my $ok = $tb->ok( 0, "$class->can(...)" ); + $tb->diag(' can_ok() called with no methods'); + return $ok; + } + + my @nok = (); + foreach my $method (@methods) { + $tb->_try(sub { $proto->can($method) }) or push @nok, $method; + } + + my $name; + $name = @methods == 1 ? "$class->can('$methods[0]')" + : "$class->can(...)"; + + my $ok = $tb->ok( !@nok, $name ); + + $tb->diag(map " $class->can('$_') failed\n", @nok); + + return $ok; +} + +#line 523 + +sub isa_ok ($$;$) { + my($object, $class, $obj_name) = @_; + my $tb = Test::More->builder; + + my $diag; + $obj_name = 'The object' unless defined $obj_name; + my $name = "$obj_name isa $class"; + if( !defined $object ) { + $diag = "$obj_name isn't defined"; + } + elsif( !ref $object ) { + $diag = "$obj_name isn't a reference"; + } + else { + # We can't use UNIVERSAL::isa because we want to honor isa() overrides + my($rslt, $error) = $tb->_try(sub { $object->isa($class) }); + if( $error ) { + if( $error =~ /^Can't call method "isa" on unblessed reference/ ) { + # Its an unblessed reference + if( !UNIVERSAL::isa($object, $class) ) { + my $ref = ref $object; + $diag = "$obj_name isn't a '$class' it's a '$ref'"; + } + } else { + die <isa on your object and got some weird error. +Here's the error. +$error +WHOA + } + } + elsif( !$rslt ) { + my $ref = ref $object; + $diag = "$obj_name isn't a '$class' it's a '$ref'"; + } + } + + + + my $ok; + if( $diag ) { + $ok = $tb->ok( 0, $name ); + $tb->diag(" $diag\n"); + } + else { + $ok = $tb->ok( 1, $name ); + } + + return $ok; +} + + +#line 592 + +sub pass (;$) { + my $tb = Test::More->builder; + $tb->ok(1, @_); +} + +sub fail (;$) { + my $tb = Test::More->builder; + $tb->ok(0, @_); +} + +#line 653 + +sub use_ok ($;@) { + my($module, @imports) = @_; + @imports = () unless @imports; + my $tb = Test::More->builder; + + my($pack,$filename,$line) = caller; + + local($@,$!,$SIG{__DIE__}); # isolate eval + + if( @imports == 1 and $imports[0] =~ /^\d+(?:\.\d+)?$/ ) { + # probably a version check. Perl needs to see the bare number + # for it to work with non-Exporter based modules. + eval <ok( !$@, "use $module;" ); + + unless( $ok ) { + chomp $@; + $@ =~ s{^BEGIN failed--compilation aborted at .*$} + {BEGIN failed--compilation aborted at $filename line $line.}m; + $tb->diag(<builder; + + my $pack = caller; + + # Try to deterine if we've been given a module name or file. + # Module names must be barewords, files not. + $module = qq['$module'] unless _is_module_name($module); + + local($!, $@, $SIG{__DIE__}); # isolate eval + local $SIG{__DIE__}; + eval <ok( !$@, "require $module;" ); + + unless( $ok ) { + chomp $@; + $tb->diag(<builder; + + unless( @_ == 2 or @_ == 3 ) { + my $msg = <ok(0); + } + + my($got, $expected, $name) = @_; + + $tb->_unoverload_str(\$expected, \$got); + + my $ok; + if( !ref $got and !ref $expected ) { # neither is a reference + $ok = $tb->is_eq($got, $expected, $name); + } + elsif( !ref $got xor !ref $expected ) { # one's a reference, one isn't + $ok = $tb->ok(0, $name); + $tb->diag( _format_stack({ vals => [ $got, $expected ] }) ); + } + else { # both references + local @Data_Stack = (); + if( _deep_check($got, $expected) ) { + $ok = $tb->ok(1, $name); + } + else { + $ok = $tb->ok(0, $name); + $tb->diag(_format_stack(@Data_Stack)); + } + } + + return $ok; +} + +sub _format_stack { + my(@Stack) = @_; + + my $var = '$FOO'; + my $did_arrow = 0; + foreach my $entry (@Stack) { + my $type = $entry->{type} || ''; + my $idx = $entry->{'idx'}; + if( $type eq 'HASH' ) { + $var .= "->" unless $did_arrow++; + $var .= "{$idx}"; + } + elsif( $type eq 'ARRAY' ) { + $var .= "->" unless $did_arrow++; + $var .= "[$idx]"; + } + elsif( $type eq 'REF' ) { + $var = "\${$var}"; + } + } + + my @vals = @{$Stack[-1]{vals}}[0,1]; + my @vars = (); + ($vars[0] = $var) =~ s/\$FOO/ \$got/; + ($vars[1] = $var) =~ s/\$FOO/\$expected/; + + my $out = "Structures begin differing at:\n"; + foreach my $idx (0..$#vals) { + my $val = $vals[$idx]; + $vals[$idx] = !defined $val ? 'undef' : + _dne($val) ? "Does not exist" : + ref $val ? "$val" : + "'$val'"; + } + + $out .= "$vars[0] = $vals[0]\n"; + $out .= "$vars[1] = $vals[1]\n"; + + $out =~ s/^/ /msg; + return $out; +} + + +sub _type { + my $thing = shift; + + return '' if !ref $thing; + + for my $type (qw(ARRAY HASH REF SCALAR GLOB CODE Regexp)) { + return $type if UNIVERSAL::isa($thing, $type); + } + + return ''; +} + +#line 925 + +sub diag { + my $tb = Test::More->builder; + + $tb->diag(@_); +} + + +#line 994 + +#'# +sub skip { + my($why, $how_many) = @_; + my $tb = Test::More->builder; + + unless( defined $how_many ) { + # $how_many can only be avoided when no_plan is in use. + _carp "skip() needs to know \$how_many tests are in the block" + unless $tb->has_plan eq 'no_plan'; + $how_many = 1; + } + + if( defined $how_many and $how_many =~ /\D/ ) { + _carp "skip() was passed a non-numeric number of tests. Did you get the arguments backwards?"; + $how_many = 1; + } + + for( 1..$how_many ) { + $tb->skip($why); + } + + local $^W = 0; + last SKIP; +} + + +#line 1081 + +sub todo_skip { + my($why, $how_many) = @_; + my $tb = Test::More->builder; + + unless( defined $how_many ) { + # $how_many can only be avoided when no_plan is in use. + _carp "todo_skip() needs to know \$how_many tests are in the block" + unless $tb->has_plan eq 'no_plan'; + $how_many = 1; + } + + for( 1..$how_many ) { + $tb->todo_skip($why); + } + + local $^W = 0; + last TODO; +} + +#line 1134 + +sub BAIL_OUT { + my $reason = shift; + my $tb = Test::More->builder; + + $tb->BAIL_OUT($reason); +} + +#line 1173 + +#'# +sub eq_array { + local @Data_Stack; + _deep_check(@_); +} + +sub _eq_array { + my($a1, $a2) = @_; + + if( grep !_type($_) eq 'ARRAY', $a1, $a2 ) { + warn "eq_array passed a non-array ref"; + return 0; + } + + return 1 if $a1 eq $a2; + + my $ok = 1; + my $max = $#$a1 > $#$a2 ? $#$a1 : $#$a2; + for (0..$max) { + my $e1 = $_ > $#$a1 ? $DNE : $a1->[$_]; + my $e2 = $_ > $#$a2 ? $DNE : $a2->[$_]; + + push @Data_Stack, { type => 'ARRAY', idx => $_, vals => [$e1, $e2] }; + $ok = _deep_check($e1,$e2); + pop @Data_Stack if $ok; + + last unless $ok; + } + + return $ok; +} + +sub _deep_check { + my($e1, $e2) = @_; + my $tb = Test::More->builder; + + my $ok = 0; + + # Effectively turn %Refs_Seen into a stack. This avoids picking up + # the same referenced used twice (such as [\$a, \$a]) to be considered + # circular. + local %Refs_Seen = %Refs_Seen; + + { + # Quiet uninitialized value warnings when comparing undefs. + local $^W = 0; + + $tb->_unoverload_str(\$e1, \$e2); + + # Either they're both references or both not. + my $same_ref = !(!ref $e1 xor !ref $e2); + my $not_ref = (!ref $e1 and !ref $e2); + + if( defined $e1 xor defined $e2 ) { + $ok = 0; + } + elsif ( _dne($e1) xor _dne($e2) ) { + $ok = 0; + } + elsif ( $same_ref and ($e1 eq $e2) ) { + $ok = 1; + } + elsif ( $not_ref ) { + push @Data_Stack, { type => '', vals => [$e1, $e2] }; + $ok = 0; + } + else { + if( $Refs_Seen{$e1} ) { + return $Refs_Seen{$e1} eq $e2; + } + else { + $Refs_Seen{$e1} = "$e2"; + } + + my $type = _type($e1); + $type = 'DIFFERENT' unless _type($e2) eq $type; + + if( $type eq 'DIFFERENT' ) { + push @Data_Stack, { type => $type, vals => [$e1, $e2] }; + $ok = 0; + } + elsif( $type eq 'ARRAY' ) { + $ok = _eq_array($e1, $e2); + } + elsif( $type eq 'HASH' ) { + $ok = _eq_hash($e1, $e2); + } + elsif( $type eq 'REF' ) { + push @Data_Stack, { type => $type, vals => [$e1, $e2] }; + $ok = _deep_check($$e1, $$e2); + pop @Data_Stack if $ok; + } + elsif( $type eq 'SCALAR' ) { + push @Data_Stack, { type => 'REF', vals => [$e1, $e2] }; + $ok = _deep_check($$e1, $$e2); + pop @Data_Stack if $ok; + } + elsif( $type ) { + push @Data_Stack, { type => $type, vals => [$e1, $e2] }; + $ok = 0; + } + else { + _whoa(1, "No type in _deep_check"); + } + } + } + + return $ok; +} + + +sub _whoa { + my($check, $desc) = @_; + if( $check ) { + die < keys %$a2 ? $a1 : $a2; + foreach my $k (keys %$bigger) { + my $e1 = exists $a1->{$k} ? $a1->{$k} : $DNE; + my $e2 = exists $a2->{$k} ? $a2->{$k} : $DNE; + + push @Data_Stack, { type => 'HASH', idx => $k, vals => [$e1, $e2] }; + $ok = _deep_check($e1, $e2); + pop @Data_Stack if $ok; + + last unless $ok; + } + + return $ok; +} + +#line 1361 + +sub eq_set { + my($a1, $a2) = @_; + return 0 unless @$a1 == @$a2; + + # There's faster ways to do this, but this is easiest. + local $^W = 0; + + # It really doesn't matter how we sort them, as long as both arrays are + # sorted with the same algorithm. + # + # Ensure that references are not accidentally treated the same as a + # string containing the reference. + # + # Have to inline the sort routine due to a threading/sort bug. + # See [rt.cpan.org 6782] + # + # I don't know how references would be sorted so we just don't sort + # them. This means eq_set doesn't really work with refs. + return eq_array( + [grep(ref, @$a1), sort( grep(!ref, @$a1) )], + [grep(ref, @$a2), sort( grep(!ref, @$a2) )], + ); +} + +#line 1551 + +1; diff --git a/lib/CGI/Cookie/XS.pm b/lib/CGI/Cookie/XS.pm new file mode 100644 index 0000000..da956b5 --- /dev/null +++ b/lib/CGI/Cookie/XS.pm @@ -0,0 +1,151 @@ +package CGI::Cookie::XS; + +use strict; +use warnings; + +our $VERSION; + +use XSLoader; +BEGIN { + $VERSION = '0.18'; + XSLoader::load(__PACKAGE__, $VERSION); +} + +sub fetch { + my $class = shift; + my $raw_cookie = $ENV{HTTP_COOKIE} || $ENV{COOKIE} or return; + $class->parse($raw_cookie); +} + +sub parse { + _parse_cookie($_[1]); +} + +1; +__END__ + +=head1 NAME + +CGI::Cookie::XS - HTTP Cookie parser in pure C + +=head1 VERSION + +This document describes CGI::Cookie::XS 0.18 released on September 2, 2009. + +=head1 SYNOPSIS + + use CGI::Cookie::XS; + + my $raw_cookie = 'foo=a%20phrase;weird; bar=yes%2C%20a%20phrase; baz=%5Ewibble&leiyh; qux=%27'; + my $res = CGI::Cookie::XS->parse($raw_cookie); + # $res is something like: + # { + # 'bar' => [ + # 'yes, a phrase' + # ], + # 'baz' => [ + # '^wibble', + # 'leiyh' + # ], + # 'foo' => [ + # 'a phrase' + # ], + # 'qux' => [ + # '\'' + # ] + # }; + + # or directly read raw cookies from the CGI environments: + $res = CGI::Cookie::XS->fetch; + +=head1 DESCRIPTION + +This module implements a very simple parser for cookies used in HTTP applications. We've found L and L rather slow according to the profiling results for our L project, hence the rewrite in C. + +This library is still in B stage and the API is still in flux. We're just following the "release early, releaes often" guideline. So please check back often ;) + +Special effort has been made to ensure this module works in the same way as the latest L (i.e., the pure Perl implementation). If you find it doesn't, please let us know. + +=head1 METHODS + +We currently provide 2 static methods, C and C. They work mostly the same way as those methods found in L and L but with the exception that our version returns plain Perl data structures rather than hashes of Perl objects (due to performance considerations). + +We'll implement some cookie dump methods in the near future. + +=over + +=item C<< $ref = CGI::Cookie::XS->parse($raw_cookie) >> + +Parses C<$raw_cookie> and returns the reference of a hash of arrays. The keys +of the hash are cookie variables' names while the values of the hash are lists of cookie variable's values. + +There is a length limit on the C<$raw_cookie>. If C<$raw_cookie> is longer than 4 KB (i.e. 4 * 1024 bytes, excluding the trailing '\0'), the overflowing part will be truncated. + +Also note that, C does not assume any encoding on the cookie values. It just decodes the encoded entries verbatim and treat them as plain "binary" stuff. + +=item C<< $ref = CGI::Cookie::XS->fetch() >> + +Reads the raw cookie from the C and C environments +(which are usually set by HTTP servers like lighttd or apache) and then +parses the value using the C method and finally returns the +results. + +=back + +=head1 TODO + +=over + +=item * + +Removing trailing spaces in cookie values. + +=back + +=head1 SOURCE CONTROL + +For the very latest version of this module, check out the source from +the Git repos below: + +L + +There is anonymous access to all. If you'd like a commit bit, please let +us know. :) + +=head1 BUGS + +There must be some serious bugs lurking somewhere. We haven't done comprehensive testing for our code yet. It's a TODO. + +Please report bugs or send wish-list to +L. + +=head1 SEE ALSO + +L, L. + +=head1 AUTHOR + +=over + +=item yuting C<< >> + +=item agentzh C<< >> + +=back + +=head1 COPYRIGHT + +Copyright (c) 2008 by Yahoo! China EEEE Works, Alibaba Inc. + +Copyright (c) 2009 by Taobao Inc., Alibaba Group. + +=head1 License + +The "MIT" License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/t/01-sanity.t b/t/01-sanity.t new file mode 100644 index 0000000..a9ae0fd --- /dev/null +++ b/t/01-sanity.t @@ -0,0 +1,171 @@ +#use CGI::Cookie::XS; + +use t::TestCookie; + +plan tests => 1 * blocks(); + +#test 'CGI::Cookie'; +no_diff; + +run_tests; + +__DATA__ + +=== TEST 1: complex cookie +--- cookie +foo=a%20phrase;haha; bar=yes%2C%20a%20phrase; baz=%5Ewibble&leiyh; qux=%27 +--- out +$VAR1 = { + 'bar' => [ + 'yes, a phrase' + ], + 'baz' => [ + '^wibble', + 'leiyh' + ], + 'foo' => [ + 'a phrase' + ], + 'qux' => [ + '\'' + ] + }; + + + +=== TEST 2: foo= +--- cookie +foo= +--- out +$VAR1 = { + 'foo' => [] + }; + + + +=== TEST 3: foo +--- cookie +foo +--- out +$VAR1 = {}; + + + +=== TEST 4: foo bar +--- cookie +foo bar +--- out +$VAR1 = {}; + + + +=== TEST 5: & +--- cookie +& +--- out +$VAR1 = {}; + + + +=== TEST 6: ; +--- cookie +; +--- out +$VAR1 = {}; + + + +=== TEST 7: , +--- cookie +, +--- out +$VAR1 = {}; + + + +=== TEST 8: && +--- cookie +&&; +--- out +$VAR1 = {}; + + + +=== TEST 9: trailing spaces and leading spaces should be trimmed +--- cookie + foo=a%3A; +--- out +$VAR1 = { + 'foo' => [ + 'a:' + ] + }; + + + +=== TEST 10: trailing spaces which should be reserved. +--- cookie +foo=a%3A +--- out +$VAR1 = { + 'foo' => [ + 'a: ' + ] + }; + + + +=== TEST 11: , sperated values +--- cookie +foo=bar,foo2=bar2, foo3=bar3;foo4 =a&b&c; foo5=a;b +--- out +$VAR1 = { + 'foo' => [ + 'bar' + ], + 'foo2' => [ + 'bar2' + ], + 'foo3' => [ + 'bar3' + ], + 'foo4 ' => [ + 'a', + 'b', + 'c' + ], + 'foo5' => [ + 'a' + ] + }; + + + +=== TEST 12: leading and trailing spaces +--- cookie + foo = bar ; foo2 = bar2 +--- out +$VAR1 = { + 'foo ' => [ + ' bar ' + ], + 'foo2 ' => [ + ' bar2 ' + ] + }; + + + +=== TEST 13: encoded leading and trailing spaces +--- cookie +%20foo = bar ;%20foo2 = bar2 +--- out +$VAR1 = { + ' foo ' => [ + ' bar ' + ], + ' foo2 ' => [ + ' bar2 ' + ] + }; + diff --git a/t/02-overflow.t b/t/02-overflow.t new file mode 100644 index 0000000..4d62d0a --- /dev/null +++ b/t/02-overflow.t @@ -0,0 +1,28 @@ +use strict; +use warnings; + +#use Smart::Comments; +use Test::More tests => 7; +BEGIN { use_ok('CGI::Cookie::XS'); } + +my $COOKIE_LEN_LIMIT = 1024 * 4; + +{ + my $val_len = $COOKIE_LEN_LIMIT - 3; + my $cookie = 'a=' . ('a' x $val_len); + my $res = CGI::Cookie::XS->parse($cookie); + ok $res, 'res okay'; + ok $res->{a}, 'var a parsed'; + is $res->{a}->[0], 'a' x $val_len, "value okay for var a"; +} + +{ + my $val_len = $COOKIE_LEN_LIMIT - 3; + my $cookie = 'a=' . ('a' x $COOKIE_LEN_LIMIT); + my $res = CGI::Cookie::XS->parse($cookie); + ok $res, 'res okay'; + ok $res->{a}, 'var a parsed'; + ### Len: length($res->{a}->[0]) + is $res->{a}->[0], 'a' x $val_len, "value okay for var a"; +} + diff --git a/t/03-bug.t b/t/03-bug.t new file mode 100644 index 0000000..fb72f30 --- /dev/null +++ b/t/03-bug.t @@ -0,0 +1,65 @@ +use t::TestCookie; + +plan tests => 1 * blocks(); + +#test 'CGI::Cookie'; +run_tests; + +__DATA__ + +=== TEST 1: successive = +# http://rt.cpan.org/Public/Bug/Display.html?id=34238 +--- cookie +foo=ba=r +--- out +$VAR1 = { + 'foo' => [ + 'ba=r' + ] + }; + + + +=== TEST 2: empty cookie +# http://rt.cpan.org/Public/Bug/Display.html?id=39120 +--- cookie +--- out +$VAR1 = {}; + + + +=== TEST 3: invalid cookie (1) +# http://rt.cpan.org/Public/Bug/Display.html?id=39120 +--- cookie +a +--- out +$VAR1 = {}; + + + +=== TEST 4: invalid cookie (2) +# http://rt.cpan.org/Public/Bug/Display.html?id=39120 +--- cookie +this-is-not-a-cookie +--- out +$VAR1 = {}; + + + +=== TEST 5: empty values +rt.cpan.org #49302 +--- cookie: lastvisit=1251731074; sessionlogin=1251760758; username=; password=; remember_login=; admin_button= +--- out +$VAR1 = { + 'admin_button' => [], + 'lastvisit' => [ + '1251731074' + ], + 'password' => [], + 'remember_login' => [], + 'sessionlogin' => [ + '1251760758' + ], + 'username' => [] + }; + diff --git a/t/99-pod-coverage.t b/t/99-pod-coverage.t new file mode 100644 index 0000000..e82204d --- /dev/null +++ b/t/99-pod-coverage.t @@ -0,0 +1,9 @@ +use Test::More; + +# XXX we need more POD... +#my $skip_all = 0; +eval "use Test::Pod::Coverage"; +#plan skip_all => "We know we don't have enough POD :(" if $skip_all; +plan skip_all => "Test::Pod::Coverage required for testing POD coverage" if $@; +all_pod_coverage_ok(); + diff --git a/t/99-pod.t b/t/99-pod.t new file mode 100644 index 0000000..92ba3f6 --- /dev/null +++ b/t/99-pod.t @@ -0,0 +1,5 @@ +use Test::More; +eval "use Test::Pod 1.00"; +plan skip_all => "Test::Pod 1.00 required for testing POD" if $@; +all_pod_files_ok(); + diff --git a/t/TestCookie.pm b/t/TestCookie.pm new file mode 100644 index 0000000..8317b64 --- /dev/null +++ b/t/TestCookie.pm @@ -0,0 +1,35 @@ +use Test::Base -Base; + +#use Smart::Comments; +use Data::Dumper; + +$Data::Dumper::Sortkeys = 1; + +my $package = 'CGI::Cookie::XS'; + +sub test ($) { + $package = shift; +} + +sub run_tests () { + eval "use $package;"; + if ($@) { die $@ } + for my $block (blocks()) { + my $name = $block->name; + my $cookie = $block->cookie; + die "$name - No --- cookie specified" if !defined $cookie; + chomp $cookie; + ### $cookie + my $res = $package->parse($cookie); + if ($package eq 'CGI::Cookie') { + for my $key (keys %$res) { + $res->{$key} = $res->{$key}->{value}; + } + } + my $out = $block->out; + die "$name - No --- out specified" if !defined $out; + is Dumper($res), $out, "$name - out okay"; + } +} + +1; diff --git a/util.c b/util.c new file mode 100644 index 0000000..cee882e --- /dev/null +++ b/util.c @@ -0,0 +1,38 @@ +#include "EXTERN.h" +#include "perl.h" +#include "XSUB.h" +#include "INLINE.h" + +char** XS_unpack_charPtrPtr(SV* arg) { + AV* avref; + char** array; + STRLEN len; + SV** elem; + int i; + + if(!SvROK(arg)) + croak("XS_unpack_charPtrPtr: arg is not a reference"); + if( SvTYPE(SvRV(arg)) != SVt_PVAV) + croak("XS_unpack_charPtrPtr: arg is not an array"); + avref = (AV*)SvRV(arg); + len = av_len( avref) + 1; + array = (char **) SvPVX( sv_2mortal( NEWSV(0, (len +1) * sizeof( char*) ))); + for(i = 0; i < len; i++ ) { + elem = av_fetch( avref, i, 0); + array[i] = (char *) SvPV( *elem, PL_na); + } + array[len] = NULL; + return array; +} + +void XS_pack_charPtrPtr( SV* arg, char** array, int count) { + int i; + AV* avref; + + avref = (AV*) sv_2mortal((SV*) newAV() ); + for( i = 0; i < count; i++) { + av_push(avref, newSVpv(array[i], strlen(array[i])) ); + } + SvSetSV( arg, newRV((SV*) avref) ); +} +