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::InstallationSet; 23 24use installer::patch::Tools; 25use installer::patch::Version; 26use installer::logger; 27 28use strict; 29 30# TODO: Detect the location of 7z.exe 31my $Unpacker = "/c/Program\\ Files/7-Zip/7z.exe"; 32 33 34 35# TODO: Is there a touch in a standard library? 36sub touch ($) 37{ 38 my ($filename) = @_; 39 40 open my $out, ">", $filename; 41 close $out; 42} 43 44 45 46 47=head1 NAME 48 49 package installer::patch::InstallationSet - Functions for handling installation sets 50 51=head1 DESCRIPTION 52 53 This package contains functions for unpacking the .exe files that 54 are created by the NSIS installer creator and the .cab files in 55 the installation sets. 56 57=cut 58 59sub UnpackExe ($$) 60{ 61 my ($filename, $destination_path) = @_; 62 63 $installer::logger::Info->printf("unpacking installation set to '%s'\n", $destination_path); 64 65 # Unpack to a temporary path and change its name to the destination path 66 # only when the unpacking has completed successfully. 67 File::Path::make_path($destination_path); 68 69 my $windows_filename = installer::patch::Tools::ToEscapedWindowsPath($filename); 70 my $windows_destination_path = installer::patch::Tools::ToEscapedWindowsPath($destination_path); 71 my $command = join(" ", 72 $Unpacker, 73 "x", 74 "-y", 75 "-o".$windows_destination_path, 76 $windows_filename); 77 my $result = qx($command); 78 79 # Check the existence of the .cab files. 80 my $cab_filename = File::Spec->catfile($destination_path, "openoffice1.cab"); 81 if ( ! -f $cab_filename) 82 { 83 installer::logger::PrintError("cab file '%s' was not extracted from installation set\n", $cab_filename); 84 return 0; 85 } 86 return 1; 87} 88 89 90 91 92=head2 UnpackCab($cab_filename, $destination_path) 93 94 Unpacking the cabinet file inside an .exe installation set is a 95 three step process because there is no directory information stored 96 inside the cab file. This has to be taken from the 'File' and 97 'Directory' tables in the .msi file. 98 99 1. Setup the directory structure of all files in the cab from the 'File' and 'Directory' tables in the msi. 100 101 2. Unpack the cab file. 102 103 3. Move the files to their destination directories. 104 105=cut 106sub UnpackCab ($$$) 107{ 108 my ($cab_filename, $msi, $destination_path) = @_; 109 110 # Step 1 111 # Extract the directory structure from the 'File' and 'Directory' tables in the given msi. 112 $installer::logger::Info->printf("setting up directory tree\n"); 113 my $file_table = $msi->GetTable("File"); 114 my $file_map = $msi->GetFileMap(); 115 116 # Step 2 117 # Unpack the .cab file to a temporary path. 118 my $temporary_destination_path = $destination_path . ".tmp"; 119 if ( -d $temporary_destination_path) 120 { 121 # Temporary directory already exists => cab file has already been unpacked (flat), nothing to do. 122 $installer::logger::Info->printf("cab file has already been unpacked to flat structure\n"); 123 } 124 else 125 { 126 UnpackCabFlat($cab_filename, $temporary_destination_path, $file_table); 127 } 128 129 # Step 3 130 # Move the files to their destinations. 131 File::Path::make_path($destination_path); 132 $installer::logger::Info->printf("moving files to their directories\n"); 133 my $count = 0; 134 foreach my $file_row (@{$file_table->GetAllRows()}) 135 { 136 my $unique_name = $file_row->GetValue('File'); 137 my $directory_item = $file_map->{$unique_name}->{'directory'}; 138 my $source_full_name = $directory_item->{'full_source_long_name'}; 139 140 my $flat_filename = File::Spec->catfile($temporary_destination_path, $unique_name); 141 my $dir_path = File::Spec->catfile($destination_path, $source_full_name); 142 my $dir_filename = File::Spec->catfile($dir_path, $unique_name); 143 144 if ( ! -d $dir_path) 145 { 146 File::Path::make_path($dir_path); 147 } 148 File::Copy::move($flat_filename, $dir_filename); 149 150 ++$count; 151 } 152 153 # Cleanup. Remove the temporary directory. It should be empty by now. 154 rmdir($temporary_destination_path); 155} 156 157 158 159 160=head2 UnpackCabFlat ($cab_filename, $destination_path, $file_table) 161 162 Unpack the flat file structure of the $cab_filename to $destination_path. 163 164 In order to detect and handle an incomplete (arborted) previous 165 extraction, the cab file is unpacked to a temprorary directory 166 that after successful extraction is renamed to $destination_path. 167 168=cut 169sub UnpackCabFlat ($$$) 170{ 171 my ($cab_filename, $destination_path, $file_table) = @_; 172 173 # Unpack the .cab file to a temporary path (note that 174 # $destination_path may alreay bee a temporary path). Using a 175 # second one prevents the lengthy flat unpacking to be repeated 176 # when another step fails. 177 178 $installer::logger::Info->printf("unpacking cab file\n"); 179 File::Path::make_path($destination_path); 180 my $windows_cab_filename = installer::patch::Tools::ToEscapedWindowsPath($cab_filename); 181 my $windows_destination_path = installer::patch::Tools::ToEscapedWindowsPath($destination_path); 182 my $command = join(" ", 183 $Unpacker, 184 "x", "-o".$windows_destination_path, 185 $windows_cab_filename, 186 "-y"); 187 open my $cmd, $command."|"; 188 my $extraction_count = 0; 189 my $file_count = $file_table->GetRowCount(); 190 while (<$cmd>) 191 { 192 my $message = $_; 193 chomp($message); 194 ++$extraction_count; 195 printf("%4d/%4d %3.2f%% \r", 196 $extraction_count, 197 $file_count, 198 $extraction_count*100/$file_count); 199 } 200 close $cmd; 201} 202 203 204 205 206=head GetUnpackedExePath ($version, $is_current_version, $language, $package_format, $product) 207 208 Convenience function that returns where a downloadable installation set is extracted to. 209 210=cut 211sub GetUnpackedExePath ($$$$$) 212{ 213 my ($version, $is_current_version, $language, $package_format, $product) = @_; 214 215 my $path = GetUnpackedPath($version, $is_current_version, $language, $package_format, $product); 216 return File::Spec->catfile($path, "unpacked"); 217} 218 219 220 221 222=head GetUnpackedCabPath ($version, $is_current_version, $language, $package_format, $product) 223 224 Convenience function that returns where a cab file is extracted 225 (with injected directory structure from the msi file) to. 226 227=cut 228sub GetUnpackedCabPath ($$$$$) 229{ 230 my ($version, $is_current_version, $language, $package_format, $product) = @_; 231 232 my $path = GetUnpackedPath($version, $is_current_version, $language, $package_format, $product); 233 return File::Spec->catfile($path, "unpacked"); 234} 235 236 237 238 239=head2 GetUnpackedPath($version, $is_current_version, $language, $package_format, $product) 240 241 Internal function for creating paths to where archives are unpacked. 242 243=cut 244sub GetUnpackedPath ($$$$$) 245{ 246 my ($version, $is_current_version, $language, $package_format, $product) = @_; 247 248 return File::Spec->catfile( 249 $ENV{'SRC_ROOT'}, 250 "instsetoo_native", 251 $ENV{'INPATH'}, 252 $product, 253 $package_format, 254 installer::patch::Version::ArrayToDirectoryName( 255 installer::patch::Version::StringToNumberArray($version)), 256 $language); 257} 258 259 260 261 262sub GetMsiFilename ($$) 263{ 264 my ($path, $version) = @_; 265 266 my $no_dot_version = installer::patch::Version::ArrayToNoDotName( 267 installer::patch::Version::StringToNumberArray( 268 $version)); 269 return File::Spec->catfile( 270 $path, 271 "openoffice" . $no_dot_version . ".msi"); 272} 273 274 275 276 277sub GetCabFilename ($$) 278{ 279 my ($path, $version) = @_; 280 281 return File::Spec->catfile( 282 $path, 283 "openoffice1.cab"); 284} 285 286 287 288 289=head2 Download($language, $release_data, $filename) 290 291 Download an installation set to $filename. The URL for the 292 download is taken from $release_data, a snippet from the 293 instsetoo_native/data/releases.xml file. 294 295=cut 296sub Download ($$$) 297{ 298 my ($language, $release_data, $filename) = @_; 299 300 my $url = $release_data->{'URL'}; 301 $release_data->{'URL'} =~ /^(.*)\/([^\/]+)$/; 302 my ($location, $basename) = ($1,$2); 303 304 $installer::logger::Info->printf("downloading %s\n", $basename); 305 $installer::logger::Info->printf(" from '%s'\n", $location); 306 my $filesize = $release_data->{'file-size'}; 307 $installer::logger::Info->printf(" expected size is %d\n", $filesize); 308 my $temporary_filename = $filename . ".part"; 309 my $resume_size = 0; 310 if ( -f $temporary_filename) 311 { 312 $resume_size = -s $temporary_filename; 313 $installer::logger::Info->printf(" trying to resume at %d/%d bytes\n", $resume_size, $filesize); 314 } 315 316 # Prepare checksum. 317 my $checksum = undef; 318 my $checksum_type = $release_data->{'checksum-type'}; 319 my $checksum_value = $release_data->{'checksum-value'}; 320 my $digest = undef; 321 if ($checksum_type eq "sha256") 322 { 323 $digest = Digest->new("SHA-256"); 324 } 325 elsif ($checksum_type eq "md5") 326 { 327 $digest = Digest->new("md5"); 328 } 329 else 330 { 331 installer::logger::PrintError( 332 "checksum type %s is not supported. Supported checksum types are: sha256,md5\n", 333 $checksum_type); 334 return 0; 335 } 336 337 # Download the extension. 338 open my $out, ">>$temporary_filename"; 339 binmode($out); 340 341 my $mode = $|; 342 my $handle = select STDOUT; 343 $| = 1; 344 select $handle; 345 346 my $agent = LWP::UserAgent->new(); 347 $agent->timeout(120); 348 $agent->show_progress(0); 349 my $last_was_redirect = 0; 350 my $bytes_read = 0; 351 $agent->add_handler('response_redirect' 352 => sub{ 353 $last_was_redirect = 1; 354 return; 355 }); 356 $agent->add_handler('response_data' 357 => sub{ 358 if ($last_was_redirect) 359 { 360 $last_was_redirect = 0; 361 # Throw away the data we got so far. 362 $digest->reset(); 363 close $out; 364 open $out, ">$temporary_filename"; 365 binmode($out); 366 } 367 my($response,$agent,$h,$data)=@_; 368 print $out $data; 369 $digest->add($data); 370 $bytes_read += length($data); 371 printf("read %*d / %d %d%% \r", 372 length($filesize), 373 $bytes_read, 374 $filesize, 375 $bytes_read*100/$filesize); 376 }); 377 my $response; 378 if ($resume_size > 0) 379 { 380 $response = $agent->get($url, 'Range' => "bytes=$resume_size-"); 381 } 382 else 383 { 384 $response = $agent->get($url); 385 } 386 close $out; 387 388 $handle = select STDOUT; 389 $| = $mode; 390 select $handle; 391 392 $installer::logger::Info->print(" \r"); 393 394 if ($response->is_success()) 395 { 396 if ($digest->hexdigest() eq $checksum_value) 397 { 398 $installer::logger::Info->PrintInfo("download was successfull\n"); 399 if ( ! rename($temporary_filename, $filename)) 400 { 401 installer::logger::PrintError("can not rename '%s' to '%s'\n", $temporary_filename, $filename); 402 return 0; 403 } 404 else 405 { 406 return 1; 407 } 408 } 409 else 410 { 411 installer::logger::PrintError("%s checksum is wrong\n", $checksum_type); 412 return 0; 413 } 414 } 415 else 416 { 417 installer::logger::PrintError("there was a download error\n"); 418 return 0; 419 } 420} 421 422 423 424 425=head2 ProvideDownloadSet ($version, $language, $package_format) 426 427 Download an installation set when it is not yet present to 428 $ENV{'TARFILE_LOCATION'}. Verify the downloaded file with the 429 checksum that is extracted from the 430 instsetoo_native/data/releases.xml file. 431 432=cut 433sub ProvideDownloadSet ($$$) 434{ 435 my ($version, $language, $package_format) = @_; 436 437 my $release_item = installer::patch::ReleasesList::Instance()->{$version}->{$package_format}->{$language}; 438 439 # Get basename of installation set from URL. 440 $release_item->{'URL'} =~ /^(.*)\/([^\/]+)$/; 441 my ($location, $basename) = ($1,$2); 442 443 # Is the installation set already present in ext_sources/ ? 444 my $need_download = 0; 445 my $ext_sources_filename = File::Spec->catfile( 446 $ENV{'TARFILE_LOCATION'}, 447 $basename); 448 if ( ! -f $ext_sources_filename) 449 { 450 $installer::logger::Info->printf("download set is not in ext_sources/ (%s)\n", $ext_sources_filename); 451 $need_download = 1; 452 } 453 else 454 { 455 $installer::logger::Info->printf("download set exists at '%s'\n", $ext_sources_filename); 456 if ($release_item->{'checksum-type'} eq 'sha256') 457 { 458 $installer::logger::Info->printf("checking SHA256 checksum\n"); 459 my $digest = Digest->new("SHA-256"); 460 open my $in, "<", $ext_sources_filename; 461 $digest->addfile($in); 462 close $in; 463 if ($digest->hexdigest() ne $release_item->{'checksum-value'}) 464 { 465 $installer::logger::Info->printf(" mismatch\n", $ext_sources_filename); 466 $need_download = 1; 467 } 468 else 469 { 470 $installer::logger::Info->printf(" match\n"); 471 } 472 } 473 } 474 475 if ($need_download) 476 { 477 if ( ! installer::patch::InstallationSet::Download( 478 $language, 479 $release_item, 480 $ext_sources_filename)) 481 { 482 return 0; 483 } 484 if ( ! -f $ext_sources_filename) 485 { 486 $installer::logger::Info->printf("download set could not be downloaded\n"); 487 return 0; 488 } 489 } 490 491 return $ext_sources_filename; 492} 493 494 495 496 497sub ProvideUnpackedExe ($$$$$) 498{ 499 my ($version, $is_current_version, $language, $package_format, $product_name) = @_; 500 501 # Check if the exe has already been unpacked. 502 my $unpacked_exe_path = installer::patch::InstallationSet::GetUnpackedExePath( 503 $version, 504 $is_current_version, 505 $language, 506 $package_format, 507 $product_name); 508 my $unpacked_exe_flag_filename = File::Spec->catfile($unpacked_exe_path, "__exe_is_unpacked"); 509 my $exe_is_unpacked = -f $unpacked_exe_flag_filename; 510 511 if ($exe_is_unpacked) 512 { 513 # Yes, exe has already been unpacked. There is nothing more to do. 514 $installer::logger::Info->printf("downloadable installation set has already been unpacked to\n"); 515 $installer::logger::Info->printf(" %s\n", $unpacked_exe_path); 516 return 1; 517 } 518 elsif ($is_current_version) 519 { 520 # For the current version the exe is created from the unpacked 521 # content and both are expected to be already present. 522 523 # In order to have the .cab and its unpacked content in one 524 # directory and don't interfere with the creation of regular 525 # installation sets, we copy the unpacked .exe into a separate 526 # directory. 527 528 my $original_path = File::Spec->catfile( 529 $ENV{'SRC_ROOT'}, 530 "instsetoo_native", 531 $ENV{'INPATH'}, 532 $product_name, 533 $package_format, 534 "install", 535 $language); 536 $installer::logger::Info->printf("creating a copy\n"); 537 $installer::logger::Info->printf(" of %s\n", $original_path); 538 $installer::logger::Info->printf(" at %s\n", $unpacked_exe_path); 539 File::Path::make_path($unpacked_exe_path) unless -d $unpacked_exe_path; 540 my ($file_count,$directory_count) = CopyRecursive($original_path, $unpacked_exe_path); 541 return 0 if ( ! defined $file_count); 542 $installer::logger::Info->printf(" copied %d files in %d directories\n", 543 $file_count, 544 $directory_count); 545 546 touch($unpacked_exe_flag_filename); 547 548 return 1; 549 } 550 else 551 { 552 # No, we have to unpack the exe. 553 554 # Provide the exe. 555 my $filename = installer::patch::InstallationSet::ProvideDownloadSet( 556 $version, 557 $language, 558 $package_format); 559 560 # Unpack it. 561 if (defined $filename) 562 { 563 if (installer::patch::InstallationSet::UnpackExe($filename, $unpacked_exe_path)) 564 { 565 $installer::logger::Info->printf("downloadable installation set has been unpacked to\n"); 566 $installer::logger::Info->printf(" %s\n", $unpacked_exe_path); 567 568 touch($unpacked_exe_flag_filename); 569 570 return 1; 571 } 572 } 573 else 574 { 575 installer::logger::PrintError("could not provide .exe installation set at '%s'\n", $filename); 576 } 577 } 578 579 return 0; 580} 581 582 583 584 585sub CopyRecursive ($$) 586{ 587 my ($source_path, $destination_path) = @_; 588 589 return (undef,undef) unless -d $source_path; 590 591 my @todo = ([$source_path, $destination_path]); 592 my $file_count = 0; 593 my $directory_count = 0; 594 while (scalar @todo > 0) 595 { 596 my ($source,$destination) = @{shift @todo}; 597 598 next if ! -d $source; 599 File::Path::make_path($destination); 600 ++$directory_count; 601 602 # Read list of files in the current source directory. 603 opendir( my $dir, $source); 604 my @files = readdir $dir; 605 closedir $dir; 606 607 # Copy all files and push all directories to @todo. 608 foreach my $file (@files) 609 { 610 next if $file =~ /^\.+$/; 611 612 my $source_file = File::Spec->catfile($source, $file); 613 my $destination_file = File::Spec->catfile($destination, $file); 614 if ( -f $source_file) 615 { 616 File::Copy::copy($source_file, $destination_file); 617 ++$file_count; 618 } 619 elsif ( -d $source_file) 620 { 621 push @todo, [$source_file, $destination_file]; 622 } 623 } 624 } 625 626 return ($file_count, $directory_count); 627} 628 629 630 631 632sub CheckLocalCopy ($$$$) 633{ 634 my ($version, $language, $package_format, $product_name) = @_; 635 636 # Compare creation times of the original .msi and its copy. 637 638 my $original_path = File::Spec->catfile( 639 $ENV{'SRC_ROOT'}, 640 "instsetoo_native", 641 $ENV{'INPATH'}, 642 $product_name, 643 $package_format, 644 "install", 645 $language); 646 647 my $copy_path = installer::patch::InstallationSet::GetUnpackedExePath( 648 $version, 649 1, 650 $language, 651 $package_format, 652 $product_name); 653 654 my $msi_basename = "openoffice" 655 . installer::patch::Version::ArrayToNoDotName( 656 installer::patch::Version::StringToNumberArray($version)) 657 . ".msi"; 658 659 my $original_msi_filename = File::Spec->catfile($original_path, $msi_basename); 660 my $copied_msi_filename = File::Spec->catfile($copy_path, $msi_basename); 661 662 my @original_msi_stats = stat($original_msi_filename); 663 my @copied_msi_stats = stat($copied_msi_filename); 664 my $original_msi_mtime = $original_msi_stats[9]; 665 my $copied_msi_mtime = $copied_msi_stats[9]; 666 667 if (defined $original_msi_mtime 668 && defined $copied_msi_mtime 669 && $original_msi_mtime > $copied_msi_mtime) 670 { 671 # The installation set is newer than its copy. 672 # Remove the copy. 673 $installer::logger::Info->printf( 674 "removing copy of installation set (version %s) because it is out of date\n", 675 $version); 676 File::Path::remove_tree($copy_path); 677 } 678} 679 680 681 682 683=head2 ProvideUnpackedCab 684 685 1a. Make sure that a downloadable installation set is present. 686 1b. or that a freshly built installation set (packed and unpacked is present) 687 2. Unpack the downloadable installation set 688 3. Unpack the .cab file. 689 690 The 'Provide' in the function name means that any step that has 691 already been made is not made again. 692 693=cut 694sub ProvideUnpackedCab ($$$$$) 695{ 696 my ($version, $is_current_version, $language, $package_format, $product_name) = @_; 697 698 if ($is_current_version) 699 { 700 # For creating patches we maintain a copy of the unpacked .exe. Make sure that that is updated when 701 # a new installation set has been built. 702 CheckLocalCopy($version, $language, $package_format, $product_name); 703 } 704 705 # Check if the cab file has already been unpacked. 706 my $unpacked_cab_path = installer::patch::InstallationSet::GetUnpackedCabPath( 707 $version, 708 $is_current_version, 709 $language, 710 $package_format, 711 $product_name); 712 my $unpacked_cab_flag_filename = File::Spec->catfile($unpacked_cab_path, "__cab_is_unpacked"); 713 my $cab_is_unpacked = -f $unpacked_cab_flag_filename; 714 715 if ($cab_is_unpacked) 716 { 717 # Yes. Cab was already unpacked. There is nothing more to do. 718 $installer::logger::Info->printf("cab has already been unpacked to\n"); 719 $installer::logger::Info->printf(" %s\n", $unpacked_cab_path); 720 721 return 1; 722 } 723 else 724 { 725 # Make sure that the exe is unpacked and the cab file exists. 726 ProvideUnpackedExe($version, $is_current_version, $language, $package_format, $product_name); 727 728 # Unpack the cab file. 729 my $unpacked_exe_path = installer::patch::InstallationSet::GetUnpackedExePath( 730 $version, 731 $is_current_version, 732 $language, 733 $package_format, 734 $product_name); 735 my $msi = new installer::patch::Msi( 736 installer::patch::InstallationSet::GetMsiFilename($unpacked_exe_path, $version), 737 $version, 738 $is_current_version, 739 $language, 740 $product_name); 741 742 my $cab_filename = installer::patch::InstallationSet::GetCabFilename( 743 $unpacked_exe_path, 744 $version); 745 if ( ! -f $cab_filename) 746 { 747 # Cab file does not exist. 748 installer::logger::PrintError( 749 "could not find .cab file at '%s'. Extraction of .exe seems to have failed.\n", 750 $cab_filename); 751 return 0; 752 } 753 754 if (installer::patch::InstallationSet::UnpackCab( 755 $cab_filename, 756 $msi, 757 $unpacked_cab_path)) 758 { 759 $installer::logger::Info->printf("unpacked cab file '%s'\n", $cab_filename); 760 $installer::logger::Info->printf(" to '%s'\n", $unpacked_cab_path); 761 762 touch($unpacked_cab_flag_filename); 763 764 return 1; 765 } 766 else 767 { 768 return 0; 769 } 770 } 771} 7721; 773