File Coverage

lib/Parse/Flash/Cookie.pm
Criterion Covered Total %
statement 268 278 96.4
branch 118 180 65.5
condition 8 15 53.3
subroutine 42 42 100.0
pod 2 2 100.0
total 438 517 84.7


line stmt bran cond sub pod time code
1             package Parse::Flash::Cookie;
2              
3 5     5   280453 use 5.008; # minimum Perl is V5.8.0
  5         22  
  5         210  
4 5     5   440 use strict;
  5         15  
  5         171  
5 5     5   30 use warnings;
  5         13  
  5         290  
6              
7             our $VERSION = '0.09';
8              
9 5     5   7539 use Log::Log4perl;
  5         353006  
  5         36  
10 5     5   3827 use XML::Writer; # to create XML output
  5         74707  
  5         138  
11 5     5   4529 use URI::Escape; # to safely display buffer in debug mode
  5         7427  
  5         331  
12 5     5   6458 use DateTime;
  5         1024873  
  5         210  
13 5     5   60 use Config; # to determine endianness
  5         12  
  5         351  
14              
15 5     5   27 use constant LENGTH_OF_SHORT => 2;
  5         11  
  5         484  
16 5     5   65 use constant LENGTH_OF_INTEGER => 2;
  5         10  
  5         258  
17 5     5   24 use constant LENGTH_OF_LONG => 4;
  5         9  
  5         198  
18 5     5   23 use constant LENGTH_OF_FLOAT => 8;
  5         9  
  5         245  
19 5     5   25 use constant END_OF_OBJECT => "\x00\x00\x09";
  5         21  
  5         230  
20              
21             # The below constants are little-endian. Adobe flash cookies are
22             # little-endian even on big-endian platforms.
23 5     5   43 use constant POSITIVE_INFINITY => "\x7F\xF0\x00\x00\x00\x00\x00\x00";
  5         7  
  5         227  
24 5     5   26 use constant NEGATIVE_INFINITY => "\xFF\xF0\x00\x00\x00\x00\x00\x00";
  5         9  
  5         261  
25 5     5   24 use constant NOT_A_NUMBER => "\x7F\xF8\x00\x00\x00\x00\x00\x00";
  5         10  
  5         5336  
26              
27             my $conf = q(
28             log4perl.category.sol.parser = WARN, ScreenAppender
29             log4perl.appender.ScreenAppender = Log::Log4perl::Appender::Screen
30             log4perl.appender.ScreenAppender.stderr = 0
31             log4perl.appender.ScreenAppender.layout = PatternLayout
32             log4perl.appender.ScreenAppender.layout.ConversionPattern=[%p] %m%n
33             );
34             Log::Log4perl::init( \$conf );
35             my $log = Log::Log4perl::->get_logger(q(sol.parser));
36              
37             my $file = undef;
38             my $FH = undef;
39             my $writer = undef;
40              
41             my %datatype = (
42                             0x0 => 'number',
43                             0x1 => 'boolean',
44                             0x2 => 'string',
45                             0x3 => 'object',
46                             0x5 => 'null',
47                             0x6 => 'undefined',
48                             0x7 => 'pointer',
49                             0x8 => 'array',
50                             0xa => 'raw-array',
51                             0xb => 'date',
52                             0xd => 'object-string-number-boolean-textformat',
53                             0xf => 'object-xml',
54                             0x10 => 'object-customclass',
55                            );
56              
57             # Return true if architecture is little-endian, otherwise false
58             sub _is_little_endian {
59 824 50   824   19555 return ( $Config{byteorder} =~ qr/^1234/ ) ? 1 : 0;
60             }
61              
62              
63             # Add an XML element to current document. Do nothing if $writer is
64             # undef. Return true.
65             sub _addXMLElem {
66              
67             # Skip if not XML mode
68 460 100   460   1566   return unless $writer;
69              
70 372         8584   my ($type, $name, $value) = @_;
71 372         1448   $writer->startTag(
72                                 'data',
73                                 'type' => $type,
74                                 'name' => $name,
75                                );
76              
77 371 100       43729   $writer->characters($value) if (defined($value));
78 371         9833   $writer->endTag();
79              
80 371         9769   return 1;
81             }
82              
83             # Parse and return type and value as list. Expects to be called in
84             # list context. Argument name is passed in order to have the
85             # individual subs create XML elements themselves.
86             sub _getTypeAndValue {
87 582     582   1264   my $name = shift;
88              
89 582 50       1506   $log->logdie("expected to be called in LIST context") if !wantarray();
90              
91             # Read data type
92 582         734   my $value = undef;
93 582         1163   my $type = _readBytes(1);
94 582         1717   my $type_as_txt = $datatype{$type};
95 582 100       1854   if (!exists($datatype{$type})) {
96 1 50       7     $log->warn(qq{Missing datatype for '$type'!}) if $log->is_warn();
97               }
98              
99             # Read element depending on type
100 582 100       3380   if($type == 0) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    100          
101 160 50       501     $log->debug(q{float}) if $log->is_debug();
102 160         1388     $value = _getFloat($name);
103               } elsif($type == 1){
104 89 50       274     $log->debug(q{bool}) if $log->is_debug();
105 89         560     $value = _getBool($name);
106               } elsif ($type == 2) {
107 206 50       765     $log->debug(q{string}) if $log->is_debug();
108 206         1378     $value = _getString($name);
109               } elsif($type == 3){
110 69 50       266     $log->debug(q{object}) if $log->is_debug();
111 69         487     $value = _getObject($name);
112               } elsif($type == 5) { # null
113 2 50       11     $log->debug(q{null}) if $log->is_debug();
114 2         16     $value = undef;
115 2         6     _addXMLElem('null', $name);
116               } elsif($type == 6) { # undef
117 2 50       12     $log->debug(q{undef}) if $log->is_debug();
118 2         18     $value = undef;
119 2         8     _addXMLElem('undef', $name);
120               } elsif($type == 7){ # pointer
121 1 50       5     $log->debug(q{pointer}) if $log->is_debug();
122 1         11     $value = _getPointer($name);
123               } elsif($type == 8){ # array
124 37 50       118     $log->debug(q{array}) if $log->is_debug();
125 37         258     $value = _getArray($name);
126               } elsif($type == 0xb){ # date
127 7         31     $value = _getDate($name);
128               } elsif($type == 0xf){ # doublestring
129 0         0     $log->logdie("Not implemented yet: doublestring");
130               } elsif($type == 0x10){ # customclass
131 8 50       33     $log->debug(q{customclass}) if $log->is_debug();
132 8         64     $value = _getObject($name, 1);
133               } else {
134 1         8     $log->logdie("Unknown type:$type" );
135               }
136              
137 580         2985   return ($type_as_txt, $value);
138             }
139              
140             # Parse object and return contents as comma separated string.
141             sub _getObject {
142 77     77   180   my $name = shift;
143 77         143   my $customClass = shift;
144 77         199   my @retvals = ();
145 77 100       255   $writer->startTag(
146                 'data',
147                 'type' => 'object',
148                 'name' => $name,
149               ) if $writer;
150              
151              LOOP:
152 77         8081   while (eof($FH) != 1) {
153             # Read until end flag is detected : 00 00 09
154 406 100       777     if (_readRaw(3) eq END_OF_OBJECT) {
155             #return join(q{,}, @retvals);
156 77         192       last LOOP;
157                 }
158              
159             # "un-read" the 3 bytes
160 329 50       3622     seek($FH, -3, 1) or $log->logdie("seek failed");
161              
162             # Read name
163 329         742     $name = _readString();
164 329 50       1218     $log->debug(qq{name:$name}) if $log->is_debug();
165              
166             # Read 2nd name if customClass is set
167 329 100       2375     if ($customClass) {
168 8         30       push @retvals, q{class_name=} . $name . q{;};
169 8         21       $name = _readString();
170 8 50       33       $log->debug(qq{name:$name (2nd name - customClass)}) if $log->is_debug();
171 8         47       $customClass = 0;
172                 }
173              
174             # Get data type and value
175 329         746     my ($type, $value) = _getTypeAndValue($name);
176              
177             {
178 5     5   36 no warnings q{uninitialized}; # allow undefined values
  5         10  
  5         1270  
  329         621  
179 329 50       1718 $log->debug(qq{type:$type value:$value}) if $log->is_debug();
180 329         4052 push @retvals, $name . q{;} . $value;
181             }
182               }
183              
184 77 100       262   $writer->endTag() if $writer;
185              
186 77         3744   return join(q{,}, @retvals);
187             }
188              
189              
190             # Parse array and return contents as comma separated string.
191             sub _getArray {
192 37     37   76   my $name = shift;
193              
194 37         74   my @retvals = ();
195 37         64   my $length = _readLong();
196 37 50       129   if($length == 0) {
197 0         0     return _getObject();
198               }
199              
200               $writer->startTag(
201 37 100       122     'data',
202                 'type' => 'array',
203                 'length' => $length,
204                 'name' => $name,
205               ) if $writer;
206              
207              ELEMENT:
208 37         5604   while ($length-- > 0) {
209 116         251     $name = _readString();
210              
211 116 50       382     if (!defined($name)) {
212 0         0       last ELEMENT;
213                 }
214              
215 116         162     my $retval = undef;
216 116         230     my ($type, $value) = _getTypeAndValue($name);
217                 {
218 5     5   26       no warnings q{uninitialized}; # allow undef values
  5         9  
  5         6975  
  116         274  
219 116 50       372       $log->debug(qq{$name;$type;$value}) if $log->is_debug();
220 116         1097       $retval = qq{$name;$type;$value};
221                 }
222 116         733     push @retvals, $retval;
223               }
224              
225 37 100       117   $writer->endTag() if $writer;
226              
227             # Now expect END_OF_OBJECT tag to be next
228 37 50       1819   if (_readRaw(3) eq END_OF_OBJECT) {
229 37         307     return join(q{,}, @retvals);
230               }
231              
232 0 0       0   $log->error(q{Did not find expected END_OF_OBJECT! at end of array!}) if $log->is_error();
233 0         0   return;
234             }
235              
236             #################################
237              
238             # Utility functions - does not generate XML output
239              
240             # Parse and return a given number of bytes (unformatted)
241             sub _readRaw {
242 443     443   639   my $len = shift;
243 443 50       912   $log->logdie("missing length argument") unless $len;
244 443         688   my $buffer = undef;
245 443         1815   my $num = read($FH, $buffer, $len);
246 443         2150   return $buffer;
247             }
248              
249             # Parse and return a string: The first 2 bytes contains the string
250             # length, succeeded by the string itself. Read length first unless
251             # length is given, otherwise read the given number of bytes.
252             sub _readString {
253 944     944   1514   my $len = shift;
254 944         1396   my $buffer = undef;
255 944         1045   my $num = undef;
256              
257 944 50 33     2858   $log->debug(qq{len not given as arg}) if $log->is_debug() && !$len;
258              
259             # read length from filehandle unless set
260 944 100       12001   $len = join(q{}, _readShort(2)) unless ($len);
261              
262             # return undef if length is zero
263 944 100       8341   return unless $len;
264              
265 903 50       2990   $log->debug(qq{len:$len}) if $log->is_debug();
266 903         7701   $num = read($FH, $buffer, $len);
267 903 50       2942   if ($log->is_debug()) {
268 0         0     $log->debug(qq{buffer:} . uri_escape($buffer));
269               }
270 903         7856   return $buffer;
271             }
272              
273             # Parse and return a given number of bytes
274             sub _readBytes {
275 806   50 806   1759   my $len = shift || 1;
276 806         865   my $buffer = undef;
277 806         1858   my $num = read($FH, $buffer, $len);
278 806         8345   return unpack 'C*', $buffer; # An unsigned char (octet) value.
279             }
280              
281             # Parse and return signed short (integer) number, default 2 bytes
282             sub _readSignedShort {
283 7   50 7   25   my $len = shift || LENGTH_OF_SHORT;
284 7         12   my $buffer = undef;
285 7         25   my $num = read($FH, $buffer, $len);
286 7 50       21   (_is_little_endian())
287                 ? return unpack 's*', reverse $buffer
288                 : return unpack 's*', $buffer;
289             }
290              
291             # Parse and return short (integer) number, default 2 bytes
292             sub _readShort {
293 660   100 660   1697   my $len = shift || LENGTH_OF_SHORT;
294 660         842   my $buffer = undef;
295 660         9354   my $num = read($FH, $buffer, $len);
296 660 50       1473   (_is_little_endian())
297                 ? return unpack 'S*', reverse $buffer
298                 : return unpack 'S*', $buffer;
299             }
300              
301             # Parse and return integer number, default 2 bytes
302             sub _readInt {
303 174   50 174   514   my $len = shift || LENGTH_OF_INTEGER;
304 174         199   my $buffer = undef;
305 174         381   my $num = read($FH, $buffer, $len);
306 174         994   return unpack 'C*', reverse $buffer;
307             }
308              
309             # Parse and return long integer number, default 4 bytes
310             sub _readLong {
311 74   50 74   365   my $len = shift || LENGTH_OF_LONG;
312 74         109   my $buffer = undef;
313 74         194   my $num = read($FH, $buffer, $len);
314 74         372   return unpack 'C*', reverse $buffer;
315             }
316              
317             # Parse and return floating point number: default 8 bytes
318             sub _readFloat {
319 167   50 167   3409   my $len = shift || LENGTH_OF_FLOAT;
320 167         205   my $buffer = undef;
321 167         389   my $num = read($FH, $buffer, $len);
322              
323             # Check special numbers - do not rely on OS/compiler to tell the
324             # truth.
325 167 100       997 if ($buffer eq POSITIVE_INFINITY) {
    100          
    50          
326 5         21 return q{inf};
327             } elsif ($buffer eq NEGATIVE_INFINITY) {
328 5         20 return q{-inf};
329             } elsif ($buffer eq NOT_A_NUMBER) {
330 0         0 return q{nan};
331             }
332            
333 157 50       327   (_is_little_endian())
334                 ? return unpack 'd*', reverse $buffer
335                 : return unpack 'd*', $buffer;
336             }
337              
338             #################################
339              
340             ### Functions that gets data and creates XML output
341              
342             # Get next boolean element. Return 1 if the element's value is
343             # non-zero, otherwise 0. Add XML node if in XML mode.
344             sub _getBool {
345 89     89   186   my $name = shift;
346 89         172   my $value = _readBytes(1);
347              
348 89 100       793   if ($value !~ qr/^[01]$/) {
349 5         19     my $orgval = $value;
350 5 50       23     $value = ($value) ? 1 : 0;
351 5 50       32     $log->warn(qq{Unexpected boolean value '$orgval' was converted to $value}) if $log->is_warn();
352               }
353              
354 89         2665   _addXMLElem('boolean', $name, $value);
355 89         389   return $value;
356             }
357              
358             # Get next string element. Return the element's value. Add XML node
359             # if in XML mode
360             sub _getString {
361 206     206   498   my $name = shift;
362 206         409   my $value = _readString();
363 206         610   _addXMLElem('string', $name, $value);
364 205         777   return $value;
365             }
366              
367             # Return floating point number - create XML
368             sub _getFloat {
369 160     160   427   my $name = shift;
370 160         348   my $value = _readFloat();
371 160         7932   _addXMLElem('number', $name, $value); # Yes it's called number, not float
372 160         548   return $value;
373             }
374              
375             # Return a date object - create XML
376             sub _getDate {
377 7     7   25   my $name = shift;
378              
379             # Date consists of a float (8 bytes) value followed by a signed short (2
380             # bytes) UTC offset
381 7         23   my $msec = _readFloat();
382 7         42 my $utcoffset = - _readSignedShort(2) / 60;
383 7 50       46   $log->debug(qq{msec:$msec utcoffset:$utcoffset}) if $log->is_debug();
384              
385             # Create datetime object starting on Jan 1st 1970 and add msec to
386             # get the given date
387 7         109   my $dt = DateTime->from_epoch( epoch => 0 )->add( seconds => $msec / 1000 );
388              
389 7 100       10152   $writer->comment("DateObject:Milliseconds Count From Jan. 1, 1970; Timezone UTC + Offset.")
390                 if $writer;
391 7 100       387   $writer->startTag(
392                 'data',
393                 'type' => 'date',
394                 'name' => $name,
395                 'msec' => $msec,
396                 'date' => $dt->ymd() . q{ } . $dt->hms(),
397                 'utcoffset' => $utcoffset,
398               ) if $writer;
399 7 100       1511   $writer->endTag() if $writer;
400              
401 7         237   my $retval = undef;
402               {
403 5     5   45     no warnings q{uninitialized}; # allow undef values
  5         9  
  5         3506  
  7         13  
404 7 50       32     $log->debug(qq{date;$msec;$utcoffset}) if $log->is_debug();
405 7         94     $retval = qq{date;$msec;$utcoffset};
406               }
407 7         66   return $retval;
408             }
409              
410             # Return a pointer. The value read indicates the element index of the
411             # element pointed to.
412             sub _getPointer {
413 1     1   3   my $name = shift;
414              
415 1         4   my $value =_readShort();
416 1 50       8   $log->debug(qq{name:$name value:$value}) if $log->is_debug();
417 1         11   _addXMLElem('pointer', $name, $value); # Yes it's called number, not float
418 1         3   return $value;
419             }
420              
421              
422             ##################################################################
423              
424              
425             # Parse and return file header - 16 bytes in total. Return name if
426             # file starts with sol header, otherwise undef. Failure means the
427             # 'TCSO' tag is missing.
428             sub _getHeader {
429              
430             # skip first 6 bytes
431 37 50   37   141   $log->debug(q{header: skip first 6 bytes}) if $log->is_debug();
432 37         313   _readString(6);
433              
434             # next 4 bytes should contain 'TSCO' tag
435 37 50       159   if (_readString(4) ne q{TCSO}) {
436 0 0       0     $log->error("missing TCSO - not a sol file") if $log->is_error();
437 0         0     return; # failure
438               }
439              
440             # Skip next 7 bytes
441 37 50       145   $log->debug(q{header: skip next 7 bytes}) if $log->is_debug();
442 37         263   _readString(7);
443              
444             # Read next byte (length of name) + the name
445 37         105   my $name = _readString(_readInt(1));
446 37 50       164   $log->debug("name:$name") if $log->is_debug();
447              
448             # Read version number
449 37         254   my $version =_readLong();
450 37 50       144   $log->debug(qq{header: version:'$version'}) if $log->is_debug();
451              
452             # TODO: Add support for version 3 sol files
453 37 100       302   if ($version != 0) {
454 2 50       8       $log->logdie(qq{SOL version '$version' is unsupported!}) if $log->is_debug();
455               }
456              
457 37         238   return $name; # ok
458             }
459              
460             # Parse and return an element. In scalar context, return element
461             # content as semi colon separated string, in list context return
462             # element's name, type and value as a list.
463             sub _getElem {
464 137     137   177   my $retval = undef;
465              
466             # Read element length and name
467 137         275   my $name = _readString(_readInt(2));
468             #$log->debug(qq{element name:$name}) if $log->is_debug();
469              
470             # Read data type and value
471 137         464   my ($type, $value) = _getTypeAndValue($name);
472              
473             # Read trailer (single byte)
474 135         389   my $trailer = _readBytes(1);
475 135 50       481   if ($trailer != 0) {
476 0 0       0     $log->warn(qq{Expected 00 trailer, got '$trailer'}) if $log->is_warn();
477               }
478              
479               {
480 5     5   33     no warnings q{uninitialized}; # allow undef values
  5         7  
  5         3264  
  135         187  
481 135 50       432     $log->info(qq{$name;$type;$value}) if $log->is_info();
482              
483             # Context sensitive return
484 135 100       956     if (wantarray()) {
485 77         832       return ($name, $type, $value);
486                 } else {
487 58         428       return qq{$name;$type;$value};
488                 }
489               }
490             }
491              
492             # parse file and return contents as a textual list
493             sub to_text {
494 17     17 1 29632   my $file = shift;
495              
496 17 100       65   $log->logdie( q{Missing argument file.}) if (!$file);
497 16 100       214   $log->logdie(qq{No such file '$file'}) if (! -f $file);
498              
499 15 50       67   $log->debug("start") if $log->is_debug();
500              
501 15 50       696   open($FH,"< $file") || $log->logdie("Error opening file $file");
502 15 50       59   $log->debug(qq{file:$file}) if $log->is_debug();
503 15         111   binmode($FH);
504              
505 15         30   my @retvals = ();
506              
507             # Read header
508 15 50       36   my $name = _getHeader() or $log->logdie("Invalid sol header");
509 15         46   push @retvals, $name;
510              
511             # Read data elements
512 15         52   while (eof($FH) != 1) {
513 58 50       161     $log->debug(q{read element}) if $log->is_debug();
514 58         338     my $string = _getElem();
515 58         346     push @retvals, $string;
516               }
517              
518 15 50       228   close($FH) or $log->logdie(q{failed to close filehandle!});
519              
520 15         216   return @retvals;
521             }
522              
523             # Parse file and return contents as a scalar containing XML
524             # representing the file's content
525             sub to_xml {
526 22     22 1 43787   my $file = shift;
527              
528 22 50       95   $log->logdie( q{Missing argument file.}) if (!$file);
529 22 50       328   $log->logdie(qq{No such file '$file'}) if (! -f $file);
530              
531 22 50       113   $log->debug("start") if $log->is_debug();
532              
533 22 50       1121   open($FH,"< $file") || $log->logdie("Error opening file $file");
534 22 50       101   $log->debug(qq{file:$file}) if $log->is_debug();
535 22         189   binmode($FH);
536              
537 22         37   my $output = undef;
538 22         154   $writer = new XML::Writer(OUTPUT => \$output, DATA_MODE => 1, DATA_INDENT => 4 );
539              
540             # Read header
541 22 50       7887   my $headername = _getHeader() or $log->logdie("Invalid sol header");
542              
543 22         194   $writer->startTag(
544                                 'sol',
545                                 'name' => $headername,
546                                 'created_by' => __PACKAGE__,
547                                 'version' => $VERSION
548                                );
549              
550             # Read data elements
551 22         3357   while (eof($FH) != 1) {
552 79 50       239     $log->debug(q{read element}) if $log->is_debug();
553 79         544     my ($name, $type, $value) = _getElem();
554               }
555              
556 20 50       369   close($FH) or $log->logdie(q{failed to close filehandle!});
557 20         71   $writer->endTag('sol');
558 20         718   $writer->end();
559              
560 20         580   return $output;
561             }
562              
563             1;
564              
565             __END__
566            
567             =pod
568            
569             =head1 NAME
570            
571             Parse::Flash::Cookie - A flash cookie parser.
572            
573             =head1 SYNOPSIS
574            
575             use Parse::Flash::Cookie;
576             my @content = Parse::Flash::Cookie::to_text("settings.sol");
577             print join("\n", @content);
578            
579             my $xml = Parse::Flash::Cookie::to_xml("settings.sol");
580             print $xml;
581            
582             =head1 DESCRIPTION
583            
584             Local Shared Object (LSO), sometimes known as flash cookies, is a
585             cookie-like data entity used by Adobe Flash Player. LSOs are stored
586             as files on the local file system with the I<.sol> extension. This
587             module reads a Local Shared Object file and return content as a list.
588            
589             =head1 FUNCTIONS
590            
591             =over
592            
593             =item to_text
594            
595             Parses file and return contents as a textual list.
596            
597             =back
598            
599             =over
600            
601             =item to_xml
602            
603             Parses file and return contents as a scalar containing XML
604             representing the file's content.
605            
606             =back
607            
608            
609             =head1 SOL DATA FORMAT
610            
611             The SOL files use a binary encoding that is I<little-endian>
612             regardless of platform architecture. This means the SOL files are
613             platform independent, but they have to be interpreted differently on
614             I<little-endian> and I<big-endian> platforms. See L<perlport> for
615             more.
616            
617             It consists of a header and any number of elements. Both header and
618             the elements have variable lengths.
619            
620             =head2 Header
621            
622             The header has the following structure:
623            
624             =over
625            
626             =item * 6 bytes (discarded)
627            
628             =item * 4 bytes that should contain the string 'TSCO'
629            
630             =item * 7 bytes (discarded)
631            
632             =item * 1 byte that signifies the length of name (X bytes)
633            
634             =item * X bytes name
635            
636             =item * 4 bytes (discarded)
637            
638             =back
639            
640             =head2 Element
641            
642             Each element has the following structure:
643            
644             =over
645            
646             =item * 2 bytes length of element name (Y bytes)
647            
648             =item * Y bytes element name
649            
650             =item * 1 byte data type
651            
652             =item * Z bytes data (depending on the data type)
653            
654             =item * 1 byte trailer
655            
656             =back
657            
658             =head1 TODO
659            
660             =head2 Pointer
661            
662             Resolve the value of object being pointed at for datatype
663             I<pointer> (instead of index).
664            
665             =head1 BUGS
666            
667             Please report any bugs or feature requests to C<bug-parse-flash-cookie at
668             rt.cpan.org>, or through the web interface at
669             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Parse-Flash-Cookie>. I will
670             be notified, and then you'll automatically be notified of progress on
671             your bug as I make changes.
672            
673             =head1 SUPPORT
674            
675             You can find documentation for this module with the perldoc command.
676            
677             perldoc Parse::Flash::Cookie
678            
679             You can also look for information at:
680            
681             =over 4
682            
683             =item * RT: CPAN's request tracker
684            
685             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Parse-Flash-Cookie>
686            
687             =item * AnnoCPAN: Annotated CPAN documentation
688            
689             L<http://annocpan.org/dist/Parse-Flash-Cookie>
690            
691             =item * CPAN Ratings
692            
693             L<http://cpanratings.perl.org/d/Parse-Flash-Cookie>
694            
695             =item * Search CPAN
696            
697             L<http://search.cpan.org/dist/Parse-Flash-Cookie>
698            
699             =back
700            
701             =head1 SEE ALSO
702            
703             =head2 L<perlport>
704            
705             =head2 Local Shared Object
706            
707             http://en.wikipedia.org/wiki/Local_Shared_Object
708            
709             =head2 Flash coders Wiki doc on .Sol File Format
710            
711             http://sourceforge.net/docman/?group_id=131628
712            
713             =head1 ALTERNATIVE IMPLEMENTATIONS
714            
715             http://objection.mozdev.org/ (Firefox extension, Javascript, by Trevor
716             Hobson)
717            
718             http://www.sephiroth.it/python/solreader.php (PHP, by Alessandro
719             Crugnola)
720            
721             http://osflash.org/s2x (Python, by Aral Balkan)
722            
723             =head1 COPYRIGHT & LICENSE
724            
725             Copyright 2007 Andreas Faafeng, all rights reserved.
726            
727             This program is free software; you can redistribute it and/or modify
728             it under the same terms as Perl itself.
729            
730             =cut
731            
732            
733