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 installer::patch::InstallationSet; 27 28use File::Basename; 29use File::Copy; 30 31use strict; 32 33 34=head1 NAME 35 36 package installer::patch::Msi - Class represents a single MSI file and gives access to its tables. 37 38=cut 39 40sub FindAndCreate($$$$$) 41{ 42 my ($class, $version, $is_current_version, $language, $product_name) = @_; 43 44 my $condensed_version = $version; 45 $condensed_version =~ s/\.//g; 46 47 # When $version is the current version we have to search the msi at a different place. 48 my $path; 49 my $filename; 50 my $is_current = 0; 51 $path = installer::patch::InstallationSet::GetUnpackedExePath( 52 $version, 53 $is_current_version, 54 $language, 55 "msi", 56 $product_name); 57 58 # Find the msi in the path.ls . 59 $filename = File::Spec->catfile($path, "openoffice".$condensed_version.".msi"); 60 $is_current = $is_current_version; 61 62 return $class->new($filename, $version, $is_current, $language, $product_name); 63} 64 65 66 67 68 69 70=head2 new($class, $filename, $version, $is_current_version, $language, $product_name) 71 72 Create a new object of the Msi class. The values of $version, $language, and $product_name define 73 where to look for the msi file. 74 75 If construction fails then IsValid() will return false. 76 77=cut 78sub new ($$$$$$) 79{ 80 my ($class, $filename, $version, $is_current_version, $language, $product_name) = @_; 81 82 if ( ! -f $filename) 83 { 84 installer::logger::PrintError("can not find the .msi file for version %s and language %s at '%s'\n", 85 $version, 86 $language, 87 $filename); 88 return undef; 89 } 90 91 my $self = { 92 'filename' => $filename, 93 'path' => dirname($filename), 94 'version' => $version, 95 'is_current_version' => $is_current_version, 96 'language' => $language, 97 'package_format' => "msi", 98 'product_name' => $product_name, 99 'tmpdir' => File::Temp->newdir(CLEANUP => 1), 100 'is_valid' => -f $filename 101 }; 102 bless($self, $class); 103 104 return $self; 105} 106 107 108 109 110sub IsValid ($) 111{ 112 my ($self) = @_; 113 114 return $self->{'is_valid'}; 115} 116 117 118 119 120=head2 Commit($self) 121 122 Write all modified tables back into the databse. 123 124=cut 125sub Commit ($) 126{ 127 my $self = shift; 128 129 my @tables_to_update = (); 130 foreach my $table (values %{$self->{'tables'}}) 131 { 132 push @tables_to_update,$table if ($table->IsModified()); 133 } 134 135 if (scalar @tables_to_update > 0) 136 { 137 $installer::logger::Info->printf("writing modified tables to database:\n"); 138 foreach my $table (@tables_to_update) 139 { 140 $installer::logger::Info->printf(" %s\n", $table->GetName()); 141 $self->PutTable($table); 142 } 143 144 foreach my $table (@tables_to_update) 145 { 146 $table->UpdateTimestamp(); 147 $table->MarkAsUnmodified(); 148 } 149 } 150} 151 152 153 154 155=head2 GetTable($seld, $table_name) 156 157 Return an MsiTable object for $table_name. Table objects are kept 158 alive for the life time of the Msi object. Therefore the second 159 call for the same table is very cheap. 160 161=cut 162sub GetTable ($$) 163{ 164 my ($self, $table_name) = @_; 165 166 my $table = $self->{'tables'}->{$table_name}; 167 if ( ! defined $table) 168 { 169 my $table_filename = File::Spec->catfile($self->{'tmpdir'}, $table_name .".idt"); 170 if ( ! -f $table_filename 171 || ! EnsureAYoungerThanB($table_filename, $self->{'fullname'})) 172 { 173 # Extract table from database to text file on disk. 174 my $truncated_table_name = length($table_name)>8 ? substr($table_name,0,8) : $table_name; 175 my $command = join(" ", 176 "msidb.exe", 177 "-d", installer::patch::Tools::ToEscapedWindowsPath($self->{'filename'}), 178 "-f", installer::patch::Tools::ToEscapedWindowsPath($self->{'tmpdir'}), 179 "-e", $table_name); 180 my $result = qx($command); 181 print $result; 182 } 183 184 # Read table into memory. 185 $table = new installer::patch::MsiTable($table_filename, $table_name); 186 $self->{'tables'}->{$table_name} = $table; 187 } 188 189 return $table; 190} 191 192 193 194 195=head2 PutTable($self, $table) 196 197 Write the given table back to the databse. 198 199=cut 200sub PutTable ($$) 201{ 202 my ($self, $table) = @_; 203 204 # Create text file from the current table content. 205 $table->WriteFile(); 206 207 my $table_name = $table->GetName(); 208 209 # Store table from text file into database. 210 my $table_filename = $table->{'filename'}; 211 212 if (length($table_name) > 8) 213 { 214 # The file name of the table data must not be longer than 8 characters (not counting the extension). 215 # The name passed as argument to the -i option may be longer. 216 my $truncated_table_name = substr($table_name,0,8); 217 my $table_truncated_filename = File::Spec->catfile( 218 dirname($table_filename), 219 $truncated_table_name.".idt"); 220 File::Copy::copy($table_filename, $table_truncated_filename) || die("can not create table file with short name"); 221 } 222 223 my $command = join(" ", 224 "msidb.exe", 225 "-d", installer::patch::Tools::ToEscapedWindowsPath($self->{'filename'}), 226 "-f", installer::patch::Tools::ToEscapedWindowsPath($self->{'tmpdir'}), 227 "-i", $table_name); 228 my $result = system($command); 229 230 if ($result != 0) 231 { 232 installer::logger::PrintError("writing table '%s' back to database failed", $table_name); 233 # For error messages see http://msdn.microsoft.com/en-us/library/windows/desktop/aa372835%28v=vs.85%29.aspx 234 } 235} 236 237 238 239 240=head2 EnsureAYoungerThanB ($filename_a, $filename_b) 241 242 Internal function (not a method) that compares to files according 243 to their last modification times (mtime). 244 245=cut 246sub EnsureAYoungerThanB ($$) 247{ 248 my ($filename_a, $filename_b) = @_; 249 250 die("file $filename_a does not exist") unless -f $filename_a; 251 die("file $filename_b does not exist") unless -f $filename_b; 252 253 my @stat_a = stat($filename_a); 254 my @stat_b = stat($filename_b); 255 256 if ($stat_a[9] <= $stat_b[9]) 257 { 258 return 0; 259 } 260 else 261 { 262 return 1; 263 } 264} 265 266 267 268 269=head2 SplitLongShortName($name) 270 271 Split $name (typically from the 'FileName' column in the 'File' 272 table or 'DefaultDir' column in the 'Directory' table) at the '|' 273 into short (8.3) and long names. If there is no '|' in $name then 274 $name is returned as both short and long name. 275 276 Returns long and short name (in this order) as array. 277 278=cut 279sub SplitLongShortName ($) 280{ 281 my ($name) = @_; 282 283 if ($name =~ /^([^\|]*)\|(.*)$/) 284 { 285 return ($2,$1); 286 } 287 else 288 { 289 return ($name,$name); 290 } 291} 292 293 294 295=head2 SplitTargetSourceLongShortName ($name) 296 297 Split $name first at the ':' into target and source parts and each 298 of those at the '|'s into long and short parts. Names that follow 299 this pattern come from the 'DefaultDir' column in the 'Directory' 300 table. 301 302=cut 303sub SplitTargetSourceLongShortName ($) 304{ 305 my ($name) = @_; 306 307 if ($name =~ /^([^:]*):(.*)$/) 308 { 309 return (installer::patch::Msi::SplitLongShortName($1), installer::patch::Msi::SplitLongShortName($2)); 310 } 311 else 312 { 313 my ($long,$short) = installer::patch::Msi::SplitLongShortName($name); 314 return ($long,$short,$long,$short); 315 } 316} 317 318 319=head2 GetDirectoryMap($self) 320 321 Return a map that maps directory unique names (column 'Directory' in table 'Directory') 322 to hashes that contains short and long source and target names. 323 324=cut 325sub GetDirectoryMap ($) 326{ 327 my ($self) = @_; 328 329 if (defined $self->{'DirectoryMap'}) 330 { 331 return $self->{'DirectoryMap'}; 332 } 333 334 my $directory_table = $self->GetTable("Directory"); 335 my %dir_map = (); 336 foreach my $row (@{$directory_table->GetAllRows()}) 337 { 338 my ($target_long_name, $target_short_name, $source_long_name, $source_short_name) 339 = installer::patch::Msi::SplitTargetSourceLongShortName($row->GetValue("DefaultDir")); 340 my $unique_name = $row->GetValue("Directory"); 341 $dir_map{$unique_name} = 342 { 343 'unique_name' => $unique_name, 344 'parent' => $row->GetValue("Directory_Parent"), 345 'default_dir' => $row->GetValue("DefaultDir"), 346 'source_long_name' => $source_long_name, 347 'source_short_name' => $source_short_name, 348 'target_long_name' => $target_long_name, 349 'target_short_name' => $target_short_name 350 }; 351 } 352 353 # Set up full names for all directories. 354 my @todo = map {$_} (keys %dir_map); 355 while (scalar @todo > 0) 356 { 357 my $key = shift @todo; 358 my $item = $dir_map{$key}; 359 next if defined $item->{'full_source_name'}; 360 361 if ($item->{'parent'} eq "") 362 { 363 # Directory has no parent => full names are the same as the name. 364 $item->{'full_source_long_name'} = $item->{'source_long_name'}; 365 $item->{'full_source_short_name'} = $item->{'source_short_name'}; 366 $item->{'full_target_long_name'} = $item->{'target_long_name'}; 367 $item->{'full_target_short_name'} = $item->{'target_short_name'}; 368 } 369 else 370 { 371 my $parent = $dir_map{$item->{'parent'}}; 372 if ( defined $parent->{'full_source_long_name'}) 373 { 374 # Parent aleady has full names => we can create the full name of the current item. 375 $item->{'full_source_long_name'} 376 = $parent->{'full_source_long_name'} . "/" . $item->{'source_long_name'}; 377 $item->{'full_source_short_name'} 378 = $parent->{'full_source_short_name'} . "/" . $item->{'source_short_name'}; 379 $item->{'full_target_long_name'} 380 = $parent->{'full_target_long_name'} . "/" . $item->{'target_long_name'}; 381 $item->{'full_target_short_name'} 382 = $parent->{'full_target_short_name'} . "/" . $item->{'target_short_name'}; 383 } 384 else 385 { 386 # Parent has to be processed before the current item can be processed. 387 # Push both to the head of the list. 388 unshift @todo, $key; 389 unshift @todo, $item->{'parent'}; 390 } 391 } 392 } 393 394 # Postprocess the path names for cleanup. 395 foreach my $item (values %dir_map) 396 { 397 foreach my $id ( 398 'full_source_long_name', 399 'full_source_short_name', 400 'full_target_long_name', 401 'full_target_short_name') 402 { 403 $item->{$id} =~ s/\/(\.\/)+/\//g; 404 $item->{$id} =~ s/^SourceDir\///; 405 $item->{$id} =~ s/^\.$//; 406 } 407 } 408 409 $self->{'DirectoryMap'} = \%dir_map; 410 return $self->{'DirectoryMap'}; 411} 412 413 414 415 416=head2 GetFileMap ($) 417 418 Return a map (hash) that maps the unique name (column 'File' in 419 the 'File' table) to data that is associated with that file, like 420 the directory or component. 421 422 The map is kept alive for the lifetime of the Msi object. All 423 calls but the first are cheap. 424 425=cut 426sub GetFileMap ($) 427{ 428 my ($self) = @_; 429 430 if (defined $self->{'FileMap'}) 431 { 432 return $self->{'FileMap'}; 433 } 434 435 my $file_table = $self->GetTable("File"); 436 my $component_table = $self->GetTable("Component"); 437 my $dir_map = $self->GetDirectoryMap(); 438 439 # Setup a map from component names to directory items. 440 my %component_to_directory_map = 441 map 442 {$_->GetValue('Component') => $_->GetValue('Directory_')} 443 @{$component_table->GetAllRows()}; 444 445 # Finally, create the map from files to directories. 446 my $file_map = {}; 447 my $file_component_index = $file_table->GetColumnIndex("Component_"); 448 my $file_file_index = $file_table->GetColumnIndex("File"); 449 foreach my $file_row (@{$file_table->GetAllRows()}) 450 { 451 my $component_name = $file_row->GetValue($file_component_index); 452 my $directory_name = $component_to_directory_map{$component_name}; 453 my $unique_name = $file_row->GetValue($file_file_index); 454 $file_map->{$unique_name} = { 455 'directory' => $dir_map->{$directory_name}, 456 'component_name' => $component_name 457 }; 458 } 459 460 $self->{'FileMap'} = $file_map; 461 return $file_map; 462} 463 464 4651; 466