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