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