#!/usr/bin/perl # # Dump a Palm PDB or PRC database. # # $Id: KeyRingpdbdump,v 1.2 2007/02/18 05:34:20 andrew Exp $ # $RedRiver: KeyRingpdbdump,v 1.1 2006/01/26 20:54:19 andrew Exp $ use strict; use Palm::PDB; use Palm::Raw; use lib 'lib'; # Load handlers for the built-in apps by default use Palm::Memo; use Palm::Address; use Palm::Datebook; use Palm::Mail; use Palm::ToDo; use Palm::Keyring; use vars qw( $VERSION %PDBHandlers %PRCHandlers $hexdump ); $VERSION = sprintf "%d.%03d_%03d_%03d", '$Revision: 1.2 $ ' =~ m{(\d+)(?:\.(\d+))}; *PDBHandlers = *Palm::PDB::PDBHandlers; *PRCHandlers = *Palm::PDB::PRCHandlers; &Palm::PDB::RegisterPRCHandlers("Palm::Raw", [ "", "" ] ); $hexdump = 1; # By default, print hex dumps of everything # Parse command-line arguments my $arg; while (($arg = $ARGV[0]) =~ /^-/) { $arg = shift; if (($arg eq "-h") || ($arg eq "-help") || ($arg eq "--help")) { print < Load (e.g., -MPalm::Address) EOT #' exit 0; } elsif ($arg =~ /^-M/) { eval "use $';"; } elsif ($arg eq "-nohex") { $hexdump = 0; } else { die "Unrecognized option: $arg\n"; } } my $fname = shift; die "No such file: $fname\n" if ! -f $fname; my $EPOCH_1904 = 2082844800; # Difference between Palm's # epoch (Jan. 1, 1904) and # Unix's epoch (Jan. 1, 1970), # in seconds. my $HeaderLen = 32+2+2+(9*4); # Size of database header my $RecIndexHeaderLen = 6; # Size of record index header my $IndexRecLen = 8; # Length of record index entry my $IndexRsrcLen = 10; # Length of resource index entry #%PDBHandlers = (); # Record handler map #%PRCHandlers = (); # Resource handler map #&Palm::PDB::rawread($fname); &rawread($fname); #package Palm::PDB; # XXX - Gross hack! sub rawread { my $self = new Palm::Raw; my $fname = shift; # Filename to read from my $buf; # Buffer into which to read stuff # Open database file open PDB, "< $fname" or die "Can't open \"$fname\": $!\n"; binmode PDB; # Parse as binary file under MS-DOS # Get the size of the file. It'll be useful later seek PDB, 0, 2; # 2 == SEEK_END. Seek to the end. $self->{_size} = tell PDB; print "File size: $self->{_size}\n"; seek PDB, 0, 0; # 0 == SEEK_START. Rewind to the beginning. # Read header my $name; my $attributes; my $version; my $ctime; my $mtime; my $baktime; my $modnum; my $appinfo_offset; my $sort_offset; my $type; my $creator; my $uniqueIDseed; read PDB, $buf, $HeaderLen; # Read the PDB header print "Database header:\n"; if ($hexdump) { &hexdump(" ", $buf); print "\n"; } # Split header into its component fields ($name, $attributes, $version, $ctime, $mtime, $baktime, $modnum, $appinfo_offset, $sort_offset, $type, $creator, $uniqueIDseed) = unpack "a32 n n N N N N N N a4 a4 N", $buf; ($self->{name} = $name) =~ s/\0*$//; $self->{attributes}{resource} = 1 if $attributes & 0x0001; $self->{attributes}{"read-only"} = 1 if $attributes & 0x0002; $self->{attributes}{"AppInfo dirty"} = 1 if $attributes & 0x0004; $self->{attributes}{backup} = 1 if $attributes & 0x0008; $self->{attributes}{"OK newer"} = 1 if $attributes & 0x0010; $self->{attributes}{reset} = 1 if $attributes & 0x0020; $self->{attributes}{open} = 1 if $attributes & 0x0040; $self->{attributes}{launchable} = 1 if $attributes & 0x0200; $self->{version} = $version; $self->{ctime} = $ctime - $EPOCH_1904; $self->{mtime} = $mtime - $EPOCH_1904; $self->{baktime} = $baktime - $EPOCH_1904; $self->{modnum} = $modnum; # _appinfo_offset and _sort_offset are private fields $self->{_appinfo_offset} = $appinfo_offset; $self->{_sort_offset} = $sort_offset; $self->{type} = $type; $self->{creator} = $creator; $self->{uniqueIDseed} = $uniqueIDseed; print <{attributes}{launchable}; print " OPEN" if $self->{attributes}{open}; print " RESET" if $self->{attributes}{reset}; print " OKNEWER" if $self->{attributes}{"OK newer"}; print " BACKUP" if $self->{attributes}{backup}; print " APPINFO-DIRTY" if $self->{attributes}{"AppInfo dirty"}; print " READ-ONLY" if $self->{attributes}{"read-only"}; print " RESOURCE" if $self->{attributes}{resource}; print "\n"; print <{attributes}{resource}) { # Look among resource handlers $handler = $PRCHandlers{$self->{creator}}{$self->{type}} || $PRCHandlers{undef}{$self->{type}} || $PRCHandlers{$self->{creator}}{""} || $PRCHandlers{""}{""}; } else { # Look among record handlers $handler = $PDBHandlers{$self->{creator}}{$self->{type}} || $PDBHandlers{""}{$self->{type}} || $PDBHandlers{$self->{creator}}{""} || $PDBHandlers{""}{""}; } if (defined($handler)) { bless $self, $handler; } else { # XXX - This should probably return 'undef' or something, # rather than die. die "No handler defined for creator \"$creator\", type \"$type\"\n"; } ## Read record/resource index # Read index header read PDB, $buf, $RecIndexHeaderLen; print "Record/resource index header:\n"; if ($hexdump) { &hexdump(" ", $buf); print "\n"; } my $next_index; my $numrecs; ($next_index, $numrecs) = unpack "N n", $buf; $self->{_numrecs} = $numrecs; print <{attributes}{resource}) { &_load_rsrc_index($self, \*PDB); } else { &_load_rec_index($self, \*PDB); } # Ignore the two NUL bytes that are usually here. We'll seek() # around them later. # Read AppInfo block, if it exists if ($self->{_appinfo_offset} != 0) { &_load_appinfo_block($self, \*PDB); } # Read sort block, if it exists if ($self->{_sort_offset} != 0) { &_load_sort_block($self, \*PDB); } # Read record/resource list if ($self->{attributes}{resource}) { &_load_resources($self, \*PDB); } else { &_load_records($self, \*PDB); } # These keys were needed for parsing the file, but are not # needed any longer. Delete them. delete $self->{_index}; delete $self->{_numrecs}; delete $self->{_appinfo_offset}; delete $self->{_sort_offset}; delete $self->{_size}; close PDB; } # _load_rec_index # Private function. Read the record index, for a record database sub _load_rec_index { my $pdb = shift; my $fh = shift; # Input file handle my $i; my $lastoffset = 0; print "Record index:\n"; # Read each record index entry in turn for ($i = 0; $i < $pdb->{_numrecs}; $i++) { my $buf; # Input buffer print " Record index entry $i\n"; # Read the next record index entry my $offset; my $attributes; my @id; # Raw ID my $id; # Numerical ID my $entry = {}; # Parsed index entry read $fh, $buf, $IndexRecLen; if ($hexdump) { &hexdump(" ", $buf); print "\n"; } # The ID field is a bit weird: it's represented as 3 # bytes, but it's really a double word (long) value. ($offset, $attributes, @id) = unpack "N C C3", $buf; if ($offset == $lastoffset) { print STDERR "Record $i has same offset as previous one: $offset\n"; } $lastoffset = $offset; $entry->{offset} = $offset; $entry->{attributes}{expunged} = 1 if $attributes & 0x80; $entry->{attributes}{dirty} = 1 if $attributes & 0x40; $entry->{attributes}{deleted} = 1 if $attributes & 0x20; $entry->{attributes}{private} = 1 if $attributes & 0x10; $entry->{id} = ($id[0] << 16) | ($id[1] << 8) | $id[2]; # The lower 4 bits of the attributes field are # overloaded: If the record has been deleted and/or # expunged, then bit 0x08 indicates whether the record # should be archived. Otherwise (if it's an ordinary, # non-deleted record), the lower 4 bits specify the # category that the record belongs in. if (($attributes & 0xa0) == 0) { $entry->{category} = $attributes & 0x0f; } else { $entry->{attributes}{archive} = 1 if $attributes & 0x08; } print <{attributes}}]} Category: $entry->{category} ID: @{[sprintf("0x%02x%02x%02x", @id)]} EOT # Put this information on a temporary array push @{$pdb->{_index}}, $entry; } } # XXX - Make this print out debugging information # _load_rsrc_index # Private function. Read the resource index, for a resource database sub _load_rsrc_index { my $pdb = shift; my $fh = shift; # Input file handle my $i; print "Resource index:\n"; # Read each resource index entry in turn for ($i = 0; $i < $pdb->{_numrecs}; $i++) { my $buf; # Input buffer print " Resource index entry $i\n"; # Read the next resource index entry my $type; my $id; my $offset; my $entry = {}; # Parsed index entry read $fh, $buf, $IndexRsrcLen; if ($hexdump) { &hexdump(" ", $buf); print "\n"; } ($type, $id, $offset) = unpack "a4 n N", $buf; $entry->{type} = $type; $entry->{id} = $id; $entry->{offset} = $offset; print <{_index}}, $entry; } } # _load_appinfo_block # Private function. Read the AppInfo block sub _load_appinfo_block { my $pdb = shift; my $fh = shift; # Input file handle my $len; # Length of AppInfo block my $buf; # Input buffer print "AppInfo block:\n"; # Sanity check: make sure we're positioned at the beginning of # the AppInfo block if (tell($fh) > $pdb->{_appinfo_offset}) { die "Bad AppInfo offset: expected ", sprintf("0x%08x", $pdb->{_appinfo_offset}), ", but I'm at ", tell($fh), "\n"; } # Seek to the right place, if necessary if (tell($fh) != $pdb->{_appinfo_offset}) { seek PDB, $pdb->{_appinfo_offset}, 0; } # There's nothing that explicitly gives the size of the # AppInfo block. Rather, it has to be inferred from the offset # of the AppInfo block (previously recorded in # $pdb->{_appinfo_offset}) and whatever's next in the file. # That's either the sort block, the first data record, or the # end of the file. if ($pdb->{_sort_offset}) { # The next thing in the file is the sort block $len = $pdb->{_sort_offset} - $pdb->{_appinfo_offset}; } elsif ((defined $pdb->{_index}) && @{$pdb->{_index}}) { # There's no sort block; the next thing in the file is # the first data record $len = $pdb->{_index}[0]{offset} - $pdb->{_appinfo_offset}; } else { # There's no sort block and there are no records. The # AppInfo block goes to the end of the file. $len = $pdb->{_size} - $pdb->{_appinfo_offset}; } # Read the AppInfo block read $fh, $buf, $len; if ($hexdump) { &hexdump(" ", $buf); print "\n"; } # Tell the real class to parse the AppInfo block $pdb->{appinfo} = $pdb->ParseAppInfoBlock($buf); # Print out the parsed values if (ref($pdb->{appinfo}) ne "") { &dumphash($pdb->{appinfo}, "\t"); print "\n"; } } # _load_sort_block # Private function. Read the sort block. sub _load_sort_block { my $pdb = shift; my $fh = shift; # Input file handle my $len; # Length of sort block my $buf; # Input buffer print "Sort block:\n"; # Sanity check: make sure we're positioned at the beginning of # the sort block if (tell($fh) > $pdb->{_sort_offset}) { die "Bad sort block offset: expected ", sprintf("0x%08x", $pdb->{_sort_offset}), ", but I'm at ", tell($fh), "\n"; } # Seek to the right place, if necessary if (tell($fh) != $pdb->{_sort_offset}) { seek PDB, $pdb->{_sort_offset}, 0; } # There's nothing that explicitly gives the size of the sort # block. Rather, it has to be inferred from the offset of the # sort block (previously recorded in $pdb->{_sort_offset}) # and whatever's next in the file. That's either the first # data record, or the end of the file. if (defined($pdb->{_index})) { # The next thing in the file is the first data record $len = $pdb->{_index}[0]{offset} - $pdb->{_sort_offset}; } else { # There are no records. The sort block goes to the end # of the file. $len = $pdb->{_size} - $pdb->{_sort_offset}; } # Read the AppInfo block read $fh, $buf, $len; if ($hexdump) { &hexdump(" ", $buf); print "\n"; } # XXX - Check to see if the sort block has some predefined # structure. If so, it might be a good idea to parse the sort # block here. # Tell the real class to parse the sort block $pdb->{sort} = $pdb->ParseSortBlock($buf); } # _load_records # Private function. Load the actual data records, for a record database # (PDB) sub _load_records { my $pdb = shift; my $fh = shift; # Input file handle my $i; print "Records:\n"; # Read each record in turn for ($i = 0; $i < $pdb->{_numrecs}; $i++) { my $len; # Length of record my $buf; # Input buffer print " Record $i\n"; # Sanity check: make sure we're where we think we # should be. if (tell($fh) > $pdb->{_index}[$i]{offset}) { # XXX - The two NULs are really optional. # die "Bad offset for record $i: expected ", # sprintf("0x%08x", # $pdb->{_index}[$i]{offset}), # " but it's at ", # sprintf("[0x%08x]", tell($fh)), "\n"; } # Seek to the right place, if necessary if (tell($fh) != $pdb->{_index}[$i]{offset}) { seek PDB, $pdb->{_index}[$i]{offset}, 0; } # Compute the length of the record: the last record # extends to the end of the file. The others extend to # the beginning of the next record. if ($i == $pdb->{_numrecs} - 1) { # This is the last record $len = $pdb->{_size} - $pdb->{_index}[$i]{offset}; } else { # This is not the last record $len = $pdb->{_index}[$i+1]{offset} - $pdb->{_index}[$i]{offset}; } # Read the record read $fh, $buf, $len; if ($hexdump) { &hexdump(" ", $buf); print "\n"; } # Tell the real class to parse the record data. Pass # &ParseRecord all of the information from the index, # plus a "data" field with the raw record data. my $record; $record = $pdb->ParseRecord( %{$pdb->{_index}[$i]}, "data" => $buf, ); my $pass = 12345; if ($i == 0) { unless (keyring_verify($record->{data}, $pass) ) { die "Incorrect password!"; } } else { $record->{decrypted} = decrypt($record->{data}, $pass); } push @{$pdb->{records}}, $record; if ($hexdump) { &hexdump(" ", $record->{decrypted}); print "\n"; } # Print out the parsed values &dumphash($record, "\t"); print "\n"; } use Data::Dumper; print Dumper $pdb; } # _load_resources # Private function. Load the actual data resources, for a resource database # (PRC) sub _load_resources { my $pdb = shift; my $fh = shift; # Input file handle my $i; print "Resources:\n"; # Read each resource in turn for ($i = 0; $i < $pdb->{_numrecs}; $i++) { my $len; # Length of record my $buf; # Input buffer print " Resource $i\n"; # Sanity check: make sure we're where we think we # should be. if (tell($fh) > $pdb->{_index}[$i]{offset}) { die "Bad offset for resource $i: expected ", sprintf("0x%08x", $pdb->{_index}[$i]{offset}), " but it's at ", sprintf("0x%08x", tell($fh)), "\n"; } # Seek to the right place, if necessary if (tell($fh) != $pdb->{_index}[$i]{offset}) { seek PDB, $pdb->{_index}[$i]{offset}, 0; } # Compute the length of the resource: the last # resource extends to the end of the file. The others # extend to the beginning of the next resource. if ($i == $pdb->{_numrecs} - 1) { # This is the last resource $len = $pdb->{_size} - $pdb->{_index}[$i]{offset}; } else { # This is not the last resource $len = $pdb->{_index}[$i+1]{offset} - $pdb->{_index}[$i]{offset}; } # Read the resource read $fh, $buf, $len; if ($hexdump) { &hexdump(" ", $buf); print "\n"; } # Tell the real class to parse the resource data. Pass # &ParseResource all of the information from the # index, plus a "data" field with the raw resource # data. my $resource; $resource = $pdb->ParseResource( %{$pdb->{_index}[$i]}, "data" => $buf, ); push @{$pdb->{resources}}, $resource; # Print out the parsed values &dumphash($resource, "\t"); print "\n"; } } sub hexdump { my $prefix = shift; # What to print in front of each line my $data = shift; # The data to dump my $maxlines = shift; # Max # of lines to dump my $offset; # Offset of current chunk for ($offset = 0; $offset < length($data); $offset += 16) { my $hex; # Hex values of the data my $ascii; # ASCII values of the data my $chunk; # Current chunk of data last if defined($maxlines) && ($offset >= ($maxlines * 16)); $chunk = substr($data, $offset, 16); ($hex = $chunk) =~ s/./sprintf "%02x ", ord($&)/ges; ($ascii = $chunk) =~ y/\040-\176/./c; printf "%s %-48s|%-16s|\n", $prefix, $hex, $ascii; } } # XXX - Ought to have a &dumparray as well. The two can call each other # recursively. sub dumphash { my $hash = shift; my $indent = shift; my $key; my $value; while (($key, $value) = each %{$hash}) { if (ref($value) eq "HASH") { print $indent, $key, ":\n"; &dumphash($value, $indent . "\t"); } elsif (ref($value) eq "ARRAY") { my($i,$j); print $indent, $key, ":\n"; for ($i = 0; $i <= $#{$value}; $i++) { if (ref($value->[$i]) eq "HASH") { print $indent, " $i:\n"; &dumphash($value->[$i], $indent . "\t"); } elsif (ref($value->[$i]) eq "ARRAY") { my @v2 = @{$value->[$i]}; for ($j = 0; $j <= $#v2; $j++) { print $indent, "\t$i-$j: [$v2[$j]]\n"; } }else { print $indent, "\t$i: [$value->[$i]]\n"; } } } else { print $indent, $key, " -> [", $value, "]\n"; } } } __END__ =head1 NAME pdbdump - Print the contents of a Palm PDB file =head1 SYNOPSIS C I<[options]> F =head1 DESCRIPTION C reads a PalmOS F<.pdb> file, parses it, and prints its contents. This includes both a hex dump of the raw data of each piece, and a human-readable list of the various values, insofar as possible. The aim of C is to allow one to verify whether a particular file is a well-formed PalmOS database file and if not, where the error lies. If the database is of a known type, C parses the AppInfo block and records. Otherwise, it simply prints out a hex dump of their contents. C includes, by default, support for most of the built-in applications. Other helper modules may be loaded with the C<-M> option. =head1 OPTIONS =over 4 =item -h -help --help Print a usage message and exit. =item -nohex Don't print the hex dump of the various parts. =item -MI C the named module. This can be useful for loading additional helper modules. =back =head1 BUGS C only recognizes record databases (C<.pdb> files), not resource databases (C<.prc> files). =head1 SEE ALSO Palm::PDB(3) =head1 AUTHOR Andrew Arensburger