#!/usr/bin/perl # # Copyright (C) 2000, Andrew Arensburger. # You may distribute this file under the terms of the Artistic # License, as specified in the README file. # # $Id: std-categories,v 3.5 2001/06/04 01:41:10 arensb Exp $ # # This is a ColdSync Sync conduit for synchronizing the categories between # a database on the Palm and one on the desktop. # # This script is rather hairy, and probably doesn't do the Right Thing in # all cases, but that's because categories aren't quite as clean as they # could be. Certainly nowhere near as pretty as record syncing. # XXX - "std-categories -config" gives a dangerous configuration: # type: */*; # this includes databases that don't have a standard AppInfo block. # What happens: # - When you add a new category, the Palm finds an empty slot, picks an # unused category ID, fills in the slot, sets its "renamed" field to # true. # The Palm will grab the first empty slot, so if you delete one category # and create another, the new category can take the slot formerly # occupied by the previous one. I.e., you won't even see an empty name to # tell you that a category has been deleted. # - When you delete a category, the Palm clears the "name" field, but # doesn't change the ID. "renamed" is false. # - When you rename a category, the Palm picks a new ID, changes the name, # and sets "renamed" to true. The index remains the same, though. use strict; use ColdSync; use ColdSync::SPC; use Palm::StdAppInfo; use vars qw( %Pappinfo $DEBUG ); use constant NumCategories => Palm::StdAppInfo::numCategories; # Number of categories in a # standard AppInfo block $DEBUG = 1; # Turns debugging on StartConduit("sync"); select STDERR; # XXX - Just for debugging my $err; my $dbinfo; # Information about currently-open database my $dbh; # Database handle # Find out which database we're dealing with $dbinfo = spc_get_dbinfo; die "401 Error reading database info" if !defined($dbinfo); # Open the database ($dbh = &dlp_OpenDB($dbinfo->{name}, 0x80)) or die "402 Can't open database"; # Get the category list from the Palm use vars qw( $Pappinfo_raw ); # Raw AppInfo block data use vars qw( %Pappinfo ); # Parsed category information from AppInfo # block $Pappinfo_raw = dlp_ReadAppBlock($dbh); # Download the AppInfo block from the Palm # XXX - Error-checking # XXX - Might be a good idea to make sure that $Pappinfo_raw is at least # long enough for a standard set of categories, in case some dumbass runs # this on a non-StdAppInfo database. &Palm::StdAppInfo::parse_StdAppInfo(\%Pappinfo, $Pappinfo_raw); # Parse the AppInfo block into a set of # categories. # XXX - Might be a good idea to make sure that category 0 has ID 0, in case # some dumbass runs this on a non-StdAppInfo database. if ($DEBUG) { print "* Palm categories:\n"; &dumphash(\%Pappinfo, ""); print "* Desktop categories:\n"; &dumphash($PDB->{appinfo}, ""); } # Phase 1: Underpants! # Phase 2: Get the names and IDs in sync between the Palm and Desktop # Phase 2.1: Look at the categories on the Palm print STDERR "===== Examining Palm categories:\n" if $DEBUG; for (my $i = 0; $i < NumCategories; $i++) { my $category = $Pappinfo{categories}[$i]; my $Dcategory; print STDERR "category $i: [$category->{name}]\n" if $DEBUG; # XXX - This logic for finding the corresponding category on the # desktop is too primitive: if the Palm category is unchanged, then # we need to find either a clean category on the desktop with the # same name, or a modified category with the same ID. if ($category->{name} eq "") { $Dcategory = &find_cat_by_id($PDB->{appinfo}, $category->{id}); if (!defined($Dcategory)) { # This category exists neither on the Palm, nor # on the desktop. Nothing to do. print STDERR "This category doesn't exist. Nothing to do.\n" if $DEBUG; next; } # This category doesn't exist on the Palm, but does # exist on the desktop if ($Dcategory->{renamed}) { # Doesn't exist on the Palm. # Exists on the desktop. # Renamed on the desktop. # -> Add it on the Palm. print STDERR "Renamed on the desktop. Renaming on Palm.\n" if $DEBUG; $category->{name} = $Dcategory->{name}; $category->{renamed} = 0; $Dcategory->{renamed} = 0; $Dcategory->{_moveto} = $i; } else { # Doesn't exist on Palm. # Exists on desktop. # Not renamed. # -> Deleted on Palm. Delete it on desktop print STDERR "Deleted on Palm. Deleting on desktop.\n" if $DEBUG; $Dcategory->{name} = ""; $category->{renamed} = 0; $Dcategory->{renamed} = 0; $Dcategory->{_moveto} = $i; } } else { # This category exists on the Palm if ($category->{renamed}) { $Dcategory = &find_cat_by_id($PDB->{appinfo}, $category->{id}); if (defined($Dcategory)) { # Exists on Palm. # Exists on desktop. # Renamed on Palm. # -> Renamed on Palm. Change name on desktop. print STDERR "Renamed on Palm. Need to rename on desktop.\n" if $DEBUG; $Dcategory->{name} = $category->{name}; $category->{renamed} = 0; $Dcategory->{renamed} = 0; $Dcategory->{_moveto} = $i; } else { # Exists on Palm. # Doesn't exist on desktop. # Renamed on Palm. # -> Add it on the desktop. print STDERR "New category. Need to add on desktop.\n" if $DEBUG; $category->{renamed} = 0; $Dcategory->{renamed} = 0; $Dcategory->{_moveto} = $i; } } else { # XXX - The category might exist, renamed, on the # desktop, in which case &find_cat_by_name won't # find it. $Dcategory = &find_cat_by_name($PDB->{appinfo}, $category->{name}); if (defined($Dcategory)) { # This category exists on the Palm and # on the desktop. It has not been renamed if ($Dcategory->{renamed}) { # Exists on Palm. # Exists on desktop. # Not renamed on Palm. # Renamed on desktop. # -> Rename on Palm. print STDERR "Renamed on desktop. Need to rename on Palm.\n" if $DEBUG; $category->{name} = $Dcategory->{name}; $category->{renamed} = 0; $Dcategory->{renamed} = 0; $Dcategory->{_moveto} = $i; } else { # Exists on Palm. # Exists on desktop. # Not renamed on Palm. # Not renamed on desktop. # -> Okay, but make sure the IDs are # in sync. print STDERR "OK. Make sure IDs are in sync.\n" if $DEBUG; $Dcategory->{id} = $category->{id}; $category->{renamed} = 0; $Dcategory->{renamed} = 0; $Dcategory->{_moveto} = $i; } } else { # Exists on Palm. # Doesn't exist on desktop. # Not renamed on Palm. # -> Not clear whether a) deleted on desktop, # or b) added (and synced) on Palm. # -> Be conservative and add it on the desktop. print STDERR "Judgment call: add on the desktop.\n" if $DEBUG; } } } } # Phase 2.2: Look at the categories on the desktop print STDERR "===== Examining desktop categories:\n" if $DEBUG; for (my $i = 0; $i < NumCategories; $i++) { my $category = $PDB->{appinfo}{categories}[$i]; print STDERR "category $i: [$category->{name}]\n" if $DEBUG; if (defined($category->{_moveto})) { print STDERR "\tSeen it already.\n" if $DEBUG; next; } if ($category->{renamed}) { # This category exists on the desktop, but not on the Palm. if ($category->{name} eq "") { # Doesn't exist on Palm. # Exists on desktop. # Deleted on desktop # -> Delete on Palm. # XXX - Is there anything to do, really? print STDERR "Deleted on desktop. Deleting on Palm.\n" if $DEBUG; $category->{_moveto} = 0; } else { # Doesn't exist on Palm. # Exists on desktop. # Renamed on desktop. # -> Add to Palm print STDERR "Added on desktop. Adding on Palm.\n" if $DEBUG; my $newindex; if ($Pappinfo{categories}[$i]{name} eq "") { # This index is free on the Palm, so we # don't have to move the category. $newindex = $i; } else { $newindex = &find_empty_cat(\%Pappinfo); if ($newindex < 0) { # No free slots available. print STDERR "No free categories. Dropping desktop category \"$category->{name}\".\n" if $DEBUG; $category->{_moveto} = 0; next; } } print STDERR "Adding on Palm, index $newindex\n" if $DEBUG; my $Pcategory = $Pappinfo{categories}[$newindex]; $Pcategory->{name} = $category->{name}; $Pcategory->{id} = $category->{id}; $Pcategory->{renamed} = 0; $category->{_moveto} = $i; } } else { # This category exists on the desktop, but not on the Palm. # It has not been renamed. # It may have been either added (and synced) on the desktop, # or it might have been deleted on the Palm. # -> Be conservative, and add it on the Palm, if possible. my $newindex; if ($Pappinfo{categories}[$i]{name} eq "") { # This index is free on the Palm, so we don't have # to move the category. $newindex = $i; } else { $newindex = &find_empty_cat(\%Pappinfo); if ($newindex < 0) { # No free slots available. print STDERR "No free categories. Dropping desktop category \"$category->{name}\".\n" if $DEBUG; $category->{_moveto} = 0; next; } } print STDERR "Adding on Palm, index $newindex\n" if $DEBUG; my $Pcategory = $Pappinfo{categories}[$newindex]; $Pcategory->{name} = $category->{name}; $Pcategory->{id} = $category->{id}; $Pcategory->{renamed} = 0; $category->{_moveto} = $i; } } # XXX - Should probably double-check that category 0 has ID 0 (Unfiled). if ($DEBUG) { print STDERR "===== Final set of categories:\n"; &dumphash(\%Pappinfo, ""); } # Phase 2.3: Move new categories to the Palm. for (@{$PDB->{records}}) { my $oldcat = $_->{category}; my $newcat = $PDB->{appinfo}{categories}[$oldcat]{_moveto}; if (!defined($newcat)) { die("555 Category $oldcat doesn't have a _moveto. Aborting."); } next if $oldcat == $newcat; print STDERR "Moving record $_->{id} from $oldcat to $newcat.\n" if $DEBUG; $_->{category} = $newcat; $_->{attributes}{dirty} = 1; } # Phase 2.4: Copy the Palm AppInfo block to the desktop PDB for (my $i = 0; $i < NumCategories; $i++) { my $Pcategory = $Pappinfo{categories}[$i]; my $Dcategory = $PDB->{appinfo}{categories}[$i]; $Dcategory->{name} = $Pcategory->{name}; $Dcategory->{id} = $Pcategory->{id}; $Dcategory->{renamed} = $Pcategory->{renamed}; } # Phase 3: Profits! # Upload the AppInfo block to the Palm. $Pappinfo_raw = &Palm::StdAppInfo::pack_StdAppInfo(\%Pappinfo); #$err = &dlp_WriteAppBlock($dbh, $Pappinfo_raw); # XXX - Error-checking $err = &dlp_CloseDB($dbh); # XXX - Error-checking #die "599 Dying for no good reason"; # XXX - Just to avoid writing the database prematurely EndConduit; sub find_cat_by_name { my $appinfo = shift; my $name = shift; for (@{$appinfo->{categories}}) { next unless $_->{name} eq $name; return $_; # Found it } return undef; # Can't find it } sub find_cat_by_id { my $appinfo = shift; my $id = shift; for (@{$appinfo->{categories}}) { next unless $_->{id} eq $id; return $_; # Found it } return undef; # Can't find it } sub find_empty_cat { my $appinfo = shift; my $i; for ($i = 0; $i < NumCategories; $i++) { next unless $appinfo->{categories}[$i]{name} eq ""; return $i; } return -1; } ##### Debugging functions 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; } } 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") { print $indent, $key, ":\n"; &dumparray($value, $indent . "\t"); } else { print $indent, $key, " -> [", $value, "]\n"; } } } sub dumparray { my $array = shift; my $indent = shift; my $i; for ($i = 0; $i <= $#{$array}; $i++) { if (ref($array->[$i]) eq "HASH") { print $indent, $i, ":\n"; &dumphash($array->[$i], $indent . "\t"); } elsif (ref($array->[$i]) eq "ARRAY") { print $indent, $i, ":\n"; &dumparray($array->[$i], $indent . "\t"); } else { print $indent, $i, ": ", $array->[$i], "\n"; } } } # This is for Emacs's benefit: # Local Variables: *** # fill-column: 75 *** # End: ***