File Coverage

blib/lib/Palm/PDB.pm
Criterion Covered Total %
statement 263 463 56.8
branch 104 252 41.2
condition 21 64 32.8
subroutine 13 24 54.1
pod 13 13 100.0
total 414 816 50.7


line stmt bran cond sub pod time code
1             package Palm::PDB;
2             #
3             # Perl module for reading and writing Palm databases (both PDB and PRC).
4             #
5             # Copyright (C) 1999, 2000, Andrew Arensburger.
6             #
7             # This program is free software; you can redistribute it and/or modify
8             # it under the same terms as Perl itself.
9             #
10             # This program is distributed in the hope that it will be useful,
11             # but WITHOUT ANY WARRANTY; without even the implied warranty of
12             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See either the
13             # GNU General Public License or the Artistic License for more details.
14             #
15             # A Palm database file (either .pdb or .prc) has the following overall
16             # structure:
17             # Header
18             # Index header
19             # Record/resource index
20             # Two NUL(?) bytes
21             # Optional AppInfo block
22             # Optional sort block
23             # Records/resources
24             # See http://www.palmos.com/dev/tech/docs/fileformats.zip
25             # for details.
26              
27 3     3   33537 use 5.006;
  3         8  
  3         92  
28 3     3   12 use strict;
  3         3  
  3         197  
29              
30             our $VERSION = '1.016';
31             # This file is part of Palm-PDB 1.016 (January 24, 2015)
32              
33             # ABSTRACT: Parse Palm database files
34              
35              
36             use constant 1.03 { # accepts hash reference
37 3         11147 dmRecordIDReservedRange => 1, # The range of upper bits in the database's
38             # uniqueIDSeed from 0 to this number are
39             # reserved and not randomly picked when a
40             #database is created.
41              
42             EPOCH_1904 => 2082844800, # Difference between Palm's
43             # epoch (Jan. 1, 1904) and
44             # Unix's epoch (Jan. 1, 1970),
45             # in seconds.
46             HeaderLen => 32+2+2+(9*4), # Size of database header
47             RecIndexHeaderLen => 6, # Size of record index header
48             IndexRecLen => 8, # Length of record index entry
49             IndexRsrcLen => 10, # Length of resource index entry
50 3     3   14 };
  3         57  
51              
52             our %PDBHandlers = (); # Record handler map
53             our %PRCHandlers = (); # Resource handler map
54              
55              
56             sub new
57             {
58 5     5 1 1403 my $class = shift;
59 5         9 my $params = shift;
60              
61 5         8 my $self = {};
62              
63              
64             # Initialize the PDB. These values are just defaults, of course.
65 5   50     32 $self->{'name'} = $params->{'name'} || "";
66 5   50     52 $self->{'attributes'} = $params->{'attributes'} || {};
67 5   50     28 $self->{'version'} = $params->{'version'} || 0;
68              
69 5         17 my $now = time;
70              
71 5   33     22 $self->{'ctime'} = $params->{'ctime'} || $now;
72 5   33     19 $self->{'mtime'} = $params->{'mtime'} || $now;
73 5   50     25 $self->{'baktime'} = $params->{'baktime'} || -(EPOCH_1904);
74              
75 5   50     17 $self->{'modnum'} = $params->{'modnum'} || 0;
76 5   50     22 $self->{'type'} = $params->{'type'} || "\0\0\0\0";
77 5   50     27 $self->{'creator'} = $params->{'creator'} || "\0\0\0\0";
78 5   50     15 $self->{'uniqueIDseed'} = $params->{'uniqueIDseed'} || 0;
79              
80 5         7 $self->{"2NULs"} = "\0\0";
81              
82             # This will be set when any elements of the object are changed
83 5         8 $self->{'dirty'} = 0;
84              
85              
86             # Calculate a proper uniqueIDseed if the user has not provided
87             # a correct one.
88 5 50       11 if ($self->{'uniqueIDseed'} <= ((dmRecordIDReservedRange + 1) << 12))
89             {
90 5         6 my $uniqueIDseed = 0;
91              
92             do
93 5         6 {
94 5         75 $uniqueIDseed = int(rand(0x0FFF));
95              
96             } while ($uniqueIDseed <= dmRecordIDReservedRange);
97              
98 5         8 $self->{'uniqueIDseed'} = $uniqueIDseed << 12;
99 5         7 $self->{'uniqueIDseed'} &= 0x00FFF000; # Isolate the upper 12 seed bits.
100             }
101              
102 5         8 bless $self, $class;
103 5         14 return $self;
104             }
105              
106             #' <-- For Emacs.
107              
108             sub RegisterPDBHandlers
109             {
110 3     3 1 5 my $handler = shift; # Name of class that'll handle
111             # these databases
112 3         26 my @types = @_;
113 3         3 my $item;
114              
115 3         6 foreach $item (@types)
116             {
117 3 50       14 if (ref($item) eq "ARRAY")
118             {
119 3         1416 $PDBHandlers{$item->[0]}{$item->[1]} = $handler;
120             } else {
121 0         0 $PDBHandlers{$item}{""} = $handler;
122             }
123             }
124             }
125              
126              
127             sub RegisterPRCHandlers
128             {
129 0     0 1 0 my $handler = shift; # Name of class that'll handle
130             # these databases
131 0         0 my @types = @_;
132 0         0 my $item;
133              
134 0         0 foreach $item (@types)
135             {
136 0 0       0 if (ref($item) eq "ARRAY")
137             {
138 0         0 $PRCHandlers{$item->[0]}{$item->[1]} = $handler;
139             } else {
140 0         0 $PRCHandlers{$item}{""} = $handler;
141             }
142             }
143             }
144              
145             #'
146              
147             # _open
148             sub _open
149             {
150 5     5   7 my($self, $mode, $fname) = @_;
151              
152 5         5 my $handle;
153              
154 5 100       10 if (ref($fname))
155             {
156             # Already a filehandle
157 1 50 33     9 if (ref($fname) eq 'GLOB'
158             or UNIVERSAL::isa($fname,"IO::Seekable"))
159             {
160 1         1 $handle = $fname;
161             }
162             # Probably a reference to a SCALAR
163             else
164             {
165 0 0       0 unless (eval 'open $handle, $mode, $fname')
166             {
167 0 0       0 if ($@ ne '')
168             {
169 0         0 die "Open of \"$fname\" unsupported: $@\n";
170             }
171             else
172             {
173 0         0 die "Can't open \"$fname\": $!\n";
174             }
175             }
176             }
177             }
178             else
179             {
180             # Before 5.6.0 "autovivified file handles" don't exist
181 4 50       11 eval 'use IO::File; $handle = new IO::File' if $] < 5.006;
182 4 50       194 open $handle, "$mode $fname"
183             or die "Can't open \"$fname\": $!\n";
184             }
185              
186 5         13 return $handle;
187             }
188              
189             # Load
190             sub Load
191             {
192 4     4 1 311 my $self = shift;
193 4         6 my $fname = shift; # Filename to read from
194 4         21 my $buf; # Buffer into which to read stuff
195              
196 4         9 my $handle = $self->_open('<', $fname);
197 4 50       11 return undef unless defined $handle;
198              
199 4         9 binmode $handle; # Read as binary file under MS-DOS
200              
201             # Get the size of the file. It'll be useful later
202 4         9 seek $handle, 0, 2; # 2 == SEEK_END. Seek to the end.
203 4         19 $self->{_size} = tell $handle;
204 4         7 seek $handle, 0, 0; # 0 == SEEK_START. Rewind to the beginning.
205              
206             # Read header
207 4         5 my $name;
208             my $attributes;
209 0         0 my $version;
210 0         0 my $ctime;
211 0         0 my $mtime;
212 0         0 my $baktime;
213 0         0 my $modnum;
214 0         0 my $appinfo_offset;
215 0         0 my $sort_offset;
216 0         0 my $type;
217 0         0 my $creator;
218 0         0 my $uniqueIDseed;
219              
220 4         45 read $handle, $buf, HeaderLen; # Read the PDB header
221              
222             # Split header into its component fields
223 4         26 ($name, $attributes, $version, $ctime, $mtime, $baktime,
224             $modnum, $appinfo_offset, $sort_offset, $type, $creator,
225             $uniqueIDseed) =
226             unpack "a32 n n N N N N N N a4 a4 N", $buf;
227              
228             # database names must include a terminating NUL.
229 4 100       70 die "bogus database name! is this really a PalmOS file?" unless $name =~ /.+\0/;
230              
231 2         11 ($self->{name} = $name) =~ s/\0.*$//;
232 2 50       7 $self->{attributes}{resource} = 1 if $attributes & 0x0001;
233 2 50       4 $self->{attributes}{"read-only"} = 1 if $attributes & 0x0002;
234 2 50       7 $self->{attributes}{"AppInfo dirty"} = 1 if $attributes & 0x0004;
235 2 100       6 $self->{attributes}{backup} = 1 if $attributes & 0x0008;
236 2 50       5 $self->{attributes}{"OK newer"} = 1 if $attributes & 0x0010;
237 2 50       6 $self->{attributes}{reset} = 1 if $attributes & 0x0020;
238 2 50       5 $self->{attributes}{open} = 1 if $attributes & 0x8000;
239 2 50       5 $self->{attributes}{launchable} = 1 if $attributes & 0x0200;
240              
241             # Attribute names as of PalmOS 5.0 ( see /Core/System/DataMgr.h )
242              
243 2 50       5 $self->{'attributes'}{'ResDB'} = 1 if $attributes & 0x0001;
244 2 50       9 $self->{'attributes'}{'ReadOnly'} = 1 if $attributes & 0x0002;
245 2 50       4 $self->{'attributes'}{'AppInfoDirty'} = 1 if $attributes & 0x0004;
246 2 100       4 $self->{'attributes'}{'Backup'} = 1 if $attributes & 0x0008;
247 2 50       10 $self->{'attributes'}{'OKToInstallNewer'} = 1 if $attributes & 0x0010;
248 2 50       5 $self->{'attributes'}{'ResetAfterInstall'} = 1 if $attributes & 0x0020;
249 2 50       5 $self->{'attributes'}{'CopyPrevention'} = 1 if $attributes & 0x0040;
250 2 50       5 $self->{'attributes'}{'Stream'} = 1 if $attributes & 0x0080;
251 2 50       5 $self->{'attributes'}{'Hidden'} = 1 if $attributes & 0x0100;
252 2 50       6 $self->{'attributes'}{'LaunchableData'} = 1 if $attributes & 0x0200;
253 2 50       5 $self->{'attributes'}{'Recyclable'} = 1 if $attributes & 0x0400;
254 2 50       4 $self->{'attributes'}{'Bundle'} = 1 if $attributes & 0x0800;
255 2 50       4 $self->{'attributes'}{'Open'} = 1 if $attributes & 0x8000;
256              
257              
258 2         3 $self->{version} = $version;
259 2         4 $self->{ctime} = $ctime - EPOCH_1904;
260 2         5 $self->{mtime} = $mtime - EPOCH_1904;
261 2         3 $self->{baktime} = $baktime - EPOCH_1904;
262 2         3 $self->{modnum} = $modnum;
263             # _appinfo_offset and _sort_offset are private fields
264 2         4 $self->{_appinfo_offset} = $appinfo_offset;
265 2         3 $self->{_sort_offset} = $sort_offset;
266 2         4 $self->{type} = $type;
267 2         3 $self->{creator} = $creator;
268 2         4 $self->{uniqueIDseed} = $uniqueIDseed;
269              
270             # XXX strictly speaking, ctime/mtime/baktime values before 1990 are quite
271             # unlikely. Palm was founded in 1992, so even allowing for some prototypes.
272             # This is another way one could detect bogus databases.
273              
274 2 100       6 if( $self->{_appinfo_offset} > $self->{_size} ) {
275 1         22 die "AppInfo block offset beyond end of file!";
276             }
277 1 50       3 if( $self->{_sort_offset} > $self->{_size} ) {
278 0         0 die "Sort block offset beyond end of file!";
279             }
280              
281             # Rebless this PDB object, depending on its type and/or
282             # creator. This allows us to magically invoke the proper
283             # &Parse*() function on the various parts of the database.
284              
285             # Look for most specific handlers first, least specific ones
286             # last. That is, first look for a handler that deals
287             # specifically with this database's creator and type, then for
288             # one that deals with this database's creator and any type,
289             # and finally for one that deals with anything.
290              
291 1         1 my $handler;
292 1 50 33     7 if ($self->{attributes}{resource} || $self->{'attributes'}{'ResDB'})
293             {
294             # Look among resource handlers
295 0   0     0 $handler = $PRCHandlers{$self->{creator}}{$self->{type}} ||
296             $PRCHandlers{undef}{$self->{type}} ||
297             $PRCHandlers{$self->{creator}}{""} ||
298             $PRCHandlers{""}{""};
299             } else {
300             # Look among record handlers
301 1   33     21 $handler = $PDBHandlers{$self->{creator}}{$self->{type}} ||
302             $PDBHandlers{""}{$self->{type}} ||
303             $PDBHandlers{$self->{creator}}{""} ||
304             $PDBHandlers{""}{""};
305             }
306              
307 1 50       2 if (defined($handler))
308             {
309 1         3 bless $self, $handler;
310             } else {
311             # XXX - This should probably return 'undef' or something,
312             # rather than die.
313 0         0 die "No handler defined for creator \"$creator\", type \"$type\"\n";
314             }
315              
316             ## Read record/resource index
317             # Read index header
318 1         2 read $handle, $buf, RecIndexHeaderLen;
319              
320 1         1 my $next_index;
321             my $numrecs;
322              
323 1         6 ($next_index, $numrecs) = unpack "N n", $buf;
324 1         3 $self->{_numrecs} = $numrecs;
325              
326             # Read the index itself
327 1 50 33     5 if ($self->{attributes}{resource} || $self->{'attributes'}{'ResDB'})
328             {
329 0         0 &_load_rsrc_index($self, $handle);
330             } else {
331 1         3 &_load_rec_index($self, $handle);
332             }
333              
334             # Read the two NUL bytes
335             # XXX - Actually, these are bogus. They don't appear in the
336             # spec. The Right Thing to do is to ignore them, and use the
337             # specified or calculated offsets, if they're sane. Sane ==
338             # appears later than the current position.
339             # read $handle, $buf, 2;
340             # $self->{"2NULs"} = $buf;
341              
342             # Read AppInfo block, if it exists
343 1 50       3 if ($self->{_appinfo_offset} != 0)
344             {
345 0         0 &_load_appinfo_block($self, $handle);
346             }
347              
348             # Read sort block, if it exists
349 1 50       5 if ($self->{_sort_offset} != 0)
350             {
351 0         0 &_load_sort_block($self, $handle);
352             }
353              
354             # Read record/resource list
355 1 50 33     5 if ($self->{attributes}{resource} || $self->{'attributes'}{'ResDB'})
356             {
357 0         0 &_load_resources($self, $handle);
358             } else {
359 1         4 &_load_records($self, $handle);
360             }
361              
362             # These keys were needed for parsing the file, but are not
363             # needed any longer. Delete them.
364 1         3 delete $self->{_index};
365 1         2 delete $self->{_numrecs};
366 1         2 delete $self->{_appinfo_offset};
367 1         1 delete $self->{_sort_offset};
368 1         2 delete $self->{_size};
369              
370 1         1 $self->{'dirty'} = 0;
371              
372 1         5 return $self;
373             }
374              
375             # _load_rec_index
376             # Private function. Read the record index, for a record database
377             sub _load_rec_index
378             {
379 1     1   2 my $pdb = shift;
380 1         1 my $fh = shift; # Input file handle
381 1         1 my $i;
382 1         2 my $lastoffset = 0;
383              
384             # Read each record index entry in turn
385 1         4 for ($i = 0; $i < $pdb->{_numrecs}; $i++)
386             {
387 3         2 my $buf; # Input buffer
388              
389             # Read the next record index entry
390             my $offset;
391 0         0 my $attributes;
392 0         0 my @id; # Raw ID
393 0         0 my $id; # Numerical ID
394 3         2 my $entry = {}; # Parsed index entry
395              
396 3         5 read $fh, $buf, IndexRecLen;
397              
398             # The ID field is a bit weird: it's represented as 3
399             # bytes, but it's really a double word (long) value.
400              
401 3         7 ($offset, $attributes, @id) = unpack "N C C3", $buf;
402              
403 3 50       5 if ($offset == $lastoffset)
404             {
405 0         0 print STDERR "Record $i has same offset as previous one: $offset\n";
406             }
407              
408 3         3 $lastoffset = $offset;
409              
410 3         6 $entry->{offset} = $offset;
411              
412 3 50       5 $entry->{attributes}{expunged} = 1 if $attributes & 0x80;
413 3 50       19 $entry->{attributes}{dirty} = 1 if $attributes & 0x40;
414 3 50       4 $entry->{attributes}{deleted} = 1 if $attributes & 0x20;
415 3 50       5 $entry->{attributes}{private} = 1 if $attributes & 0x10;
416              
417             # Attribute names as of PalmOS 5.0 ( see /Core/System/DataMgr.h )
418              
419 3 50       5 $entry->{'attributes'}{'Delete'} = 1 if $attributes & 0x80;
420 3 50       5 $entry->{'attributes'}{'Dirty'} = 1 if $attributes & 0x40;
421 3 50       4 $entry->{'attributes'}{'Busy'} = 1 if $attributes & 0x20;
422 3 50       5 $entry->{'attributes'}{'Secret'} = 1 if $attributes & 0x10;
423              
424 3         5 $entry->{id} = ($id[0] << 16) |
425             ($id[1] << 8) |
426             $id[2];
427              
428             # The lower 4 bits of the attributes field are
429             # overloaded: If the record has been deleted and/or
430             # expunged, then bit 0x08 indicates whether the record
431             # should be archived. Otherwise (if it's an ordinary,
432             # non-deleted record), the lower 4 bits specify the
433             # category that the record belongs in.
434 3 50       5 if (($attributes & 0xa0) == 0)
435             {
436 3         3 $entry->{category} = $attributes & 0x0f;
437             } else {
438 0 0       0 $entry->{attributes}{archive} = 1
439             if $attributes & 0x08;
440             }
441              
442             # Put this information on a temporary array
443 3         3 push @{$pdb->{_index}}, $entry;
  3         10  
444             }
445             }
446              
447             # _load_rsrc_index
448             # Private function. Read the resource index, for a resource database
449             sub _load_rsrc_index
450             {
451 0     0   0 my $pdb = shift;
452 0         0 my $fh = shift; # Input file handle
453 0         0 my $i;
454              
455             # Read each resource index entry in turn
456 0         0 for ($i = 0; $i < $pdb->{_numrecs}; $i++)
457             {
458 0         0 my $buf; # Input buffer
459              
460             # Read the next resource index entry
461             my $type;
462 0         0 my $id;
463 0         0 my $offset;
464 0         0 my $entry = {}; # Parsed index entry
465              
466 0         0 read $fh, $buf, IndexRsrcLen;
467              
468 0         0 ($type, $id, $offset) = unpack "a4 n N", $buf;
469              
470 0         0 $entry->{type} = $type;
471 0         0 $entry->{id} = $id;
472 0         0 $entry->{offset} = $offset;
473              
474 0         0 push @{$pdb->{_index}}, $entry;
  0         0  
475             }
476             }
477              
478             # _load_appinfo_block
479             # Private function. Read the AppInfo block
480             sub _load_appinfo_block
481             {
482 0     0   0 my $pdb = shift;
483 0         0 my $fh = shift; # Input file handle
484 0         0 my $len; # Length of AppInfo block
485             my $buf; # Input buffer
486              
487             # Sanity check: make sure we're positioned at the beginning of
488             # the AppInfo block
489 0 0       0 if (tell($fh) > $pdb->{_appinfo_offset})
490             {
491 0         0 die "Bad AppInfo offset: expected ",
492             sprintf("0x%08x", $pdb->{_appinfo_offset}),
493             ", but I'm at ",
494             tell($fh), "\n";
495             }
496              
497             # Seek to the right place, if necessary
498 0 0       0 if (tell($fh) != $pdb->{_appinfo_offset})
499             {
500 0         0 seek $fh, $pdb->{_appinfo_offset}, 0;
501             }
502              
503             # There's nothing that explicitly gives the size of the
504             # AppInfo block. Rather, it has to be inferred from the offset
505             # of the AppInfo block (previously recorded in
506             # $pdb->{_appinfo_offset}) and whatever's next in the file.
507             # That's either the sort block, the first data record, or the
508             # end of the file.
509              
510 0 0 0     0 if ($pdb->{_sort_offset})
    0          
511 0         0 {
512             # The next thing in the file is the sort block
513 0         0 $len = $pdb->{_sort_offset} - $pdb->{_appinfo_offset};
514             } elsif ((defined $pdb->{_index}) && @{$pdb->{_index}})
515             {
516             # There's no sort block; the next thing in the file is
517             # the first data record
518 0         0 $len = $pdb->{_index}[0]{offset} -
519             $pdb->{_appinfo_offset};
520             } else {
521             # There's no sort block and there are no records. The
522             # AppInfo block goes to the end of the file.
523 0         0 $len = $pdb->{_size} - $pdb->{_appinfo_offset};
524             }
525              
526             # Read the AppInfo block
527 0         0 read $fh, $buf, $len;
528              
529             # Tell the real class to parse the AppInfo block
530 0         0 $pdb->{appinfo} = $pdb->ParseAppInfoBlock($buf);
531             }
532              
533             # _load_sort_block
534             # Private function. Read the sort block.
535             sub _load_sort_block
536             {
537 0     0   0 my $pdb = shift;
538 0         0 my $fh = shift; # Input file handle
539 0         0 my $len; # Length of sort block
540             my $buf; # Input buffer
541              
542             # Sanity check: make sure we're positioned at the beginning of
543             # the sort block
544 0 0       0 if (tell($fh) > $pdb->{_sort_offset})
545             {
546 0         0 die "Bad sort block offset: expected ",
547             sprintf("0x%08x", $pdb->{_sort_offset}),
548             ", but I'm at ",
549             tell($fh), "\n";
550             }
551              
552             # Seek to the right place, if necessary
553 0 0       0 if (tell($fh) != $pdb->{_sort_offset})
554             {
555 0         0 seek $fh, $pdb->{_sort_offset}, 0;
556             }
557              
558             # There's nothing that explicitly gives the size of the sort
559             # block. Rather, it has to be inferred from the offset of the
560             # sort block (previously recorded in $pdb->{_sort_offset})
561             # and whatever's next in the file. That's either the first
562             # data record, or the end of the file.
563              
564 0 0       0 if (defined($pdb->{_index}))
565             {
566             # The next thing in the file is the first data record
567 0         0 $len = $pdb->{_index}[0]{offset} -
568             $pdb->{_sort_offset};
569             } else {
570             # There are no records. The sort block goes to the end
571             # of the file.
572 0         0 $len = $pdb->{_size} - $pdb->{_sort_offset};
573             }
574              
575             # Read the AppInfo block
576 0         0 read $fh, $buf, $len;
577              
578             # XXX - Check to see if the sort block has some predefined
579             # structure. If so, it might be a good idea to parse the sort
580             # block here.
581              
582             # Tell the real class to parse the sort block
583 0         0 $pdb->{sort} = $pdb->ParseSortBlock($buf);
584             }
585              
586             # _load_records
587             # Private function. Load the actual data records, for a record database
588             # (PDB)
589             sub _load_records
590             {
591 1     1   1 my $pdb = shift;
592 1         4 my $fh = shift; # Input file handle
593 1         1 my $i;
594              
595             # Read each record in turn
596 1         4 for ($i = 0; $i < $pdb->{_numrecs}; $i++)
597             {
598 3         3 my $len; # Length of record
599             my $buf; # Input buffer
600              
601             # Sanity check: make sure we're where we think we
602             # should be.
603 3 50       7 if (tell($fh) > $pdb->{_index}[$i]{offset})
604             {
605 0         0 die "Bad offset for record $i: expected ",
606             sprintf("0x%08x",
607             $pdb->{_index}[$i]{offset}),
608             " but it's at ",
609             sprintf("[0x%08x]", tell($fh)), "\n";
610             }
611              
612 3 50       7 if( $pdb->{_index}[$i]{offset} > $pdb->{_size} ) {
613 0         0 die "corruption: Record $i beyond end of database!";
614             }
615              
616             # Seek to the right place, if necessary
617 3 100       7 if (tell($fh) != $pdb->{_index}[$i]{offset})
618             {
619 1         4 seek $fh, $pdb->{_index}[$i]{offset}, 0;
620             }
621              
622             # Compute the length of the record: the last record
623             # extends to the end of the file. The others extend to
624             # the beginning of the next record.
625 3 100       6 if ($i == $pdb->{_numrecs} - 1)
626             {
627             # This is the last record
628 1         2 $len = $pdb->{_size} -
629             $pdb->{_index}[$i]{offset};
630             } else {
631             # This is not the last record
632 2         5 $len = $pdb->{_index}[$i+1]{offset} -
633             $pdb->{_index}[$i]{offset};
634             }
635              
636             # Read the record
637 3         7 read $fh, $buf, $len;
638              
639             # Tell the real class to parse the record data. Pass
640             # &ParseRecord all of the information from the index,
641             # plus a "data" field with the raw record data.
642 3         2 my $record;
643              
644 3         11 $record = $pdb->ParseRecord(
645 3         4 %{$pdb->{_index}[$i]},
646             "data" => $buf,
647             );
648 3         4 push @{$pdb->{records}}, $record;
  3         8  
649             }
650             }
651              
652             # _load_resources
653             # Private function. Load the actual data resources, for a resource database
654             # (PRC)
655             sub _load_resources
656             {
657 0     0   0 my $pdb = shift;
658 0         0 my $fh = shift; # Input file handle
659 0         0 my $i;
660              
661             # Read each resource in turn
662 0         0 for ($i = 0; $i < $pdb->{_numrecs}; $i++)
663             {
664 0         0 my $len; # Length of record
665             my $buf; # Input buffer
666              
667             # Sanity check: make sure we're where we think we
668             # should be.
669 0 0       0 if (tell($fh) > $pdb->{_index}[$i]{offset})
670             {
671 0         0 die "Bad offset for resource $i: expected ",
672             sprintf("0x%08x",
673             $pdb->{_index}[$i]{offset}),
674             " but it's at ",
675             sprintf("0x%08x", tell($fh)), "\n";
676             }
677              
678 0 0       0 if( $pdb->{_index}[$i]{offset} > $pdb->{_size} ) {
679 0         0 die "corruption: Resource $i beyond end of database!";
680             }
681              
682             # Seek to the right place, if necessary
683 0 0       0 if (tell($fh) != $pdb->{_index}[$i]{offset})
684             {
685 0         0 seek $fh, $pdb->{_index}[$i]{offset}, 0;
686             }
687              
688             # Compute the length of the resource: the last
689             # resource extends to the end of the file. The others
690             # extend to the beginning of the next resource.
691 0 0       0 if ($i == $pdb->{_numrecs} - 1)
692             {
693             # This is the last resource
694 0         0 $len = $pdb->{_size} -
695             $pdb->{_index}[$i]{offset};
696             } else {
697             # This is not the last resource
698 0         0 $len = $pdb->{_index}[$i+1]{offset} -
699             $pdb->{_index}[$i]{offset};
700             }
701              
702             # Read the resource
703 0         0 read $fh, $buf, $len;
704              
705             # Tell the real class to parse the resource data. Pass
706             # &ParseResource all of the information from the
707             # index, plus a "data" field with the raw resource
708             # data.
709 0         0 my $resource;
710              
711 0         0 $resource = $pdb->ParseResource(
712 0         0 %{$pdb->{_index}[$i]},
713             "data" => $buf,
714             );
715 0         0 push @{$pdb->{resources}}, $resource;
  0         0  
716             }
717             }
718              
719             #' <-- For Emacs
720              
721             sub Write
722             {
723 1     1 1 299 my $self = shift;
724 1         1 my $fname = shift; # Output file name
725 1         2 my @record_data;
726             my @deleted_records;
727              
728 1 50       4 die "Can't write a database with no name\n"
729             unless $self->{name} ne "";
730              
731 1         6 my $handle = $self->_open('>', $fname);
732 1 50       7 return undef unless defined $handle;
733              
734             # Open file
735 1         4 binmode $handle; # Write as binary file under MS-DOS
736              
737             # Get AppInfo block
738 1         6 my $appinfo_block = $self->PackAppInfoBlock;
739              
740             # Get sort block
741 1         5 my $sort_block = $self->PackSortBlock;
742              
743 1         2 my $index_len;
744              
745             # Get records or resources
746 1 50 33     10 if ($self->{attributes}{resource} || $self->{'attributes'}{'ResDB'})
747             {
748             # Resource database
749 0         0 my $resource;
750              
751 0         0 foreach $resource (@{$self->{resources}})
  0         0  
752             {
753 0         0 my $type;
754             my $id;
755 0         0 my $data;
756              
757             # Get all the stuff that goes in the index, as
758             # well as the resource data.
759 0         0 $type = $resource->{type};
760 0         0 $id = $resource->{id};
761 0         0 $data = $self->PackResource($resource);
762              
763 0         0 push @record_data, [ $type, $id, $data ];
764             }
765             # Figure out size of index
766 0         0 $index_len = RecIndexHeaderLen +
767             @record_data * IndexRsrcLen;
768             } else {
769 1         2 my $record;
770              
771 1         1 foreach $record (@{$self->{records}})
  1         4  
772             {
773 3         4 my $attributes;
774             my $id;
775 0         0 my $data;
776              
777             # XXX - Should probably check the length of this
778             # record and not add it to the record if it's 0.
779              
780             # Get all the stuff that goes in the index, as
781             # well as the record data.
782 3         3 $attributes = 0;
783 3 50 33     15 if ($record->{attributes}{expunged} ||
784             $record->{attributes}{deleted})
785             {
786 0 0       0 $attributes |= 0x08
787             if $record->{attributes}{archive};
788             } else {
789 3         4 $attributes = ($record->{category} & 0x0f);
790             }
791 3 50       9 $attributes |= 0x80
792             if $record->{attributes}{expunged};
793 3 50       15 $attributes |= 0x40
794             if $record->{attributes}{dirty};
795 3 50       6 $attributes |= 0x20
796             if $record->{attributes}{deleted};
797 3 50       9 $attributes |= 0x10
798             if $record->{attributes}{private};
799              
800 3 50       10 $attributes |= 0x80 if $record->{'attributes'}{'Delete'};
801 3 50       7 $attributes |= 0x40 if $record->{'attributes'}{'Dirty'};
802 3 50       8 $attributes |= 0x20 if $record->{'attributes'}{'Busy'};
803 3 50       6 $attributes |= 0x10 if $record->{'attributes'}{'Secret'};
804              
805 3         4 $id = $record->{id};
806              
807 3         10 $data = $self->PackRecord($record);
808 3 50       8 if ($attributes & 0x80) {
809 0         0 push @deleted_records, [ $attributes, $id, $data ];
810             }
811             else {
812 3         9 push @record_data, [ $attributes, $id, $data ];
813             }
814              
815             }
816             # put deleted records at end (RT#101666)
817 1         3 push @record_data, @deleted_records;
818             # Figure out size of index
819 1         3 $index_len = RecIndexHeaderLen +
820             @record_data * IndexRecLen;
821             }
822              
823 1         1 my $header;
824 1         2 my $attributes = 0x0000;
825 1         2 my $appinfo_offset;
826             my $sort_offset;
827              
828             # Build attributes field
829 1 50       16 $attributes =
    50          
    50          
    50          
    50          
    50          
    50          
830             ($self->{attributes}{resource} ? 0x0001 : 0) |
831             ($self->{attributes}{"read-only"} ? 0x0002 : 0) |
832             ($self->{attributes}{"AppInfo dirty"} ? 0x0004 : 0) |
833             ($self->{attributes}{backup} ? 0x0008 : 0) |
834             ($self->{attributes}{"OK newer"} ? 0x0010 : 0) |
835             ($self->{attributes}{reset} ? 0x0020 : 0) |
836             ($self->{attributes}{open} ? 0x8000 : 0);
837              
838 1 50       3 $attributes |= 0x0001 if $self->{'attributes'}{'ResDB'};
839 1 50       3 $attributes |= 0x0002 if $self->{'attributes'}{'ReadOnly'};
840 1 50       5 $attributes |= 0x0004 if $self->{'attributes'}{'AppInfoDirty'};
841 1 50       4 $attributes |= 0x0008 if $self->{'attributes'}{'Backup'};
842 1 50       4 $attributes |= 0x0010 if $self->{'attributes'}{'OKToInstallNewer'};
843 1 50       3 $attributes |= 0x0020 if $self->{'attributes'}{'ResetAfterInstall'};
844 1 50       10 $attributes |= 0x0040 if $self->{'attributes'}{'CopyPrevention'};
845 1 50       4 $attributes |= 0x0080 if $self->{'attributes'}{'Stream'};
846 1 50       7 $attributes |= 0x0100 if $self->{'attributes'}{'Hidden'};
847 1 50       5 $attributes |= 0x0200 if $self->{'attributes'}{'LaunchableData'};
848 1 50       4 $attributes |= 0x0400 if $self->{'attributes'}{'Recyclable'};
849 1 50       4 $attributes |= 0x0800 if $self->{'attributes'}{'Bundle'};
850 1 50       5 $attributes |= 0x8000 if $self->{'attributes'}{'Open'};
851              
852              
853             # Calculate AppInfo block offset
854 1 50 33     10 if ((!defined($appinfo_block)) || ($appinfo_block eq ""))
855             {
856             # There's no AppInfo block
857 1         3 $appinfo_offset = 0;
858             } else {
859             # Offset of AppInfo block from start of file
860 0         0 $appinfo_offset = HeaderLen + $index_len + 2;
861             }
862              
863             # Calculate sort block offset
864 1 50 33     4 if ((!defined($sort_block)) || ($sort_block eq ""))
865             {
866             # There's no sort block
867 1         4 $sort_offset = 0;
868             } else {
869             # Offset of sort block...
870 0 0       0 if ($appinfo_offset == 0)
871             {
872             # ...from start of file
873 0         0 $sort_offset = HeaderLen + $index_len + 2;
874             } else {
875             # ...or just from start of AppInfo block
876 0         0 $sort_offset = $appinfo_offset +
877             length($appinfo_block);
878             }
879             }
880              
881             # Write header
882 1         12 $header = pack "a32 n n N N N N N N a4 a4 N",
883             $self->{name},
884             $attributes,
885             $self->{version},
886             $self->{ctime} + EPOCH_1904,
887             $self->{mtime} + EPOCH_1904,
888             $self->{baktime} + EPOCH_1904,
889             $self->{modnum},
890             $appinfo_offset,
891             $sort_offset,
892             $self->{type},
893             $self->{creator},
894             $self->{uniqueIDseed};
895             ;
896              
897 1         8 print $handle "$header";
898              
899             # Write index header
900 1         1 my $index_header;
901              
902 1         2 $index_header = pack "N n", 0, scalar @record_data;
903 1         3 print $handle "$index_header";
904              
905             # Write index
906 1         1 my $rec_offset; # Offset of next record/resource
907              
908             # Calculate offset of first record/resource
909 1 50       5 if ($sort_offset != 0)
    50          
910             {
911 0         0 $rec_offset = $sort_offset + length($sort_block);
912             } elsif ($appinfo_offset != 0)
913             {
914 0         0 $rec_offset = $appinfo_offset + length($appinfo_block);
915             } else {
916 1         2 $rec_offset = HeaderLen + $index_len + 2;
917             }
918              
919 1 50 33     5 if ($self->{attributes}{resource} || $self->{'attributes'}{'ResDB'})
920             {
921             # Resource database
922             # Record database
923 0         0 my $rsrc_data;
924              
925 0         0 foreach $rsrc_data (@record_data)
926             {
927 0         0 my $type;
928             my $id;
929 0         0 my $data;
930 0         0 my $index_data;
931              
932 0         0 ($type, $id, $data) = @{$rsrc_data};
  0         0  
933 0         0 $index_data = pack "a4 n N",
934             $type,
935             $id,
936             $rec_offset;
937 0         0 print $handle "$index_data";
938              
939 0         0 $rec_offset += length($data);
940             }
941             } else {
942             # Record database
943 1         2 my $rec_data;
944              
945 1         2 foreach $rec_data (@record_data)
946             {
947 3         3 my $attributes;
948             my $data;
949 0         0 my $id;
950 0         0 my $index_data;
951              
952             # XXX - Probably shouldn't write this record if
953             # length($data) == 0
954 3         2 ($attributes, $id, $data) = @{$rec_data};
  3         5  
955              
956 3 50       6 if (length($data) == 0)
957             {
958 0         0 warn printf("Write: Warning: record 0x%08x has length 0\n", $id)
959             }
960              
961 3         7 $index_data = pack "N C C3",
962             $rec_offset,
963             $attributes,
964             ($id >> 16) & 0xff,
965             ($id >> 8) & 0xff,
966             $id & 0xff;
967 3         3 print $handle "$index_data";
968              
969 3         4 $rec_offset += length($data);
970             }
971             }
972              
973             # Write the two NULs
974 1 50       3 if (length($self->{"2NULs"}) == 2)
975             {
976 1         2 print $handle $self->{"2NULs"};
977             } else {
978 0         0 print $handle "\0\0";
979             }
980              
981             # Write AppInfo block
982 1 50       2 print $handle $appinfo_block unless $appinfo_offset == 0;
983              
984             # Write sort block
985 1 50       3 print $handle $sort_block unless $sort_offset == 0;
986              
987             # Write record/resource list
988 1         1 my $record;
989 1         1 foreach $record (@record_data)
990             {
991 3         2 my $data;
992              
993 3 50 33     17 if ($self->{attributes}{resource} || $self->{'attributes'}{'ResDB'})
994             {
995             # Resource database
996 0         0 my $type;
997             my $id;
998              
999 0         0 ($type, $id, $data) = @{$record};
  0         0  
1000             } else {
1001 3         2 my $attributes;
1002             my $id;
1003              
1004 3         2 ($attributes, $id, $data) = @{$record};
  3         5  
1005             }
1006 3         4 print $handle $data;
1007             }
1008              
1009 1         48 return $self;
1010             }
1011              
1012              
1013             # PDB::new_Record()
1014             # Create a new, initialized record, and return a reference to it.
1015             # The record is initially marked as being dirty, since that's usually
1016             # the Right Thing.
1017             sub new_Record
1018             {
1019 3     3 1 4 my $classname = shift;
1020 3         3 my $retval = {};
1021              
1022             # Initialize the record
1023 3         7 $retval->{'category'} = 0; # Unfiled, by convention
1024 3         7 $retval->{'attributes'} = {
1025             # expunged => 0,
1026             dirty => 1, # Note: originally dirty
1027             'Dirty' => 1,
1028             # deleted => 0,
1029             # private => 0,
1030             # archive => 0,
1031             };
1032 3         4 $retval->{'id'} = 0; # Initially, no record ID
1033              
1034 3         3 return $retval;
1035             }
1036              
1037             #'
1038              
1039             sub is_Dirty
1040             {
1041 0     0 1 0 my $self = shift;
1042              
1043             # try the quick and easy tests first
1044 0 0       0 return 1 if $self->{'dirty'};
1045 0 0       0 return 1 if $self->{'attributes'}{'AppInfoDirty'};
1046 0 0       0 return 1 if $self->{'attributes'}{'AppInfo dirty'};
1047              
1048             # okay, check the records. Note that resource entries appear to
1049             # have no dirty flags for us to use.
1050 0 0 0     0 if (!$self->{attributes}{resource} and !$self->{'attributes'}{'ResDB'})
1051             {
1052 0         0 my $record;
1053              
1054 0         0 foreach $record (@{$self->{records}})
  0         0  
1055             {
1056 0 0       0 return 1 if $record->{'attributes'}{'Dirty'};
1057 0 0       0 return 1 if $record->{'attributes'}{'dirty'};
1058             }
1059             }
1060              
1061 0         0 return 0;
1062             }
1063              
1064             #'
1065              
1066             # append_Record
1067             # Append the given records to the database's list of records. If no
1068             # records are given, create one, append it, and return a reference to
1069             # it.
1070             sub append_Record
1071             {
1072 3     3 1 1520 my $self = shift;
1073              
1074 3 50       9 unless (@_)
1075             {
1076             # No arguments given. Create a new record.
1077 3         24 my $record = $self->new_Record;
1078              
1079             # Validate the unique ID.
1080 3 50       15 $self->_setUniqueID($record)
1081             if $record->{'id'} eq 0;
1082              
1083 3         2 push @{$self->{records}}, $record;
  3         6  
1084              
1085             # Update the "last modification time".
1086 3         4 $self->{mtime} = time;
1087 3         3 $self->{dirty} = 1;
1088              
1089 3         7 return $record;
1090             }
1091              
1092             # Validate the unique IDs.
1093 0         0 foreach my $record (@_)
1094             {
1095 0 0       0 $self->_setUniqueID($record)
1096             if $record->{'id'} eq 0;
1097             }
1098              
1099             # At least one argument was given. Append all of the arguments
1100             # to the list of records, and return the first one.
1101 0         0 push @{$self->{records}}, @_;
  0         0  
1102              
1103             # Update the "last modification time".
1104 0         0 $self->{mtime} = time;
1105 0         0 $self->{'dirty'} = 1;
1106              
1107 0         0 return $_[0];
1108             }
1109              
1110             sub _setUniqueID
1111             {
1112 3     3   4 my($self, $record) = @_;
1113              
1114             # Bump the seed to prevent a uniqueIDseed of 0 which represents
1115             # an unassigned uniqueID.
1116             # XXX IMHO this just couldn't happen given the way the seed it's
1117             # generated. But if Palm OS goes this way maybe it's better to do
1118             # the same.
1119              
1120 3         7 $self->{'uniqueIDseed'}++;
1121              
1122             # Check for wrap around. Remember that an uniqueID is made of only 24 bits.
1123 3 50       6 $self->{'uniqueIDseed'} = (dmRecordIDReservedRange + 1) << 12
1124             if ($self->{'uniqueIDseed'} & 0xFF000000);
1125              
1126             # Copy the seed into the new record.
1127 3         5 $record->{'id'} = $self->{'uniqueIDseed'};
1128             }
1129              
1130              
1131             # new_Resource
1132             # Create a new, initialized resource, and return a reference to it.
1133             sub new_Resource
1134             {
1135 0     0 1   my $classname = shift;
1136 0           my $retval = {};
1137              
1138             # Initialize the resource
1139 0           $retval->{type} = "\0\0\0\0";
1140 0           $retval->{id} = 0;
1141              
1142 0           return $retval;
1143             }
1144              
1145             #'
1146              
1147             # append_Resource
1148             # Append the given resources to the database's list of resources. If no
1149             # resources are given, create one, append it, and return a reference to
1150             # it.
1151             sub append_Resource
1152             {
1153 0     0 1   my $self = shift;
1154              
1155 0 0         unless (@_)
1156             {
1157             # No arguments given. Create a new resource
1158 0           my $resource = $self->new_Resource;
1159              
1160 0           push @{$self->{resources}}, $resource;
  0            
1161              
1162             # Update the "last modification time".
1163 0           $self->{mtime} = time;
1164 0           $self->{'dirty'} = 1;
1165              
1166 0           return $resource;
1167             }
1168              
1169             # At least one argument was given. Append all of the arguments
1170             # to the list of resources, and return the first one.
1171 0           push @{$self->{resources}}, @_;
  0            
1172              
1173             # Update the "last modification time".
1174 0           $self->{mtime} = time;
1175 0           $self->{'dirty'} = 1;
1176              
1177 0           return $_[0];
1178             }
1179              
1180              
1181             # findRecordByID
1182             # Returns a reference to the record with the given ID, or 'undef' if
1183             # it doesn't exist.
1184             sub findRecordByID
1185             {
1186 0     0 1   my $self = shift;
1187 0           my $id = shift;
1188              
1189 0 0         return undef if $id eq "";
1190              
1191 0           for (@{$self->{records}})
  0            
1192             {
1193 0 0         next unless $_->{id} == $id;
1194 0           return $_; # Found it
1195             }
1196              
1197 0           return undef; # Not found
1198             }
1199              
1200             #'
1201              
1202             # delete_Record
1203             # $pdb->delete_Record($record ?, $expunge?)
1204             #
1205             # Mark the given record for deletion. If $expunge is true, mark the
1206             # record for deletion without an archive.
1207              
1208             sub delete_Record
1209             {
1210 0     0 1   my $self = shift;
1211 0           my $record = shift;
1212 0           my $expunge = shift;
1213              
1214 0           $record->{attributes}{deleted} = 1;
1215 0 0         if ($expunge)
1216             {
1217 0           $record->{attributes}{expunged} = 1;
1218 0           $record->{attributes}{archive} = 0;
1219             } else {
1220 0           $record->{attributes}{expunged} = 0;
1221 0           $record->{attributes}{archive} = 1;
1222             }
1223              
1224             # Update the "last modification time".
1225 0           $self->{mtime} = time;
1226 0           $self->{'dirty'} = 1;
1227             }
1228              
1229             #'
1230              
1231             sub remove_Record($$)
1232             {
1233 0     0 1   my $self = shift;
1234 0           my $record = shift;
1235              
1236 0           for (my $i = 0; $i <= $#{$self->{records}}; $i ++)
  0            
1237             {
1238 0 0         if ($self->{records}->[$i] == $record)
1239             {
1240             # make a copy of the records array. This is really necessary
1241             # because there's frequently something using the records reference
1242             # for iteration purposes (like the doc example) and we can't
1243             # just start splicing that apart (tried, failed).
1244             # So we have to make a new copy. This does, unfortunately,
1245             # make remove_Record() more expensive that you'd expect.
1246 0           $self->{records} = [ @{$self->{records}} ];
  0            
1247              
1248             # remove the record index.
1249 0           splice @{$self->{records}}, $i, 1;
  0            
1250              
1251 0           $self->{mtime} = time;
1252 0           $self->{'dirty'} = 1;
1253              
1254 0           last;
1255             }
1256             }
1257             }
1258              
1259             1;
1260              
1261             __END__