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