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 strict;
27
28
29=head1 NAME
30
31    package installer::patch::Msi - Class represents a single MSI file and gives access to its tables.
32
33=cut
34
35
36
37=head2 new($class, $version, $language, $product_name)
38
39    Create a new object of the Msi class.  The values of $version, $language, and $product_name define
40    where to look for the msi file.
41
42    If construction fails then IsValid() will return false.
43
44=cut
45sub new ($$$$)
46{
47    my ($class, $version, $language, $product_name) = @_;
48
49    my $path = installer::patch::InstallationSet::GetUnpackedMsiPath(
50        $version,
51        $language,
52        "msi",
53        $product_name);
54
55    # Find the msi in the path.
56    my $filename = undef;
57    if ( -d $path)
58    {
59        my @msi_files = glob(File::Spec->catfile($path, "*.msi"));
60        if (scalar @msi_files != 1)
61        {
62            printf STDERR ("there are %d msi files in %s, should be 1", scalar @msi_files, $filename);
63            $filename = "";
64        }
65        else
66        {
67            $filename = $msi_files[0];
68        }
69    }
70    else
71    {
72        installer::logger::PrintError("can not access path '%s' to find msi\n", $path);
73        return undef;
74    }
75
76    if ( ! -f $filename)
77    {
78        installer::logger::PrintError("can not access MSI file at '%s'\n", $filename);
79        return undef;
80    }
81
82    my $self = {
83        'filename' => $filename,
84        'path' => $path,
85        'version' => $version,
86        'language' => $language,
87        'package_format' => "msi",
88        'product_name' => $product_name,
89        'tmpdir' => File::Temp->newdir(CLEANUP => 1),
90        'is_valid' => -f $filename
91    };
92    bless($self, $class);
93
94    return $self;
95}
96
97
98
99
100sub IsValid ($)
101{
102    my ($self) = @_;
103
104    return $self->{'is_valid'};
105}
106
107
108
109
110=head2 GetTable($seld, $table_name)
111
112    Return an MsiTable object for $table_name.  Table objects are kept
113    alive for the life time of the Msi object.  Therefore the second
114    call for the same table is very cheap.
115
116=cut
117sub GetTable ($$)
118{
119    my ($self, $table_name) = @_;
120
121    my $table = $self->{'tables'}->{$table_name};
122    if ( ! defined $table)
123    {
124        my $table_filename = File::Spec->catfile($self->{'tmpdir'}, $table_name .".idt");
125        if ( ! -f $table_filename
126            || ! EnsureAYoungerThanB($table_filename, $self->{'fullname'}))
127        {
128            # Extract table from database to text file on disk.
129            my $truncated_table_name = length($table_name)>8 ? substr($table_name,0,8) : $table_name;
130            my $command = join(" ",
131                "msidb.exe",
132                "-d", installer::patch::Tools::CygpathToWindows($self->{'filename'}),
133                "-f", installer::patch::Tools::CygpathToWindows($self->{'tmpdir'}),
134                "-e", $table_name);
135            my $result = qx($command);
136            print $result;
137        }
138
139        # Read table into memory.
140        $table = new installer::patch::MsiTable($table_filename, $table_name);
141        $self->{'tables'}->{$table_name} = $table;
142    }
143
144    return $table;
145}
146
147
148
149
150=head2 EnsureAYoungerThanB ($filename_a, $filename_b)
151
152    Internal function (not a method) that compares to files according
153    to their last modification times (mtime).
154
155=cut
156sub EnsureAYoungerThanB ($$)
157{
158    my ($filename_a, $filename_b) = @_;
159
160    die("file $filename_a does not exist") unless -f $filename_a;
161    die("file $filename_b does not exist") unless -f $filename_b;
162
163    my @stat_a = stat($filename_a);
164    my @stat_b = stat($filename_b);
165
166    if ($stat_a[9] <= $stat_b[9])
167    {
168        return 0;
169    }
170    else
171    {
172        return 1;
173    }
174}
175
176
177
178
179=head2 SplitLongShortName($name)
180
181    Split $name (typically from the 'FileName' column in the 'File'
182    table or 'DefaultDir' column in the 'Directory' table) at the '|'
183    into short (8.3) and long names.  If there is no '|' in $name then
184    $name is returned as both short and long name.
185
186    Returns long and short name (in this order) as array.
187
188=cut
189sub SplitLongShortName ($)
190{
191    my ($name) = @_;
192
193    if ($name =~ /^([^\|]*)\|(.*)$/)
194    {
195        return ($2,$1);
196    }
197    else
198    {
199        return ($name,$name);
200    }
201}
202
203
204
205=head2 SplitTargetSourceLongShortName ($name)
206
207    Split $name first at the ':' into target and source parts and each
208    of those at the '|'s into long and short parts.  Names that follow
209    this pattern come from the 'DefaultDir' column in the 'Directory'
210    table.
211
212=cut
213sub SplitTargetSourceLongShortName ($)
214{
215    my ($name) = @_;
216
217    if ($name =~ /^([^:]*):(.*)$/)
218    {
219        return (installer::patch::Msi::SplitLongShortName($1), installer::patch::Msi::SplitLongShortName($2));
220    }
221    else
222    {
223        my ($long,$short) = installer::patch::Msi::SplitLongShortName($name);
224        return ($long,$short,$long,$short);
225    }
226}
227
228
229
230
231=head2 GetFileToDirectoryMap ($)
232
233    Return a map (hash) that maps the unique name (column 'File' in
234    the 'File' table) to its directory names.  Each value is a
235    reference to an array of two elements: the source path and the
236    target path.
237
238    The map is kept alive for the lifetime of the Msi object.  All
239    calls but the first are cheap.
240
241=cut
242sub GetFileToDirectoryMap ($)
243{
244    my ($self) = @_;
245
246    if (defined $self->{'FileToDirectoryMap'})
247    {
248        return $self->{'FileToDirectoryMap'};
249    }
250
251    my $file_table = $self->GetTable("File");
252    my $directory_table = $self->GetTable("Directory");
253    my $component_table = $self->GetTable("Component");
254    $installer::logger::Info->printf("got access to tables File, Directory, Component\n");
255
256    my %dir_map = ();
257    foreach my $row (@{$directory_table->GetAllRows()})
258    {
259        my ($target_name, undef, $source_name, undef)
260            = installer::patch::Msi::SplitTargetSourceLongShortName($row->GetValue("DefaultDir"));
261        $dir_map{$row->GetValue("Directory")} = {
262            'parent' => $row->GetValue("Directory_Parent"),
263            'source_name' => $source_name,
264            'target_name' => $target_name};
265    }
266
267    # Set up full names for all directories.
268    my @todo = map {$_} (keys %dir_map);
269    my $process_count = 0;
270    my $push_count = 0;
271    while (scalar @todo > 0)
272    {
273        ++$process_count;
274
275        my $key = shift @todo;
276        my $item = $dir_map{$key};
277        next if defined $item->{'full_source_name'};
278
279        if ($item->{'parent'} eq "")
280        {
281            # Directory has no parent => full names are the same as the name.
282            $item->{'full_source_name'} = $item->{'source_name'};
283            $item->{'full_target_name'} = $item->{'target_name'};
284        }
285        else
286        {
287            my $parent = $dir_map{$item->{'parent'}};
288            if ( defined $parent->{'full_source_name'})
289            {
290                # Parent aleady has full names => we can create the full name of the current item.
291                $item->{'full_source_name'} = $parent->{'full_source_name'} . "/" . $item->{'source_name'};
292                $item->{'full_target_name'} = $parent->{'full_target_name'} . "/" . $item->{'target_name'};
293            }
294            else
295            {
296                # Parent has to be processed before the current item can be processed.
297                # Push both to the head of the list.
298                unshift @todo, $key;
299                unshift @todo, $item->{'parent'};
300
301                ++$push_count;
302            }
303        }
304    }
305
306    foreach my $key (keys %dir_map)
307    {
308        $dir_map{$key}->{'full_source_name'} =~ s/\/(\.\/)+/\//g;
309        $dir_map{$key}->{'full_source_name'} =~ s/^SourceDir\///;
310        $dir_map{$key}->{'full_target_name'} =~ s/\/(\.\/)+/\//g;
311        $dir_map{$key}->{'full_target_name'} =~ s/^SourceDir\///;
312    }
313    $installer::logger::Info->printf("for %d directories there where %d processing steps and %d pushes\n",
314        $directory_table->GetRowCount(),
315        $process_count,
316        $push_count);
317
318    # Setup a map from component names to directory items.
319    my %component_to_directory_map = map {$_->GetValue('Component') => $_->GetValue('Directory_')} @{$component_table->GetAllRows()};
320
321    # Finally, create the map from files to directories.
322    my $map = {};
323    my $file_component_index = $file_table->GetColumnIndex("Component_");
324    my $file_file_index = $file_table->GetColumnIndex("File");
325    foreach my $file_row (@{$file_table->GetAllRows()})
326    {
327        my $component_name = $file_row->GetValue($file_component_index);
328        my $directory_name = $component_to_directory_map{$component_name};
329        my $dir_item = $dir_map{$directory_name};
330        my $unique_name = $file_row->GetValue($file_file_index);
331        $map->{$unique_name} = [$dir_item->{'full_source_name'},$dir_item->{'full_target_name'}];
332    }
333
334    $installer::logger::Info->printf("got full paths for %d files\n",
335        $file_table->GetRowCount());
336
337    $self->{'FileToDirectoryMap'} = $map;
338    return $map;
339}
340
341
3421;
343