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 installer::languages::get_normalized_language($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 78 79sub new ($$;$$$$) 80{ 81 my ($class, $filename, $version, $is_current_version, $language, $product_name) = @_; 82 83 if ( ! -f $filename) 84 { 85 installer::logger::PrintError("can not find the .msi file for version %s and language %s at '%s'\n", 86 $version, 87 $language, 88 $filename); 89 return undef; 90 } 91 92 my $self = { 93 'filename' => $filename, 94 'path' => dirname($filename), 95 'version' => $version, 96 'is_current_version' => $is_current_version, 97 'language' => $language, 98 'package_format' => "msi", 99 'product_name' => $product_name, 100 'tmpdir' => File::Temp->newdir(CLEANUP => 1), 101 'is_valid' => -f $filename 102 }; 103 bless($self, $class); 104 105 # Fill in some missing values from the 'Properties' table. 106 if ( ! (defined $version && defined $language && defined $product_name)) 107 { 108 my $property_table = $self->GetTable("Property"); 109 110 $self->{'version'} = $property_table->GetValue("Property", "DEFINEDVERSION", "Value") 111 unless defined $self->{'version'}; 112 $self->{'product_name'} = $property_table->GetValue("Property", "DEFINEDPRODUCT", "Value") 113 unless defined $self->{'product_name'}; 114 115 my $language = $property_table->GetValue("Property", "ProductLanguage", "Value"); 116 # TODO: Convert numerical language id to language name. 117 $self->{'language'} = $language 118 unless defined $self->{'language'}; 119 } 120 121 return $self; 122} 123 124 125 126 127sub IsValid ($) 128{ 129 my ($self) = @_; 130 131 return $self->{'is_valid'}; 132} 133 134 135 136 137=head2 Commit($self) 138 139 Write all modified tables back into the database. 140 141=cut 142 143sub Commit ($) 144{ 145 my $self = shift; 146 147 my @tables_to_update = (); 148 foreach my $table (values %{$self->{'tables'}}) 149 { 150 push @tables_to_update,$table if ($table->IsModified()); 151 } 152 153 if (scalar @tables_to_update > 0) 154 { 155 $installer::logger::Info->printf("writing modified tables to database:\n"); 156 foreach my $table (@tables_to_update) 157 { 158 $installer::logger::Info->printf(" %s\n", $table->GetName()); 159 $self->PutTable($table); 160 } 161 162 foreach my $table (@tables_to_update) 163 { 164 $table->UpdateTimestamp(); 165 $table->MarkAsUnmodified(); 166 } 167 } 168} 169 170 171 172 173=head2 GetTable($seld, $table_name) 174 175 Return an MsiTable object for $table_name. Table objects are kept 176 alive for the life time of the Msi object. Therefore the second 177 call for the same table is very cheap. 178 179=cut 180 181sub GetTable ($$) 182{ 183 my ($self, $table_name) = @_; 184 185 my $table = $self->{'tables'}->{$table_name}; 186 if ( ! defined $table) 187 { 188 my $table_filename = File::Spec->catfile($self->{'tmpdir'}, $table_name .".idt"); 189 if ( ! -f $table_filename 190 || ! EnsureAYoungerThanB($table_filename, $self->{'fullname'})) 191 { 192 # Extract table from database to text file on disk. 193 my $truncated_table_name = length($table_name)>8 ? substr($table_name,0,8) : $table_name; 194 my $command = join(" ", 195 "msidb.exe", 196 "-d", installer::patch::Tools::ToEscapedWindowsPath($self->{'filename'}), 197 "-f", installer::patch::Tools::ToEscapedWindowsPath($self->{'tmpdir'}), 198 "-e", $table_name); 199 my $result = qx($command); 200 } 201 202 # Read table into memory. 203 $table = new installer::patch::MsiTable($table_filename, $table_name); 204 $self->{'tables'}->{$table_name} = $table; 205 } 206 207 return $table; 208} 209 210 211 212 213=head2 PutTable($self, $table) 214 215 Write the given table back to the database. 216 217=cut 218 219sub PutTable ($$) 220{ 221 my ($self, $table) = @_; 222 223 # Create text file from the current table content. 224 $table->WriteFile(); 225 226 my $table_name = $table->GetName(); 227 228 # Store table from text file into database. 229 my $table_filename = $table->{'filename'}; 230 231 if (length($table_name) > 8) 232 { 233 # The file name of the table data must not be longer than 8 characters (not counting the extension). 234 # The name passed as argument to the -i option may be longer. 235 my $truncated_table_name = substr($table_name,0,8); 236 my $table_truncated_filename = File::Spec->catfile( 237 dirname($table_filename), 238 $truncated_table_name.".idt"); 239 File::Copy::copy($table_filename, $table_truncated_filename) || die("can not create table file with short name"); 240 } 241 242 my $command = join(" ", 243 "msidb.exe", 244 "-d", installer::patch::Tools::ToEscapedWindowsPath($self->{'filename'}), 245 "-f", installer::patch::Tools::ToEscapedWindowsPath($self->{'tmpdir'}), 246 "-i", $table_name); 247 my $result = system($command); 248 249 if ($result != 0) 250 { 251 installer::logger::PrintError("writing table '%s' back to database failed", $table_name); 252 # For error messages see http://msdn.microsoft.com/en-us/library/windows/desktop/aa372835%28v=vs.85%29.aspx 253 } 254} 255 256 257 258 259=head2 EnsureAYoungerThanB ($filename_a, $filename_b) 260 261 Internal function (not a method) that compares to files according 262 to their last modification times (mtime). 263 264=cut 265 266sub EnsureAYoungerThanB ($$) 267{ 268 my ($filename_a, $filename_b) = @_; 269 270 die("file $filename_a does not exist") unless -f $filename_a; 271 die("file $filename_b does not exist") unless -f $filename_b; 272 273 my @stat_a = stat($filename_a); 274 my @stat_b = stat($filename_b); 275 276 if ($stat_a[9] <= $stat_b[9]) 277 { 278 return 0; 279 } 280 else 281 { 282 return 1; 283 } 284} 285 286 287 288 289=head2 SplitLongShortName($name) 290 291 Split $name (typically from the 'FileName' column in the 'File' 292 table or 'DefaultDir' column in the 'Directory' table) at the '|' 293 into short (8.3) and long names. If there is no '|' in $name then 294 $name is returned as both short and long name. 295 296 Returns long and short name (in this order) as array. 297 298=cut 299 300sub SplitLongShortName ($) 301{ 302 my ($name) = @_; 303 304 if ($name =~ /^([^\|]*)\|(.*)$/) 305 { 306 return ($2,$1); 307 } 308 else 309 { 310 return ($name,$name); 311 } 312} 313 314 315 316=head2 SplitTargetSourceLongShortName ($name) 317 318 Split $name first at the ':' into target and source parts and each 319 of those at the '|'s into long and short parts. Names that follow 320 this pattern come from the 'DefaultDir' column in the 'Directory' 321 table. 322 323=cut 324 325sub SplitTargetSourceLongShortName ($) 326{ 327 my ($name) = @_; 328 329 if ($name =~ /^([^:]*):(.*)$/) 330 { 331 return (installer::patch::Msi::SplitLongShortName($1), installer::patch::Msi::SplitLongShortName($2)); 332 } 333 else 334 { 335 my ($long,$short) = installer::patch::Msi::SplitLongShortName($name); 336 return ($long,$short,$long,$short); 337 } 338} 339 340 341 342 343sub SetupFullNames ($$); 344sub SetupFullNames ($$) 345{ 346 my ($item, $directory_map) = @_; 347 348 # Don't process any item twice. 349 return if defined $item->{'full_source_name'}; 350 351 my $parent = $item->{'parent'}; 352 if (defined $parent) 353 { 354 # Process the parent first. 355 if ( ! defined $parent->{'full_source_long_name'}) 356 { 357 SetupFullNames($parent, $directory_map); 358 } 359 360 # Prepend the full names of the parent to our names. 361 $item->{'full_source_long_name'} 362 = $parent->{'full_source_long_name'} . "/" . $item->{'source_long_name'}; 363 $item->{'full_source_short_name'} 364 = $parent->{'full_source_short_name'} . "/" . $item->{'source_short_name'}; 365 $item->{'full_target_long_name'} 366 = $parent->{'full_target_long_name'} . "/" . $item->{'target_long_name'}; 367 $item->{'full_target_short_name'} 368 = $parent->{'full_target_short_name'} . "/" . $item->{'target_short_name'}; 369 } 370 else 371 { 372 # Directory has no parent => full names are the same as the name. 373 $item->{'full_source_long_name'} = $item->{'source_long_name'}; 374 $item->{'full_source_short_name'} = $item->{'source_short_name'}; 375 $item->{'full_target_long_name'} = $item->{'target_long_name'}; 376 $item->{'full_target_short_name'} = $item->{'target_short_name'}; 377 } 378} 379 380 381 382 383=head2 GetDirectoryMap($self) 384 385 Return a map that maps directory unique names (column 'Directory' in table 'Directory') 386 to hashes that contains short and long source and target names. 387 388=cut 389 390sub GetDirectoryMap ($) 391{ 392 my ($self) = @_; 393 394 if (defined $self->{'DirectoryMap'}) 395 { 396 return $self->{'DirectoryMap'}; 397 } 398 399 # Initialize the directory map. 400 my $directory_table = $self->GetTable("Directory"); 401 my $directory_map = (); 402 foreach my $row (@{$directory_table->GetAllRows()}) 403 { 404 my ($target_long_name, $target_short_name, $source_long_name, $source_short_name) 405 = installer::patch::Msi::SplitTargetSourceLongShortName($row->GetValue("DefaultDir")); 406 my $unique_name = $row->GetValue("Directory"); 407 $directory_map->{$unique_name} = 408 { 409 'unique_name' => $unique_name, 410 'parent_name' => $row->GetValue("Directory_Parent"), 411 'default_dir' => $row->GetValue("DefaultDir"), 412 'source_long_name' => $source_long_name, 413 'source_short_name' => $source_short_name, 414 'target_long_name' => $target_long_name, 415 'target_short_name' => $target_short_name 416 }; 417 } 418 419 # Add references to parent directories. 420 foreach my $item (values %$directory_map) 421 { 422 $item->{'parent'} = $directory_map->{$item->{'parent_name'}}; 423 } 424 425 # Set up full names for all directories. 426 foreach my $item (values %$directory_map) 427 { 428 SetupFullNames($item, $directory_map); 429 } 430 431 # Cleanup the names. 432 foreach my $item (values %$directory_map) 433 { 434 foreach my $id ( 435 'full_source_long_name', 436 'full_source_short_name', 437 'full_target_long_name', 438 'full_target_short_name') 439 { 440 $item->{$id} =~ s/\/(\.\/)+/\//g; 441 $item->{$id} =~ s/^SourceDir\///; 442 $item->{$id} =~ s/^\.$//; 443 } 444 } 445 446 $self->{'DirectoryMap'} = $directory_map; 447 return $self->{'DirectoryMap'}; 448} 449 450 451 452 453=head2 GetFileMap ($) 454 455 Return a map (hash) that maps the unique name (column 'File' in 456 the 'File' table) to data that is associated with that file, like 457 the directory or component. 458 459 The map is kept alive for the lifetime of the Msi object. All 460 calls but the first are cheap. 461 462=cut 463 464sub GetFileMap ($) 465{ 466 my ($self) = @_; 467 468 if (defined $self->{'FileMap'}) 469 { 470 return $self->{'FileMap'}; 471 } 472 473 my $file_table = $self->GetTable("File"); 474 my $component_table = $self->GetTable("Component"); 475 my $dir_map = $self->GetDirectoryMap(); 476 477 # Setup a map from component names to directory items. 478 my %component_to_directory_map = 479 map 480 {$_->GetValue('Component') => $_->GetValue('Directory_')} 481 @{$component_table->GetAllRows()}; 482 483 # Finally, create the map from files to directories. 484 my $file_map = {}; 485 my $file_component_index = $file_table->GetColumnIndex("Component_"); 486 my $file_file_index = $file_table->GetColumnIndex("File"); 487 my $file_filename_index = $file_table->GetColumnIndex("FileName"); 488 foreach my $file_row (@{$file_table->GetAllRows()}) 489 { 490 my $component_name = $file_row->GetValue($file_component_index); 491 my $directory_name = $component_to_directory_map{$component_name}; 492 my $unique_name = $file_row->GetValue($file_file_index); 493 my $file_name = $file_row->GetValue($file_filename_index); 494 my ($long_name, $short_name) = SplitLongShortName($file_name); 495 $file_map->{$unique_name} = { 496 'directory' => $dir_map->{$directory_name}, 497 'component_name' => $component_name, 498 'file_name' => $file_name, 499 'long_name' => $long_name, 500 'short_name' => $short_name 501 }; 502 } 503 504 $self->{'FileMap'} = $file_map; 505 return $file_map; 506} 507 508 5091; 510