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