Storable stand alone tests
[p5sagit/p5-mst-13.2.git] / ext / Storable / t / malice.t
1 #!./perl -w
2
3 #
4 #  Copyright 2002, Larry Wall.
5 #
6 #  You may redistribute only under the same terms as Perl 5, as specified
7 #  in the README file that comes with the distribution.
8 #
9
10 # I'm trying to keep this test easily backwards compatible to 5.004, so no
11 # qr//;
12
13 # This test tries to craft malicious data to test out as many different
14 # error traps in Storable as possible
15 # It also acts as a test for read_header
16
17 sub BEGIN {
18     if ($ENV{PERL_CORE}){
19         chdir('t') if -d 't';
20         @INC = ('.', '../lib');
21     }
22     require Config; import Config;
23     if ($ENV{PERL_CORE} and $Config{'extensions'} !~ /\bStorable\b/) {
24         print "1..0 # Skip: Storable was not built\n";
25         exit 0;
26     }
27 }
28
29 use strict;
30 use vars qw($file_magic_str $other_magic $network_magic $major $minor
31             $minor_write $fancy);
32 $file_magic_str = 'pst0';
33 $other_magic = 7 + length($Config{byteorder});
34 $network_magic = 2;
35 $major = 2;
36 $minor = 5;
37 $minor_write = $] > 5.007 ? 5 : 4;
38
39 use Test::More;
40
41 # If it's 5.7.3 or later the hash will be stored with flags, which is
42 # 2 extra bytes. There are 2 * 2 * 2 tests per byte in the body and header
43 # common to normal and network order serialised objects (hence the 8)
44 # There are only 2 * 2 tests per byte in the parts of the header not present
45 # for network order, and 2 tests per byte on the 'pst0' "magic number" only
46 # present in files, but not in things store()ed to memory
47 $fancy = ($] > 5.007 ? 2 : 0);
48
49 plan tests => 378 + length($Config{byteorder}) * 4 + $fancy * 8;
50
51 use Storable qw (store retrieve freeze thaw nstore nfreeze);
52
53 my $file = "malice.$$";
54 die "Temporary file 'malice.$$' already exists" if -e $file;
55
56 END { while (-f $file) {unlink $file or die "Can't unlink '$file': $!" }}
57
58 # The chr 256 is a hack to force the hash to always have the utf8 keys flag
59 # set on 5.7.3 and later. Otherwise the test fails if run with -Mutf8 because
60 # only there does the hash has the flag on, and hence only there is it stored
61 # as a flagged hash, which is 2 bytes longer
62 my %hash = (perl => 'rules', chr 256, '');
63 delete $hash{chr 256};
64
65 sub test_hash {
66   my $clone = shift;
67   is (ref $clone, "HASH", "Get hash back");
68   is (scalar keys %$clone, 1, "with 1 key");
69   is ((keys %$clone)[0], "perl", "which is correct");
70   is ($clone->{perl}, "rules");
71 }
72
73 sub test_header {
74   my ($header, $isfile, $isnetorder) = @_;
75   is (!!$header->{file}, !!$isfile, "is file");
76   is ($header->{major}, $major, "major number");
77   is ($header->{minor}, $minor_write, "minor number");
78   is (!!$header->{netorder}, !!$isnetorder, "is network order");
79  SKIP: {
80     skip "Network order header has no sizes", 5 if ($isnetorder);
81     is ($header->{byteorder}, $Config{byteorder}, "byte order");
82     is ($header->{intsize}, $Config{intsize}, "int size");
83     is ($header->{longsize}, $Config{longsize}, "long size");
84     is ($header->{ptrsize}, $Config{ptrsize}, "long size");
85     is ($header->{nvsize}, $Config{nvsize} || $Config{doublesize} || 8,
86         "nv size"); # 5.00405 doesn't even have doublesize in config.
87   }
88 }
89
90 sub store_and_retrieve {
91   my $data = shift;
92   unlink $file or die "Can't unlink '$file': $!";
93   open FH, ">$file" or die "Can't open '$file': $!";
94   binmode FH;
95   print FH $data or die "Can't print to '$file': $!";
96   close FH or die "Can't close '$file': $!";
97
98   return  eval {retrieve $file};
99 }
100
101 sub freeze_and_thaw {
102   my $data = shift;
103   return eval {thaw $data};
104 }
105
106 sub test_truncated {
107   my ($data, $sub, $magic_len, $what) = @_;
108   for my $i (0 .. length ($data) - 1) {
109     my $short = substr $data, 0, $i;
110
111     my $clone = &$sub($short);
112     is (defined ($clone), '', "truncated $what to $i should fail");
113     if ($i < $magic_len) {
114       like ($@, "/^Magic number checking on storable $what failed/",
115           "Should croak with magic number warning");
116     } else {
117       is ($@, "", "Should not set \$\@");
118     }
119   }
120 }
121
122 sub test_corrupt {
123   my ($data, $sub, $what, $name) = @_;
124
125   my $clone = &$sub($data);
126   is (defined ($clone), '', "$name $what should fail");
127   like ($@, $what, $name);
128 }
129
130 sub test_things {
131   my ($contents, $sub, $what, $isnetwork) = @_;
132   my $isfile = $what eq 'file';
133   my $file_magic = $isfile ? length $file_magic_str : 0;
134
135   my $header = Storable::read_magic ($contents);
136   test_header ($header, $isfile, $isnetwork);
137
138   # Test that if we re-write it, everything still works:
139   my $clone = &$sub ($contents);
140
141   is ($@, "", "There should be no error");
142
143   test_hash ($clone);
144
145   # Now lets check the short version:
146   test_truncated ($contents, $sub, $file_magic
147                   + ($isnetwork ? $network_magic : $other_magic), $what);
148
149   my $copy;
150   if ($isfile) {
151     $copy = $contents;
152     substr ($copy, 0, 4) = 'iron';
153     test_corrupt ($copy, $sub, "/^File is not a perl storable/",
154                   "magic number");
155   }
156
157   $copy = $contents;
158   # Needs to be more than 1, as we're already coding a spread of 1 minor version
159   # number on writes (2.5, 2.4). May increase to 2 if we figure we can do 2.3
160   # on 5.005_03 (No utf8).
161   # 4 allows for a small safety margin
162   # (Joke:
163   # Question: What is the value of pi?
164   # Mathematician answers "It's pi, isn't it"
165   # Physicist answers "3.1, within experimental error"
166   # Engineer answers "Well, allowing for a small safety margin,   18"
167   # )
168   my $minor4 = $header->{minor} + 4;
169   substr ($copy, $file_magic + 1, 1) = chr $minor4;
170   {
171     # Now by default newer minor version numbers are not a pain.
172     $clone = &$sub($copy);
173     is ($@, "", "by default no error on higher minor");
174     test_hash ($clone);
175
176     local $Storable::accept_future_minor = 0;
177     test_corrupt ($copy, $sub,
178                   "/^Storable binary image v$header->{major}\.$minor4 more recent than I am \\(v$header->{major}\.$minor\\)/",
179                   "higher minor");
180   }
181
182   $copy = $contents;
183   my $major1 = $header->{major} + 1;
184   substr ($copy, $file_magic, 1) = chr 2*$major1;
185   test_corrupt ($copy, $sub,
186                 "/^Storable binary image v$major1\.$header->{minor} more recent than I am \\(v$header->{major}\.$minor\\)/",
187                 "higher major");
188
189   # Continue messing with the previous copy
190   my $minor1 = $header->{minor} - 1;
191   substr ($copy, $file_magic + 1, 1) = chr $minor1;
192   test_corrupt ($copy, $sub,
193                 "/^Storable binary image v$major1\.$minor1 more recent than I am \\(v$header->{major}\.$minor\\)/",
194               "higher major, lower minor");
195
196   my $where;
197   if (!$isnetwork) {
198     # All these are omitted from the network order header.
199     # I'm not sure if it's correct to omit the byte size stuff.
200     $copy = $contents;
201     substr ($copy, $file_magic + 3, length $header->{byteorder})
202       = reverse $header->{byteorder};
203
204     test_corrupt ($copy, $sub, "/^Byte order is not compatible/",
205                   "byte order");
206     $where = $file_magic + 3 + length $header->{byteorder};
207     foreach (['intsize', "Integer"],
208              ['longsize', "Long integer"],
209              ['ptrsize', "Pointer integer"],
210              ['nvsize', "Double"]) {
211       my ($key, $name) = @$_;
212       $copy = $contents;
213       substr ($copy, $where++, 1) = chr 0;
214       test_corrupt ($copy, $sub, "/^$name size is not compatible/",
215                     "$name size");
216     }
217   } else {
218     $where = $file_magic + $network_magic;
219   }
220
221   # Just the header and a tag 255. As 26 is currently the highest tag, this
222   # is "unexpected"
223   $copy = substr ($contents, 0, $where) . chr 255;
224
225   test_corrupt ($copy, $sub,
226                 "/^Corrupted storable $what \\(binary v$header->{major}.$header->{minor}\\)/",
227                 "bogus tag");
228
229   # Now drop the minor version number
230   substr ($copy, $file_magic + 1, 1) = chr $minor1;
231
232   test_corrupt ($copy, $sub,
233                 "/^Corrupted storable $what \\(binary v$header->{major}.$minor1\\)/",
234                 "bogus tag, minor less 1");
235   # Now increase the minor version number
236   substr ($copy, $file_magic + 1, 1) = chr $minor4;
237
238   # local $Storable::DEBUGME = 1;
239   # This is the delayed croak
240   test_corrupt ($copy, $sub,
241                 "/^Storable binary image v$header->{major}.$minor4 contains data of type 255. This Storable is v$header->{major}.$minor and can only handle data types up to 25/",
242                 "bogus tag, minor plus 4");
243   # And check again that this croak is not delayed:
244   {
245     # local $Storable::DEBUGME = 1;
246     local $Storable::accept_future_minor = 0;
247     test_corrupt ($copy, $sub,
248                   "/^Storable binary image v$header->{major}\.$minor4 more recent than I am \\(v$header->{major}\.$minor\\)/",
249                   "higher minor");
250   }
251 }
252
253 sub slurp {
254   my $file = shift;
255   local (*FH, $/);
256   open FH, "<$file" or die "Can't open '$file': $!";
257   binmode FH;
258   my $contents = <FH>;
259   die "Can't read $file: $!" unless defined $contents;
260   return $contents;
261 }
262
263
264 ok (defined store(\%hash, $file));
265
266 my $expected = 20 + length ($file_magic_str) + $other_magic + $fancy;
267 my $length = -s $file;
268
269 die "Don't seem to have written file '$file' as I can't get its length: $!"
270   unless defined $file;
271
272 die "Expected file to be $expected bytes (sizeof long is $Config{longsize}) but it is $length"
273   unless $length == $expected;
274
275 # Read the contents into memory:
276 my $contents = slurp $file;
277
278 # Test the original direct from disk
279 my $clone = retrieve $file;
280 test_hash ($clone);
281
282 # Then test it.
283 test_things($contents, \&store_and_retrieve, 'file');
284
285 # And now try almost everything again with a Storable string
286 my $stored = freeze \%hash;
287 test_things($stored, \&freeze_and_thaw, 'string');
288
289 # Network order.
290 unlink $file or die "Can't unlink '$file': $!";
291
292 ok (defined nstore(\%hash, $file));
293
294 $expected = 20 + length ($file_magic_str) + $network_magic + $fancy;
295 $length = -s $file;
296
297 die "Don't seem to have written file '$file' as I can't get its length: $!"
298   unless defined $file;
299
300 die "Expected file to be $expected bytes (sizeof long is $Config{longsize}) but it is $length"
301   unless $length == $expected;
302
303 # Read the contents into memory:
304 $contents = slurp $file;
305
306 # Test the original direct from disk
307 $clone = retrieve $file;
308 test_hash ($clone);
309
310 # Then test it.
311 test_things($contents, \&store_and_retrieve, 'file', 1);
312
313 # And now try almost everything again with a Storable string
314 $stored = nfreeze \%hash;
315 test_things($stored, \&freeze_and_thaw, 'string', 1);