File Coverage

blib/lib/File/Mork.pm
Criterion Covered Total %
statement 137 154 88.9
branch 45 68 66.1
condition 10 19 52.6
subroutine 15 17 88.2
pod 5 8 62.5
total 212 266 79.7


line stmt bran cond sub pod time code
1             package File::Mork;
2              
3 3     3   104489 use strict;
  3         8  
  3         175  
4 3     3   90 use vars qw($VERSION $ERROR);
  3         6  
  3         259  
5 3     3   3817 use POSIX qw(strftime);
  3         37099  
  3         23  
6 3     3   14300 use Encode;
  3         60113  
  3         18263  
7              
8             $VERSION = "0.3";
9              
10             =head1 NAME
11              
12             File::Mork - a module to read Mozilla URL history files
13              
14             =head1 SYNOPSIS
15              
16             my $mork = File::Mork->new($filename, verbose => 1)
17             || die $File::Mork::ERROR."\n";
18              
19              
20             foreach my $entry ($mork->entries) {
21             while (my($key,$val) = each %$entry) {
22             printf ("%14s = %s\n", $key, $val);
23             }
24             }
25              
26             =head1 DESCRIPTION
27              
28             This is a module that can read the Mozilla URL history file -- normally
29             $HOME/.mozilla/default/*.slt/history.dat -- and extract the id, url,
30             name, hostname, first visted dat, last visited date and visit count.
31              
32             To find your history file it might be worth using B
33             which has some platform-independent code for finding the profiles of
34             various Mozilla-isms (including Firefox, Camino, K-Meleon, etc.).
35              
36             =cut
37              
38             =head1 METHODS
39              
40             =head2 new [opts]
41              
42             Takes a filename and parses that file.
43              
44             Returns C on error, setting C<$File::Mork::Error>.
45              
46             Takes an optional hash of options
47              
48             =over 4
49              
50             =item
51              
52             verbose
53              
54             A value up to 3 - defines the level of verbosity
55              
56             =item
57              
58             age
59              
60             A ctime which forces C to only parse entries later than this.
61              
62             =back
63              
64             =cut
65              
66             sub new {
67 2     2 1 891 my ($class, $file, %opts) = @_;
68 2         9 my $self = bless \%opts, $class;
69              
70 2   50     24 $self->{verbose} ||= 0;
71              
72 2 100       11 unless ($self->parse($file)) {
73 1         3 $ERROR = $self->{error};
74 1         15 return;
75             }
76              
77 1         9 return $self;
78             }
79              
80              
81             ##########################################################################
82             # Define the messy regexen up here
83             ##########################################################################
84              
85             my $top_level_comment = qr@//.*\n@;
86              
87             my $key_table_re = qr/ < \s* < # "< <"
88             \( a=c \) > # "(a=c)>"
89             (?> ([^>]*) ) > \s* # Grab anything that's not ">"
90             /sx;
91              
92             my $value_table_re = qr/ < ( .*?\) )> \s* /sx;
93              
94             my $table_re = qr/ \{ -? # "{" or "{-"
95             [\da-f]+ : # hex, ":"
96             (?> .*?\{ ) # Eat up to a {...
97             ((?> .*?\} ) # and then the closing }...
98             (?> .*?\} )) # Finally, grab the table section
99             \s* /six;
100              
101             my $row_re = qr/ ( (?> \[ [^]]* \] # "["..."]"
102             \s*)+ ) # Perhaps repeated many times
103             /sx;
104              
105             my $section_begin_re = qr/ \@\$\$\{ # "@$${"
106             ([\dA-F]+) # hex
107             \{\@ \s* # "{@"
108             /six;
109              
110              
111             my $crlf = qr/\x0a\x0d|\x0d\x0a|\x0a|\x0d/; # We are liberal in what we accept.
112             # But then, so is a six dollar whore.
113              
114             =head2 parse
115              
116             Internal method to parse the file. Obviously.
117              
118             =cut
119              
120             sub parse {
121 2     2 1 4 my ($self, $file) = @_;
122              
123 2 50       11 $self->{since} = ($self->{age} ? time() - $self->{age} : 0);
124 2         5 $self->{section} = "top level";
125 2         6 $self->{section_end_re} = undef;
126              
127              
128              
129             ##########################################################################
130             # Read in the file.
131             ##########################################################################
132              
133 2         7 local $/ = undef;
134 2         6 local *IN;
135              
136 2         5 $self->{file} = $file;
137 2         4 $self->{total} = 0;
138 2         6 $self->{skipped} = 0;
139              
140 2 100       89 unless (open (IN, $file)) {
141 1         12 $self->{error} = "Couldn't open $file : $!";
142 1         8 return;
143             }
144              
145 1         9 $self->debug("reading ...",1);
146 1         38 my $body = ;
147 1         11 close IN;
148              
149              
150 1         108 $body =~ s/($crlf)/\n/gs; # Windows Mozilla uses \r\n
151             # Presumably Mac Mozilla is similarly dumb
152              
153 1         6 $body =~ s/\\\\/\$5C/gs; # Sometimes backslash is quoted with a
154             # backslash; convert to hex.
155 1         19 $body =~ s/\\\)/\$29/gs; # close-paren is quoted with a backslash;
156             # convert to hex.
157 1         14 $body =~ s/\\\n//gs; # backslash at end of line is continuation.
158              
159             ##########################################################################
160             # Figure out what we're looking at, and parse it.
161             ##########################################################################
162              
163 1         4 $self->debug("parsing ...",1);
164 1         4 pos($body) = 0;
165 1         4 my $length = length($body);
166              
167 1         5 while( pos($body) < $length ) {
168 10         22 my $section_end_re = $self->{section_end_re};
169             # Key table
170              
171 10 100 66     418 if ( $body =~ m/\G$key_table_re/gc ) {
    100          
    100          
    50          
    100          
    100          
    50          
172 1 50       5 return unless $self->parse_key_table($1);
173              
174             # Values
175             } elsif ( $body =~ m/\G$value_table_re/gco ) {
176 2 50       9 return unless $self->parse_value_table($1);
177              
178             # Table
179             } elsif ( $body =~ m/\G$table_re/gco ) {
180 2 50       7 return unless $self->parse_table($1);
181              
182             # Rows (-> table)
183             } elsif ( $body =~ m/\G$row_re/gco ) {
184 0 0       0 return unless $self->parse_table($1);
185              
186             # Section begin
187             } elsif ( $body =~ m/\G$section_begin_re/gco ) {
188 2         4 my $section = $1;
189 2         31 $self->{section_end_re} = qr/\@\$\$\}$section\}\@\s*/s;
190 2         8 $self->{section} = $section;
191             # Section end
192             } elsif ( $section_end_re && $body =~ m/\G$section_end_re/gc ) {
193 2         5 $self->{section_end_re} = undef;
194 2         12 $self->{section} = "top level";
195              
196             # Comment
197             } elsif ( $body =~ m/\G$top_level_comment/gco ) {
198             #no-op
199              
200             } else {
201             # $body =~ m/\G (.{0,300}) /gcsx; print "<$1>\n";
202 0         0 return $self->error($self->{section}.": Cannot parse");
203             }
204             }
205              
206 1 50       5 if($self->{section_end_re}) {
207 0         0 return $self->error("Unterminated section ".$self->{section});
208             }
209              
210              
211 1         7 $self->debug("sorting...",1);
212              
213 7         38 my @entries = map { File::Mork::Entry->new(%$_) }
  13         22  
214 1         10 sort { $b->{LastVisitDate} <=>
215 1         3 $a->{LastVisitDate} } values(%{$self->{row_hash}});
216              
217 1         9 $self->debug("done! (".$self->{total}." total, ".$self->{skipped}." skipped)",1);
218              
219 1         3 for (qw(key_table val_table row_hash total skipped)) {
220 5         37 $self->{$_} = undef;
221             }
222              
223 1         379 $self->{entries} = \@entries;
224 1         13 return 1;
225             }
226              
227             =head2 entries
228              
229             Return a list of C objects sorted by B.
230              
231             =cut
232              
233             sub entries {
234 1     1 1 3 return @{$_[0]->{entries}};
  1         8  
235             }
236              
237              
238             ##########################################################################
239             # parse a row and column table
240             ##########################################################################
241              
242             sub parse_table {
243 2     2 0 7 my($self, $table_part) = (@_);
244              
245 2         5 $self->debug("",3);
246              
247             # Assumption: no relevant spaces in values in this section
248 2         20 $table_part =~ s/\s+//g;
249              
250             # print $table_part; #exit(0);
251              
252             # Grab each complete [...] block
253 2         14 while( $table_part =~ m/\G [^[]* \[ # find a "["
254             ( [^]]+ ) \] # capture up to "]"
255             /gcx ) {
256 7         15 $_ = $1;
257              
258 7         46 my ($id, @cells) = split (m/[()]+/s);
259              
260 7 50       29 next unless scalar(@cells);
261              
262             # Trim junk
263 7         15 $id =~ s/^-//;
264 7         11 $id =~ s/:.*//;
265              
266 7 50       33 my %hash = ($self->{row_hash}->{$id}) ? %{$self->{row_hash}->{$id}} :
  0         0  
267             ( 'ID' => $id,
268             'LastVisitDate' => 0 );
269              
270 7         12 foreach (@cells) {
271 35 50       71 next unless $_;
272              
273 35         610 my ($keyi, $which, $vali) =
274             m/^\^ ([-\dA-F]+)
275             ([\^=])
276             (.*)
277             $/xi;
278              
279 35 50       71 return $self->error("unparsable cell: $_\n") unless defined ($vali);
280              
281             # If the key isn't in the key table, ignore it
282             #
283 35         66 my $key = $self->{key_table}->{$keyi};
284 35 50       60 next unless defined($key);
285              
286 35 100       77 my $val = ($which eq '='
287             ? $vali
288             : $self->{val_table}->{$vali});
289              
290 35 100 100     522 if ($key eq 'LastVisitDate' || $key eq 'FirstVisitDate') {
291 12         27 $val = int ($val / 1000000); # we don't need milliseconds, dude.
292             }
293              
294 35         91 $hash{$key} = $val;
295             #print "$id: $key -> $val\n";
296             }
297              
298              
299 7 50 0     23 if ($self->{age} && ($hash{LastVisitDate} || $self->{since}) < $self->{since}) {
      33        
300 0         0 $self->debug("skipping old: $hash{LastVisitDate} $hash{URL}",3);
301 0         0 $self->{skipped}++;
302 0         0 next;
303             }
304              
305 7         9 $self->{total}++;
306 7         49 $self->{row_hash}->{$id} = \%hash;
307             }
308 2         13 return 1;
309             }
310              
311              
312             ##########################################################################
313             # parse a values table
314             ##########################################################################
315              
316             sub parse_value_table {
317 2     2 0 10 my($self, $val_part) = (@_);
318              
319 2 50       7 return 1 unless $val_part;
320              
321 2         82 my @pairs = split (m/\(([^\)]+)\)/, $val_part);
322 2         6 $val_part = undef;
323              
324 2         6 $self->debug("",3);
325              
326 2         4 foreach (@pairs) {
327 58 100       183 next unless (m/[^\s]/s);
328 29         134 my ($key, $val) = m/([\dA-F]*)[\t\n ]*=[\t\n ]*(.*)/i;
329              
330 29 50       61 if (! defined ($val)) {
331 0         0 $self->debug($self->{section}.": unparsable val: $_");
332 0         0 next;
333             }
334              
335             # recognize the byte order of UTF-16 encoding
336 29 100 66     118 if (! defined ($self->{byte_order}) && $val =~ m/(?:BE|LE)/) {
337 1         4 $self->{byte_order} = $val;
338             }
339              
340             # Assume that URLs and LastVisited are never hexilated; so
341             # don't bother unhexilating if we won't be using Name, etc.
342 29 100       77 if($val =~ m/\$/) {
343 6 50       18 if ( defined $self->{byte_order} ) {
344 6         11 my $encoding = 'UTF-16' . $self->{byte_order};
345 6         28 $val =~ s/\$([\dA-F]{2})/chr(hex($1))/ge;
  319         949  
346 6         25 $val = encode_utf8(decode($encoding, $val));
347             }
348             else {
349             # Approximate wchar_t -> ASCII and remove NULs
350 0         0 $val =~ s/\$00//g; # faster if we remove these first
351 0         0 $val =~ s/\$([\dA-F]{2})/chr(hex($1))/ge;
  0         0  
352             }
353             }
354              
355 29         16716 $self->{val_table}->{$key} = $val;
356 29         114 $self->debug($self->{section}.": val $key = \"$val\"", 3);
357             }
358 2         28 return 1;
359             }
360              
361              
362             ##########################################################################
363             # parse a key table
364             ##########################################################################
365              
366             sub parse_key_table {
367 1     1 0 70 my ($self, $key_table) = (@_);
368              
369 1         5 $self->debug("",3);
370              
371 1         8 $key_table =~ s@\s+//.*$@@gm;
372              
373 1         40 my @pairs = split (m/\(([^\)]+)\)/s, $key_table);
374 1         4 $key_table = undef;
375              
376 1         3 foreach (@pairs) {
377 26 100       78 next unless (m/[^\s]/s);
378 13         60 my ($key, $val) = m/([\dA-F]+)\s*=\s*(.*)/i;
379 13 50       32 return $self->error ("unparsable key: $_") unless defined ($val);
380              
381             # savie the other fields that we aren't interested in.
382 13         38 $self->{key_table}->{$key} = $val;
383 13         92 $self->debug($self->{section}.": key $key = \"$val\"",3);
384             }
385 1         10 return 1;
386             }
387              
388              
389             =head2 error
390              
391             Internal method to set the internal error message
392              
393             =cut
394              
395             sub error {
396 0     0 1 0 my ($self, $message) = @_;
397 0         0 $self->{error} = $self->{file}.": $message";
398 0         0 return undef;
399             }
400              
401             =head2 debug
402              
403             Internal method to print out a debug message if it's a higher priority
404             than the the current verbosity level.
405              
406             =cut
407              
408             sub debug {
409 51     51 1 77 my ($self, $message, $level) = @_;
410 51   50     130 $level ||= 0;
411 51 50       309 return if $self->{verbose} < $level;
412 0 0       0 print STDERR "".(($message eq "")? "\n" : $self->{file}.": $message\n" );
413             }
414              
415              
416             =head1 THE UGLY TRUTH LAID BARE
417              
418             I
419              
420             In Netscape Navigator 1.0 through 4.0, the history.db file was just a
421             Berkeley DBM file. You could trivially bind to it from Perl, and pull
422             out the URLs and last-access time. In Mozilla, this has been replaced
423             with a "Mork" database for which no tools exist.
424              
425             Let me make it clear that McCusker is a complete barking lunatic.
426             This is just about the stupidest file format I've ever seen.
427              
428             http://www.mozilla.org/mailnews/arch/mork/primer.txt
429             http://jwz.livejournal.com/312657.html
430             http://www.jwz.org/doc/mailsum.html
431             http://bugzilla.mozilla.org/show_bug.cgi?id=241438
432              
433             In brief, let's count its sins:
434              
435             =over 4
436              
437             =item
438            
439             Two different numerical namespaces that overlap.
440              
441             =item
442              
443             It can't decide what kind of character-quoting syntax to use:
444             Backslash? Hex encoding with dollar-sign?
445              
446             =item
447              
448             C++ line comments are allowed sometimes, but sometimes // is just a
449             pair of characters in a URL.
450              
451             =item
452              
453             It goes to all this serious compression effort (two different
454             string-interning hash tables) and then writes out Unicode strings
455             without using UTF-8: writes out the unpacked wchar_t characters!
456              
457             =item
458              
459             Worse, it hex-encodes each wchar_t with a 3-byte encoding, meaning the
460             file size will be 3x or 6x (depending on whether whchar_t is 2 bytes or
461             4 bytes.)
462              
463             =item
464              
465             It masquerades as a "textual" file format when in fact it's just
466             another binary-blob file, except that it represents all its magic
467             numbers in ASCII. It's not human-readable, it's not hand-editable, so
468             the only benefit there is to the fact that it uses short lines and
469             doesn't use binary characters is that it makes the file bigger. Oh wait,
470             my mistake, that isn't actually a benefit at all.
471              
472             =back
473              
474             Pure comedy.
475              
476              
477             =head1 AUTHOR
478              
479             Module-ised by Simon Wistow
480              
481             based on
482              
483             http://www.jwz.org/hacks/mork.pl
484              
485             Created: 3-Mar-2004 by Jamie Zawinski, Anonymous, and Jacob Post.
486              
487              
488             =head1 COPYRIGHT
489              
490             Copyright © 2004 Jamie Zawinski
491              
492             =head1 LICENSE
493              
494             Permission to use, copy, modify, distribute, and sell this software and its
495             documentation for any purpose is hereby granted without fee, provided that
496             the above copyright notice appear in all copies and that both that
497             copyright notice and this permission notice appear in supporting
498             documentation. No representations are made about the suitability of this
499             software for any purpose. It is provided "as is" without express or
500             implied warranty.
501              
502             =head1 BUGS
503              
504             Might be a bit memory heavy? Could do with an iterator interface.
505              
506             Can't write Mork dbs.
507              
508             =head1 SEE ALSO
509              
510             http://www.livejournal.com/users/jwz/312657.html
511              
512             http://www.erys.org/resume/netscape/mork/jwz.html
513              
514             =cut
515              
516              
517             package File::Mork::Entry;
518 3     3   37 use strict;
  3         6  
  3         145  
519 3     3   30 use vars qw($AUTOLOAD);
  3         7  
  3         668  
520              
521             =head1 NAME
522              
523             File::Mork::Entry - an single entry in a mork DB
524              
525             =head1 METHODS
526              
527             All methods except C take an optional argument to set them.
528              
529             =head2 new <%opts>
530              
531             blesses C<%opts> into the class File::Mork::Entry
532              
533             =cut
534              
535             sub new {
536 7     7   31 my ($class, %self) = @_;
537 7         29 return bless \%self, $class;
538             }
539              
540              
541             =head2 ID
542              
543             The internal id of the entry
544              
545             =head2 URL
546              
547             The url visited
548              
549             =head2 NAME
550              
551             The name of the url visited
552              
553             =head2 Hostname
554              
555             The hostname of the url visited
556              
557             =head2 FirstVisitDate
558              
559             The first time this url was visited as a C
560              
561             =head2 LastVisitDate
562              
563             The last time this url was visited as a C
564              
565             =head2 Hidden
566              
567             Whether this URL is hidden from the history list or not
568              
569             =head2 VisitCount
570              
571             The number of times this url has been visited
572              
573             =head2 ByteOrder
574              
575             The byte order - this is associated with ID number 1.
576              
577             =cut
578              
579 0     0   0 sub DESTROY { }
580              
581             sub AUTOLOAD {
582 14     14   3045 my $self = shift;
583 14         20 my $attr = $AUTOLOAD;
584 14         57 $attr =~ s/.*:://;
585              
586 14 50       44 $self->{$attr} = $_[0] if @_;
587 14         77 return $self->{$attr};
588             }
589              
590             1;