@@ -6,139 +6,178 @@ package Module::Metadata::ExtractVersion;
6
6
use parent ' Exporter' ;
7
7
our @EXPORT_OK = qw/ eval_version/ ;
8
8
9
- use Carp qw/ croak/ ;
9
+ use File::Spec;
10
+ use File::Temp 0.18;
11
+ use IPC::Open3 qw( open3) ;
12
+ use Symbol ' gensym' ;
13
+
14
+ # Win32 is slow to spawn processes
15
+ my $TIMEOUT = $^O eq ' MSWin32' ? 5 : 2;
10
16
11
17
=func eval_version
12
18
19
+ my $version = eval_version( q[our $VERSION = "1.23"] );
20
+
13
21
Given a (decoded) string (usually a single line) that contains a C<$VERSION >
14
22
declaration, this function will evaluate it in a L<Safe> compartment in a
15
- separate process. If the C<$VERSION > is a valid version string according to
16
- L<version> , it will return it as a string, otherwise, it will return undef.
23
+ separate process. The extracted string is returned; it is B<not > validated
24
+ against required syntax for versions at this level, so the caller should
25
+ normally do something like C<< version::is_lax($version) >> before proceeding
26
+ to use this data.
17
27
18
28
=cut
19
29
20
30
sub eval_version
21
31
{
22
- my (%args ) = @_ ;
23
-
24
- return _evaluate_version_line(
25
- $args {sigil },
26
- $args {variable_name },
27
- $args {string },
28
- $args {filename },
29
- );
30
- }
32
+ my ( $string , $timeout ) = @_ ;
33
+ $timeout = $TIMEOUT unless defined $timeout ;
34
+
35
+ # what $VERSION are we looking for?
36
+ my ( $sigil , $var ) = $string =~ / ([\$ *])(([\w\:\' ]*)\b VERSION)\b .*\= / ;
37
+ return unless $sigil && $var ;
38
+
39
+ # munge string: remove "use version" as we do that already and the "use"
40
+ # will get stopped by the Safe compartment
41
+ $string =~ s / (?:use|require)\s +version[^;]*/ 1/ ;
42
+
43
+ # create test file
44
+ my $temp = File::Temp-> new;
45
+ print {$temp } _pl_template( $string , $sigil , $var );
46
+ close $temp ;
47
+
48
+ my $rc ;
49
+ my $result ;
50
+ my $err = gensym;
51
+ my $pid = open3(my $in , my $out , $err , $^X, $temp );
52
+ my $killer ;
53
+ if ($^O eq ' MSWin32' ) {
54
+ $killer = fork ;
55
+ if (!defined $killer ) {
56
+ die " Can't fork: $! " ;
57
+ }
58
+ elsif ($killer == 0) {
59
+ sleep $timeout ;
60
+ kill ' KILL' , $pid ;
61
+ exit 0;
62
+ }
63
+ }
64
+ my $got = eval {
65
+ local $SIG {ALRM } = sub { die " alarm\n " };
66
+ alarm $timeout ;
67
+ local $/ ;
68
+ $result = readline $out ;
69
+ my $c = waitpid $pid , 0;
70
+ alarm 0;
71
+ ( $c != $pid ) || $? ;
72
+ };
73
+ if ( $@ eq " alarm\n " ) {
74
+ kill ' KILL' , $pid ;
75
+ waitpid $pid , 0;
76
+ $rc = $? ;
77
+ }
78
+ else {
79
+ $rc = $got ;
80
+ }
81
+ if ($killer ) {
82
+ kill ' KILL' , $killer ;
83
+ waitpid $killer , 0;
84
+ }
31
85
32
- # transported directly from Module::Metadata
33
- {
34
- my $pn = 0;
35
- sub _evaluate_version_line {
36
- my ( $sigil , $variable_name , $line , $filename ) = @_ ;
37
-
38
- # Some of this code came from the ExtUtils:: hierarchy.
39
-
40
- # We compile into $vsub because 'use version' would cause
41
- # compiletime/runtime issues with local()
42
- my $vsub ;
43
- $pn ++; # everybody gets their own package
44
- my $eval = qq{ BEGIN { my \$ dummy = q# Hide from _packages_inside()
45
- #; package Module::Metadata::_version::p$pn ;
46
- use version;
47
- no strict;
48
- no warnings;
49
-
50
- \$ vsub = sub {
51
- local $sigil$variable_name ;
52
- \$ $variable_name =undef;
53
- $line ;
54
- \$ $variable_name
55
- };
56
- }} ;
57
-
58
- $eval = $1 if $eval =~ m { ^(.+)} s ;
59
-
60
- local $^W;
61
- # Try to get the $VERSION
62
- eval $eval ;
63
- # some modules say $VERSION = $Foo::Bar::VERSION, but Foo::Bar isn't
64
- # installed, so we need to hunt in ./lib for it
65
- if ( $@ =~ / Can't locate/ && -d ' lib' ) {
66
- local @INC = (' lib' ,@INC );
67
- eval $eval ;
68
- }
69
- warn " Error evaling version line '$eval ' in $filename : $@ \n "
70
- if $@ ;
71
- (ref ($vsub ) eq ' CODE' ) or
72
- croak " failed to build version sub for $filename " ;
73
- my $result = eval { $vsub -> () };
74
- croak " Could not get version from $filename by executing:\n $eval \n\n The fatal error was: $@ \n "
75
- if $@ ;
76
-
77
- # Upgrade it into a version object
78
- my $version = eval { _dwim_version($result ) };
79
-
80
- croak " Version '$result ' from $filename does not appear to be valid:\n $eval \n\n The fatal error was: $@ \n "
81
- unless defined $version ; # "0" is OK!
82
-
83
- return $version ;
84
- }
85
- }
86
+ return if $rc || !defined $result ; # error condition
86
87
87
- # Try to DWIM when things fail the lax version test in obvious ways
88
- {
89
- my @version_prep = (
90
- # Best case, it just works
91
- sub { return shift },
92
-
93
- # If we still don't have a version, try stripping any
94
- # trailing junk that is prohibited by lax rules
95
- sub {
96
- my $v = shift ;
97
- $v =~ s { ([0-9])[a-z-].*$} { $1 } i ; # 1.23-alpha or 1.23b
98
- return $v ;
99
- },
100
-
101
- # Activestate apparently creates custom versions like '1.23_45_01', which
102
- # cause version.pm to think it's an invalid alpha. So check for that
103
- # and strip them
104
- sub {
105
- my $v = shift ;
106
- my $num_dots = () = $v =~ m { (\. )} g ;
107
- my $num_unders = () = $v =~ m { (_)} g ;
108
- my $leading_v = substr ($v ,0,1) eq ' v' ;
109
- if ( ! $leading_v && $num_dots < 2 && $num_unders > 1 ) {
110
- $v =~ s { _} {} g ;
111
- $num_unders = () = $v =~ m { (_)} g ;
112
- }
113
- return $v ;
114
- },
115
-
116
- # Worst case, try numifying it like we would have before version objects
117
- sub {
118
- my $v = shift ;
119
- no warnings ' numeric' ;
120
- return 0 + $v ;
121
- },
122
-
123
- );
124
-
125
- sub _dwim_version {
126
- my ($result ) = shift ;
127
-
128
- return $result if ref ($result ) eq ' version' ;
129
-
130
- my ($version , $error );
131
- for my $f (@version_prep ) {
132
- $result = $f -> ($result );
133
- $version = eval { version-> new($result ) };
134
- $error ||= $@ if $@ ; # capture first failure
135
- last if defined $version ;
136
- }
88
+ # # print STDERR "# C<< $string >> --> $result" if $result =~ /^ERROR/;
89
+ return if $result =~ / ^ERROR/ ;
137
90
138
- croak $error unless defined $version ;
91
+ $result =~ s / [ \r\n ]+ \z // ;
139
92
140
- return $version ;
141
- }
93
+ # treat '' the same as undef: no version was found
94
+ undef $result if $result eq ' ' ;
95
+
96
+ return $result ;
97
+ }
98
+
99
+ sub _pl_template {
100
+ my ( $string , $sigil , $var ) = @_ ;
101
+ return <<"HERE"
102
+ use version;
103
+ use Safe;
104
+ use File::Spec;
105
+ open STDERR, '>', File::Spec->devnull;
106
+ open STDIN, '<', File::Spec->devnull;
107
+
108
+ my \$ comp = Safe->new;
109
+ \$ comp->permit("entereval"); # for MBARBON/Module-Info-0.30.tar.gz
110
+ \$ comp->share("*version::new");
111
+ \$ comp->share("*version::numify");
112
+ \$ comp->share_from('main', ['*version::',
113
+ '*Exporter::',
114
+ '*DynaLoader::']);
115
+ \$ comp->share_from('version', ['&qv']);
116
+
117
+ my \$ code = <<'END';
118
+ local $sigil$var ;
119
+ \$ $var = undef;
120
+ do {
121
+ $string
122
+ };
123
+ \$ $var ;
124
+ END
125
+
126
+ my \$ result = \$ comp->reval(\$ code);
127
+ print "ERROR: \$ @\n " if \$ @;
128
+ exit unless defined \$ result;
129
+
130
+ eval { \$ result = version->parse(\$ result)->stringify };
131
+ print \$ result;
132
+
133
+ HERE
142
134
}
143
135
144
136
1;
137
+
138
+ =head1 SYNOPSIS
139
+
140
+ use Version::Eval qw/eval_version/;
141
+
142
+ my $version = eval_version( $unsafe_string );
143
+
144
+ =head1 DESCRIPTION
145
+
146
+ Package versions are defined by a string such as this:
147
+
148
+ package Foo;
149
+ our $VERSION = "1.23";
150
+
151
+ If we want to know the version of a F<.pm> file, we can
152
+ load it and check C<Foo- > VERSION> for the package. But that means any
153
+ buggy or hostile code in F<Foo.pm> gets run.
154
+
155
+ The safe thing to do is to parse out a string that looks like an assignment
156
+ to C<$VERSION > and then evaluate it. But even that could be hostile:
157
+
158
+ package Foo;
159
+ our $VERSION = do { my $n; $n++ while 1 }; # infinite loop
160
+
161
+ This module executes a potential version string in a separate process in
162
+ a L<Safe> compartment with a timeout to avoid as much risk as possible.
163
+
164
+ Hostile code might still attempt to consume excessive resources, but the
165
+ timeout should limit the problem.
166
+
167
+ =head1 SEE ALSO
168
+
169
+ =over 4
170
+
171
+ * L<Parse::PMFile>
172
+
173
+ * L<V>
174
+
175
+ * L<use Module::Info>
176
+
177
+ * L<Module::InstalledVersion>
178
+
179
+ * L<Module::Version>
180
+
181
+ =back
182
+
183
+ =cut
0 commit comments