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 installer::languages::get_normalized_language($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 if (defined $filesize) 308 { 309 $installer::logger::Info->printf(" expected size is %d\n", $filesize); 310 } 311 else 312 { 313 $installer::logger::Info->printf(" file size is not yet known\n"); 314 } 315 my $temporary_filename = $filename . ".part"; 316 my $resume_size = 0; 317 318 # Prepare checksum. 319 my $checksum = undef; 320 my $checksum_type = $release_data->{'checksum-type'}; 321 my $checksum_value = $release_data->{'checksum-value'}; 322 my $digest = undef; 323 if ( ! defined $checksum_value) 324 { 325 # No checksum available. Skip test. 326 } 327 elsif ($checksum_type eq "sha256") 328 { 329 $digest = Digest->new("SHA-256"); 330 } 331 elsif ($checksum_type eq "md5") 332 { 333 $digest = Digest->new("md5"); 334 } 335 else 336 { 337 installer::logger::PrintError( 338 "checksum type %s is not supported. Supported checksum types are: sha256,md5\n", 339 $checksum_type); 340 return 0; 341 } 342 343 # Download the extension. 344 open my $out, ">$temporary_filename"; 345 binmode($out); 346 347 my $mode = $|; 348 my $handle = select STDOUT; 349 $| = 1; 350 select $handle; 351 352 my $agent = LWP::UserAgent->new(); 353 $agent->timeout(120); 354 $agent->show_progress(0); 355 my $last_was_redirect = 0; 356 my $bytes_read = 0; 357 $agent->add_handler('response_redirect' 358 => sub{ 359 $last_was_redirect = 1; 360 return; 361 }); 362 $agent->add_handler('response_data' 363 => sub{ 364 if ($last_was_redirect) 365 { 366 $last_was_redirect = 0; 367 # Throw away the data we got so far. 368 $digest->reset() if defined $digest; 369 close $out; 370 open $out, ">$temporary_filename"; 371 binmode($out); 372 } 373 my($response,$agent,$h,$data)=@_; 374 print $out $data; 375 $digest->add($data) if defined $digest; 376 $bytes_read += length($data); 377 if (defined $filesize) 378 { 379 printf("read %*d / %d %d%% \r", 380 length($filesize), 381 $bytes_read, 382 $filesize, 383 $bytes_read*100/$filesize); 384 } 385 else 386 { 387 printf("read %6.2f MB\r", $bytes_read/(1024.0*1024.0)); 388 } 389 }); 390 my $response; 391 if ($resume_size > 0) 392 { 393 $response = $agent->get($url, 'Range' => "bytes=$resume_size-"); 394 } 395 else 396 { 397 $response = $agent->get($url); 398 } 399 close $out; 400 401 $handle = select STDOUT; 402 $| = $mode; 403 select $handle; 404 405 $installer::logger::Info->print(" \r"); 406 407 if ($response->is_success()) 408 { 409 if ( ! defined $digest 410 || $digest->hexdigest() eq $checksum_value) 411 { 412 $installer::logger::Info->print("download was successfull\n"); 413 if ( ! rename($temporary_filename, $filename)) 414 { 415 installer::logger::PrintError("can not rename '%s' to '%s'\n", $temporary_filename, $filename); 416 return 0; 417 } 418 else 419 { 420 return 1; 421 } 422 } 423 else 424 { 425 installer::logger::PrintError("%s checksum is wrong\n", $checksum_type); 426 return 0; 427 } 428 } 429 else 430 { 431 installer::logger::PrintError("there was a download error\n"); 432 return 0; 433 } 434} 435 436 437 438 439=head2 ProvideDownloadSet ($version, $language, $package_format) 440 441 Download an installation set when it is not yet present to 442 $ENV{'TARFILE_LOCATION'}. Verify the downloaded file with the 443 checksum that is extracted from the 444 instsetoo_native/data/releases.xml file. 445 446=cut 447sub ProvideDownloadSet ($$$) 448{ 449 my ($version, $language, $package_format) = @_; 450 451 my $release_item = installer::patch::ReleasesList::Instance()->{$version}->{$package_format}->{$language}; 452 453 # Get basename of installation set from URL. 454 $release_item->{'URL'} =~ /^(.*)\/([^\/]+)$/; 455 my ($location, $basename) = ($1,$2); 456 457 # Is the installation set already present in ext_sources/ ? 458 my $need_download = 0; 459 my $ext_sources_filename = File::Spec->catfile( 460 $ENV{'TARFILE_LOCATION'}, 461 $basename); 462 if ( ! -f $ext_sources_filename) 463 { 464 $installer::logger::Info->printf("download set is not in ext_sources/ (%s)\n", $ext_sources_filename); 465 $need_download = 1; 466 } 467 else 468 { 469 $installer::logger::Info->printf("download set exists at '%s'\n", $ext_sources_filename); 470 if (defined $release_item->{'checksum-value'} 471 && $release_item->{'checksum-type'} eq 'sha256') 472 { 473 $installer::logger::Info->printf("checking SHA256 checksum\n"); 474 my $digest = Digest->new("SHA-256"); 475 open my $in, "<", $ext_sources_filename; 476 $digest->addfile($in); 477 close $in; 478 if ($digest->hexdigest() ne $release_item->{'checksum-value'}) 479 { 480 $installer::logger::Info->printf(" mismatch\n", $ext_sources_filename); 481 $need_download = 1; 482 } 483 else 484 { 485 $installer::logger::Info->printf(" match\n"); 486 } 487 } 488 } 489 490 if ($need_download) 491 { 492 if ( ! installer::patch::InstallationSet::Download( 493 $language, 494 $release_item, 495 $ext_sources_filename)) 496 { 497 return 0; 498 } 499 if ( ! -f $ext_sources_filename) 500 { 501 $installer::logger::Info->printf("download set could not be downloaded\n"); 502 return 0; 503 } 504 } 505 506 return $ext_sources_filename; 507} 508 509 510 511 512sub ProvideUnpackedExe ($$$$$) 513{ 514 my ($version, $is_current_version, $language, $package_format, $product_name) = @_; 515 516 # Check if the exe has already been unpacked. 517 my $unpacked_exe_path = installer::patch::InstallationSet::GetUnpackedExePath( 518 $version, 519 $is_current_version, 520 $language, 521 $package_format, 522 $product_name); 523 my $unpacked_exe_flag_filename = File::Spec->catfile($unpacked_exe_path, "__exe_is_unpacked"); 524 my $exe_is_unpacked = -f $unpacked_exe_flag_filename; 525 526 if ($exe_is_unpacked) 527 { 528 # Yes, exe has already been unpacked. There is nothing more to do. 529 $installer::logger::Info->printf("downloadable installation set has already been unpacked to\n"); 530 $installer::logger::Info->printf(" %s\n", $unpacked_exe_path); 531 return 1; 532 } 533 elsif ($is_current_version) 534 { 535 # For the current version the exe is created from the unpacked 536 # content and both are expected to be already present. 537 538 # In order to have the .cab and its unpacked content in one 539 # directory and don't interfere with the creation of regular 540 # installation sets, we copy the unpacked .exe into a separate 541 # directory. 542 543 my $original_path = File::Spec->catfile( 544 $ENV{'SRC_ROOT'}, 545 "instsetoo_native", 546 $ENV{'INPATH'}, 547 $product_name, 548 $package_format, 549 "install", 550 $language); 551 $installer::logger::Info->printf("creating a copy\n"); 552 $installer::logger::Info->printf(" of %s\n", $original_path); 553 $installer::logger::Info->printf(" at %s\n", $unpacked_exe_path); 554 File::Path::make_path($unpacked_exe_path) unless -d $unpacked_exe_path; 555 my ($file_count,$directory_count) = CopyRecursive($original_path, $unpacked_exe_path); 556 return 0 if ( ! defined $file_count); 557 $installer::logger::Info->printf(" copied %d files in %d directories\n", 558 $file_count, 559 $directory_count); 560 561 touch($unpacked_exe_flag_filename); 562 563 return 1; 564 } 565 else 566 { 567 # No, we have to unpack the exe. 568 569 # Provide the exe. 570 my $filename = installer::patch::InstallationSet::ProvideDownloadSet( 571 $version, 572 $language, 573 $package_format); 574 575 # Unpack it. 576 if (defined $filename) 577 { 578 if (installer::patch::InstallationSet::UnpackExe($filename, $unpacked_exe_path)) 579 { 580 $installer::logger::Info->printf("downloadable installation set has been unpacked to\n"); 581 $installer::logger::Info->printf(" %s\n", $unpacked_exe_path); 582 583 touch($unpacked_exe_flag_filename); 584 585 return 1; 586 } 587 } 588 else 589 { 590 installer::logger::PrintError("could not provide .exe installation set at '%s'\n", $filename); 591 } 592 } 593 594 return 0; 595} 596 597 598 599 600sub CopyRecursive ($$) 601{ 602 my ($source_path, $destination_path) = @_; 603 604 return (undef,undef) unless -d $source_path; 605 606 my @todo = ([$source_path, $destination_path]); 607 my $file_count = 0; 608 my $directory_count = 0; 609 while (scalar @todo > 0) 610 { 611 my ($source,$destination) = @{shift @todo}; 612 613 next if ! -d $source; 614 File::Path::make_path($destination); 615 ++$directory_count; 616 617 # Read list of files in the current source directory. 618 opendir( my $dir, $source); 619 my @files = readdir $dir; 620 closedir $dir; 621 622 # Copy all files and push all directories to @todo. 623 foreach my $file (@files) 624 { 625 next if $file =~ /^\.+$/; 626 627 my $source_file = File::Spec->catfile($source, $file); 628 my $destination_file = File::Spec->catfile($destination, $file); 629 if ( -f $source_file) 630 { 631 File::Copy::copy($source_file, $destination_file); 632 ++$file_count; 633 } 634 elsif ( -d $source_file) 635 { 636 push @todo, [$source_file, $destination_file]; 637 } 638 } 639 } 640 641 return ($file_count, $directory_count); 642} 643 644 645 646 647sub CheckLocalCopy ($$$$) 648{ 649 my ($version, $language, $package_format, $product_name) = @_; 650 651 # Compare creation times of the original .msi and its copy. 652 653 my $original_path = File::Spec->catfile( 654 $ENV{'SRC_ROOT'}, 655 "instsetoo_native", 656 $ENV{'INPATH'}, 657 $product_name, 658 $package_format, 659 "install", 660 $language); 661 662 my $copy_path = installer::patch::InstallationSet::GetUnpackedExePath( 663 $version, 664 1, 665 $language, 666 $package_format, 667 $product_name); 668 669 my $msi_basename = "openoffice" 670 . installer::patch::Version::ArrayToNoDotName( 671 installer::patch::Version::StringToNumberArray($version)) 672 . ".msi"; 673 674 my $original_msi_filename = File::Spec->catfile($original_path, $msi_basename); 675 my $copied_msi_filename = File::Spec->catfile($copy_path, $msi_basename); 676 677 my @original_msi_stats = stat($original_msi_filename); 678 my @copied_msi_stats = stat($copied_msi_filename); 679 my $original_msi_mtime = $original_msi_stats[9]; 680 my $copied_msi_mtime = $copied_msi_stats[9]; 681 682 if (defined $original_msi_mtime 683 && defined $copied_msi_mtime 684 && $original_msi_mtime > $copied_msi_mtime) 685 { 686 # The installation set is newer than its copy. 687 # Remove the copy. 688 $installer::logger::Info->printf( 689 "removing copy of installation set (version %s) because it is out of date\n", 690 $version); 691 File::Path::remove_tree($copy_path); 692 } 693} 694 695 696 697 698=head2 ProvideUnpackedCab 699 700 1a. Make sure that a downloadable installation set is present. 701 1b. or that a freshly built installation set (packed and unpacked is present) 702 2. Unpack the downloadable installation set 703 3. Unpack the .cab file. 704 705 The 'Provide' in the function name means that any step that has 706 already been made is not made again. 707 708=cut 709sub ProvideUnpackedCab ($$$$$) 710{ 711 my ($version, $is_current_version, $language, $package_format, $product_name) = @_; 712 713 if ($is_current_version) 714 { 715 # For creating patches we maintain a copy of the unpacked .exe. Make sure that that is updated when 716 # a new installation set has been built. 717 CheckLocalCopy($version, $language, $package_format, $product_name); 718 } 719 720 # Check if the cab file has already been unpacked. 721 my $unpacked_cab_path = installer::patch::InstallationSet::GetUnpackedCabPath( 722 $version, 723 $is_current_version, 724 $language, 725 $package_format, 726 $product_name); 727 my $unpacked_cab_flag_filename = File::Spec->catfile($unpacked_cab_path, "__cab_is_unpacked"); 728 my $cab_is_unpacked = -f $unpacked_cab_flag_filename; 729 730 if ($cab_is_unpacked) 731 { 732 # Yes. Cab was already unpacked. There is nothing more to do. 733 $installer::logger::Info->printf("cab has already been unpacked to\n"); 734 $installer::logger::Info->printf(" %s\n", $unpacked_cab_path); 735 736 return 1; 737 } 738 else 739 { 740 # Make sure that the exe is unpacked and the cab file exists. 741 ProvideUnpackedExe($version, $is_current_version, $language, $package_format, $product_name); 742 743 # Unpack the cab file. 744 my $unpacked_exe_path = installer::patch::InstallationSet::GetUnpackedExePath( 745 $version, 746 $is_current_version, 747 $language, 748 $package_format, 749 $product_name); 750 my $msi = new installer::patch::Msi( 751 installer::patch::InstallationSet::GetMsiFilename($unpacked_exe_path, $version), 752 $version, 753 $is_current_version, 754 $language, 755 $product_name); 756 757 my $cab_filename = installer::patch::InstallationSet::GetCabFilename( 758 $unpacked_exe_path, 759 $version); 760 if ( ! -f $cab_filename) 761 { 762 # Cab file does not exist. 763 installer::logger::PrintError( 764 "could not find .cab file at '%s'. Extraction of .exe seems to have failed.\n", 765 $cab_filename); 766 return 0; 767 } 768 769 if (installer::patch::InstallationSet::UnpackCab( 770 $cab_filename, 771 $msi, 772 $unpacked_cab_path)) 773 { 774 $installer::logger::Info->printf("unpacked cab file '%s'\n", $cab_filename); 775 $installer::logger::Info->printf(" to '%s'\n", $unpacked_cab_path); 776 777 touch($unpacked_cab_flag_filename); 778 779 return 1; 780 } 781 else 782 { 783 return 0; 784 } 785 } 786} 7871; 788