File Coverage

blib/lib/Palm/ListDB/Writer.pm
Criterion Covered Total %
statement 114 129 88.3
branch 30 56 53.5
condition 9 18 50.0
subroutine 11 11 100.0
pod 5 5 100.0
total 169 219 77.1


line stmt bran cond sub pod time code
1             package Palm::ListDB::Writer;
2              
3             # ListDB.pm -- Create databases for Palm List application.
4             # RCS Info : $Id: Writer.pm,v 1.11 2010/06/02 06:13:59 jv Exp $
5             # Author : Johan Vromans
6             # Created On : Sun Aug 31 20:18:31 2003
7             # Last Modified By: Johan Vromans
8             # Last Modified On: Wed Jun 2 08:13:39 2010
9             # Update Count : 83
10             # Status : Unknown, Use with caution!
11              
12             =head1 NAME
13              
14             Palm::ListDB::Writer - Create databases for Palm List application
15              
16             =head1 SYNOPSIS
17              
18             use Palm::ListDB::Writer;
19             my $db = new Palm::ListDB::Writer
20             "MyDataBase",
21             "label1" => "Name",
22             "label2" => "Address");
23             $db->add("Private", "Wife", "16 Localstreet, Netcity", "Some comments");
24             $db->add("Public", "John Doe", "1 Main St., Connecticut", "Blah blah");
25             $db->write("addr.pdb");
26              
27             =head1 ABSTRACT
28              
29             Palm::ListDB::Writer creates databases for the Palm utility List.
30              
31             =head1 DESCRIPTION
32              
33             Palm::ListDB::Writer can be used to create databases for the Palm
34             utility List, a simple but convenient database application. List is
35             written by Andrew Low (roo@magma.ca, http://www.magma.ca/~roo).
36              
37             List databases consist of a collection of records, each having two
38             label fields and one note field (arbitrary data).
39              
40             The basic usage is to create a Palm::ListDB::Writer object with
41             attributes like the name of the database, the primary and secondary
42             field labels, and then add records by calling its add() method.
43              
44             The write() method writes the collected data out in the form of a Palm
45             database.
46              
47             Limitations posed by the Palm operating system and/or the List
48             application: database name - 31 characters; field names - 15
49             characters; category names - 15 characters; 15 categories excluding
50             the default (Unfiled) catagory; field values - 63 characters; length
51             of note data - 1023 characters.
52              
53             This module is not related to L; the latter can also
54             import Palm List databases, and requires some knowledge about Palm
55             databases.
56              
57             =cut
58              
59 1     1   61738 use strict;
  1         1  
  1         41  
60 1     1   5 use warnings;
  1         2  
  1         30  
61 1     1   6 use Carp;
  1         6  
  1         1698  
62              
63             our $VERSION = sprintf('%d.%02d', '$Revision: 1.11 $ ' =~ /(\d+)\.(\d+)/);
64              
65             my @_atts = qw(cat truncate readonly private backup
66             autocat label1 label2);
67              
68             =head1 METHODS
69              
70             =over 4
71              
72             =item new I, [ I ]
73              
74             Default constructor for a new database object.
75              
76             new() requires one argument, the name of the database.
77              
78             Initial attributes for the database can be specified after the
79             database name in the form of key => value pairs:
80              
81             =over 4
82              
83             =item label1
84              
85             The name for the primary record field, max. 15 characters.
86              
87             =item label2
88              
89             The name for the secondary record field, max. 15 characters.
90              
91             =item cat
92              
93             An array reference with the names of the categories. Max. 15
94             categories are allowed, and category names must not exceed 15
95             characters in length.
96              
97             =item autocat
98              
99             If non-zero, new categories are automatically added when records are
100             added. Defaults to true if no initial category names were supplied.
101              
102             Additional methods can be added later with the add_cat() method.
103              
104             =item readonly
105              
106             If true, the database will be readonly and cannot be modified by the
107             List application.
108              
109             =item backup
110              
111             If false, the database will not be backupped upon the next HotSync.
112             Note that the List application may change this, for example when
113             modifications are made.
114              
115             =item private
116              
117             If true, the database is private and cannot be beamed.
118              
119             =item truncate
120              
121             Controls truncation of names and fields that are too long.
122             If zero, no truncation takes place (the program is terminated).
123             If one, excess data for the record fields is truncated.
124             If two, also truncates names for categories and fields.
125             If three, also truncates the name of the database if needed.
126              
127             =back
128              
129             =cut
130              
131             sub new($;@) {
132 1     1 1 1563 my ($pkg, $name, %opts) = @_;
133 1         11 my $self =
134             { name => $name,
135             cat => [],
136             _cat => {},
137             truncate => 0,
138             readonly => 0,
139             private => 0,
140             backup => 1,
141             autocat => undef,
142             label1 => "Field1",
143             label2 => "Field2",
144             _data => [],
145             };
146 1         4 bless($self, $pkg);
147              
148 1         16 $self->{name} = $self->_checklen("Database name", $name, 31, 2);
149              
150 1         3 foreach my $att ( @_atts ) {
151 8 100       20 if ( exists($opts{$att}) ) {
152 1         5 $self->{$att} = delete($opts{$att});
153             }
154             }
155 1 50       6 croak(__PACKAGE__.": Unknown constructor attributes: ".
156             join(" ", sort(keys(%opts)))) if %opts;
157              
158 1 50       5 $self->{autocat} = @{$self->{cat}} ? 0 : 1
  1 50       5  
159             unless defined $self->{autocat};
160 1         1 unshift(@{$self->{cat}}, "Unfiled");
  1         4  
161 1         3 my @a = @{$self->{cat}};
  1         4  
162 1         2 $self->{cat} = [];
163 1         3 foreach my $cat ( @a ) {
164 4         10 $self->_addcat($cat);
165             }
166 1         5 $self->{label1} = $self->_checklen("Label1", $self->{label1}, 15, 1);
167 1         3 $self->{label2} = $self->_checklen("Label2", $self->{label2}, 15, 1);
168 1         8 $self->{ctime} = $self->{mtime} = $self->{btime} = time;
169 1         4 $self;
170             }
171              
172             sub _checklen {
173 15     15   26 my ($self, $name, $value, $max, $lvl) = @_;
174 15 50       30 if ( length($value) > $max ) {
175 0         0 my $v = substr($value,0,30);
176 0         0 $v =~ s/[^\040-\177]/./g;
177 0         0 my $msg = __PACKAGE__.": $name (".
178             $v.") too long (".
179             length($value)." > $max)";
180 0 0       0 if ( $self->{truncate} > $lvl ) {
181 0         0 warn("$msg, truncated\n");
182 0         0 substr($value, $max) = "";
183             }
184             else {
185 0         0 croak($msg);
186             }
187             }
188 15         44 $value;
189             }
190              
191             sub _addcat {
192 6     6   9 my ($self, $value) = @_;
193 6         12 $value = $self->_checklen("Category name", $value, 15, 1);
194 6 50       16 return $self->{_cat}->{$value}
195             if defined($self->{_cat}->{$value});
196 6 50       7 if ( @{$self->{cat}} == 16 ) {
  6         16  
197 0         0 croak(__PACKAGE__.": Too many categories ($value)");
198             }
199 6         7 push(@{$self->{cat}}, $value);
  6         13  
200 6         9 $self->{_cat}->{$value} = @{$self->{cat}};
  6         23  
201             }
202              
203             =item add I, I, I, I
204              
205             As the name suggests, add() adds records to the database.
206              
207             Add() takes exactly four arguments: the category for the record, its
208             first field, its second field, and the note data. Fields may be left
209             empty (or undefined), but not all of them.
210              
211             If the named category does not exists, and autocat is in effect, it is
212             automatically added to the list of categories.
213              
214             Add() returns true if the record was successfully added.
215              
216             =cut
217              
218             sub add($$$$$) {
219 2     2 1 7 my ($self, $cat, $f1, $f2, $note) = @_;
220              
221 2 100       9 if ( $self->{_cat}->{$cat} ) {
    50          
222 1         3 $cat = $self->{_cat}->{$cat};
223             }
224             elsif ( $self->{autocat} ) {
225 1         4 $cat = $self->_addcat($cat);
226             }
227             else {
228 0         0 carp(__PACKAGE__.": Unknown category ($cat)");
229 0         0 return 0;
230             }
231 2 0 33     16 if ( $f1 eq "" && $f2 eq "" && $note eq "" ) {
      33        
232 0         0 carp(__PACKAGE__.": Record needs data");
233 0         0 return 0;
234             }
235 2   50     2 push(@{$self->{_data}},
  2   50     10  
      50        
236             [$cat,
237             $self->_checklen("field1", $f1||"", 63, 1),
238             $self->_checklen("field2", $f2||"", 63, 1),
239             $self->_checklen("note", $note||"", 1023, 0)]);
240 2         8 1;
241             }
242              
243             =item add_cat I
244              
245             Adds a new category. One parameter, the name of the category to be
246             added. If the category already exists, nothing happens.
247              
248             =cut
249              
250             sub add_cat($$) {
251 1     1 1 254 my ($self, $cat) = @_;
252 1         2 my $catcode = $self->{_cat}->{$cat};
253 1 50       4 return $catcode if $catcode;
254 1         4 $self->_addcat($cat);
255             }
256              
257             =item categories
258              
259             Returns an array with the current set of categories.
260             Note that this excludes the (default) 'Unfiled' category.
261              
262             =cut
263              
264             sub categories($) {
265 1     1 1 2 my ($self) = @_;
266 1         2 my @a = @{$self->{cat}};
  1         4  
267 1         2 shift(@a);
268 1         6 @a;
269             }
270              
271             =item write I
272              
273             Write() takes one argument: the file name for the database.
274              
275             Returns true if the database was successfully written.
276              
277             =cut
278              
279             sub write($$) {
280 1     1 1 3 my ($self, $file) = @_;
281 1 50       16 unless ( @{$self->{_data}} ) {
  1         6  
282 0         0 carp(__PACKAGE__.": No records to write to $file");
283 0         0 return 0;
284             }
285              
286             # Based on information derived from code by Gustaf Naeser and
287             # Darren Dunham.
288              
289 1         2 my $n_records = scalar(@{$self->{_data}});
  1         2  
290 1         3 my $dbname = $self->{name};
291 1         2 my $field1label = $self->{label1};
292 1         2 my $field2label = $self->{label2};
293 1         2 my $numcats = @{$self->{cat}};
  1         2  
294              
295             # Pre-sort the records. This eliminates the need for List to resort,
296             # which makes opening the database very fast.
297 1     1   2078 use locale;
  1         233  
  1         5  
298 1 50 33     10 my @records = sort {
299 1         5 lc($a->[1]) cmp lc($b->[1])
300             ||
301             lc($a->[2]) cmp lc($b->[2])
302             ||
303             $a->[0] <=> $b->[0]
304 1         2 } @{$self->{_data}};
305              
306 1         2 my $fh;
307 1 50       128 open($fh, ">$file")
308             || croak(__PACKAGE__.": $file: $!");
309 1         4 binmode($fh);
310              
311             # Structure of the database
312             #
313             # - Database header (78 bytes)
314             # - Index table ($n_records * 8 bytes + 0x8000 padding)
315             # - Application info (512 bytes)
316             # - Data (records)
317              
318             # The database header (78 bytes)
319             # 32 bytes database name, nul filled, nul terminated
320             # 2 bytes of attributes, set to 0x0008 (backup)
321             # 2 bytes of version information, set to 0x0000
322             # 12 bytes dates (creation, modification, last backup; 4 bytes each)
323             # 4 bytes modification number, set to 0x00000000
324             # 4 bytes offset to application info
325             # 4 bytes offset to sort info (set to 0x00000000)
326             # 4 bytes type = "DATA"
327             # 4 bytes creator = "LSdb"
328             # 4 bytes unique seed, set to 0x00000000
329             # 4 bytes next record list, set to 0x00000000
330             # 2 bytes number of records
331              
332 1         3 my $headerfmt = "Z32 n n NNN N N N A4 A4 N N n";
333 1 50       748 my $hdr = pack($headerfmt,
    50          
334             $dbname,
335             0 | ($self->{backup} ? 0x0008 : 0x0)
336             | ($self->{private} ? 0x0040 : 0x0),
337             0x0000,
338             $self->{ctime},
339             $self->{mtime},
340             $self->{btime},
341             0,
342             78 + ($n_records * 8) + 2,
343             0,
344             "DATA",
345             "LSdb",
346             0,
347             0,
348             $n_records);
349 1 50       4 croak(__PACKAGE__.": Header is ".length($hdr)." instead of 78")
350             unless length($hdr) == 78;
351 1         4 print $fh ($hdr);
352              
353             # Index table (8 bytes/record + 0x8000 padding)
354             # 4 bytes offset to record data
355             # 1 byte attributes = index of the category the record belongs to
356             # 3 bytes unique id = index of the record (counting from 0)
357              
358 1         3 my $offset = (78 + ($n_records * 8) + 512 + 2);
359 1         2 my $index = 0;
360 1         2 foreach my $record ( @records ) {
361 2         5 my ($cat, $field1, $field2, $note, $len) = @$record;
362 2         4 $len = 3;
363 2 50       6 $len += length($field1)+1 if $field1 ne "";
364 2 50       5 $len += length($field2)+1 if $field2 ne "";
365 2 50       5 $len += length($note)+1 if $note ne "";
366 2 50       4 $len++ if $len == 3;
367 2         6 print $fh (pack("NN", $offset, $index | (($cat-1) << 24)));
368 2         3 $offset += $len;
369 2         5 $index++;
370             }
371             # Padding.
372 1         2 print $fh (pack("n", 0x8000));
373              
374             # Application info (size = 512 bytes)
375             # 2 bytes renamed categories, set to 0x000e
376             # 16 * 16 bytes of category labels, nul padded, nul terminated
377             # 16 * 1 byte of category unique ids
378             # (first (Unfiled)) = 0x00
379             # (rest) index + 0x0f if used, index otherwise
380             # E.g. 00 10 11 12 14 15 06 07 08 09 0a 0b 0c 0d 0e 0f
381             # 1 byte last unique id, set to the highest category unique id
382             # 1 byte display style, set to 0x00 (no resort, field1/field2)
383             # 1 byte write protect, 0x00 for off, 0x01 for on
384             # 1 byte last category, 0xff for all, 0x00 for Unfiled
385             # (The category view the DB opens with)
386             # 16 bytes custom field 1 label, nul padded, nul terminated
387             # 16 bytes custom field 2 label, as above
388             # 202 bytes padding to make the size 512 bytes
389              
390             # Note: repeat groups ups the requirement to 5.8. Not needed.
391 1         2 my $appinfofmt = "n ".("Z16" x 16)." C16 C C C C Z16Z16 x202";
392 16 100       56 my $appinfo = pack($appinfofmt,
393             0x000e,
394 16 100 100     65 (map { $self->{cat}->[$_] || '' } 0..15),
395 1 50       4 (map { $_ && $_ < $numcats ? $_ + 0x0f : $_ } 0..15),
396             $numcats - 1 + 0x0f,
397             0x00, # no resort, field1/field2
398             # 0x80, # force resort, field1/field2
399             # 0x81, # force resort, field2/field1
400             $self->{readonly} ? 0x01 : 0x00,
401             0xff, # last category -- all
402             $field1label, $field2label);
403 1 50       7 croak(__PACKAGE__.": AppInfo is ".length($appinfo)." instead of 512")
404             unless length($appinfo) == 512;
405 1         2 print $fh ($appinfo);
406              
407             # Records
408             # 1 byte offset to field 1, 0 if no data in field
409             # 1 byte offset to field 2, 0 if no data in field
410             # 1 byte offset to note, 0 if no data in field
411             # up to 3 0x00 terminated fields of max length 63, 63, 1023
412             # If no fields, then a nul pad is necessary (though this will never
413             # be the case since we disallow that).
414              
415 1         3 foreach my $record ( @records ) {
416 2         5 my ($cat, $field1, $field2, $note) = @$record;
417 2         2 $offset = 3;
418 2         5 foreach ( $field1, $field2, $note ) {
419 6         8 my $len = length($_);
420 6 50       9 if ( $len ) {
421 6         10 print $fh (pack("C", $offset));
422 6         10 $offset += $len + 1;
423             }
424             else {
425 0         0 print $fh (pack("C", 0));
426             }
427             }
428 2         3 foreach ( $field1, $field2, $note ) {
429 6 50       11 next unless length($_);
430 6         16 print $fh (pack("a*x", $_));
431             }
432             }
433              
434             # Everything has been written
435 1         65 close($fh);
436              
437 1         6 1;
438             }
439              
440             1;
441             __END__