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