1*c9b362f6SAndre Fischer#************************************************************** 2*c9b362f6SAndre Fischer# 3*c9b362f6SAndre Fischer# Licensed to the Apache Software Foundation (ASF) under one 4*c9b362f6SAndre Fischer# or more contributor license agreements. See the NOTICE file 5*c9b362f6SAndre Fischer# distributed with this work for additional information 6*c9b362f6SAndre Fischer# regarding copyright ownership. The ASF licenses this file 7*c9b362f6SAndre Fischer# to you under the Apache License, Version 2.0 (the 8*c9b362f6SAndre Fischer# "License"); you may not use this file except in compliance 9*c9b362f6SAndre Fischer# with the License. You may obtain a copy of the License at 10*c9b362f6SAndre Fischer# 11*c9b362f6SAndre Fischer# http://www.apache.org/licenses/LICENSE-2.0 12*c9b362f6SAndre Fischer# 13*c9b362f6SAndre Fischer# Unless required by applicable law or agreed to in writing, 14*c9b362f6SAndre Fischer# software distributed under the License is distributed on an 15*c9b362f6SAndre Fischer# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16*c9b362f6SAndre Fischer# KIND, either express or implied. See the License for the 17*c9b362f6SAndre Fischer# specific language governing permissions and limitations 18*c9b362f6SAndre Fischer# under the License. 19*c9b362f6SAndre Fischer# 20*c9b362f6SAndre Fischer#************************************************************** 21*c9b362f6SAndre Fischer 22*c9b362f6SAndre Fischerpackage installer::patch::Msi; 23*c9b362f6SAndre Fischer 24*c9b362f6SAndre Fischeruse installer::patch::MsiTable; 25*c9b362f6SAndre Fischeruse installer::patch::Tools; 26*c9b362f6SAndre Fischeruse strict; 27*c9b362f6SAndre Fischer 28*c9b362f6SAndre Fischer 29*c9b362f6SAndre Fischer=head1 NAME 30*c9b362f6SAndre Fischer 31*c9b362f6SAndre Fischer package installer::patch::Msi - Class represents a single MSI file and gives access to its tables. 32*c9b362f6SAndre Fischer 33*c9b362f6SAndre Fischer=cut 34*c9b362f6SAndre Fischer 35*c9b362f6SAndre Fischer 36*c9b362f6SAndre Fischer 37*c9b362f6SAndre Fischer=head2 new($class, $version, $language, $product_name) 38*c9b362f6SAndre Fischer 39*c9b362f6SAndre Fischer Create a new object of the Msi class. The values of $version, $language, and $product_name define 40*c9b362f6SAndre Fischer where to look for the msi file. 41*c9b362f6SAndre Fischer 42*c9b362f6SAndre Fischer If construction fails then IsValid() will return false. 43*c9b362f6SAndre Fischer 44*c9b362f6SAndre Fischer=cut 45*c9b362f6SAndre Fischersub new ($$$$) 46*c9b362f6SAndre Fischer{ 47*c9b362f6SAndre Fischer my ($class, $version, $language, $product_name) = @_; 48*c9b362f6SAndre Fischer 49*c9b362f6SAndre Fischer my $path = installer::patch::InstallationSet::GetUnpackedMsiPath( 50*c9b362f6SAndre Fischer $version, 51*c9b362f6SAndre Fischer $language, 52*c9b362f6SAndre Fischer "msi", 53*c9b362f6SAndre Fischer $product_name); 54*c9b362f6SAndre Fischer 55*c9b362f6SAndre Fischer # Find the msi in the path. 56*c9b362f6SAndre Fischer my $filename = undef; 57*c9b362f6SAndre Fischer if ( -d $path) 58*c9b362f6SAndre Fischer { 59*c9b362f6SAndre Fischer my @msi_files = glob(File::Spec->catfile($path, "*.msi")); 60*c9b362f6SAndre Fischer if (scalar @msi_files != 1) 61*c9b362f6SAndre Fischer { 62*c9b362f6SAndre Fischer printf STDERR ("there are %d msi files in %s, should be 1", scalar @msi_files, $filename); 63*c9b362f6SAndre Fischer $filename = ""; 64*c9b362f6SAndre Fischer } 65*c9b362f6SAndre Fischer else 66*c9b362f6SAndre Fischer { 67*c9b362f6SAndre Fischer $filename = $msi_files[0]; 68*c9b362f6SAndre Fischer } 69*c9b362f6SAndre Fischer } 70*c9b362f6SAndre Fischer else 71*c9b362f6SAndre Fischer { 72*c9b362f6SAndre Fischer installer::logger::PrintError("can not access path '%s' to find msi\n", $path); 73*c9b362f6SAndre Fischer return undef; 74*c9b362f6SAndre Fischer } 75*c9b362f6SAndre Fischer 76*c9b362f6SAndre Fischer if ( ! -f $filename) 77*c9b362f6SAndre Fischer { 78*c9b362f6SAndre Fischer installer::logger::PrintError("can not access MSI file at '%s'\n", $filename); 79*c9b362f6SAndre Fischer return undef; 80*c9b362f6SAndre Fischer } 81*c9b362f6SAndre Fischer 82*c9b362f6SAndre Fischer my $self = { 83*c9b362f6SAndre Fischer 'filename' => $filename, 84*c9b362f6SAndre Fischer 'path' => $path, 85*c9b362f6SAndre Fischer 'version' => $version, 86*c9b362f6SAndre Fischer 'language' => $language, 87*c9b362f6SAndre Fischer 'package_format' => "msi", 88*c9b362f6SAndre Fischer 'product_name' => $product_name, 89*c9b362f6SAndre Fischer 'tmpdir' => File::Temp->newdir(CLEANUP => 1), 90*c9b362f6SAndre Fischer 'is_valid' => -f $filename 91*c9b362f6SAndre Fischer }; 92*c9b362f6SAndre Fischer bless($self, $class); 93*c9b362f6SAndre Fischer 94*c9b362f6SAndre Fischer return $self; 95*c9b362f6SAndre Fischer} 96*c9b362f6SAndre Fischer 97*c9b362f6SAndre Fischer 98*c9b362f6SAndre Fischer 99*c9b362f6SAndre Fischer 100*c9b362f6SAndre Fischersub IsValid ($) 101*c9b362f6SAndre Fischer{ 102*c9b362f6SAndre Fischer my ($self) = @_; 103*c9b362f6SAndre Fischer 104*c9b362f6SAndre Fischer return $self->{'is_valid'}; 105*c9b362f6SAndre Fischer} 106*c9b362f6SAndre Fischer 107*c9b362f6SAndre Fischer 108*c9b362f6SAndre Fischer 109*c9b362f6SAndre Fischer 110*c9b362f6SAndre Fischer=head2 GetTable($seld, $table_name) 111*c9b362f6SAndre Fischer 112*c9b362f6SAndre Fischer Return an MsiTable object for $table_name. Table objects are kept 113*c9b362f6SAndre Fischer alive for the life time of the Msi object. Therefore the second 114*c9b362f6SAndre Fischer call for the same table is very cheap. 115*c9b362f6SAndre Fischer 116*c9b362f6SAndre Fischer=cut 117*c9b362f6SAndre Fischersub GetTable ($$) 118*c9b362f6SAndre Fischer{ 119*c9b362f6SAndre Fischer my ($self, $table_name) = @_; 120*c9b362f6SAndre Fischer 121*c9b362f6SAndre Fischer my $table = $self->{'tables'}->{$table_name}; 122*c9b362f6SAndre Fischer if ( ! defined $table) 123*c9b362f6SAndre Fischer { 124*c9b362f6SAndre Fischer my $table_filename = File::Spec->catfile($self->{'tmpdir'}, $table_name .".idt"); 125*c9b362f6SAndre Fischer if ( ! -f $table_filename 126*c9b362f6SAndre Fischer || ! EnsureAYoungerThanB($table_filename, $self->{'fullname'})) 127*c9b362f6SAndre Fischer { 128*c9b362f6SAndre Fischer # Extract table from database to text file on disk. 129*c9b362f6SAndre Fischer my $truncated_table_name = length($table_name)>8 ? substr($table_name,0,8) : $table_name; 130*c9b362f6SAndre Fischer my $command = join(" ", 131*c9b362f6SAndre Fischer "msidb.exe", 132*c9b362f6SAndre Fischer "-d", installer::patch::Tools::CygpathToWindows($self->{'filename'}), 133*c9b362f6SAndre Fischer "-f", installer::patch::Tools::CygpathToWindows($self->{'tmpdir'}), 134*c9b362f6SAndre Fischer "-e", $table_name); 135*c9b362f6SAndre Fischer my $result = qx($command); 136*c9b362f6SAndre Fischer print $result; 137*c9b362f6SAndre Fischer } 138*c9b362f6SAndre Fischer 139*c9b362f6SAndre Fischer # Read table into memory. 140*c9b362f6SAndre Fischer $table = new installer::patch::MsiTable($table_filename, $table_name); 141*c9b362f6SAndre Fischer $self->{'tables'}->{$table_name} = $table; 142*c9b362f6SAndre Fischer } 143*c9b362f6SAndre Fischer 144*c9b362f6SAndre Fischer return $table; 145*c9b362f6SAndre Fischer} 146*c9b362f6SAndre Fischer 147*c9b362f6SAndre Fischer 148*c9b362f6SAndre Fischer 149*c9b362f6SAndre Fischer 150*c9b362f6SAndre Fischer=head2 EnsureAYoungerThanB ($filename_a, $filename_b) 151*c9b362f6SAndre Fischer 152*c9b362f6SAndre Fischer Internal function (not a method) that compares to files according 153*c9b362f6SAndre Fischer to their last modification times (mtime). 154*c9b362f6SAndre Fischer 155*c9b362f6SAndre Fischer=cut 156*c9b362f6SAndre Fischersub EnsureAYoungerThanB ($$) 157*c9b362f6SAndre Fischer{ 158*c9b362f6SAndre Fischer my ($filename_a, $filename_b) = @_; 159*c9b362f6SAndre Fischer 160*c9b362f6SAndre Fischer die("file $filename_a does not exist") unless -f $filename_a; 161*c9b362f6SAndre Fischer die("file $filename_b does not exist") unless -f $filename_b; 162*c9b362f6SAndre Fischer 163*c9b362f6SAndre Fischer my @stat_a = stat($filename_a); 164*c9b362f6SAndre Fischer my @stat_b = stat($filename_b); 165*c9b362f6SAndre Fischer 166*c9b362f6SAndre Fischer if ($stat_a[9] <= $stat_b[9]) 167*c9b362f6SAndre Fischer { 168*c9b362f6SAndre Fischer return 0; 169*c9b362f6SAndre Fischer } 170*c9b362f6SAndre Fischer else 171*c9b362f6SAndre Fischer { 172*c9b362f6SAndre Fischer return 1; 173*c9b362f6SAndre Fischer } 174*c9b362f6SAndre Fischer} 175*c9b362f6SAndre Fischer 176*c9b362f6SAndre Fischer 177*c9b362f6SAndre Fischer 178*c9b362f6SAndre Fischer 179*c9b362f6SAndre Fischer=head2 SplitLongShortName($name) 180*c9b362f6SAndre Fischer 181*c9b362f6SAndre Fischer Split $name (typically from the 'FileName' column in the 'File' 182*c9b362f6SAndre Fischer table or 'DefaultDir' column in the 'Directory' table) at the '|' 183*c9b362f6SAndre Fischer into short (8.3) and long names. If there is no '|' in $name then 184*c9b362f6SAndre Fischer $name is returned as both short and long name. 185*c9b362f6SAndre Fischer 186*c9b362f6SAndre Fischer Returns long and short name (in this order) as array. 187*c9b362f6SAndre Fischer 188*c9b362f6SAndre Fischer=cut 189*c9b362f6SAndre Fischersub SplitLongShortName ($) 190*c9b362f6SAndre Fischer{ 191*c9b362f6SAndre Fischer my ($name) = @_; 192*c9b362f6SAndre Fischer 193*c9b362f6SAndre Fischer if ($name =~ /^([^\|]*)\|(.*)$/) 194*c9b362f6SAndre Fischer { 195*c9b362f6SAndre Fischer return ($2,$1); 196*c9b362f6SAndre Fischer } 197*c9b362f6SAndre Fischer else 198*c9b362f6SAndre Fischer { 199*c9b362f6SAndre Fischer return ($name,$name); 200*c9b362f6SAndre Fischer } 201*c9b362f6SAndre Fischer} 202*c9b362f6SAndre Fischer 203*c9b362f6SAndre Fischer 204*c9b362f6SAndre Fischer 205*c9b362f6SAndre Fischer=head2 SplitTargetSourceLongShortName ($name) 206*c9b362f6SAndre Fischer 207*c9b362f6SAndre Fischer Split $name first at the ':' into target and source parts and each 208*c9b362f6SAndre Fischer of those at the '|'s into long and short parts. Names that follow 209*c9b362f6SAndre Fischer this pattern come from the 'DefaultDir' column in the 'Directory' 210*c9b362f6SAndre Fischer table. 211*c9b362f6SAndre Fischer 212*c9b362f6SAndre Fischer=cut 213*c9b362f6SAndre Fischersub SplitTargetSourceLongShortName ($) 214*c9b362f6SAndre Fischer{ 215*c9b362f6SAndre Fischer my ($name) = @_; 216*c9b362f6SAndre Fischer 217*c9b362f6SAndre Fischer if ($name =~ /^([^:]*):(.*)$/) 218*c9b362f6SAndre Fischer { 219*c9b362f6SAndre Fischer return (installer::patch::Msi::SplitLongShortName($1), installer::patch::Msi::SplitLongShortName($2)); 220*c9b362f6SAndre Fischer } 221*c9b362f6SAndre Fischer else 222*c9b362f6SAndre Fischer { 223*c9b362f6SAndre Fischer my ($long,$short) = installer::patch::Msi::SplitLongShortName($name); 224*c9b362f6SAndre Fischer return ($long,$short,$long,$short); 225*c9b362f6SAndre Fischer } 226*c9b362f6SAndre Fischer} 227*c9b362f6SAndre Fischer 228*c9b362f6SAndre Fischer 229*c9b362f6SAndre Fischer 230*c9b362f6SAndre Fischer 231*c9b362f6SAndre Fischer=head2 GetFileToDirectoryMap ($) 232*c9b362f6SAndre Fischer 233*c9b362f6SAndre Fischer Return a map (hash) that maps the unique name (column 'File' in 234*c9b362f6SAndre Fischer the 'File' table) to its directory names. Each value is a 235*c9b362f6SAndre Fischer reference to an array of two elements: the source path and the 236*c9b362f6SAndre Fischer target path. 237*c9b362f6SAndre Fischer 238*c9b362f6SAndre Fischer The map is kept alive for the lifetime of the Msi object. All 239*c9b362f6SAndre Fischer calls but the first are cheap. 240*c9b362f6SAndre Fischer 241*c9b362f6SAndre Fischer=cut 242*c9b362f6SAndre Fischersub GetFileToDirectoryMap ($) 243*c9b362f6SAndre Fischer{ 244*c9b362f6SAndre Fischer my ($self) = @_; 245*c9b362f6SAndre Fischer 246*c9b362f6SAndre Fischer if (defined $self->{'FileToDirectoryMap'}) 247*c9b362f6SAndre Fischer { 248*c9b362f6SAndre Fischer return $self->{'FileToDirectoryMap'}; 249*c9b362f6SAndre Fischer } 250*c9b362f6SAndre Fischer 251*c9b362f6SAndre Fischer my $file_table = $self->GetTable("File"); 252*c9b362f6SAndre Fischer my $directory_table = $self->GetTable("Directory"); 253*c9b362f6SAndre Fischer my $component_table = $self->GetTable("Component"); 254*c9b362f6SAndre Fischer $installer::logger::Info->printf("got access to tables File, Directory, Component\n"); 255*c9b362f6SAndre Fischer 256*c9b362f6SAndre Fischer my %dir_map = (); 257*c9b362f6SAndre Fischer foreach my $row (@{$directory_table->GetAllRows()}) 258*c9b362f6SAndre Fischer { 259*c9b362f6SAndre Fischer my ($target_name, undef, $source_name, undef) 260*c9b362f6SAndre Fischer = installer::patch::Msi::SplitTargetSourceLongShortName($row->GetValue("DefaultDir")); 261*c9b362f6SAndre Fischer $dir_map{$row->GetValue("Directory")} = { 262*c9b362f6SAndre Fischer 'parent' => $row->GetValue("Directory_Parent"), 263*c9b362f6SAndre Fischer 'source_name' => $source_name, 264*c9b362f6SAndre Fischer 'target_name' => $target_name}; 265*c9b362f6SAndre Fischer } 266*c9b362f6SAndre Fischer 267*c9b362f6SAndre Fischer # Set up full names for all directories. 268*c9b362f6SAndre Fischer my @todo = map {$_} (keys %dir_map); 269*c9b362f6SAndre Fischer my $process_count = 0; 270*c9b362f6SAndre Fischer my $push_count = 0; 271*c9b362f6SAndre Fischer while (scalar @todo > 0) 272*c9b362f6SAndre Fischer { 273*c9b362f6SAndre Fischer ++$process_count; 274*c9b362f6SAndre Fischer 275*c9b362f6SAndre Fischer my $key = shift @todo; 276*c9b362f6SAndre Fischer my $item = $dir_map{$key}; 277*c9b362f6SAndre Fischer next if defined $item->{'full_source_name'}; 278*c9b362f6SAndre Fischer 279*c9b362f6SAndre Fischer if ($item->{'parent'} eq "") 280*c9b362f6SAndre Fischer { 281*c9b362f6SAndre Fischer # Directory has no parent => full names are the same as the name. 282*c9b362f6SAndre Fischer $item->{'full_source_name'} = $item->{'source_name'}; 283*c9b362f6SAndre Fischer $item->{'full_target_name'} = $item->{'target_name'}; 284*c9b362f6SAndre Fischer } 285*c9b362f6SAndre Fischer else 286*c9b362f6SAndre Fischer { 287*c9b362f6SAndre Fischer my $parent = $dir_map{$item->{'parent'}}; 288*c9b362f6SAndre Fischer if ( defined $parent->{'full_source_name'}) 289*c9b362f6SAndre Fischer { 290*c9b362f6SAndre Fischer # Parent aleady has full names => we can create the full name of the current item. 291*c9b362f6SAndre Fischer $item->{'full_source_name'} = $parent->{'full_source_name'} . "/" . $item->{'source_name'}; 292*c9b362f6SAndre Fischer $item->{'full_target_name'} = $parent->{'full_target_name'} . "/" . $item->{'target_name'}; 293*c9b362f6SAndre Fischer } 294*c9b362f6SAndre Fischer else 295*c9b362f6SAndre Fischer { 296*c9b362f6SAndre Fischer # Parent has to be processed before the current item can be processed. 297*c9b362f6SAndre Fischer # Push both to the head of the list. 298*c9b362f6SAndre Fischer unshift @todo, $key; 299*c9b362f6SAndre Fischer unshift @todo, $item->{'parent'}; 300*c9b362f6SAndre Fischer 301*c9b362f6SAndre Fischer ++$push_count; 302*c9b362f6SAndre Fischer } 303*c9b362f6SAndre Fischer } 304*c9b362f6SAndre Fischer } 305*c9b362f6SAndre Fischer 306*c9b362f6SAndre Fischer foreach my $key (keys %dir_map) 307*c9b362f6SAndre Fischer { 308*c9b362f6SAndre Fischer $dir_map{$key}->{'full_source_name'} =~ s/\/(\.\/)+/\//g; 309*c9b362f6SAndre Fischer $dir_map{$key}->{'full_source_name'} =~ s/^SourceDir\///; 310*c9b362f6SAndre Fischer $dir_map{$key}->{'full_target_name'} =~ s/\/(\.\/)+/\//g; 311*c9b362f6SAndre Fischer $dir_map{$key}->{'full_target_name'} =~ s/^SourceDir\///; 312*c9b362f6SAndre Fischer } 313*c9b362f6SAndre Fischer $installer::logger::Info->printf("for %d directories there where %d processing steps and %d pushes\n", 314*c9b362f6SAndre Fischer $directory_table->GetRowCount(), 315*c9b362f6SAndre Fischer $process_count, 316*c9b362f6SAndre Fischer $push_count); 317*c9b362f6SAndre Fischer 318*c9b362f6SAndre Fischer # Setup a map from component names to directory items. 319*c9b362f6SAndre Fischer my %component_to_directory_map = map {$_->GetValue('Component') => $_->GetValue('Directory_')} @{$component_table->GetAllRows()}; 320*c9b362f6SAndre Fischer 321*c9b362f6SAndre Fischer # Finally, create the map from files to directories. 322*c9b362f6SAndre Fischer my $map = {}; 323*c9b362f6SAndre Fischer my $file_component_index = $file_table->GetColumnIndex("Component_"); 324*c9b362f6SAndre Fischer my $file_file_index = $file_table->GetColumnIndex("File"); 325*c9b362f6SAndre Fischer foreach my $file_row (@{$file_table->GetAllRows()}) 326*c9b362f6SAndre Fischer { 327*c9b362f6SAndre Fischer my $component_name = $file_row->GetValue($file_component_index); 328*c9b362f6SAndre Fischer my $directory_name = $component_to_directory_map{$component_name}; 329*c9b362f6SAndre Fischer my $dir_item = $dir_map{$directory_name}; 330*c9b362f6SAndre Fischer my $unique_name = $file_row->GetValue($file_file_index); 331*c9b362f6SAndre Fischer $map->{$unique_name} = [$dir_item->{'full_source_name'},$dir_item->{'full_target_name'}]; 332*c9b362f6SAndre Fischer } 333*c9b362f6SAndre Fischer 334*c9b362f6SAndre Fischer $installer::logger::Info->printf("got full paths for %d files\n", 335*c9b362f6SAndre Fischer $file_table->GetRowCount()); 336*c9b362f6SAndre Fischer 337*c9b362f6SAndre Fischer $self->{'FileToDirectoryMap'} = $map; 338*c9b362f6SAndre Fischer return $map; 339*c9b362f6SAndre Fischer} 340*c9b362f6SAndre Fischer 341*c9b362f6SAndre Fischer 342*c9b362f6SAndre Fischer1; 343