File Coverage

blib/lib/Win32/InstallShield.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Win32::InstallShield;
2              
3 1     1   33887 use Carp;
  1         3  
  1         188  
4 1     1   1801 use IO::File;
  1         16504  
  1         159  
5 1     1   2380 use XML::Parser;
  0            
  0            
6             use Data::Dumper;
7             use Encode;
8              
9             use strict;
10             use warnings;
11              
12             our $AUTOLOAD;
13             our $VERSION = 0.7;
14              
15             my %component_attr_values = (
16             LocalOnly => 0,
17             SourceOnly => 1,
18             Optional => 2,
19             RegistryKeyPath => 4,
20             SharedDllRefCount => 8,
21             Permanent => 16,
22             ODBCDataSource => 32,
23             Transitive => 64,
24             NeverOverwrite => 128,
25             '64bit' => 256,
26             DisableRegistryReflection => 512,
27             UninstallOnSupersedence => 1024,
28             AttributesShared => 2048,
29             );
30              
31             my %component_attr_names = map { $component_attr_values{$_} => $_ } keys %component_attr_values;
32              
33             =head1 NAME
34              
35             Win32::InstallShield - InstallShield data file interface
36              
37             =head1 SYNOPSIS
38              
39             use InstallShield;
40              
41             # Constructors
42             $is = Win32::InstallShield->new();
43             $is = Win32::InstallShield->new( $ism_file );
44              
45             =head1 ABSTRACT
46              
47             An OO interface for manipulating InstallShield XML .ism files
48              
49             =head1 DESCRIPTION
50              
51             This module provides an interface to add, remove and modifify rows
52             in an InstallShield .ism file. It only supports versions of
53             InstallShield that save their data as XML.
54              
55             =head1 EXAMPLES
56              
57             This example updates the product version.
58              
59             use Win32::InstallShield;
60              
61             $is = Win32::InstallShield->new( $ism_file );
62             $is->UpdateProperty(
63             {
64             Property => 'ProductVersion',
65             Value => '1.2.3.4',
66             }
67             );
68              
69             $is->savefile( $ism_file );
70              
71             =head1 METHODS
72              
73             =over 4
74              
75             =item I
76              
77             $is = Win32::InstallShield->new();
78             $is = Win32::InstallShield->new( $installshield_filename );
79             $is = Win32::InstallShield->new( $io_file_handle );
80              
81             The constructor. Can optionally be called with the same
82             arguments as I.
83              
84             =cut
85             sub new {
86             my $proto = shift;
87             my $class = ref($proto) || $proto;
88              
89             my $self = {
90             parser => XML::Parser->new(Style => 'Tree'),
91             summary_fields => [], # list of valid summary fields from the DTD
92             parsed => {}, # tables that the user has read or modified
93             sections => {}, # the contents of the original file
94             order => [], # the order in which sections appear in the file
95             tables => {}, # the tables that appear in the ism file
96             foreign_keys => {}, # tables referenced by foreign keys
97             correct_case => {}, # stores case-sensitive table names
98             filename => undef,
99             encoding => undef,
100             };
101              
102             bless $self, $class;
103            
104             if(@_) {
105             $self->loadfile( shift );
106             }
107              
108             return $self;
109             }
110              
111             sub DESTROY {
112             }
113              
114             sub AUTOLOAD {
115             my $self = shift;
116             my $name = $AUTOLOAD;
117             $name =~ s/.*://;
118             if($name =~ /^(addorupdate|searchhash|searcharray|gethash|getarray|add|del|update|purge)_?(.*)$/i) {
119             my ($op, $table) = (lc($1), lc($2));
120              
121             if($table eq 'row') {
122             $table = lc(shift @_);
123             }
124              
125             unless($self->{'sections'}{$table}) {
126             carp("No such table: $2");
127             return;
128             }
129              
130             if($op eq 'gethash') {
131             return $self->_get_row_hash($table, @_);
132             } elsif($op eq 'getarray') {
133             return $self->_get_row_array($table, @_);
134             } elsif($op eq 'searchhash') {
135             return $self->_search_row_hash($table, @_);
136             } elsif($op eq 'searcharray') {
137             return $self->_search_row_array($table, @_);
138             } elsif($op eq 'addorupdate') {
139             return $self->_add_or_update_row($table, @_);
140             } elsif($op eq 'add') {
141             return $self->_add_row($table, @_);
142             } elsif($op eq 'del') {
143             return $self->_del_row($table, @_);
144             } elsif($op eq 'update') {
145             return $self->_update_row($table, @_);
146             } elsif($op eq 'purge') {
147             return $self->_purge_row($table, @_);
148             }
149              
150             } else {
151             croak("Invalid method $name");
152             }
153             }
154              
155             # internal function, opens a file and returns the
156             # IO::File filehandle if given a filename. if called
157             # with an IO::File filehandle, it just returns the filehandle.
158             sub _openfile {
159             my ($file, $mode) = @_;
160              
161             if(!defined($file)) {
162             carp("File name or handle required\n");
163             return (undef, 0);
164             }
165              
166             if(ref($file)) {
167             if(ref($file) eq 'IO::File') {
168             return ($file, 0);
169             } else {
170             carp("Invalid file argument: " . ref($file) . "\n");
171             return (undef, 0);
172             }
173             } else {
174             my $fh = IO::File->new($file, $mode);
175             my $long_mode = ($mode eq 'r') ? 'read' : 'write';
176             unless(defined($fh)) {
177             carp("Unable to $long_mode $file $!");
178             return (undef, 0);
179             }
180             return ($fh, 1);
181             }
182              
183             }
184              
185             =item I
186              
187             $is->set_encoding('');
188              
189             Sets the encoding that will be used when writing tables that
190             have been modified. By default, the encoding will be whatever
191             appears in the XML declaration at the beginning of the ism.
192             If none appears, no encoding will be done.
193              
194             =cut
195             sub set_encoding {
196             my ($self, $encoding) = @_;
197             $self->{'encoding'} = $encoding;
198             }
199              
200             =item I
201              
202             my $encoding = $is->get_encoding();
203              
204             Returns the encoding that will be used when writing tables
205             that have been modified. Returns undef if the encoding is
206             unknown, in which case no encoding will be done.
207              
208             =cut
209             sub get_encoding {
210             my ($self) = @_;
211             return $self->{'encoding'};
212             }
213              
214             =item I
215              
216             $is->loadfile( $filename );
217             $is->loadfile( $io_file_handle );
218              
219             Loads an InstallShield ism file. Can be called
220             with either a filename or an IO::File object that is
221             opened in read ("r") mode.
222             Returns 1 on success, 0 on failure.
223              
224             =cut
225             sub loadfile {
226             my ($self, $file) = @_;
227              
228             my ($fh, $i_opened_file) = _openfile($file, "r");
229              
230             unless(defined($fh)) {
231             return 0;
232             }
233              
234             my $return = $self->load( join('', <$fh>) );
235             if($i_opened_file) {
236             $fh->close();
237             $self->{'filename'} = $file;
238             }
239             return $return;
240             }
241              
242             =item I
243              
244             $is->load( $ism_text );
245              
246             Loads the supplied text of an InstallShield ism file.
247             Returns 1 on success, 0 on failure.
248              
249             =cut
250             sub load {
251             my ($self, $data) = @_;
252              
253             # loading wipes out whatever was previously loaded
254             $self->{'parsed'} = {};
255             $self->{'sections'} = {};
256             $self->{'order'} = [];
257             $self->{'tables'} = {};
258             $self->{'foreign_keys'} = {};
259             $self->{'correct_case'} = {};
260             $self->{'filename'} = undef;
261             $self->{'encoding'} = undef;
262              
263             my $section = 'header';
264             my $lastsection = $section;
265              
266             push(@{$self->{'order'}}, $section);
267              
268             my @lines = split("\n", $data);
269              
270             foreach (@lines) {
271             if(/^
272             $section = 'dtd';
273             } elsif(/^
274             $section = 'msi';
275             } elsif(/^\s*/) {
276             $section = 'summary';
277             } elsif(/^\s*]+>([^<]+)";
278             $section = lc($1);
279             $self->{'correct_case'}{$section} = $1;
280             # remember which sections are tables
281             $self->{'tables'}{$1} = 1;
282             } elsif(/^<\/msi>/) {
283             $section = 'trailer';
284             }
285            
286             if($section ne $lastsection) {
287             $lastsection = $section;
288             push(@{$self->{'order'}}, $section);
289             }
290              
291             # remember what tables each foreign key appears in
292             if(/^\s*
293             my $colname = $1;
294             if($colname =~ /_$/) {
295             unless(exists($self->{'foreign_keys'}{$colname})) {
296             $self->{'foreign_keys'}{$colname} = [];
297             }
298             push(@{$self->{'foreign_keys'}{$colname}}, $section);
299             }
300             }
301              
302             push(@{$self->{'sections'}{$section}}, $_);
303             }
304              
305             # find the file encoding
306             foreach my $line (@{$self->{sections}->{header}}) {
307             if($line =~ /\sencoding="([^"]+)"/) {
308             $self->{'encoding'} = $1;
309             last;
310             }
311             }
312              
313             return 1;
314             }
315              
316             =item I
317              
318             $is->savefile( );
319             $is->savefile( $filename );
320             $is->savefile( $io_file_handle );
321              
322             Stores the ism data in a file. Can be called
323             with either a filename or an IO::File object that is
324             opened in write ("w") mode. If no argument is passed,
325             and the last load was via a filename, savefile will
326             default to the filename previously supplied.
327             Returns 1 on success, 0 on failure.
328              
329             =cut
330             sub savefile {
331             my ($self, $file) = @_;
332            
333             unless(defined($file)) {
334             if(defined($self->{'filename'})) {
335             $file = $self->{'filename'};
336             } else {
337             carp("You must provide a filename to save to");
338             return 0;
339             }
340             }
341            
342             my ($fh, $i_opened_file) = _openfile($file, "w");
343              
344             unless(defined($fh)) {
345             return 0;
346             }
347              
348             print $fh $self->save();
349             $fh->close() if($i_opened_file);
350             return 1;
351             }
352              
353             =item I
354              
355             $is->save();
356              
357             Returns the ism data as a string.
358              
359             =cut
360             sub save {
361             my ($self) = @_;
362            
363             my $encoding = $self->{'encoding'};
364             my $has_encoding = defined($encoding);
365              
366             my $text = '';
367              
368             foreach my $section (@{$self->{'order'}}) {
369             if($self->{'parsed'}{$section}) {
370             # the table has been (possibly) modified, so rebuild it
371             if($section eq 'summary') {
372             $text .= ($has_encoding) ?
373             encode($encoding, $self->_save_summary) :
374             $self->_save_summary;
375             } else {
376             $text .= ($has_encoding) ?
377             encode($encoding, $self->_save_table($section)) :
378             $self->_save_table($section);
379             }
380              
381             } else {
382             # when the last table gets modified, we end up with an
383             # extra newline
384             if($section eq 'trailer') {
385             $text =~ s/\n\n$/\n/;
386             }
387             # section wasn't touched, just spit out the stored text
388             $text .= join("\n", @{$self->{'sections'}{$section}}) . "\n";
389             }
390             }
391             return $text;
392             }
393              
394             sub _save_summary {
395             my ($self) = @_;
396              
397             my $count = 0;
398             my %order =
399             map { $_ => $count++ }
400             $self->summary_fields;
401              
402             my $summary = $self->{'parsed'}{'summary'};
403              
404             my $text = "\t\n";
405              
406             foreach my $field (sort { $order{$a} <=> $order{$b} } keys %order) {
407             if(exists($summary->{$field})) {
408             if(!defined($summary->{$field}) || $summary->{$field} eq '') {
409             $text .= "\t\t<$field/>\n";
410             } else {
411             $text .= "\t\t<$field>" . $summary->{$field} . "\n";
412             }
413             }
414             }
415              
416             $text .= "\t\n\t\n";
417              
418             return $text;
419             }
420              
421             # internal function. formats the data in a table that has
422             # been modified back to the appropriate output format
423             sub _save_table {
424             my ($self, $table) = @_;
425              
426             my $p = $self->_parsed($table);
427             my $text = "\t
428             foreach my $key (sort keys %{$p->{'attributes'}}) {
429             $text .= " $key=\"$p->{'attributes'}{$key}\"";
430             }
431             $text .= ">\n";
432              
433             foreach my $col (@{$p->{'columns'}}) {
434             $text .= "\t\t
435             if($col->{'is_key'}) {
436             $text .= ' key="yes"';
437             }
438             $text .= ' def="' . $col->{'type'} . $col->{'width'} . '"';
439             $text .= ">$col->{'name'}\n";
440             }
441             foreach my $key (sort keys %{$p->{'data'}}) {
442             my $row = $p->{'data'}{$key};
443             $text .= "\t\t";
444             foreach my $col (@$row) {
445             if(defined($col) and length($col) > 0) {
446             $text .= "" . _xml_escape($col) . "
447             } else {
448             $text .= "";
449             }
450             }
451             $text .= "\n";
452             }
453              
454             $text .= "\t
\n\n";
455              
456             return $text;
457             }
458              
459             # internal function. parses the text of an IS table
460             # so that it can be easily manipulated
461             sub _parse_table {
462             my ($self, $table) = @_;
463            
464             $table = lc($table);
465             return if($self->{'parsed'}{$table});
466            
467             my $text = $self->{'sections'}{$table};
468             unless(defined($text)) {
469             carp("No such table $table");
470             return;
471             }
472              
473             my @cols;
474             my %data;
475              
476             my $xml = join("\n", @$text);
477             my @parsed = @{$self->{'parser'}->parse($xml)->[1]};
478              
479             my $attributes = shift @parsed;
480              
481             while(@parsed) {
482             my $type = shift @parsed;
483             if($type eq 'col') {
484             my $columns = shift @parsed;
485             my $column_name = $columns->[2];
486             my $is_key = ( defined($columns->[0]{'key'}) and $columns->[0]{'key'} eq 'yes' );
487             my ($type, $width) = ($columns->[0]{'def'} =~ /(\w)(\d+)/);
488             push(@cols, {
489             name => $column_name,
490             is_key => $is_key,
491             type => $type,
492             width => $width,
493             });
494             } elsif($type eq 'row') {
495             my $columns = shift @parsed;
496             my @row;
497             my $lookup_key = '';
498             foreach my $i (0..$#cols) {
499             my $value = $columns->[ ($i+1)*2 ][2];
500             $row[$i] = $value;
501              
502             if($cols[$i]{'is_key'}) {
503             my $key_value = $value;
504             unless(defined($key_value)) { $key_value = ''; }
505             $lookup_key .= sprintf("%-" . $cols[$i]{'width'} . "s", $key_value)
506             }
507             }
508             $data{ $lookup_key } = \@row;
509             } else {
510             # ignore text
511             shift @parsed;
512             }
513             }
514            
515             $self->{'parsed'}{$table} = {
516             attributes => $attributes,
517             columns => \@cols,
518             data => \%data,
519             };
520             }
521              
522             sub _parsed {
523             my ($self, $table) = @_;
524             $table = lc($table);
525             unless(exists($self->{'parsed'}{$table})) {
526             $self->_parse_table($table);
527             }
528             return $self->{'parsed'}{$table};
529             }
530              
531             =item I
532              
533             my $tables = $is->tables();
534              
535             Returns an arrayref containing a list of all the tables
536             that were found in the ISM file.
537              
538             =cut
539             sub tables {
540             my ($self) = @_;
541             return [ sort keys %{$self->{'tables'}} ];
542             }
543              
544             =item I
545              
546             if($is->has_table( 'ModuleSignature' ) {
547             print "This is a merge module\n";
548             }
549              
550             Returns true if a table exists with the supplied name, false otherwise.
551             Table names are case-insensitive.
552              
553             =cut
554             sub has_table {
555             my ($self, $table) = @_;
556             return exists($self->{'sections'}{lc($table)});
557             }
558              
559             =item I
560              
561             my $is_key = $is->column_is_key( $table, $column_name );
562              
563             Returns true if the column is a key column, false
564             other wise. Returns undef if the column doesn't exist.
565              
566             =cut
567             sub column_is_key {
568             my ($self, $table, $column) = @_;
569             my $p = $self->_parsed($table);
570             foreach my $col (@{$p->{'columns'}}) {
571             if($col->{'name'} eq $column) {
572             return $col->{'is_key'};
573             }
574             }
575             return;
576             }
577              
578             =item I
579              
580             my $width = $is->column_width( $table, $column_name );
581              
582             Returns the width of the named column. Returns undef if
583             the column doesn't exist.
584              
585             =cut
586             sub column_width {
587             my ($self, $table, $column) = @_;
588             my $p = $self->_parsed($table);
589             return $p->{'columns'}{$column}{'width'};
590             }
591              
592             =item I
593              
594             my $type = $is->column_type( $table, $column_name );
595              
596             Returns the type of the named column. Returns undef if the
597             column doesn't exist.
598              
599             =cut
600             sub column_type {
601             my ($self, $table, $column) = @_;
602             my $p = $self->_parsed($table);
603             return $p->{'columns'}{$column}{'type'};
604             }
605              
606             =item I
607              
608             my $columns = $is->columns( $table );
609              
610             Returns an array ref containing the names of the columns
611             in the given table.
612              
613             =cut
614             sub columns {
615             my ($self, $table) = @_;
616             my $p = $self->_parsed($table);
617             my @cols;
618             foreach my $col (@{$p->{'columns'}}) {
619             push(@cols, $col->{'name'});
620             }
621             return \@cols;
622             }
623              
624             =item I
625              
626             my $key_columns = $is->key_columns( $table );
627              
628             Returns an array ref containing the names of the
629             key columns in the given table.
630              
631             =cut
632             sub key_columns {
633             my ($self, $table) = @_;
634             my $p = $self->_parsed($table);
635             my @keys;
636             foreach my $col (@{$p->{'columns'}}) {
637             if($col->{'is_key'}) {
638             push(@keys, $col->{'name'});
639             }
640             }
641             return \@keys;
642             }
643              
644             sub _find_row {
645             my ($self, $table, $rowdata) = @_;
646             my $p = $self->_parsed($table);
647              
648             my $lookup_key = $self->_build_key( $table, $rowdata );
649              
650             if(exists($p->{'data'}{$lookup_key})) {
651             return $lookup_key;
652             } else {
653             return;
654             }
655             }
656              
657             sub _search_row {
658             my ($self, $table, $rowdata) = @_;
659             my @results;
660             my $p = $self->_parsed($table);
661             foreach my $row (values %{$p->{'data'}}) {
662             my $match = 1;
663             foreach my $i (0..$#{$rowdata}) {
664             # undef means they don't care about this column
665             if(defined($rowdata->[$i])) {
666             # empty string from the user matches undef in the data
667             if(defined($row->[$i])) {
668             if(ref($rowdata->[$i]) eq 'Regexp') {
669             if($row->[$i] !~ /$rowdata->[$i]/) {
670             $match = 0;
671             last;
672             }
673             } elsif($rowdata->[$i] ne $row->[$i]) {
674             $match = 0;
675             last;
676             }
677             } elsif($rowdata->[$i] ne '') {
678             $match = 0;
679             last;
680             }
681             }
682             }
683             if($match) {
684             push(@results, $row);
685             }
686             }
687             return \@results;
688             }
689              
690             # the lookup key is just the primary key columns concatenated together,
691             # with padding to the full column length. this function builds the key
692             # given the column values
693             sub _build_key {
694             my ($self, $table, $values) = @_;
695              
696             my $p = $self->_parsed($table);
697             my $lookup_key = '';
698              
699             # build the lookup key by concatenating the key columns
700             foreach my $i (0..$#{$p->{'columns'}}) {
701             if($p->{'columns'}[$i]{'is_key'}) {
702             my $width = $p->{'columns'}[$i]{'width'};
703             $lookup_key .= sprintf("%-${width}s",
704             (defined($values->[$i])) ? $values->[$i] : '');
705             }
706             }
707              
708             return $lookup_key;
709             }
710              
711             # takes the various formats allowed for specifying row data,
712             # and returns a consistent structure to be used by other methods.
713             # also fills in any missing columns with undef
714             sub _reformat_args {
715             my ($self, $table, @args) = @_;
716              
717             my $p = $self->_parsed($table);
718             my $row = [];
719              
720             if(ref($args[0]) eq 'ARRAY') {
721             $row = $args[0];
722             } elsif(ref($args[0]) eq 'HASH') {
723             my $h = $args[0];
724             foreach my $col (@{$p->{'columns'}}) {
725             push(@$row, $h->{ $col->{'name'} });
726             }
727             } else {
728             $row = \@args;
729             }
730              
731             # if the user left columns off the end, fill them
732             # with undef
733             my $missing_columns = $#{$p->{'columns'}} - $#{$row};
734             if($missing_columns > 0) {
735             for( 1..$missing_columns ) {
736             push(@{$row}, undef);
737             }
738             }
739             return $row;
740             }
741              
742             sub _check_args {
743             my ($self, $table, @args) = @_;
744              
745             my $p = $self->_parsed($table);
746             my $row = $self->_reformat_args($table, @args);
747              
748             unless( $#{$row} eq $#{$p->{'columns'}} ) {
749             carp("Wrong number of columns supplied for table $table");
750             return;
751             }
752              
753             foreach my $i (0..$#{$row}) {
754             next unless(defined($row->[$i]));
755             my $type = $p->{'columns'}[$i]{'type'};
756             if($type =~ /^i$/i) {
757             if($row->[$i] =~ /[^\d-]/) {
758             croak("Value in $p->{'columns'}[$i]{'name'} column must be numeric");
759             return;
760             }
761             } else {
762             my $width = $p->{'columns'}[$i]{'width'};
763             if($width > 0 and length($row->[$i]) > $width) {
764             croak("Value in $p->{'columns'}[$i]{'name'} column is too long");
765             return;
766             }
767             }
768             }
769              
770             return $row;
771             }
772              
773             =item I
774              
775             my $version = $is->property('ProductVersion');
776             my $success = $is->property('ProductVersion', $version);
777              
778             Gets or sets the value associated with a property. If a value is
779             supplied, it will attempt to update the property and return 1
780             on success and 0 on failure. undef is returned if the property does not exist.
781              
782             =cut
783             sub property {
784             my ($self, $property, $value) = @_;
785             unless(defined($self->getHash_Property({ Property=>$property }))) {
786             return;
787             }
788             if(defined($value)) {
789             $self->update_Property({ Property=>$property, Value=>$value });
790             }
791             return $self->getHash_Property({ Property=>$property });
792             }
793              
794             =item I
795              
796             my $summary_value = $is->summary( $summary_field );
797             my $success = $is->summary( $summary_field, $value );
798              
799             my $summary_table = $is->summary;
800              
801             Gets or sets the value associated with a field in the summary table.
802             If no field name is provided, a reference to a hash containing all
803             of the summary field/value pairs.
804              
805             =cut
806             sub summary {
807             my ($self, $field, $value) = @_;
808              
809             unless(exists($self->{'parsed'}{'summary'})) {
810             $self->_parse_summary;
811             }
812            
813             my $data = $self->{'parsed'}{'summary'};
814              
815             return $data unless(defined($field));
816              
817             if(defined($value)) {
818             # make sure this summary field is allowed by the DTD
819             return 0 unless($self->valid_summary_field($field));
820             $data->{$field} = $value;
821             return 1;
822             } else {
823             return $data->{$field};
824             }
825             }
826              
827             =item I
828              
829             my @field_names = $is->summary_fields;
830              
831             Returns a list of the valid fields for the summary table, as they appear
832             in the DTD embedded in the ISM file.
833              
834             =cut
835             sub summary_fields {
836             my ($self) = @_;
837            
838             $self->_parse_summary unless(defined($self->{'parsed'}{'summary'}));
839              
840             return @{$self->{'summary_fields'}};
841             }
842              
843             =item I
844            
845             my $is_valid = $is->valid_summary_field( $field_name );
846              
847             Returns 1 if the field $field_name is valid according to the DTD
848             in the ISM file, 0 otherwise.
849              
850             =cut
851             sub valid_summary_field {
852             my ($self, $field) = @_;
853              
854             return 0 unless(defined($field));
855              
856             foreach my $valid_field ($self->summary_fields) {
857             return 1 if($field eq $valid_field);
858             }
859              
860             return 0;
861             }
862              
863             # get the list of valid summary fields from the DTD
864             sub _parse_summary_fields {
865             my ($self) = @_;
866              
867             my $text = join('', @{$self->{'sections'}{'dtd'}});
868              
869             my ($summary_fields_text) = $text =~ /
870              
871             $summary_fields_text =~ s/[\?\s]//g;
872            
873             my @summary_fields = split(',', $summary_fields_text);
874            
875             $self->{'summary_fields'} = \@summary_fields;
876             }
877              
878             # turn the XML for the summary table into something we can manipulate easily
879             sub _parse_summary {
880             my ($self) = @_;
881              
882             $self->_parse_summary_fields;
883              
884             my $text = $self->{'sections'}{'summary'};
885             unless(defined($text)) {
886             carp("No summary found");
887             return;
888             }
889              
890             my %data;
891              
892             my $xml = join("\n", @$text);
893             my @parsed = @{$self->{'parser'}->parse($xml)->[1]};
894              
895             my $attributes = shift @parsed;
896              
897             while(@parsed) {
898             my $type = shift @parsed;
899              
900             # ignore text
901             if($type eq '0') {
902             shift @parsed;
903             next;
904             }
905              
906             my $value = shift @parsed;
907             $data{ $type } = $value->[2];
908              
909             }
910              
911             $self->{'parsed'}{'summary'} = \%data;
912             }
913              
914             =item I
915              
916             my $components = $is->featureComponents( $feature );
917              
918             Returns an arrayref of the components in the named feature. Returns
919             undef if the feature does not exist.
920              
921             =cut
922             sub featureComponents {
923             my ($self, $feature) = @_;
924             my $list = $self->searchHash_FeatureComponents({ Feature_=>$feature });
925             unless(@{$list}) {
926             return;
927             }
928              
929             my @components = sort map { $_->{'Component_'} } @{$list};
930              
931             return \@components;
932             }
933              
934             =back
935              
936             =head1 COMPONENT ATTRIBUTES
937              
938              
939             All of the attribute methods can accept an attribute as either
940             a name or a value. The name can be prefixed with msidbComponentAttributes
941             as it is in the MSI documentation, but it is not required.
942              
943             Valid attributes:
944             LocalOnly 0
945             SourceOnly 1
946             Optional 2
947             RegistryKeyPath 4
948             SharedDllRefCount 8
949             Permanent 16
950             ODBCDataSource 32
951             Transitive 64
952             NeverOverwrite 128
953             64bit 256
954             DisableRegistryReflection 512
955             UninstallOnSupersedence 1024
956             AttributesShared 2048
957              
958             =over 4
959              
960             =item I
961              
962             my $success = $is->set_component_attribute( $component_name, '64bit', 1 );
963              
964             Update the value of a single component attribute flag. Returns 1 on success,
965             0 on failure.
966              
967             =cut
968             sub set_component_attribute {
969             my ($self, $component_name, $attribute, $bit_on) = @_;
970              
971             my $attr_num = $self->get_component_attribute_value( $attribute );
972              
973             return 0 unless(defined($attr_num));
974              
975             if($attr_num == 0) {
976             $attr_num = 1;
977             $bit_on = !$bit_on;
978             }
979              
980             my $component = $self->getHash_Component($component_name);
981              
982             return 0 unless(defined($component));
983              
984             if($bit_on) {
985             $component->{'Attributes'} |= $attr_num;
986             } else {
987             my $inverted_attr_num = ~$attr_num;
988             $component->{'Attributes'} &= $inverted_attr_num;
989             }
990              
991             return $self->update_component($component);
992              
993             }
994              
995             =item I
996              
997             my $is_64bit = $is->get_component_attribute( $component_name, '64bit' );
998             my $is_shared = $is->get_component_attribute( $component_name, 8 );
999              
1000             Returns 1 if the named component has the given attribute set, 0 otherwise.
1001             Returns undef if the component does not exist, or the attribute is invalid.
1002             The attribute name or value can be used.
1003              
1004             =cut
1005             sub get_component_attribute {
1006             my ($self, $component_name, $attribute) = @_;
1007              
1008             my $invert = 0;
1009             my $attr_num = $self->get_component_attribute_value( $attribute );
1010            
1011             return unless(defined($attr_num));
1012              
1013             # for some reason, the docs have bit 1 listed twice, once for
1014             # on and once for off (as hex value 0x0)
1015             if($attr_num == 0) {
1016             $attr_num = 1;
1017             $invert = 1;
1018             }
1019              
1020             my $component = $self->getHash_Component($component_name);
1021            
1022             # must find exactly one component with this name
1023             return unless(defined($component));
1024              
1025             my $set = ($component->{'Attributes'} & $attr_num) ? 1 : 0;
1026            
1027             $set = !$set if($invert);
1028              
1029             return $set;
1030             }
1031              
1032             =item I
1033              
1034             my $attr_number = $is->get_component_attribute_value( 'LocalOnly' );
1035              
1036             Given a component attribute name, returns the bit value associated
1037             with the attribute. The msidbComponentAttributes prefix for attribute names
1038             is accepted, but not required. Given a valid attribute value, simply returns
1039             the value. Returns undef on invalid input.
1040              
1041             =cut
1042             sub get_component_attribute_value {
1043             my ($self, $attribute) = @_;
1044            
1045             $attribute =~ s/^msidbComponentAttributes//;
1046             if($attribute =~ /^\d+$/) {
1047             if(exists($component_attr_names{$attribute})) {
1048             return $attribute;
1049             }
1050             } elsif(exists($component_attr_values{$attribute})) {
1051             return $component_attr_values{$attribute};
1052             }
1053              
1054             return;
1055             }
1056              
1057             =item I
1058              
1059             my $attr_name = $is->get_component_attribute_name( 512 );
1060              
1061             Given a component attribute value, returns the name associated
1062             with the value. Given a valid attribute name, simply returns
1063             the name. The msidbComponentAttributes prefix for attribute names
1064             is accepted, but not required. Returns undef on invalid input.
1065              
1066             =cut
1067             sub get_component_attribute_name {
1068             my ($self, $attribute) = @_;
1069            
1070             $attribute =~ s/^msidbComponentAttributes//;
1071             if($attribute =~ /^\d+$/) {
1072             if(exists($component_attr_names{$attribute})) {
1073             return $component_attr_values{$attribute};
1074             }
1075             } elsif(exists($component_attr_values{$attribute})) {
1076             return $component_attr_names{$attribute};
1077             }
1078              
1079             return;
1080             }
1081              
1082             =item I
1083              
1084             my @attr_names = $is->valid_component_attributes;
1085              
1086             Returns a list of valid attribute names.
1087              
1088             =cut
1089             sub valid_component_attributes {
1090             return map { $component_attr_names{$_} } sort { $a <=> $b } keys %component_attr_names;
1091             }
1092              
1093             =back
1094              
1095             =head1 ROW MANIPULATION METHOD SYNTAX
1096              
1097             Row manipulation methods can be called in different ways.
1098             First, they are all case insensitve, and the '_' is
1099             optional, so for the 'Property' table, these are equivilent:
1100              
1101             $is->add_row( 'Property', $rowdata );
1102             $is->AddRow( 'Property', $rowdata );
1103              
1104             Also, you can call each method using the table name in
1105             place of the word 'row', so these are equivilent to the
1106             two above:
1107              
1108             $is->add_property( $rowdata );
1109             $is->AddProperty( $rowdata );
1110              
1111             All row manipulation methods are called with a set of data
1112             describing a row. In the methods below, it is represented by
1113             the variable $rowdata. It can be passed to the function in
1114             one of three formats: a list, an array ref or a hash ref.
1115              
1116             List
1117              
1118             You can simply put the columns in an array in the correct
1119             order (which you can get by looking at the ism or calling
1120             the I method), and pass it to the method.
1121              
1122             my @rowdata = ( 'Column_1_Value', 'Column_2_value' );
1123             $success = $is->update_row( $table, @rowdata );
1124              
1125             Array ref
1126              
1127             You can do the same as above, but pass it as a single
1128             array reference.
1129              
1130             $success = $is->update_row( $table, \@rowdata );
1131              
1132             Hash ref
1133              
1134             You can also pass a hash ref, using column names
1135             as keys.
1136              
1137             my %rowdata = (
1138             Property => 'ProductVersion',
1139             Value => '1.2.3.4',
1140             ISComments => '',
1141             );
1142             $success = $is->update_row( $table, \%rowdata );
1143              
1144             =head1 ROW MANIPULATION METHODS
1145              
1146             =over 4
1147              
1148             =item I
1149              
1150             my $row = $is->getHash_row( $table, $rowdata );
1151              
1152             Returns a hash ref containing the data that matches the keys
1153             supplied in $rowdata. Returns undef if the row is not found.
1154              
1155             =cut
1156             sub _get_row_hash {
1157             my ($self, $table, @args) = @_;
1158             my $args = $self->_reformat_args($table, @args);
1159             my $rowkey = $self->_find_row($table, $args);
1160             if(defined($rowkey)) {
1161             my %rowdata;
1162             my $p = $self->_parsed($table);
1163            
1164             foreach my $i (0..$#{$p->{'columns'}}) {
1165             $rowdata{ $p->{'columns'}[$i]{'name'} } = $p->{'data'}{$rowkey}[$i];
1166             }
1167             return \%rowdata;
1168             } else {
1169             return;
1170             }
1171             }
1172              
1173             =item I
1174              
1175             my $row = $is->getArray_row( $table, $rowdata );
1176              
1177             Returns an array ref containing the data that matches the keys
1178             supplied in $rowdata. Returns undef if the row is not found.
1179              
1180             =cut
1181             sub _get_row_array {
1182             my ($self, $table, @args) = @_;
1183             my $args = $self->_reformat_args($table, @args);
1184             my $rowkey = $self->_find_row($table, $args);
1185             if(defined($rowkey)) {
1186             my $p = $self->_parsed($table);
1187             return $p->{'data'}{$rowkey};
1188             } else {
1189             return;
1190             }
1191             }
1192              
1193             =item I
1194              
1195             my $success = $is->update_row( $table, $rowdata );
1196              
1197             Updates the row that matches the keys supplied in
1198             $rowdata. Any columns for which an undef is supplied
1199             will remain unchanged. An empty string will force
1200             the column to be empty. Returns 1 on success, 0 on
1201             failure.
1202              
1203             =cut
1204             sub _update_row {
1205             my ($self, $table, @args) = @_;
1206             my $rowdata = $self->_check_args($table, @args);
1207             unless(defined($rowdata)) {
1208             return 0;
1209             }
1210             my $rowkey = $self->_find_row($table, $rowdata);
1211             unless(defined($rowkey)) {
1212             carp("Failed to locate row in $table for update");
1213             return 0;
1214             }
1215             my $p = $self->_parsed($table);
1216             foreach my $i (0..$#{$rowdata}) {
1217             if(defined($rowdata->[$i])) {
1218             $p->{'data'}{$rowkey}[$i] = $rowdata->[$i];
1219             }
1220             }
1221             return 1;
1222             }
1223              
1224             =item I
1225              
1226             my $success = $is->add_row( $table, $rowdata );
1227              
1228             Adds a row containing the data in $rowdata. Returns
1229             1 on success, 0 on failure.
1230              
1231             =cut
1232             sub _add_row {
1233             my ($self, $table, @args) = @_;
1234             my $rowdata = $self->_check_args($table, @args);
1235             unless(defined($rowdata)) {
1236             return 0;
1237             }
1238             my $rowkey = $self->_find_row($table, $rowdata);
1239             if(defined($rowkey)) {
1240             carp("Row to add in '$table' table already exists");
1241             return 0;
1242             }
1243             $rowkey = $self->_build_key($table, $rowdata);
1244             unless(defined($rowkey)) {
1245             return 0;
1246             }
1247             my $p = $self->_parsed($table);
1248             $p->{'data'}{$rowkey} = $rowdata;
1249             return 1;
1250             }
1251              
1252             =item I
1253              
1254             my $success = $is->del_row( $table, $rowdata );
1255              
1256             Deletes the row that matches the keys supplied in
1257             $rowdata. Returns 1 on success, 0 on failure.
1258              
1259             =cut
1260             sub _del_row {
1261             my ($self, $table, @args) = @_;
1262             my $args = $self->_reformat_args($table, @args);
1263             my $rowkey = $self->_find_row($table, $args);
1264             unless(defined($rowkey)) {
1265             carp("Failed to locate row in $table for delete");
1266             return 0;
1267             }
1268             my $p = $self->_parsed($table);
1269             delete($p->{'data'}{$rowkey});
1270             return 1;
1271             }
1272              
1273             =item I
1274              
1275             $is->purge_row( $table, $rowdata );
1276             $is->purge_row( 'Component', 'Awesome.dll' );
1277             $is->PurgeComponent( 'Awesome.dll' );
1278              
1279             Removes the row that matches the key in $rowdata from the given table, and any rows
1280             in other tables with foreign keys that reference it. Key values are
1281             case sensitive. This only works for tables with a key column that has
1282             the same name as the table, which seems to be the only way you can use
1283             foreign keys in an ISM in any case. Returns 1 on success, 0 on failure.
1284              
1285             =cut
1286             sub _purge_row {
1287             my ($self, $table, $key_value) = @_;
1288              
1289             # make sure the key exists in the table
1290             my $rowkey = $self->_find_row($table, $self->_reformat_args($table, $key_value));
1291             unless(defined($rowkey)) {
1292             return 0;
1293             }
1294              
1295             $self->_del_row($table, $rowkey);
1296              
1297             my $foreign_key_col = $self->{'correct_case'}{$table} . '_';
1298              
1299             foreach my $table (@{$self->{'foreign_keys'}{$foreign_key_col}}) {
1300             my $rows_to_delete = $self->_search_row_array($table, { $foreign_key_col => $key_value });
1301             if(@{$rows_to_delete}) {
1302             foreach my $row (@{$rows_to_delete}) {
1303             $self->_del_row($table, $row) or return 0;
1304             }
1305             }
1306             }
1307              
1308             return 1;
1309             }
1310              
1311             =item I
1312              
1313             my $success = $is->add_or_update_row( $table, $rowdata );
1314              
1315             Adds a row if no row exists with the supplied keys, updates
1316             the matching row otherwise.
1317              
1318             =cut
1319             sub _add_or_update_row {
1320             my ($self, $table, @args) = @_;
1321             my $args = $self->_reformat_args($table, @args);
1322             my $rowkey = $self->_find_row($table, $args);
1323             if(defined($rowkey)) {
1324             return $self->_update_row($table, $args);
1325             } else {
1326             return $self->_add_row($table, $args);
1327             }
1328             }
1329              
1330             =item I
1331              
1332             my $rows = $is->searchHash_row( $table, $rowdata );
1333              
1334             Returns any rows in the given table that match the supplied
1335             columns. The return value is an arrayref, where each entry is
1336             a hash as would be returned by I. Returns an empty
1337             arrayref if no matches are found. Returns the entire table if
1338             no $rowdata argument is provided.
1339              
1340             Columns with undefined values will be ignored for matching purposes.
1341             Values used for matching can be either literal strings, in which
1342             case an exact match is required, or quoted regular expressions such as:
1343              
1344             my $rows = $is->searchHash_row( 'Property', { Property=>qr/^_/ } );
1345              
1346             This would search for all properties that begin with an underscore.
1347              
1348             =cut
1349             sub _search_row_hash {
1350             my ($self, $table, @args) = @_;
1351             my $args = $self->_reformat_args($table, @args);
1352             my $results = $self->_search_row($table, $args);
1353              
1354             my @hash_results;
1355             my $p = $self->_parsed($table);
1356            
1357             foreach my $row (@{$results}) {
1358             my %rowdata;
1359             foreach my $i (0..$#{$p->{'columns'}}) {
1360             $rowdata{ $p->{'columns'}[$i]{'name'} } = $row->[$i];
1361             }
1362             push(@hash_results, \%rowdata);
1363             }
1364              
1365             return \@hash_results;
1366             }
1367              
1368             =item I
1369              
1370             my $rows = $is->searchArray_row( $table, $rowdata );
1371              
1372             Works the same as I, but returns an arrayref containing
1373             arrayrefs, like I instead of hashrefs.
1374              
1375             =cut
1376             sub _search_row_array {
1377             my ($self, $table, @args) = @_;
1378             my $args = $self->_reformat_args($table, @args);
1379             return $self->_search_row($table, $args);
1380             }
1381              
1382             # this is (almost) a copy of the xml_escape function in XML::Parser::Expat.
1383             # The version there doesn't seem to work properly on data that was read
1384             # in via XML::Parser, because a call to study causes subsequent matches to
1385             # fail
1386             sub _xml_escape {
1387             my $text = shift @_;
1388              
1389             $text =~ s/\&/\&/g;
1390             $text =~ s/
1391             $text =~ s/>/\>/g;
1392              
1393             foreach (@_) {
1394             die "xml_escape: '$_' isn't a single character" if length($_) > 1;
1395              
1396             if ($_ eq '"') {
1397             $text =~ s/\"/\"/;
1398             }
1399             elsif ($_ eq "'") {
1400             $text =~ s/\'/\'/;
1401             }
1402             else {
1403             my $rep = '&#' . sprintf('x%X', ord($_)) . ';';
1404             if (/\W/) {
1405             my $ptrn = "\\$_";
1406             $text =~ s/$ptrn/$rep/g;
1407             }
1408             else {
1409             $text =~ s/$_/$rep/g;
1410             }
1411             }
1412             }
1413             $text;
1414             }
1415              
1416             =back
1417              
1418             =head1 AUTHOR
1419              
1420             Kirk Baucom, Ekbaucom@schizoid.comE
1421              
1422             =head1 COPYRIGHT AND LICENSE
1423              
1424             Copyright 2003 by Kirk Baucom
1425              
1426             This library is free software; you can redistribute it and/or modify
1427             it under the same terms as Perl itself.
1428              
1429             =cut
1430              
1431             1;
1432              
1433             __DATA__