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::Msi;
23
24use installer::patch::MsiTable;
25use installer::patch::Tools;
26use installer::patch::InstallationSet;
27
28use File::Basename;
29use File::Copy;
30
31use strict;
32
33
34=head1 NAME
35
36    package installer::patch::Msi - Class represents a single MSI file and gives access to its tables.
37
38=cut
39
40sub FindAndCreate($$$$$)
41{
42    my ($class, $version, $is_current_version, $language, $product_name) = @_;
43
44    my $condensed_version = $version;
45    $condensed_version =~ s/\.//g;
46
47    # When $version is the current version we have to search the msi at a different place.
48    my $path;
49    my $filename;
50    my $is_current = 0;
51    $path = installer::patch::InstallationSet::GetUnpackedExePath(
52        $version,
53        $is_current_version,
54        installer::languages::get_normalized_language($language),
55        "msi",
56        $product_name);
57
58    # Find the msi in the path.ls .
59    $filename = File::Spec->catfile($path, "openoffice".$condensed_version.".msi");
60    $is_current = $is_current_version;
61
62    return $class->new($filename, $version, $is_current, $language, $product_name);
63}
64
65
66
67
68
69
70=head2 new($class, $filename, $version, $is_current_version, $language, $product_name)
71
72    Create a new object of the Msi class.  The values of $version, $language, and $product_name define
73    where to look for the msi file.
74
75    If construction fails then IsValid() will return false.
76
77=cut
78
79sub new ($$$$$$)
80{
81    my ($class, $filename, $version, $is_current_version, $language, $product_name) = @_;
82
83    if ( ! -f $filename)
84    {
85        installer::logger::PrintError("can not find the .msi file for version %s and language %s at '%s'\n",
86            $version,
87            $language,
88            $filename);
89        return undef;
90    }
91
92    my $self = {
93        'filename' => $filename,
94        'path' => dirname($filename),
95        'version' => $version,
96        'is_current_version' => $is_current_version,
97        'language' => $language,
98        'package_format' => "msi",
99        'product_name' => $product_name,
100        'tmpdir' => File::Temp->newdir(CLEANUP => 1),
101        'is_valid' => -f $filename
102    };
103    bless($self, $class);
104
105    return $self;
106}
107
108
109
110
111sub IsValid ($)
112{
113    my ($self) = @_;
114
115    return $self->{'is_valid'};
116}
117
118
119
120
121=head2 Commit($self)
122
123    Write all modified tables back into the databse.
124
125=cut
126
127sub Commit ($)
128{
129    my $self = shift;
130
131    my @tables_to_update = ();
132    foreach my $table (values %{$self->{'tables'}})
133    {
134        push @tables_to_update,$table if ($table->IsModified());
135    }
136
137    if (scalar @tables_to_update > 0)
138    {
139        $installer::logger::Info->printf("writing modified tables to database:\n");
140        foreach my $table (@tables_to_update)
141        {
142            $installer::logger::Info->printf("    %s\n", $table->GetName());
143            $self->PutTable($table);
144        }
145
146        foreach my $table (@tables_to_update)
147        {
148            $table->UpdateTimestamp();
149            $table->MarkAsUnmodified();
150        }
151    }
152}
153
154
155
156
157=head2 GetTable($seld, $table_name)
158
159    Return an MsiTable object for $table_name.  Table objects are kept
160    alive for the life time of the Msi object.  Therefore the second
161    call for the same table is very cheap.
162
163=cut
164
165sub GetTable ($$)
166{
167    my ($self, $table_name) = @_;
168
169    my $table = $self->{'tables'}->{$table_name};
170    if ( ! defined $table)
171    {
172        my $table_filename = File::Spec->catfile($self->{'tmpdir'}, $table_name .".idt");
173        if ( ! -f $table_filename
174            || ! EnsureAYoungerThanB($table_filename, $self->{'fullname'}))
175        {
176            # Extract table from database to text file on disk.
177            my $truncated_table_name = length($table_name)>8 ? substr($table_name,0,8) : $table_name;
178            my $command = join(" ",
179                "msidb.exe",
180                "-d", installer::patch::Tools::ToEscapedWindowsPath($self->{'filename'}),
181                "-f", installer::patch::Tools::ToEscapedWindowsPath($self->{'tmpdir'}),
182                "-e", $table_name);
183            my $result = qx($command);
184            print $result;
185        }
186
187        # Read table into memory.
188        $table = new installer::patch::MsiTable($table_filename, $table_name);
189        $self->{'tables'}->{$table_name} = $table;
190    }
191
192    return $table;
193}
194
195
196
197
198=head2 PutTable($self, $table)
199
200    Write the given table back to the databse.
201
202=cut
203
204sub PutTable ($$)
205{
206    my ($self, $table) = @_;
207
208    # Create text file from the current table content.
209    $table->WriteFile();
210
211    my $table_name = $table->GetName();
212
213    # Store table from text file into database.
214    my $table_filename = $table->{'filename'};
215
216    if (length($table_name) > 8)
217    {
218        # The file name of the table data must not be longer than 8 characters (not counting the extension).
219        # The name passed as argument to the -i option may be longer.
220        my $truncated_table_name = substr($table_name,0,8);
221        my $table_truncated_filename = File::Spec->catfile(
222            dirname($table_filename),
223            $truncated_table_name.".idt");
224        File::Copy::copy($table_filename, $table_truncated_filename) || die("can not create table file with short name");
225    }
226
227    my $command = join(" ",
228        "msidb.exe",
229        "-d", installer::patch::Tools::ToEscapedWindowsPath($self->{'filename'}),
230        "-f", installer::patch::Tools::ToEscapedWindowsPath($self->{'tmpdir'}),
231        "-i", $table_name);
232    my $result = system($command);
233
234    if ($result != 0)
235    {
236        installer::logger::PrintError("writing table '%s' back to database failed", $table_name);
237        # For error messages see http://msdn.microsoft.com/en-us/library/windows/desktop/aa372835%28v=vs.85%29.aspx
238    }
239}
240
241
242
243
244=head2 EnsureAYoungerThanB ($filename_a, $filename_b)
245
246    Internal function (not a method) that compares to files according
247    to their last modification times (mtime).
248
249=cut
250
251sub EnsureAYoungerThanB ($$)
252{
253    my ($filename_a, $filename_b) = @_;
254
255    die("file $filename_a does not exist") unless -f $filename_a;
256    die("file $filename_b does not exist") unless -f $filename_b;
257
258    my @stat_a = stat($filename_a);
259    my @stat_b = stat($filename_b);
260
261    if ($stat_a[9] <= $stat_b[9])
262    {
263        return 0;
264    }
265    else
266    {
267        return 1;
268    }
269}
270
271
272
273
274=head2 SplitLongShortName($name)
275
276    Split $name (typically from the 'FileName' column in the 'File'
277    table or 'DefaultDir' column in the 'Directory' table) at the '|'
278    into short (8.3) and long names.  If there is no '|' in $name then
279    $name is returned as both short and long name.
280
281    Returns long and short name (in this order) as array.
282
283=cut
284
285sub SplitLongShortName ($)
286{
287    my ($name) = @_;
288
289    if ($name =~ /^([^\|]*)\|(.*)$/)
290    {
291        return ($2,$1);
292    }
293    else
294    {
295        return ($name,$name);
296    }
297}
298
299
300
301=head2 SplitTargetSourceLongShortName ($name)
302
303    Split $name first at the ':' into target and source parts and each
304    of those at the '|'s into long and short parts.  Names that follow
305    this pattern come from the 'DefaultDir' column in the 'Directory'
306    table.
307
308=cut
309
310sub SplitTargetSourceLongShortName ($)
311{
312    my ($name) = @_;
313
314    if ($name =~ /^([^:]*):(.*)$/)
315    {
316        return (installer::patch::Msi::SplitLongShortName($1), installer::patch::Msi::SplitLongShortName($2));
317    }
318    else
319    {
320        my ($long,$short) = installer::patch::Msi::SplitLongShortName($name);
321        return ($long,$short,$long,$short);
322    }
323}
324
325
326=head2 GetDirectoryMap($self)
327
328    Return a map that maps directory unique names (column 'Directory' in table 'Directory')
329    to hashes that contains short and long source and target names.
330
331=cut
332
333sub GetDirectoryMap ($)
334{
335    my ($self) = @_;
336
337    if (defined $self->{'DirectoryMap'})
338    {
339        return $self->{'DirectoryMap'};
340    }
341
342    my $directory_table = $self->GetTable("Directory");
343    my %dir_map = ();
344    foreach my $row (@{$directory_table->GetAllRows()})
345    {
346        my ($target_long_name, $target_short_name, $source_long_name, $source_short_name)
347            = installer::patch::Msi::SplitTargetSourceLongShortName($row->GetValue("DefaultDir"));
348        my $unique_name = $row->GetValue("Directory");
349        $dir_map{$unique_name} =
350        {
351            'unique_name' => $unique_name,
352            'parent' => $row->GetValue("Directory_Parent"),
353            'default_dir' => $row->GetValue("DefaultDir"),
354            'source_long_name' => $source_long_name,
355            'source_short_name' => $source_short_name,
356            'target_long_name' => $target_long_name,
357            'target_short_name' => $target_short_name
358        };
359    }
360
361    # Set up full names for all directories.
362    my @todo = map {$_} (keys %dir_map);
363    while (scalar @todo > 0)
364    {
365        my $key = shift @todo;
366        my $item = $dir_map{$key};
367        next if defined $item->{'full_source_name'};
368
369        if ($item->{'parent'} eq "")
370        {
371            # Directory has no parent => full names are the same as the name.
372            $item->{'full_source_long_name'} = $item->{'source_long_name'};
373            $item->{'full_source_short_name'} = $item->{'source_short_name'};
374            $item->{'full_target_long_name'} = $item->{'target_long_name'};
375            $item->{'full_target_short_name'} = $item->{'target_short_name'};
376        }
377        else
378        {
379            my $parent = $dir_map{$item->{'parent'}};
380            if ( defined $parent->{'full_source_long_name'})
381            {
382                # Parent aleady has full names => we can create the full name of the current item.
383                $item->{'full_source_long_name'}
384                    = $parent->{'full_source_long_name'} . "/" . $item->{'source_long_name'};
385                $item->{'full_source_short_name'}
386                    = $parent->{'full_source_short_name'} . "/" . $item->{'source_short_name'};
387                $item->{'full_target_long_name'}
388                    = $parent->{'full_target_long_name'} . "/" . $item->{'target_long_name'};
389                $item->{'full_target_short_name'}
390                    = $parent->{'full_target_short_name'} . "/" . $item->{'target_short_name'};
391            }
392            else
393            {
394                # Parent has to be processed before the current item can be processed.
395                # Push both to the head of the list.
396                unshift @todo, $key;
397                unshift @todo, $item->{'parent'};
398            }
399        }
400    }
401
402    # Postprocess the path names for cleanup.
403    foreach my $item (values %dir_map)
404    {
405        foreach my $id (
406            'full_source_long_name',
407            'full_source_short_name',
408            'full_target_long_name',
409            'full_target_short_name')
410        {
411            $item->{$id} =~ s/\/(\.\/)+/\//g;
412            $item->{$id} =~ s/^SourceDir\///;
413            $item->{$id} =~ s/^\.$//;
414        }
415    }
416
417    $self->{'DirectoryMap'} = \%dir_map;
418    return $self->{'DirectoryMap'};
419}
420
421
422
423
424=head2 GetFileMap ($)
425
426    Return a map (hash) that maps the unique name (column 'File' in
427    the 'File' table) to data that is associated with that file, like
428    the directory or component.
429
430    The map is kept alive for the lifetime of the Msi object.  All
431    calls but the first are cheap.
432
433=cut
434
435sub GetFileMap ($)
436{
437    my ($self) = @_;
438
439    if (defined $self->{'FileMap'})
440    {
441        return $self->{'FileMap'};
442    }
443
444    my $file_table = $self->GetTable("File");
445    my $component_table = $self->GetTable("Component");
446    my $dir_map = $self->GetDirectoryMap();
447
448    # Setup a map from component names to directory items.
449    my %component_to_directory_map =
450        map
451        {$_->GetValue('Component') => $_->GetValue('Directory_')}
452        @{$component_table->GetAllRows()};
453
454    # Finally, create the map from files to directories.
455    my $file_map = {};
456    my $file_component_index = $file_table->GetColumnIndex("Component_");
457    my $file_file_index = $file_table->GetColumnIndex("File");
458    foreach my $file_row (@{$file_table->GetAllRows()})
459    {
460        my $component_name = $file_row->GetValue($file_component_index);
461        my $directory_name = $component_to_directory_map{$component_name};
462        my $unique_name = $file_row->GetValue($file_file_index);
463        $file_map->{$unique_name} = {
464            'directory' => $dir_map->{$directory_name},
465            'component_name' => $component_name
466        };
467    }
468
469    $self->{'FileMap'} = $file_map;
470    return $file_map;
471}
472
473
4741;
475