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