File Coverage

blib/lib/Clarion.pm
Criterion Covered Total %
statement 187 198 94.4
branch 72 86 83.7
condition 10 16 62.5
subroutine 24 32 75.0
pod 8 27 29.6
total 301 359 83.8


line stmt bran cond sub pod time code
1             package Clarion;
2              
3 4     4   106318 use 5.006;
  4         15  
  4         145  
4 4     4   25 use strict;
  4         6  
  4         133  
5 4     4   20 use warnings;
  4         12  
  4         129  
6              
7 4     4   4479 use FileHandle;
  4         59548  
  4         23  
8              
9             our $VERSION = '1.02';
10              
11             =head1 NAME
12              
13             Clarion - Perl module for reading CLARION 2.1 data files
14              
15             =head1 DESCRIPTION
16              
17             This is a perl module to access CLARION 2.1 files.
18             At the moment only read access to the files is implemented.
19             "Encrypted" (owned) files are processed transparently,
20             there is no need to specify the password of a file.
21              
22             =head1 SYNOPSIS
23              
24             use Clarion;
25              
26             my $dbh=new Clarion "customer.dat";
27              
28             print $dbh->file_struct;
29              
30             for ( 1 .. $dbh->last_record ) {
31             my $r=$dbh->get_record_hash($_);
32             next if $r->{_DELETED};
33             print $r->{CODE}." ".$r->{NAME}." ".$r->{PHONE}."\n";
34             }
35              
36             $dbh->close();
37              
38             =head1 METHODS
39              
40             =over 4
41              
42             =cut
43              
44 0     0 0 0 sub FILLOCK { 0x01; } # file is locked
45 16     16 0 50 sub FILOWN { 0x02; } # file is owned
46 48     48 0 122 sub FILCRYP { 0x04; } # records are encrypted
47 13     13 0 56 sub FILMEMO { 0x08; } # memo file exists
48 0     0 0 0 sub FILCOMP { 0x10; } # file is compressed
49 6     6 0 18 sub FILRCLM { 0x20; } # reclaim deleted records
50 6     6 0 15 sub FILREAD { 0x40; } # file is read only
51 6     6 0 27 sub FILCRET { 0x80; } # file may be created
52              
53 0     0 0 0 sub RECNEW { 0x01; } # bit 0 - new record
54 0     0 0 0 sub RECOLD { 0x02; } # bit 1 - old record
55 0     0 0 0 sub RECREV { 0x04; } # bit 2 - revised record
56 49     49 0 83 sub RECDEL { 0x10; } # bit 4 - deleted record
57 0     0 0 0 sub RECHLD { 0x40; } # bit 6 - record held
58              
59             =item $h=new Clarion ["file.dat" [, 1]]
60              
61             Create object for reading Clarion file. If file name is specified then
62             associate the DAT file with the object. "Encrypted" files are processed
63             transparently, you do not need to specify the password of a file.
64              
65             If the third argument (skipMemo) specified, memo field will not be
66             processed at all.
67              
68             =cut
69              
70             sub new {
71 11     11 0 281 my $self={};
72 11         24 bless $self, shift;
73              
74 11 100       97 $self->open(@_) if @_;
75 11         32 return $self;
76             }
77              
78             =item $h->close
79              
80             Close all open file handles.
81              
82             =cut
83              
84             sub close {
85 17     17 1 2931 my $self=shift;
86 17 100       55 if($self->{fh}) {
87 10         103 $self->{fh}->close();
88 10         199 delete $self->{fh};
89             }
90 17 100       258 if($self->{fhMemo}) {
91 4         15 $self->{fhMemo}->close();
92 4         288 delete $self->{fhMemo};
93             }
94             }
95              
96             sub DESTROY {
97 11     11   3101 shift->close;
98             }
99              
100             =item $h->open('file.dat' [, 1])
101              
102             Read and parse header of Clarion file.
103              
104             If second argument given, skip processing of memo field.
105              
106             =cut
107              
108             sub open {
109 10     10 1 22 my ($self, $fileName, $skipMemo)=@_;
110              
111 10 50       66 my $fh=new FileHandle $fileName
112             or die("Cannot open '$fileName': $!\n");
113 10         1075 binmode($fh);
114 10         38 $self->{fh}=$fh;
115              
116             # Read file signature & header
117 10         37 my ($filesig, $sfatr)=unpack('a2 S', $self->readData(4, 'header'));
118 10 50       33 die "Not a Clarion 2.1 file '$fileName'!\n" if $filesig ne 'C3';
119 10         22 $self->{name}=$fileName;
120 10         20 $self->{sfatr}=$sfatr;
121 10         25 my $header=$self->readData(2*9+31+9*4-4, 'header');
122            
123             # File is encrypted?
124 10 100       138 if($sfatr & FILOWN) {
125             # Looking for key; 4 variants exist
126 7         29 $self->{Key}=[unpack('x8 CX2C', $header)]; # numdels, high word
127             # $self->{Key}=[unpack('x68 CX2C', $header)]; # reserved, low word
128             # $self->{Key}=[unpack('x70 CX2C', $header)]; # reserved, high word
129             # $self->{Key}=[unpack('x68 CC', $header)]; # reserved, middle word
130 7         24 $header=$self->decrypt($header);
131             }
132              
133             # Parse header itself
134 10         111 my @X=unpack('C L L S S S S L L L L A12 A12 A3 A3 S S L L L S', $header);
135 10         35 foreach my $f(qw(numbkeys numrecs numdels numflds numpics nummars reclen offset
136             logeof logbof freerec recname memnam filpre recpre memolen memowid
137             reserved chgtime chgdate reserved2)) {
138 210         450 $self->{header}{$f}=shift @X;
139             }
140            
141             # Read field descriptions & build record template
142 10         30 $self->{fields}=[];
143 10         20 $self->{decimal_fields}=[];
144 10         31 $self->{record}{unpack}='';
145 10         33 $self->{record}{No}=0;
146 10         46 for(my $i=0; $i<$self->{header}{numflds}; $i++) {
147 74         178 @X=unpack('C A16 S S C C S S', $self->readData(3+16+2*4, 'field descriptor', 1));
148 74         288 my $fd={};
149 74         315 foreach my $f(qw(fldtype fldname foffset length decsig decdec arrnum picnum)) {
150 592         1120 $fd->{$f}=shift @X;
151             }
152 74         109 push @{$self->{fields}}, $fd;
  74         152  
153 74 100       181 push @{$self->{decimal_fields}}, $fd if 8==$fd->{fldtype};
  17         32  
154 74         107 my $n=$fd->{fldname};
155 74         341 $n=~s/^.+?://;
156 74         160 $fd->{Name}=$n;
157 74         187 $self->{field_map}{$n}=$fd->{No}=$i;
158 74         135 my $c=qw(a l d A A C s G)[$fd->{fldtype}];
159 74 100       140 $c='a' unless $c;
160 74 100       375 $c.=$fd->{length} if uc($c)eq 'A';
161 74 100       180 $c='a'.$fd->{length}.' X'.$fd->{length}.' ' if 'G' eq $c;
162 74         297 $self->{record}{unpack}.=$c.' ';
163             }
164              
165             # Read key descriptions
166 10         41 $self->{keys}=[];
167 10         44 for(my $i=$self->{header}{numbkeys}; $i>0; $i--) {
168 21         50 @X=unpack('C A16 C C', $self->readData(1+16+1+1, 'key descriptor', 1));
169 21         45 my $kd={};
170 21         33 foreach my $f(qw(numcomps keynams comptype complen)) {
171 84         172 $kd->{$f}=shift @X;
172             }
173 21         31 push @{$self->{keys}}, $kd;
  21         41  
174              
175             # Read key parts
176 21         38 $kd->{parts}=[];
177 21         61 for(my $j=$kd->{numcomps}; $j>0; $j--) {
178 28         60 @X=unpack('C S S C', $self->readData(1+2+2+1, 'key element', 1));
179 28         58 my $kp={};
180 28         41 foreach my $f(qw(fldtype fldnum elmoff elmlen)) {
181 112         226 $kp->{$f}=shift @X;
182             }
183 28         39 push @{$kd->{parts}}, $kp;
  28         122  
184             }
185             }
186              
187 10 100 100     46 return if defined($skipMemo) or !($sfatr & FILMEMO);
188             # Reading memo...
189 4         24 $fileName=~s/\.[^\.\\\/]*$//;
190 4         8 $fileName.='.mem';
191 4 50       96 $fh=new FileHandle $fileName
192             or die("Cannot open memo '$fileName': $!\n");
193 4         392 binmode($fh);
194 4         9 $self->{fhMemo}=$fh;
195              
196             # Read memo file signature
197 4         76 read($fh, $filesig, 2);
198 4 50       18 die "Not a Clarion 2.1 memo '$fileName'!\n" if $filesig ne 'M3';
199 4         43 my $m={
200             isMemo=>1,
201 4         12 No=>scalar @{$self->{fields}},
202             Name=>$self->{header}{memnam},
203             fldname=>$self->{header}{memnam}.':'.$self->{header}{filpre},
204             length=>$self->{header}{memolen},
205             };
206 4         7 push @{$self->{fields}}, $m;
  4         11  
207 4         17 $self->{field_map}{$m->{Name}}=$m->{No};
208             }
209              
210             =item $n=$dbh->last_record;
211              
212             Returns the number of records in the database file.
213              
214             =cut
215              
216             sub last_record {
217 3     3 1 1329 return shift->{header}{numrecs};
218             }
219              
220             =item $n=$dbh->bof;
221              
222             Returns the physical number of first logical record.
223              
224             =cut
225              
226             sub bof {
227 0     0 1 0 return shift->{header}{logbof};
228             }
229              
230             =item $n=$dbh->eof;
231              
232             Returns the physical number of last logical record.
233              
234             =cut
235              
236             sub eof {
237 0     0 1 0 return shift->{header}{logeof};
238             }
239              
240             # Internal function to read a record
241              
242             sub readRecord {
243 38     38 0 42 my ($self, $n)=@_;
244 38   66     108 $n||=$self->{record}{No}+1;
245 38 100 66     177 return if $n<1 or $n>$self->{header}{numrecs};
246 35         94 $self->{record}{data}=[];
247 35         71 $self->{record}{No}=$n;
248 35         295 seek($self->{fh}, $self->{header}{offset}+$self->{header}{reclen}*($n-1), 0);
249              
250 35         76 ($self->{record}{rhd}, $self->{record}{rptr})=unpack('C L', $self->readData(5, 'record'));
251 35         114 my @Data=unpack($self->{record}{unpack},
252             $self->readData($self->{header}{reclen}-5, 'record', $self->{sfatr} & FILCRYP));
253              
254             # Convert decimal() fields, if any
255 35         58 foreach my $f(@{$self->{decimal_fields}}) {
  35         62  
256 49         121 $Data[$f->{No}]=unpackBCD($Data[$f->{No}], $f->{decsig}, $f->{decdec});
257             }
258 35         74 $self->{record}{data}=\@Data;
259              
260 35 100       101 return 1 unless $self->{fhMemo};
261              
262             # Read memo...
263 14         16 my $memo;
264 14 50       29 $n=($self->{record}{rhd} & RECDEL)? 0 : $self->{record}{rptr};
265 14         41 while($n) {
266 7         54 seek($self->{fhMemo}, ($n-1)*256+6, 0);
267 7         25 $n=unpack('L', $self->readMemo(4));
268 7         18 my $m=$self->readMemo(252);
269 7 100       19 $m=$self->decrypt($m) if $self->{sfatr} & FILCRYP;
270 7 50       20 $memo='' unless defined($memo);
271 7         21 $memo.=$m;
272             }
273 14 100       104 $memo=~s/( +|\00+)\z// if $memo;
274 14         26 push @Data, $memo;
275              
276 14         57 return 1;
277             }
278              
279             =item @r=$dbh->get_record([ $n [, @fields]]);
280              
281             Returns a list of data (field values) from the specified record.
282             The first parameter in the call is the number of the physical
283             record. If you do not specify any other parameters, all fields are
284             returned in the same order as they appear in the file. You can also
285             put list of field names after the record number and then only those
286             will be returned. The first value of the returned list is always the
287             logical (0 or not 0) value saying whether the record is deleted or not.
288              
289             If first argument is omited (or undef) then reads next record from file.
290              
291             =cut
292              
293             sub get_record {
294 36     36 1 2634 my ($self, $n, @fields)=@_;
295              
296 36 100       64 $self->readRecord($n) or return;
297              
298 33 50       105 return ($self->{record}{rhd} & RECDEL, @{$self->{record}{data}})
  33         155  
299             unless @fields;
300              
301             return
302 0         0 $self->{record}{rhd} & RECDEL,
303             map($self->{record}{data}[$self->{field_map}{$_}], @fields);
304             }
305              
306             =item $r=$dbh->get_record_hash([ $n [, @fields]]);
307              
308             Returns reference to hash containing field values indexed by field names.
309             The name of the deleted flag is C<_DELETED>. The first parameter in the call
310             is the number of the physical record (can be omited to read next record if
311             avaialable). If you do not specify any other parameters, all fields are returned.
312             You can also put list of field names after the record number and then only those
313             will be returned.
314              
315             =cut
316              
317             sub get_record_hash {
318 2     2 1 8 my ($self, $n, @fields)=@_;
319              
320 2 50       8 $self->readRecord($n) or return;
321              
322 2         48 my %res= @fields ?
323             map(($_, $self->{record}{data}[$self->{field_map}{$_}]), @fields) :
324 2 50       5 map(($_->{Name}, $self->{record}{data}[$_->{No}]), @{$self->{fields}});
325            
326 2         10 $res{_DELETED}=$self->{record}{rhd} & RECDEL;
327 2         26 return \%res;
328             }
329              
330             =item $struct = $dbh->file_struct;
331              
332             This returns CLARION file structure as a string.
333              
334             =cut
335              
336             sub file_struct {
337 6     6 1 1029 my $self=shift;
338              
339 6         12 my $res=$self->{name};
340 6         25 $res=~s/\.dat$//i;
341 6         20 $res=~s/^.*[\/\\]//;
342 6         11 $res=uc($res);
343              
344 6         23 $res.="\tFILE,NAME('$res'),PRE('$self->{header}{filpre}')";
345              
346 6 100       16 $res.=",OWNER('???')" if $self->{sfatr} & FILOWN;
347 6 100       18 $res.=",ENCRYPT" if $self->{sfatr} & FILCRYP;
348 6 100       15 $res.=",CREATE" if $self->{sfatr} & FILCRET;
349 6 50       55 $res.=",RECLAIM" if $self->{sfatr} & FILRCLM;
350 6 100       24 $res.=",PROTECT" if $self->{sfatr} & FILREAD;
351 6 100       14 $res.="\n$self->{header}{memnam}\tMEMO($self->{header}{memolen})"
352             if $self->{sfatr} & FILMEMO;
353              
354 6         15 $res.="\n$self->{header}{recname}\tRECORD\n";
355            
356 6         8 for my $f(@{$self->{fields}}) {
  6         14  
357 45 100       85 next if $f->{isMemo};
358 42         59 $res.=$f->{Name}."\t";
359 42         52 my $t=qw(? LONG REAL . . BYTE SHORT . DECIMAL)[$f->{fldtype}];
360 42 50 33     177 if(!$t or '?' eq $t) {
361 0         0 $t='UNKNOWN TYPE';
362 0         0 $res.='!';
363             }
364 42 100       63 if('.' eq $t){
365 9         14 $res.="STRING($f->{length})";
366 9 100       22 $res.="\t!GROUP" if 7==$f->{fldtype};
367             } else {
368 33         34 $res.=$t;
369 33 100       81 $res.="(".($f->{decsig}+$f->{decdec}).",$f->{decdec})"
370             if 8==$f->{fldtype};
371             }
372 42         55 $res.="\n";
373             }
374 6         41 return $res."\t. .\n";
375             }
376              
377             # Clarion "decryption"
378              
379             sub decrypt {
380 146     146 0 230 my ($self, $str)=@_;
381 146 100       469 return $str unless defined($self->{Key});
382 110         132 my $res='';
383 110         112 do{
384 1858         2718 my($c1, $c2)=unpack('C2', $str);
385 1858 100       3756 defined($c2) or return $res.$str;
386 1748         3485 $res.=pack('C2', $c1^$self->{Key}[0], $c2^$self->{Key}[1]);
387 1748         4187 $str=unpack('x2 a*', $str);
388             }while(1);
389             }
390              
391             sub readData {
392 213     213 0 330 my ($self, $len, $what, $decrypt)=@_;
393 213   50     1037 my $rc=read($self->{fh}, my $buf, $len)||0;
394 213 50       481 die "Read error Clarion file ($what) ($rc bytes read instead of $len)!\n"
395             if $rc!=$len;
396 213 100       717 return $decrypt? $self->decrypt($buf) : $buf;
397             }
398              
399             sub readMemo {
400 14     14 0 18 my ($self, $len)=@_;
401 14   50     144 my $rc=read($self->{fhMemo}, my $buf, $len)||0;
402 14 50       30 die "Read error Clarion memo ($rc bytes read instead of $len)!\n"
403             if $rc!=$len;
404 14         31 return $buf;
405             }
406              
407             # Convert BCD to string
408              
409             sub unpackBCD {
410 49     49 0 70 my ($bcd, $decsig, $decdec)=@_;
411 49         94 $bcd=unpack('H*', $bcd);
412              
413 49 100       107 my $sign=substr($bcd, 0, 1) eq '0' ? '' : '-';
414 49         63 $bcd=substr($bcd, 1);
415 49 50       112 $bcd=~s/\D/9/g and
416             warn "Incorrect DECIMAL value!\n";
417            
418 49         66 my $sig=substr($bcd, 0, $decsig);
419 49         122 $sig=~s/^0+//;
420 49 100       84 $sig='0' if !length($sig);
421              
422 49         62 my $dec=substr($bcd, $decsig, $decdec);
423 49         85 $dec=~s/0+$//;
424 49 100       101 $sig.='.' if length($dec);
425              
426 49         178 return $sign.$sig.$dec;
427             }
428              
429             1;
430             __END__