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