File Coverage

blib/lib/Netscape/Cache.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             # -*- perl -*-
2              
3             #
4             # $Id: Cache.pm,v 1.21.1.7 2007/11/05 20:51:06 eserte Exp $
5             # Author: Slaven Rezic
6             #
7             # Copyright (C) 1997 Slaven Rezic. All rights reserved.
8             # This package is free software; you can redistribute it and/or
9             # modify it under the same terms as Perl itself.
10             #
11             # Mail: eserte@cs.tu-berlin.de
12             # WWW: http://user.cs.tu-berlin.de/~eserte/
13             #
14              
15             =head1 NAME
16              
17             Netscape::Cache - object class for accessing Netscape cache files
18              
19             =head1 SYNOPSIS
20              
21             The object oriented interface:
22              
23             use Netscape::Cache;
24              
25             $cache = new Netscape::Cache;
26             while (defined($url = $cache->next_url)) {
27             print $url, "\n";
28             }
29              
30             while (defined($o = $cache->next_object)) {
31             print
32             $o->{'URL'}, "\n",
33             $o->{'CACHEFILE'}, "\n",
34             $o->{'LAST_MODIFIED'}, "\n",
35             $o->{'MIME_TYPE'}, "\n";
36             }
37              
38             The TIEHASH interface:
39              
40             use Netscape::Cache;
41              
42             tie %cache, 'Netscape::Cache';
43             foreach (sort keys %cache) {
44             print $cache{$_}->{URL}, "\n";
45             }
46              
47             =head1 DESCRIPTION
48              
49             The B module implements an object class for
50             accessing the filenames and URLs of the cache files used by the
51             Netscape web browser.
52              
53             Note: You can also use the undocumented pseudo-URLs C,
54             C and C to access your disk
55             cache, memory cache and global history.
56              
57             There is also an interface for using tied hashes.
58              
59             Netscape uses the old Berkeley DB format (version 1.85) for its cache
60             index C. Versions 2 and newer of Berkeley DB are
61             incompatible with the old format (L), so you have either
62             to downgrade or to convert the database using B and
63             B. See L for a
64             (experimental) converter function.
65              
66             =cut
67              
68             package Netscape::Cache;
69 1     1   8329 use strict;
  1         1  
  1         45  
70 1         115 use vars qw($Default_Preferences $Default_40_Preferences @Try_Preferences
71             $Default_Cache_Dir @Default_Cache_Index
72 1     1   6 $Debug $Home $OS_Type $VERSION);
  1         1  
73              
74 1     1   1413 use DB_File;
  0            
  0            
75              
76             if (defined $DB_File::db_version and $DB_File::db_version > 1) {
77             warn "Netscape::Cache works only if Berkeley db version 1 is\n" .
78             "installed. Please use the convert_185_2xx function to convert\n" .
79             "the cache index to the new db format (see manpage).\n";
80             }
81              
82             if ($^O =~ /^((ms)?(win|dos)|os2)/i) {
83             $Default_Preferences = 'C:\NETSCAPE\NETSCAPE.INI';
84             @Try_Preferences = qw(D:\NETSCAPE\NETSCAPE.INI
85             C:\INTERNET\NETSCAPE\NETSCAPE.INI
86             D:\INTERNET\NETSCAPE\NETSCAPE.INI
87             C:\PROGRAMS\NETSCAPE\NETSCAPE.INI);
88             $Default_Cache_Dir = 'C:\NETSCAPE\CACHE';
89             @Default_Cache_Index = qw(FAT.DB INDEX.DB);
90             $OS_Type = 'win';
91             } else {
92             $Home = $ENV{'HOME'} || (getpwuid($>))[7];
93             $Default_Preferences = "$Home/.netscape/preferences";
94             @Try_Preferences = ();
95             $Default_40_Preferences = "$Home/.netscape/preferences.js";
96             $Default_Cache_Dir = "$Home/.netscape/cache";
97             @Default_Cache_Index = qw(index.db FAT.DB fat.db Fat.db);
98             $OS_Type = 'unix';
99             }
100              
101             $Debug = 0;
102             $VERSION = '0.46';
103              
104             =head1 CONSTRUCTOR
105              
106             $cache = new Netscape::Cache(-cachedir => "$ENV{HOME}/.netscape/cache");
107              
108             This creates a new instance of the B object class. The
109             I<-cachedir> argument is optional. By default, the cache directory setting
110             is retrieved from C<~/.netscape/preferences>. The index file is normally
111             named C on Unix systems and C on Microsoft systems. It may
112             be changed with the I<-index> argument.
113              
114             If the Netscape cache index file does not exist, a warning message
115             will be generated, and the constructor will return C.
116              
117             =cut
118              
119             sub new ($;%) {
120             my($pkg, %a) = @_;
121             my($try, $indexfile);
122             my $cachedir = $a{-cachedir} || get_cache_dir() || $Default_Cache_Dir;
123             if ($a{'-index'}) {
124             $indexfile =
125             ($a{'-index'} =~ m|^/| ? $a{'-index'} : "$cachedir/$a{'-index'}");
126             } else {
127             foreach $try (@Default_Cache_Index) { #try all the names
128             $indexfile = "$cachedir/$try";
129             last if -f $indexfile; #exit when we find one
130             }
131             }
132             if (-f $indexfile) {
133             my %cache;
134             my $self = {};
135             if (!tie %cache, 'DB_File', $indexfile) {
136             warn
137             "Can't tie <$indexfile>. Maybe you are using version 2.x.x\n",
138             "of the Berkeley DB library?\n";
139             return undef;
140             }
141             $self->{CACHE} = \%cache;
142             $self->{CACHEDIR} = $cachedir;
143             $self->{CACHEFILE} = $indexfile;
144             bless $self, $pkg;
145             } else {
146             warn "No cache db found. Try to set the cache direcetory with\n" .
147             "-cachedir and the index file with -index.\n";
148             undef;
149             }
150             }
151              
152             sub TIEHASH ($;@) {
153             shift->new(@_);
154             }
155              
156             =head1 METHODS
157              
158             The B class implements the following methods:
159              
160             =over
161              
162             =item *
163              
164             B - reset cache index to first URL
165              
166             =item *
167              
168             B - get next URL from cache index
169              
170             =item *
171              
172             B - get next URL as a full B from
173             cache index
174              
175             =item *
176              
177             B - get a B for a given URL
178              
179             =back
180              
181             Each of the methods are described separately below.
182              
183             =head2 next_url
184              
185             $url = $history->next_url;
186              
187             This method returns the next URL from the cache index. Unlike
188             B, this method returns a string and not an
189             URI::URL-like object.
190              
191             This method is faster than B, since it does only evaluate the
192             URL of the cached file.
193              
194             =cut
195              
196             sub next_url ($) {
197             my $self = shift;
198             my $url;
199             do {
200             my $key = each %{ $self->{CACHE} };
201             return undef if !defined $key;
202             $url = Netscape::Cache::Object::url($key);
203             } while !$url;
204             $url;
205             }
206              
207             =head2 next_object
208              
209             $cache->next_object;
210              
211             This method returns the next URL from the cache index as a
212             B object. See below for accessing the components
213             (cache filename, content length, mime type and more) of this object.
214              
215             =cut
216              
217             sub next_object ($) {
218             my $self = shift;
219             my $o;
220             do {
221             my($key, $value) = each %{ $self->{CACHE} };
222             return undef if !defined $key;
223             $o = Netscape::Cache::Object->new($key, $value);
224             } while !defined $o;
225             $o;
226             }
227              
228             sub FIRSTKEY ($) {
229             my $self = shift;
230             $self->rewind;
231             my $o = $self->next_object;
232             defined $o ? $o->{URL} : undef;
233             }
234              
235             sub NEXTKEY ($) {
236             my $self = shift;
237             my $o = $self->next_object;
238             defined $o ? $o->{URL} : undef;
239             }
240              
241             =head2 get_object
242              
243             $cache->get_object;
244              
245             This method returns the B object for a given URL.
246             If the URL does not live in the cache index, then the returned value will be
247             undefined.
248              
249             =cut
250              
251             sub get_object ($$) {
252             my($self, $url) = @_;
253             my $key = Netscape::Cache::Object::_make_key_from_url($url);
254             my $value = $self->{CACHE}{$key};
255             $value ? new Netscape::Cache::Object($key, $value) : undef;
256             }
257              
258             sub FETCH ($$) {
259             shift->get_object(@_);
260             }
261              
262             sub EXISTS ($$) {
263             my($self, $url) = @_;
264             my $key = Netscape::Cache::Object::_make_key_from_url($url);
265             exists $self->{CACHE}{$key};
266             }
267              
268             =head2 delete_object
269              
270             Deletes URL from cache index and the related file from the cache.
271              
272             B Do not use B while in a B loop!
273             It is better to collect all objects for delete in a list and do the
274             deletion after the loop, otherwise you can get strange behavior (e.g.
275             malloc panics).
276              
277             =cut
278              
279             sub delete_object ($$) {
280             my($self, $url) = @_;
281             my $f = $self->{CACHEDIR} . "/" . $self->{CACHEFILE};
282             if (-e $f) {
283             return undef if !unlink $f;
284             }
285             delete $self->{CACHE}{$url->{'_KEY'}};
286             }
287              
288             sub DELETE ($$) {
289             my($self, $url) = @_;
290             my $key = Netscape::Cache::Object::_make_key_from_url($url);
291             delete $self->{CACHE}{$key};
292             }
293              
294             =head2 rewind
295              
296             $cache->rewind();
297              
298             This method is used to move the internal pointer of the cache index to
299             the first URL in the cache index. You do not need to bother with this
300             if you have just created the object, but it does not harm anything if
301             you do.
302              
303             =cut
304              
305             sub rewind ($) {
306             my $self = shift;
307             reset %{ $self->{CACHE} };
308             }
309              
310             sub CLEAR {
311             die "CLEARs are not permitted";
312             }
313              
314             sub STORE {
315             die "STOREs are not permitted";
316             }
317              
318             sub DESTROY ($) {
319             my $self = shift;
320             untie %{ $self->{CACHE} };
321             }
322              
323             =head2 get_object_by_cachefile
324              
325             $o = $cache->get_object_by_cachefile($cachefile);
326              
327             Finds the corresponding entry for a cache file and returns the object,
328             or undef if there is no such C<$cachefile>. This is useful, if you find
329             something in your cache directory by using B and you want to
330             know the URL and other attributes of this file.
331              
332             WARNING: Do not use this method while iterating with B, B
333             or B, because this method does iterating itself and would mess up
334             the previous iteration.
335              
336             =cut
337              
338             sub get_object_by_cachefile {
339             my($self, $cachefile) = @_;
340             $self->rewind;
341             my $o;
342             while(defined($o = $self->next_object)) {
343             if ($cachefile eq $o->{'CACHEFILE'}) {
344             return $o;
345             }
346             }
347             undef;
348             }
349              
350             =head2 get_object_by_cachefile
351              
352             $url = $cache->get_url_by_cachefile($cachefile);
353              
354             Finds the corresponding URL for a cache file. This method is implemented
355             using B.
356              
357             =cut
358              
359             sub get_url_by_cachefile {
360             my($self, $cachefile) = @_;
361             my $o = $self->get_object_by_cachefile($cachefile);
362             if (defined $o) {
363             $o->{'URL'};
364             } else {
365             undef;
366             }
367             }
368              
369             # internal subroutine to get the cache directory from Netscape's preferences
370             sub get_cache_dir {
371             my $cache_dir;
372             if ($Default_40_Preferences && open(PREFS, $Default_40_Preferences)) {
373             # try preferences from netscape 4.0
374             while() {
375             if (/user_pref\("browser.cache.directory",\s*"([^\"]+)"\)/) {
376             $cache_dir = $1;
377             last;
378             }
379             }
380             close PREFS;
381             }
382             if (!$cache_dir) {
383             my $pref;
384             TRY:
385             foreach $pref ($Default_Preferences, @Try_Preferences) {
386             if (open(PREFS, $pref)) {
387             if ($OS_Type eq 'unix') {
388             while() {
389             if (/^CACHE_DIR:\s*(.*)$/) {
390             $cache_dir = $1;
391             last;
392             }
393             }
394             } elsif ($OS_Type eq 'win') {
395             my $cache_section_found;
396             while() { # read .ini file
397             if ($cache_section_found) {
398             if (/^cache dir=(.*)$/i) {
399             ($cache_dir = $1) =~ s/\r//g; # strip ^M
400             last;
401             } elsif (/^\[/) { # new section found
402             undef $cache_section_found;
403             redo; # maybe the new section is a cache section too?
404             }
405             } elsif (/^\[Cache\]/i) { # cache section found
406             $cache_section_found++;
407             }
408             }
409             }
410             close PREFS;
411             last TRY;
412             }
413             }
414             }
415             if ($OS_Type eq 'unix' && defined $cache_dir) {
416             $cache_dir =~ s|^~/|$Home/|;
417             }
418             $cache_dir;
419             }
420              
421             =head2 convert_185_2xx
422              
423             $newindex = Netscape::Cache::convert_185_2xx($origindex [, $tmploc])
424              
425             This is a (experimental) utility for converting C to the new
426             Berkeley DB 2.x.x format. Note that this function will not overwrite
427             the original C, but rather copy the converted index to
428             C<$tmploc> or C, if C<$tmploc> is not given.
429             B returns the filename of the new created index file.
430             The converted index is only temporary, and all write access is
431             useless.
432              
433             Usage example:
434              
435             my $newindex = Netscape::Cache::convert_185_2xx($indexfile);
436             my $o = new Netscape::Cache -index => $newindex;
437              
438             =cut
439              
440             sub convert_185_2xx {
441             my($indexfile, $tmploc) = @_;
442             my $success = 0;
443             my $tmpdir;
444             foreach (qw(/tmp /temp .)) {
445             if (-d $_ && -w $_) {
446             $tmpdir = $_;
447             last;
448             }
449             }
450             die "No /tmp or /temp directory writeable" if !defined $tmpdir;
451             die "usage: convert_185_2xx(indexfile [,tmploc])"
452             unless defined $indexfile;
453             $tmploc = "$tmpdir/index.$$.db"
454             unless defined $tmploc;
455             my $tmpdump = "$tmpdir/dump";
456             system("db_dump185 $indexfile > $tmpdump");
457             if ($?) { warn $!;
458             goto CLEANUP }
459             chmod 0600, $tmpdump;
460             system("db_load $tmploc < $tmpdump");
461             if ($?) { warn $!;
462             unlink $tmploc;
463             goto CLEANUP }
464             chmod 0600, $tmploc;
465             $success++;
466             CLEANUP:
467             unlink $tmpdump;
468             $success ? $tmploc : undef;
469             }
470              
471             package Netscape::Cache::Object;
472             use strict;
473             use vars qw($Debug);
474              
475             $Debug = $Netscape::Cache::Debug;
476              
477             =head1 Netscape::Cache::Object
478              
479             B and B return an object of the class
480             B. This object is simply a hash, which members
481             have to be accessed directly (no methods).
482              
483             An example:
484              
485             $o = $cache->next_object;
486             print $o->{'URL'}, "\n";
487              
488             =over 4
489              
490             =item URL
491              
492             The URL of the cached object
493              
494             =item COMPLETE_URL
495              
496             The complete URL with the query string attached (only Netscape 4.x).
497              
498             =item CACHEFILE
499              
500             The filename of the cached URL in the cache directory. To construct the full
501             path use (C<$cache> is a B object and C<$o> a
502             B object)
503              
504             $cache->{'CACHEDIR'} . "/" . $o->{'CACHEFILE'}
505              
506             =item CACHEFILE_SIZE
507              
508             The size of the cache file.
509              
510             =item CONTENT_LENGTH
511              
512             The length of the cache file as specified in the HTTP response header.
513             In general, SIZE and CONTENT_LENGTH are equal. If you interrupt a transfer of
514             a file, only the first part of the file is written to the cache, resulting
515             in a smaller CONTENT_LENGTH than SIZE.
516              
517             =item LAST_MODIFIED
518              
519             The date of last modification of the URL as unix time (seconds since
520             epoch). Use
521              
522             scalar localtime $o->{'LAST_MODIFIED'}
523              
524             to get a human readable date.
525              
526             =item LAST_VISITED
527              
528             The date of last visit.
529              
530             =item EXPIRE_DATE
531              
532             If defined, the date of expiry for the URL.
533              
534             =item MIME_TYPE
535              
536             The MIME type of the URL (eg. text/html or image/jpeg).
537              
538             =item ENCODING
539              
540             The encoding of the URL (eg. x-gzip for gzipped data).
541              
542             =item CHARSET
543              
544             The charset of the URL (eg. iso-8859-1).
545              
546             =item NS_VERSION
547              
548             The version of Netscape which created this cache file (C<3> for
549             Netscape 2.x and 3.x, C<4> for Netscape 4.0x and C<5> for Netscape
550             4.5).
551              
552             =back
553              
554             =cut
555              
556             sub new ($$;$) {
557             my($pkg, $key, $value) = @_;
558              
559             return undef if !defined $value || $value eq '';
560              
561             my $url = url($key);
562             return undef if !$url;
563              
564             my $self = {};
565             bless $self, $pkg;
566             $self->{URL} = $url;
567              
568             $self->{'_KEY'} = $key;
569              
570             my($rest, $len, $last_modified, $expire_date);
571             ($self->{NS_VERSION},
572             $last_modified,
573             $self->{LAST_VISITED},
574             $expire_date,
575             $self->{CACHEFILE_SIZE},
576             $self->{'_XXX_FLAG_2'}) = unpack("V6", substr($value, 4));
577             ($self->{CACHEFILE}, $rest) = split(/\000/, substr($value, 33), 2);
578             $self->{'_XXX_FLAG_3'} = unpack("V", substr($rest, 4, 4));
579             $self->{'_XXX_FLAG_4'} = unpack("V", substr($rest, 25, 4));
580             $self->{LAST_MODIFIED} = $last_modified if $last_modified != 0;
581             $self->{EXPIRE_DATE} = $expire_date if $expire_date != 0;
582            
583             if ($Debug) {
584             $self->_report(1, $key, $value,
585             "<".substr($rest, 0, 4)."><".substr($rest, 8, 17)
586             ."><".substr($rest, 29, 4).">")
587             if substr($rest, 0, 4) =~ /[^\000]/
588             || substr($rest, 8, 17) =~ /[^\000]/
589             || substr($rest, 29, 4) =~ /[^\000]/;
590             }
591            
592             my $inx;
593             if ($self->{NS_VERSION} >= 5) {
594             $inx = 21;
595             } else {
596             $inx = 33;
597             }
598             $len = unpack("V", substr($rest, $inx, 4));
599             if ($len) {
600             $self->{MIME_TYPE} = substr($rest, $inx+4, $len-1);
601             }
602             $rest = substr($rest, $inx+4 + $len);
603            
604             $len = unpack("V", substr($rest, 0, 4));
605             if ($len) {
606             $self->{ENCODING} = substr($rest, 4, $len-1);
607             }
608             $rest = substr($rest, 4 + $len);
609            
610             $len = unpack("V", substr($rest, 0, 4));
611             if ($len) {
612             $self->{CHARSET} = substr($rest, 4, $len-1);
613             }
614             $rest = substr($rest, 4 + $len);
615            
616             $self->{CONTENT_LENGTH} = unpack("V", substr($rest, 1, 4));
617             $rest = substr($rest, 5);
618              
619             $len = unpack("V", substr($rest, 0, 4));
620             if ($len) {
621             $self->{COMPLETE_URL} = substr($rest, 4, $len-1);
622             }
623             $rest = substr($rest, 4 + $len);
624              
625             if ($Debug) {
626             $self->_report(2, $key, $value)
627             if $rest =~ /[^\000]/;
628              
629             my $record_length = unpack("V", substr($value, 0, 4));
630             warn "Invalid length for value of <$key>\n"
631             if $record_length != length($value);
632             $self->_report(4, $key, $value)
633             if $self->{'_XXX_FLAG_2'} != 0 && $self->{'_XXX_FLAG_2'} != 1;
634             $self->_report(5, $key, $value)
635             if $self->{'_XXX_FLAG_3'} != 1;
636             $self->_report(6, $key, $value)
637             if $self->{'_XXX_FLAG_4'} != 0 && $self->{'_XXX_FLAG_4'} != 1;
638             }
639              
640             $self;
641             }
642              
643             sub url ($) {
644             my $key = shift;
645             my $keylen2 = unpack("V", substr($key, 4, 4));
646             my $keylen1 = unpack("V", substr($key, 0, 4));
647             if ($keylen1 == $keylen2 + 12) {
648             substr($key, 8, $keylen2-1);
649             } # else probably one of INT_CACHESIZE etc.
650             }
651              
652             sub _report {
653             my($self, $errno, $key, $value, $addinfo) = @_;
654             if ($self->{'_ERROR'} && $Debug < 2) {
655             warn "Error number $errno\n";
656             } else {
657             warn
658             "Please report:\nError number $errno\nURL: "
659             . $self->{URL} . "\nEncoded URL: <"
660             . join("", map { sprintf "%02x", ord $_ } split(//, $key))
661             . ">\nEncoded Properties: <"
662             . join("", map { sprintf "%02x", ord $_ } split(//, $value))
663             . ">\n"
664             . ($addinfo ? "Additional Info: <$addinfo>\n" : "")
665             . "\n";
666             }
667             $self->{'_ERROR'}++;
668             }
669              
670             sub _make_key_from_url ($) {
671             my $url = shift;
672             pack("V", length($url)+13) . pack("V", length($url)+1)
673             . $url . ("\000" x 5);
674             }
675              
676             =head1 AN EXAMPLE PROGRAM
677              
678             This program loops through all cache objects and prints a HTML-ified list.
679             The list is sorted by URL, but you can sort it by last visit date or size,
680             too.
681              
682             use Netscape::Cache;
683              
684             $cache = new Netscape::Cache;
685              
686             while ($o = $cache->next_object) {
687             push(@url, $o);
688             }
689             # sort by name
690             @url = sort {$a->{'URL'} cmp $b->{'URL'}} @url;
691             # sort by visit time
692             #@url = sort {$b->{'LAST_VISITED'} <=> $a->{'LAST_VISITED'}} @url;
693             # sort by mime type
694             #@url = sort {$a->{'MIME_TYPE'} cmp $b->{'MIME_TYPE'}} @url;
695             # sort by size
696             #@url = sort {$b->{'CACHEFILE_SIZE'} <=> $a->{'CACHEFILE_SIZE'}} @url;
697              
698             print "
    \n";
699             foreach (@url) {
700             print
701             "
  • 702             $cache->{'CACHEDIR'}, "/", $_->{'CACHEFILE'}, "\">",
    703             $_->{'URL'}, " ",
    704             scalar localtime $_->{'LAST_VISITED'}, "
    ",
    705             "type: ", $_->{'MIME_TYPE'},
    706             ",size: ", $_->{'CACHEFILE_SIZE'}, "\n";
    707             }
    708             print "\n";
    709              
    710             =head1 FORMAT OF index.db
    711              
    712             Here is a short description of the format of index.db. All integers
    713             are in VAX byte order (little endian). Time is specified as seconds
    714             since epoch.
    715              
    716             Key:
    717              
    718             Offset Type/Length Description
    719              
    720             0 long Length of key entry
    721             4 long Length of URL with trailing \0
    722             8 string URL (null-terminated)
    723             +0 string filled with \0
    724              
    725             Value:
    726              
    727             Offset Type/Length Description
    728              
    729             0 long Length of value entry
    730             4 long A version number (see NS_VERSION)
    731             8 long Last modified
    732             12 long Last visited
    733             16 long Expire date
    734             20 long Size of cachefile
    735             24 ... Unknown
    736             29 long Length of cache filename with trailing \0
    737             33 string Cache filename (null-terminated)
    738             +0 ... Unknown
    739             +33 long Length of mime type with trailing \0
    740             +37 string Mime type (null-terminated)
    741             +0 long Length of content encoding with trailing \0
    742             +4 string Content encoding (null-terminated)
    743             +0 long Length of charset with trailing \0
    744             +4 string Charset (null-terminated)
    745             +0 ... Unknown
    746             +1 long Content length
    747             +5 long Length of the complete URL with trailing \0
    748             +9 string Complete URL (null-terminated)
    749              
    750             =head1 ENVIRONMENT
    751              
    752             The B module examines the following environment variables:
    753              
    754             =over 4
    755              
    756             =item HOME
    757              
    758             Home directory of the user, used to find Netscape's preferences
    759             (C<$HOME/.netscape>). Otherwise, if not set, retrieve the home directory
    760             from the passwd file.
    761              
    762             =back
    763              
    764             =head1 BUGS
    765              
    766             There are still some unknown fields (_XXX_FLAG_{2,3,4}).
    767              
    768             You can't use B while looping with B. See the
    769             question "What happens if I add or remove keys from a hash while iterating
    770             over it?" in L.
    771              
    772             B or B on the tied hash are slower than the object
    773             oriented equivalents B or B.
    774              
    775             =head1 SEE ALSO
    776              
    777             L
    778              
    779             =head1 AUTHOR
    780              
    781             Slaven Rezic
    782              
    783             Thanks to: Fernando Santagata
    784              
    785             =head1 COPYRIGHT
    786              
    787             Copyright (c) 1997 Slaven Rezic. All rights reserved.
    788             This module is free software; you can redistribute it and/or modify
    789             it under the same terms as Perl itself.
    790              
    791             =cut
    792              
    793             1;