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 28 29my $Unpacker = "/c/Program\\ Files/7-Zip/7z.exe"; 30 31=head1 NAME 32 33 package installer::patch::InstallationSet - Functions for handling installation sets 34 35=head1 DESCRIPTION 36 37 This package contains functions for unpacking the .exe files that 38 are created by the NSIS installer creator and the .cab files in 39 the installation sets. 40 41=cut 42 43sub UnpackExe ($$) 44{ 45 my ($filename, $destination_path) = @_; 46 47 $installer::logger::Info->printf("unpacking installation set to '%s'\n", $destination_path); 48 49 # Unpack to a temporary path and change its name to the destination path 50 # only when the unpacking has completed successfully. 51 my $temporary_destination_path = $destination_path . ".tmp"; 52 File::Path::make_path($temporary_destination_path); 53 54 my $windows_filename = installer::patch::Tools::CygpathToWindows($filename); 55 my $windows_destination_path = installer::patch::Tools::CygpathToWindows($temporary_destination_path); 56 my $command = join(" ", 57 $Unpacker, 58 "x", "-o".$windows_destination_path, 59 $windows_filename); 60 my $result = qx($command); 61 62 # Check the existence of the .cab files. 63 my $cab_filename = File::Spec->catfile($temporary_destination_path, "openoffice1.cab"); 64 if ( ! -f $cab_filename) 65 { 66 installer::logger::PrintError("cab file '%s' was not extracted from installation set\n", $cab_filename); 67 return 0; 68 } 69 if (rename($temporary_destination_path, $destination_path) == 0) 70 { 71 installer::logger::PrintError("can not rename temporary extraction directory\n"); 72 return 0; 73 } 74 return 1; 75} 76 77 78 79 80=head2 UnpackCab($cab_filename, $destination_path) 81 82 Unpacking the cabinet file inside an .exe installation set is a 83 three step process because there is no directory information stored 84 inside the cab file. This has to be taken from the 'File' and 85 'Directory' tables in the .msi file. 86 87 1. Setup the directory structure of all files in the cab from the 'File' and 'Directory' tables in the msi. 88 89 2. Unpack the cab file. 90 91 3. Move the files to their destination directories. 92 93=cut 94sub UnpackCab ($$$) 95{ 96 my ($cab_filename, $msi, $destination_path) = @_; 97 98 # Step 1 99 # Extract the directory structure from the 'File' and 'Directory' tables in the given msi. 100 $installer::logger::Info->printf("setting up directory tree\n"); 101 my $file_table = $msi->GetTable("File"); 102 my $file_to_directory_map = $msi->GetFileToDirectoryMap(); 103 104 # Step 2 105 # Unpack the .cab file to a temporary path. 106 my $temporary_destination_path = $destination_path . ".tmp"; 107 if ( -d $temporary_destination_path) 108 { 109 # Temporary directory already exists => cab file has already been unpacked (flat), nothing to do. 110 $installer::logger::Info->printf("cab file has already been unpacked to flat structure\n"); 111 } 112 else 113 { 114 UnpackCabFlat($cab_filename, $temporary_destination_path, $file_table); 115 } 116 117 # Step 3 118 # Move the files to their destinations. 119 File::Path::make_path($destination_path); 120 $installer::logger::Info->printf("moving files to their directories\n"); 121 my $count = 0; 122 foreach my $file_row (@{$file_table->GetAllRows()}) 123 { 124 my $unique_name = $file_row->GetValue('File'); 125 my $directory_full_names = $file_to_directory_map->{$unique_name}; 126 my ($source_full_name, $target_full_name) = @$directory_full_names; 127 128 my $flat_filename = File::Spec->catfile($temporary_destination_path, $unique_name); 129 my $dir_path = File::Spec->catfile($destination_path, $source_full_name); 130 my $dir_filename = File::Spec->catfile($dir_path, $unique_name); 131 132 printf("%d: making path %s and copying %s to %s\n", 133 $count, 134 $dir_path, 135 $unique_name, 136 $dir_filename); 137 File::Path::make_path($dir_path); 138 File::Copy::move($flat_filename, $dir_filename); 139 140 ++$count; 141 } 142 143 # Cleanup. Remove the temporary directory. It should be empty by now. 144 rmdir($temporary_destination_path); 145} 146 147 148 149 150=head2 UnpackCabFlat ($cab_filename, $destination_path, $file_table) 151 152 Unpack the flat file structure of the $cab_filename to $destination_path. 153 154 In order to detect and handle an incomplete (arborted) previous 155 extraction, the cab file is unpacked to a temprorary directory 156 that after successful extraction is renamed to $destination_path. 157 158=cut 159sub UnpackCabFlat ($$$) 160{ 161 my ($cab_filename, $destination_path, $file_table) = @_; 162 163 # Unpack the .cab file to a temporary path (note that 164 # $destination_path may alreay bee a temporary path). Using a 165 # second one prevents the lengthy flat unpacking to be repeated 166 # when another step fails. 167 168 $installer::logger::Info->printf("unpacking cab file\n"); 169 my $temporary_destination_path = $destination_path . ".tmp"; 170 File::Path::make_path($temporary_destination_path); 171 my $windows_cab_filename = installer::patch::Tools::CygpathToWindows($cab_filename); 172 my $windows_destination_path = installer::patch::Tools::CygpathToWindows($temporary_destination_path); 173 my $command = join(" ", 174 $Unpacker, 175 "x", "-o".$windows_destination_path, 176 $windows_cab_filename, 177 "-y"); 178 printf("running command '%s'\n", $command); 179 open my $cmd, $command."|"; 180 my $extraction_count = 0; 181 my $file_count = $file_table->GetRowCount(); 182 while (<$cmd>) 183 { 184 my $message = $_; 185 chomp($message); 186 ++$extraction_count; 187 printf("%4d/%4d %3.2f%% \r", 188 $extraction_count, 189 $file_count, 190 $extraction_count*100/$file_count); 191 } 192 close $cmd; 193 printf("extraction done \n"); 194 195 rename($temporary_destination_path, $destination_path) 196 || installer::logger::PrintError( 197 "can not rename the temporary directory '%s' to '%s'\n", 198 $temporary_destination_path, 199 $destination_path); 200} 201 202 203 204 205=head GetUnpackedMsiPath ($version, $language, $package_format, $product) 206 207 Convenience function that returns where a downloadable installation set is extracted to. 208 209=cut 210sub GetUnpackedMsiPath ($$$$) 211{ 212 my ($version, $language, $package_format, $product) = @_; 213 214 return File::Spec->catfile( 215 GetUnpackedPath($version, $language, $package_format, $product), 216 "unpacked_msi"); 217} 218 219 220 221 222=head GetUnpackedCabPath ($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, $language, $package_format, $product) = @_; 231 232 return File::Spec->catfile( 233 GetUnpackedPath($version, $language, $package_format, $product), 234 "unpacked_cab"); 235} 236 237 238 239 240=head2 GetUnpackedPath($version, $language, $package_format, $product) 241 242 Internal function for creating paths to where archives are unpacked. 243 244=cut 245sub GetUnpackedPath ($$$$) 246{ 247 my ($version, $language, $package_format, $product) = @_; 248 249 return File::Spec->catfile( 250 $ENV{'SRC_ROOT'}, 251 "instsetoo_native", 252 $ENV{'INPATH'}, 253 $product, 254 $package_format, 255 installer::patch::Version::ArrayToDirectoryName(installer::patch::Version::StringToNumberArray($version)), 256 $language); 257} 258 259 260 261 262=head2 Download($language, $release_data, $filename) 263 264 Download an installation set to $filename. The URL for the 265 download is taken from $release_data, a snippet from the 266 instsetoo_native/data/releases.xml file. 267 268=cut 269sub Download ($$$) 270{ 271 my ($language, $release_data, $filename) = @_; 272 273 my $url = $release_data->{'URL'}; 274 $release_data->{'URL'} =~ /^(.*)\/([^\/]+)$/; 275 my ($location, $basename) = ($1,$2); 276 277 $installer::logger::Info->printf("downloading %s\n", $basename); 278 $installer::logger::Info->printf(" from '%s'\n", $location); 279 my $filesize = $release_data->{'file-size'}; 280 $installer::logger::Info->printf(" expected size is %d\n", $filesize); 281 my $temporary_filename = $filename . ".part"; 282 my $resume_size = 0; 283 if ( -f $temporary_filename) 284 { 285 $resume_size = -s $temporary_filename; 286 $installer::logger::Info->printf(" trying to resume at %d/%d bytes\n", $resume_size, $filesize); 287 } 288 289 # Prepare checksum. 290 my $checksum = undef; 291 my $checksum_type = $release_data->{'checksum-type'}; 292 my $checksum_value = $release_data->{'checksum-value'}; 293 my $digest = undef; 294 if ($checksum_type eq "sha256") 295 { 296 $digest = Digest->new("SHA-256"); 297 } 298 elsif ($checksum_type eq "md5") 299 { 300 $digest = Digest->new("md5"); 301 } 302 else 303 { 304 installer::logger::PrintError( 305 "checksum type %s is not supported. Supported checksum types are: sha256,md5\n", 306 $checksum_type); 307 return 0; 308 } 309 310 # Download the extension. 311 open my $out, ">>$temporary_filename"; 312 binmode($out); 313 314 my $mode = $|; 315 my $handle = select STDOUT; 316 $| = 1; 317 select $handle; 318 319 my $agent = LWP::UserAgent->new(); 320 $agent->timeout(120); 321 $agent->show_progress(0); 322 my $last_was_redirect = 0; 323 my $bytes_read = 0; 324 $agent->add_handler('response_redirect' 325 => sub{ 326 $last_was_redirect = 1; 327 return; 328 }); 329 $agent->add_handler('response_data' 330 => sub{ 331 if ($last_was_redirect) 332 { 333 $last_was_redirect = 0; 334 # Throw away the data we got so far. 335 $digest->reset(); 336 close $out; 337 open $out, ">$temporary_filename"; 338 binmode($out); 339 } 340 my($response,$agent,$h,$data)=@_; 341 print $out $data; 342 $digest->add($data); 343 $bytes_read += length($data); 344 printf("read %*d / %d %d%% \r", 345 length($filesize), 346 $bytes_read, 347 $filesize, 348 $bytes_read*100/$filesize); 349 }); 350 my $response; 351 if ($resume_size > 0) 352 { 353 $response = $agent->get($url, 'Range' => "bytes=$resume_size-"); 354 } 355 else 356 { 357 $response = $agent->get($url); 358 } 359 close $out; 360 361 $handle = select STDOUT; 362 $| = $mode; 363 select $handle; 364 365 $installer::logger::Info->print(" \r"); 366 367 if ($response->is_success()) 368 { 369 if ($digest->hexdigest() eq $checksum_value) 370 { 371 $installer::logger::Info->PrintInfo("download was successfull\n"); 372 if ( ! rename($temporary_filename, $filename)) 373 { 374 installer::logger::PrintError("can not rename '%s' to '%s'\n", $temporary_filename, $filename); 375 return 0; 376 } 377 else 378 { 379 return 1; 380 } 381 } 382 else 383 { 384 installer::logger::PrintError("%s checksum is wrong\n", $checksum_type); 385 return 0; 386 } 387 } 388 else 389 { 390 installer::logger::PrintError("there was a download error\n"); 391 return 0; 392 } 393} 394 395 396 397 398=head2 ProvideDownloadSet ($version, $language, $package_format) 399 400 Download an installation set when it is not yet present to 401 $ENV{'TARFILE_LOCATION'}. Verify the downloaded file with the 402 checksum that is extracted from the 403 instsetoo_native/data/releases.xml file. 404 405=cut 406sub ProvideDownloadSet ($$$) 407{ 408 my ($version, $language, $package_format) = @_; 409 410 my $release_item = installer::patch::ReleasesList::Instance()->{$version}->{$package_format}->{$language}; 411 412 # Get basename of installation set from URL. 413 $release_item->{'URL'} =~ /^(.*)\/([^\/]+)$/; 414 my ($location, $basename) = ($1,$2); 415 416 # Is the installation set already present in ext_sources/ ? 417 my $need_download = 0; 418 my $ext_sources_filename = File::Spec->catfile( 419 $ENV{'TARFILE_LOCATION'}, 420 $basename); 421 if ( ! -f $ext_sources_filename) 422 { 423 $installer::logger::Info->printf("download set is not in ext_sources/ (%s)\n", $ext_sources_filename); 424 $need_download = 1; 425 } 426 else 427 { 428 $installer::logger::Info->printf("download set exists at '%s'\n", $ext_sources_filename); 429 if ($release_item->{'checksum-type'} eq 'sha256') 430 { 431 $installer::logger::Info->printf("checking SHA256 checksum\n"); 432 my $digest = Digest->new("SHA-256"); 433 open my $in, "<", $ext_sources_filename; 434 $digest->addfile($in); 435 close $in; 436 if ($digest->hexdigest() ne $release_item->{'checksum-value'}) 437 { 438 $installer::logger::Info->printf(" mismatch\n", $ext_sources_filename); 439 $need_download = 1; 440 } 441 else 442 { 443 $installer::logger::Info->printf(" match\n"); 444 } 445 } 446 } 447 448 if ($need_download) 449 { 450 if ( ! installer::patch::InstallationSet::Download( 451 $language, 452 $release_item, 453 $ext_sources_filename)) 454 { 455 return 0; 456 } 457 if ( ! -f $ext_sources_filename) 458 { 459 $installer::logger::Info->printf("download set could not be downloaded\n"); 460 return 0; 461 } 462 } 463 464 return $ext_sources_filename; 465} 466 4671; 468