File Coverage

blib/lib/Palm/PDB.pm
Criterion Covered Total %
statement 261 460 56.7
branch 103 250 41.2
condition 21 64 32.8
subroutine 13 24 54.1
pod 13 13 100.0
total 411 811 50.6


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   87428 use strict;
  3         7  
  3         146  
28 3     3   17 use vars qw( $VERSION %PDBHandlers %PRCHandlers );
  3         6  
  3         463  
29              
30             $VERSION = '1.015';
31             # This file is part of Palm-PDB 1.015 (August 9, 2014)
32              
33             # ABSTRACT: Parse Palm database files
34              
35              
36             use constant 1.03 { # accepts hash reference
37 3         22514 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   18 };
  3         167  
51              
52             %PDBHandlers = (); # Record handler map
53             %PRCHandlers = (); # Resource handler map
54              
55              
56             sub new
57             {
58 5     5 1 1797 my $class = shift;
59 5         10 my $params = shift;
60              
61 5         9 my $self = {};
62              
63              
64             # Initialize the PDB. These values are just defaults, of course.
65 5   50     35 $self->{'name'} = $params->{'name'} || "";
66 5   50     27 $self->{'attributes'} = $params->{'attributes'} || {};
67 5   50     30 $self->{'version'} = $params->{'version'} || 0;
68              
69 5         19 my $now = time;
70              
71 5   33     27 $self->{'ctime'} = $params->{'ctime'} || $now;
72 5   33     23 $self->{'mtime'} = $params->{'mtime'} || $now;
73 5   50     26 $self->{'baktime'} = $params->{'baktime'} || -(EPOCH_1904);
74              
75 5   50     21 $self->{'modnum'} = $params->{'modnum'} || 0;
76 5   50     29 $self->{'type'} = $params->{'type'} || "\0\0\0\0";
77 5   50     23 $self->{'creator'} = $params->{'creator'} || "\0\0\0\0";
78 5   50     35 $self->{'uniqueIDseed'} = $params->{'uniqueIDseed'} || 0;
79              
80 5         10 $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       15 if ($self->{'uniqueIDseed'} <= ((dmRecordIDReservedRange + 1) << 12))
89             {
90 5         6 my $uniqueIDseed = 0;
91              
92             do
93 5         5 {
94 5         135 $uniqueIDseed = int(rand(0x0FFF));
95              
96             } while ($uniqueIDseed <= dmRecordIDReservedRange);
97              
98 5         10 $self->{'uniqueIDseed'} = $uniqueIDseed << 12;
99 5         11 $self->{'uniqueIDseed'} &= 0x00FFF000; # Isolate the upper 12 seed bits.
100             }
101              
102 5         8 bless $self, $class;
103 5         17 return $self;
104             }
105              
106             #' <-- For Emacs.
107              
108             sub RegisterPDBHandlers
109             {
110 3     3 1 6 my $handler = shift; # Name of class that'll handle
111             # these databases
112 3         29 my @types = @_;
113 3         6 my $item;
114              
115 3         9 foreach $item (@types)
116             {
117 3 50       13 if (ref($item) eq "ARRAY")
118             {
119 3         2432 $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   11 my($self, $mode, $fname) = @_;
151              
152 5         7 my $handle;
153              
154 5 100       15 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         2 $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       265 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 340 my $self = shift;
193 4         7 my $fname = shift; # Filename to read from
194 4         26 my $buf; # Buffer into which to read stuff
195              
196 4         12 my $handle = $self->_open('<', $fname);
197 4 50       15 return undef unless defined $handle;
198              
199 4         10 binmode $handle; # Read as binary file under MS-DOS
200              
201             # Get the size of the file. It'll be useful later
202 4         20 seek $handle, 0, 2; # 2 == SEEK_END. Seek to the end.
203 4         19 $self->{_size} = tell $handle;
204 4         18 seek $handle, 0, 0; # 0 == SEEK_START. Rewind to the beginning.
205              
206             # Read header
207 4         7 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         77 read $handle, $buf, HeaderLen; # Read the PDB header
221              
222             # Split header into its component fields
223 4         33 ($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       72 die "bogus database name! is this really a PalmOS file?" unless $name =~ /.+\0/;
230              
231 2         16 ($self->{name} = $name) =~ s/\0.*$//;
232 2 50       37 $self->{attributes}{resource} = 1 if $attributes & 0x0001;
233 2 50       7 $self->{attributes}{"read-only"} = 1 if $attributes & 0x0002;
234 2 50       8 $self->{attributes}{"AppInfo dirty"} = 1 if $attributes & 0x0004;
235 2 100       7 $self->{attributes}{backup} = 1 if $attributes & 0x0008;
236 2 50       7 $self->{attributes}{"OK newer"} = 1 if $attributes & 0x0010;
237 2 50       5 $self->{attributes}{reset} = 1 if $attributes & 0x0020;
238 2 50       5 $self->{attributes}{open} = 1 if $attributes & 0x8000;
239 2 50       7 $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       4 $self->{'attributes'}{'ResDB'} = 1 if $attributes & 0x0001;
244 2 50       6 $self->{'attributes'}{'ReadOnly'} = 1 if $attributes & 0x0002;
245 2 50       15 $self->{'attributes'}{'AppInfoDirty'} = 1 if $attributes & 0x0004;
246 2 100       7 $self->{'attributes'}{'Backup'} = 1 if $attributes & 0x0008;
247 2 50       6 $self->{'attributes'}{'OKToInstallNewer'} = 1 if $attributes & 0x0010;
248 2 50       5 $self->{'attributes'}{'ResetAfterInstall'} = 1 if $attributes & 0x0020;
249 2 50       11 $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       6 $self->{'attributes'}{'Bundle'} = 1 if $attributes & 0x0800;
255 2 50       6 $self->{'attributes'}{'Open'} = 1 if $attributes & 0x8000;
256              
257              
258 2         4 $self->{version} = $version;
259 2         5 $self->{ctime} = $ctime - EPOCH_1904;
260 2         2 $self->{mtime} = $mtime - EPOCH_1904;
261 2         4 $self->{baktime} = $baktime - EPOCH_1904;
262 2         4 $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         5 $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       11 if( $self->{_appinfo_offset} > $self->{_size} ) {
275 1         19 die "AppInfo block offset beyond end of file!";
276             }
277 1 50       4 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     6 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     15 $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         2 my $next_index;
321             my $numrecs;
322              
323 1         3 ($next_index, $numrecs) = unpack "N n", $buf;
324 1         2 $self->{_numrecs} = $numrecs;
325              
326             # Read the index itself
327 1 50 33     12 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       10 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       3 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     13 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         5 delete $self->{_index};
365 1         1 delete $self->{_numrecs};
366 1         3 delete $self->{_appinfo_offset};
367 1         1 delete $self->{_sort_offset};
368 1         2 delete $self->{_size};
369              
370 1         2 $self->{'dirty'} = 0;
371              
372 1         3 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         2 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         3 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         14 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         9 ($offset, $attributes, @id) = unpack "N C C3", $buf;
402              
403 3 50       10 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       6 $entry->{attributes}{expunged} = 1 if $attributes & 0x80;
413 3 50       8 $entry->{attributes}{dirty} = 1 if $attributes & 0x40;
414 3 50       5 $entry->{attributes}{deleted} = 1 if $attributes & 0x20;
415 3 50       6 $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       7 $entry->{'attributes'}{'Delete'} = 1 if $attributes & 0x80;
420 3 50       13 $entry->{'attributes'}{'Dirty'} = 1 if $attributes & 0x40;
421 3 50       5 $entry->{'attributes'}{'Busy'} = 1 if $attributes & 0x20;
422 3 50       7 $entry->{'attributes'}{'Secret'} = 1 if $attributes & 0x10;
423              
424 3         6 $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         13 $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         4 push @{$pdb->{_index}}, $entry;
  3         13  
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         2 my $fh = shift; # Input file handle
593 1         1 my $i;
594              
595             # Read each record in turn
596 1         5 for ($i = 0; $i < $pdb->{_numrecs}; $i++)
597             {
598 3         4 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       9 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       9 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       9 if (tell($fh) != $pdb->{_index}[$i]{offset})
618             {
619 1         8 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       7 if ($i == $pdb->{_numrecs} - 1)
626             {
627             # This is the last record
628 1         3 $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         8 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         4 my $record;
643              
644 3         16 $record = $pdb->ParseRecord(
645 3         3 %{$pdb->{_index}[$i]},
646             "data" => $buf,
647             );
648 3         4 push @{$pdb->{records}}, $record;
  3         16  
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 350 my $self = shift;
724 1         2 my $fname = shift; # Output file name
725 1         2 my @record_data;
726              
727 1 50       4 die "Can't write a database with no name\n"
728             unless $self->{name} ne "";
729              
730 1         7 my $handle = $self->_open('>', $fname);
731 1 50       6 return undef unless defined $handle;
732              
733             # Open file
734 1         4 binmode $handle; # Write as binary file under MS-DOS
735              
736             # Get AppInfo block
737 1         6 my $appinfo_block = $self->PackAppInfoBlock;
738              
739             # Get sort block
740 1         3 my $sort_block = $self->PackSortBlock;
741              
742 1         2 my $index_len;
743              
744             # Get records or resources
745 1 50 33     9 if ($self->{attributes}{resource} || $self->{'attributes'}{'ResDB'})
746             {
747             # Resource database
748 0         0 my $resource;
749              
750 0         0 foreach $resource (@{$self->{resources}})
  0         0  
751             {
752 0         0 my $type;
753             my $id;
754 0         0 my $data;
755              
756             # Get all the stuff that goes in the index, as
757             # well as the resource data.
758 0         0 $type = $resource->{type};
759 0         0 $id = $resource->{id};
760 0         0 $data = $self->PackResource($resource);
761              
762 0         0 push @record_data, [ $type, $id, $data ];
763             }
764             # Figure out size of index
765 0         0 $index_len = RecIndexHeaderLen +
766             ($#record_data + 1) * IndexRsrcLen;
767             } else {
768 1         1 my $record;
769              
770 1         2 foreach $record (@{$self->{records}})
  1         3  
771             {
772 3         4 my $attributes;
773             my $id;
774 0         0 my $data;
775              
776             # XXX - Should probably check the length of this
777             # record and not add it to the record if it's 0.
778              
779             # Get all the stuff that goes in the index, as
780             # well as the record data.
781 3         4 $attributes = 0;
782 3 50 33     16 if ($record->{attributes}{expunged} ||
783             $record->{attributes}{deleted})
784             {
785 0 0       0 $attributes |= 0x08
786             if $record->{attributes}{archive};
787             } else {
788 3         5 $attributes = ($record->{category} & 0x0f);
789             }
790 3 50       7 $attributes |= 0x80
791             if $record->{attributes}{expunged};
792 3 50       16 $attributes |= 0x40
793             if $record->{attributes}{dirty};
794 3 50       7 $attributes |= 0x20
795             if $record->{attributes}{deleted};
796 3 50       7 $attributes |= 0x10
797             if $record->{attributes}{private};
798              
799 3 50       8 $attributes |= 0x80 if $record->{'attributes'}{'Delete'};
800 3 50       8 $attributes |= 0x40 if $record->{'attributes'}{'Dirty'};
801 3 50       5 $attributes |= 0x20 if $record->{'attributes'}{'Busy'};
802 3 50       7 $attributes |= 0x10 if $record->{'attributes'}{'Secret'};
803              
804 3         4 $id = $record->{id};
805              
806 3         7 $data = $self->PackRecord($record);
807              
808 3         7 push @record_data, [ $attributes, $id, $data ];
809             }
810             # Figure out size of index
811 1         3 $index_len = RecIndexHeaderLen +
812             ($#record_data + 1) * IndexRecLen;
813             }
814              
815 1         1 my $header;
816 1         2 my $attributes = 0x0000;
817 1         1 my $appinfo_offset;
818             my $sort_offset;
819              
820             # Build attributes field
821 1 50       12 $attributes =
    50          
    50          
    50          
    50          
    50          
    50          
822             ($self->{attributes}{resource} ? 0x0001 : 0) |
823             ($self->{attributes}{"read-only"} ? 0x0002 : 0) |
824             ($self->{attributes}{"AppInfo dirty"} ? 0x0004 : 0) |
825             ($self->{attributes}{backup} ? 0x0008 : 0) |
826             ($self->{attributes}{"OK newer"} ? 0x0010 : 0) |
827             ($self->{attributes}{reset} ? 0x0020 : 0) |
828             ($self->{attributes}{open} ? 0x8000 : 0);
829              
830 1 50       4 $attributes |= 0x0001 if $self->{'attributes'}{'ResDB'};
831 1 50       2 $attributes |= 0x0002 if $self->{'attributes'}{'ReadOnly'};
832 1 50       3 $attributes |= 0x0004 if $self->{'attributes'}{'AppInfoDirty'};
833 1 50       3 $attributes |= 0x0008 if $self->{'attributes'}{'Backup'};
834 1 50       3 $attributes |= 0x0010 if $self->{'attributes'}{'OKToInstallNewer'};
835 1 50       8 $attributes |= 0x0020 if $self->{'attributes'}{'ResetAfterInstall'};
836 1 50       3 $attributes |= 0x0040 if $self->{'attributes'}{'CopyPrevention'};
837 1 50       2 $attributes |= 0x0080 if $self->{'attributes'}{'Stream'};
838 1 50       8 $attributes |= 0x0100 if $self->{'attributes'}{'Hidden'};
839 1 50       8 $attributes |= 0x0200 if $self->{'attributes'}{'LaunchableData'};
840 1 50       3 $attributes |= 0x0400 if $self->{'attributes'}{'Recyclable'};
841 1 50       3 $attributes |= 0x0800 if $self->{'attributes'}{'Bundle'};
842 1 50       2 $attributes |= 0x8000 if $self->{'attributes'}{'Open'};
843              
844              
845             # Calculate AppInfo block offset
846 1 50 33     4 if ((!defined($appinfo_block)) || ($appinfo_block eq ""))
847             {
848             # There's no AppInfo block
849 1         2 $appinfo_offset = 0;
850             } else {
851             # Offset of AppInfo block from start of file
852 0         0 $appinfo_offset = HeaderLen + $index_len + 2;
853             }
854              
855             # Calculate sort block offset
856 1 50 33     9 if ((!defined($sort_block)) || ($sort_block eq ""))
857             {
858             # There's no sort block
859 1         2 $sort_offset = 0;
860             } else {
861             # Offset of sort block...
862 0 0       0 if ($appinfo_offset == 0)
863             {
864             # ...from start of file
865 0         0 $sort_offset = HeaderLen + $index_len + 2;
866             } else {
867             # ...or just from start of AppInfo block
868 0         0 $sort_offset = $appinfo_offset +
869             length($appinfo_block);
870             }
871             }
872              
873             # Write header
874 1         11 $header = pack "a32 n n N N N N N N a4 a4 N",
875             $self->{name},
876             $attributes,
877             $self->{version},
878             $self->{ctime} + EPOCH_1904,
879             $self->{mtime} + EPOCH_1904,
880             $self->{baktime} + EPOCH_1904,
881             $self->{modnum},
882             $appinfo_offset,
883             $sort_offset,
884             $self->{type},
885             $self->{creator},
886             $self->{uniqueIDseed};
887             ;
888              
889 1         13 print $handle "$header";
890              
891             # Write index header
892 1         2 my $index_header;
893              
894 1         2 $index_header = pack "N n", 0, ($#record_data+1);
895 1         2 print $handle "$index_header";
896              
897             # Write index
898 1         1 my $rec_offset; # Offset of next record/resource
899              
900             # Calculate offset of first record/resource
901 1 50       6 if ($sort_offset != 0)
    50          
902             {
903 0         0 $rec_offset = $sort_offset + length($sort_block);
904             } elsif ($appinfo_offset != 0)
905             {
906 0         0 $rec_offset = $appinfo_offset + length($appinfo_block);
907             } else {
908 1         2 $rec_offset = HeaderLen + $index_len + 2;
909             }
910              
911 1 50 33     7 if ($self->{attributes}{resource} || $self->{'attributes'}{'ResDB'})
912             {
913             # Resource database
914             # Record database
915 0         0 my $rsrc_data;
916              
917 0         0 foreach $rsrc_data (@record_data)
918             {
919 0         0 my $type;
920             my $id;
921 0         0 my $data;
922 0         0 my $index_data;
923              
924 0         0 ($type, $id, $data) = @{$rsrc_data};
  0         0  
925 0         0 $index_data = pack "a4 n N",
926             $type,
927             $id,
928             $rec_offset;
929 0         0 print $handle "$index_data";
930              
931 0         0 $rec_offset += length($data);
932             }
933             } else {
934             # Record database
935 1         1 my $rec_data;
936              
937 1         7 foreach $rec_data (@record_data)
938             {
939 3         4 my $attributes;
940             my $data;
941 0         0 my $id;
942 0         0 my $index_data;
943              
944             # XXX - Probably shouldn't write this record if
945             # length($data) == 0
946 3         3 ($attributes, $id, $data) = @{$rec_data};
  3         4  
947              
948 3 50       8 if (length($data) == 0)
949             {
950 0         0 warn printf("Write: Warning: record 0x%08x has length 0\n", $id)
951             }
952              
953 3         7 $index_data = pack "N C C3",
954             $rec_offset,
955             $attributes,
956             ($id >> 16) & 0xff,
957             ($id >> 8) & 0xff,
958             $id & 0xff;
959 3         4 print $handle "$index_data";
960              
961 3         5 $rec_offset += length($data);
962             }
963             }
964              
965             # Write the two NULs
966 1 50       5 if (length($self->{"2NULs"}) == 2)
967             {
968 1         1 print $handle $self->{"2NULs"};
969             } else {
970 0         0 print $handle "\0\0";
971             }
972              
973             # Write AppInfo block
974 1 50       3 print $handle $appinfo_block unless $appinfo_offset == 0;
975              
976             # Write sort block
977 1 50       3 print $handle $sort_block unless $sort_offset == 0;
978              
979             # Write record/resource list
980 1         1 my $record;
981 1         2 foreach $record (@record_data)
982             {
983 3         2 my $data;
984              
985 3 50 33     15 if ($self->{attributes}{resource} || $self->{'attributes'}{'ResDB'})
986             {
987             # Resource database
988 0         0 my $type;
989             my $id;
990              
991 0         0 ($type, $id, $data) = @{$record};
  0         0  
992             } else {
993 3         8 my $attributes;
994             my $id;
995              
996 3         2 ($attributes, $id, $data) = @{$record};
  3         6  
997             }
998 3         5 print $handle $data;
999             }
1000              
1001 1         58 return $self;
1002             }
1003              
1004              
1005             # PDB::new_Record()
1006             # Create a new, initialized record, and return a reference to it.
1007             # The record is initially marked as being dirty, since that's usually
1008             # the Right Thing.
1009             sub new_Record
1010             {
1011 3     3 1 3 my $classname = shift;
1012 3         6 my $retval = {};
1013              
1014             # Initialize the record
1015 3         7 $retval->{'category'} = 0; # Unfiled, by convention
1016 3         10 $retval->{'attributes'} = {
1017             # expunged => 0,
1018             dirty => 1, # Note: originally dirty
1019             'Dirty' => 1,
1020             # deleted => 0,
1021             # private => 0,
1022             # archive => 0,
1023             };
1024 3         8 $retval->{'id'} = 0; # Initially, no record ID
1025              
1026 3         4 return $retval;
1027             }
1028              
1029             #'
1030              
1031             sub is_Dirty
1032             {
1033 0     0 1 0 my $self = shift;
1034              
1035             # try the quick and easy tests first
1036 0 0       0 return 1 if $self->{'dirty'};
1037 0 0       0 return 1 if $self->{'attributes'}{'AppInfoDirty'};
1038 0 0       0 return 1 if $self->{'attributes'}{'AppInfo dirty'};
1039              
1040             # okay, check the records. Note that resource entries appear to
1041             # have no dirty flags for us to use.
1042 0 0 0     0 if (!$self->{attributes}{resource} and !$self->{'attributes'}{'ResDB'})
1043             {
1044 0         0 my $record;
1045              
1046 0         0 foreach $record (@{$self->{records}})
  0         0  
1047             {
1048 0 0       0 return 1 if $record->{'attributes'}{'Dirty'};
1049 0 0       0 return 1 if $record->{'attributes'}{'dirty'};
1050             }
1051             }
1052              
1053 0         0 return 0;
1054             }
1055              
1056             #'
1057              
1058             # append_Record
1059             # Append the given records to the database's list of records. If no
1060             # records are given, create one, append it, and return a reference to
1061             # it.
1062             sub append_Record
1063             {
1064 3     3 1 2036 my $self = shift;
1065              
1066 3 50       12 if ($#_ < 0)
1067             {
1068             # No arguments given. Create a new record.
1069 3         28 my $record = $self->new_Record;
1070              
1071             # Validate the unique ID.
1072 3 50       22 $self->_setUniqueID($record)
1073             if $record->{'id'} eq 0;
1074              
1075 3         3 push @{$self->{records}}, $record;
  3         8  
1076              
1077             # Update the "last modification time".
1078 3         5 $self->{mtime} = time;
1079 3         4 $self->{dirty} = 1;
1080              
1081 3         9 return $record;
1082             }
1083              
1084             # Validate the unique IDs.
1085 0         0 foreach my $record (@_)
1086             {
1087 0 0       0 $self->_setUniqueID($record)
1088             if $record->{'id'} eq 0;
1089             }
1090              
1091             # At least one argument was given. Append all of the arguments
1092             # to the list of records, and return the first one.
1093 0         0 push @{$self->{records}}, @_;
  0         0  
1094              
1095             # Update the "last modification time".
1096 0         0 $self->{mtime} = time;
1097 0         0 $self->{'dirty'} = 1;
1098              
1099 0         0 return $_[0];
1100             }
1101              
1102             sub _setUniqueID
1103             {
1104 3     3   5 my($self, $record) = @_;
1105              
1106             # Bump the seed to prevent a uniqueIDseed of 0 which represents
1107             # an unassigned uniqueID.
1108             # XXX IMHO this just couldn't happen given the way the seed it's
1109             # generated. But if Palm OS goes this way maybe it's better to do
1110             # the same.
1111              
1112 3         4 $self->{'uniqueIDseed'}++;
1113              
1114             # Check for wrap around. Remember that an uniqueID is made of only 24 bits.
1115 3 50       11 $self->{'uniqueIDseed'} = (dmRecordIDReservedRange + 1) << 12
1116             if ($self->{'uniqueIDseed'} & 0xFF000000);
1117              
1118             # Copy the seed into the new record.
1119 3         6 $record->{'id'} = $self->{'uniqueIDseed'};
1120             }
1121              
1122              
1123             # new_Resource
1124             # Create a new, initialized resource, and return a reference to it.
1125             sub new_Resource
1126             {
1127 0     0 1   my $classname = shift;
1128 0           my $retval = {};
1129              
1130             # Initialize the resource
1131 0           $retval->{type} = "\0\0\0\0";
1132 0           $retval->{id} = 0;
1133              
1134 0           return $retval;
1135             }
1136              
1137             #'
1138              
1139             # append_Resource
1140             # Append the given resources to the database's list of resources. If no
1141             # resources are given, create one, append it, and return a reference to
1142             # it.
1143             sub append_Resource
1144             {
1145 0     0 1   my $self = shift;
1146              
1147 0 0         if ($#_ < 0)
1148             {
1149             # No arguments given. Create a new resource
1150 0           my $resource = $self->new_Resource;
1151              
1152 0           push @{$self->{resources}}, $resource;
  0            
1153              
1154             # Update the "last modification time".
1155 0           $self->{mtime} = time;
1156 0           $self->{'dirty'} = 1;
1157              
1158 0           return $resource;
1159             }
1160              
1161             # At least one argument was given. Append all of the arguments
1162             # to the list of resources, and return the first one.
1163 0           push @{$self->{resources}}, @_;
  0            
1164              
1165             # Update the "last modification time".
1166 0           $self->{mtime} = time;
1167 0           $self->{'dirty'} = 1;
1168              
1169 0           return $_[0];
1170             }
1171              
1172              
1173             # findRecordByID
1174             # Returns a reference to the record with the given ID, or 'undef' if
1175             # it doesn't exist.
1176             sub findRecordByID
1177             {
1178 0     0 1   my $self = shift;
1179 0           my $id = shift;
1180              
1181 0 0         return undef if $id eq "";
1182              
1183 0           for (@{$self->{records}})
  0            
1184             {
1185 0 0         next unless $_->{id} == $id;
1186 0           return $_; # Found it
1187             }
1188              
1189 0           return undef; # Not found
1190             }
1191              
1192             #'
1193              
1194             # delete_Record
1195             # $pdb->delete_Record($record ?, $expunge?)
1196             #
1197             # Mark the given record for deletion. If $expunge is true, mark the
1198             # record for deletion without an archive.
1199              
1200             sub delete_Record
1201             {
1202 0     0 1   my $self = shift;
1203 0           my $record = shift;
1204 0           my $expunge = shift;
1205              
1206 0           $record->{attributes}{deleted} = 1;
1207 0 0         if ($expunge)
1208             {
1209 0           $record->{attributes}{expunged} = 1;
1210 0           $record->{attributes}{archive} = 0;
1211             } else {
1212 0           $record->{attributes}{expunged} = 0;
1213 0           $record->{attributes}{archive} = 1;
1214             }
1215              
1216             # Update the "last modification time".
1217 0           $self->{mtime} = time;
1218 0           $self->{'dirty'} = 1;
1219             }
1220              
1221             #'
1222              
1223             sub remove_Record($$)
1224             {
1225 0     0 1   my $self = shift;
1226 0           my $record = shift;
1227              
1228 0           for (my $i = 0; $i <= $#{$self->{records}}; $i ++)
  0            
1229             {
1230 0 0         if ($self->{records}->[$i] == $record)
1231             {
1232             # make a copy of the records array. This is really necessary
1233             # because there's frequently something using the records reference
1234             # for iteration purposes (like the doc example) and we can't
1235             # just start splicing that apart (tried, failed).
1236             # So we have to make a new copy. This does, unfortunately,
1237             # make remove_Record() more expensive that you'd expect.
1238 0           $self->{records} = [ @{$self->{records}} ];
  0            
1239              
1240             # remove the record index.
1241 0           splice @{$self->{records}}, $i, 1;
  0            
1242              
1243 0           $self->{mtime} = time;
1244 0           $self->{'dirty'} = 1;
1245              
1246 0           last;
1247             }
1248             }
1249             }
1250              
1251             1;
1252              
1253             __END__