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