#************************************************************** # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # #************************************************************** package installer::patch::Msi; use installer::patch::MsiTable; use installer::patch::Tools; use strict; =head1 NAME package installer::patch::Msi - Class represents a single MSI file and gives access to its tables. =cut =head2 new($class, $version, $language, $product_name) Create a new object of the Msi class. The values of $version, $language, and $product_name define where to look for the msi file. If construction fails then IsValid() will return false. =cut sub new ($$$$) { my ($class, $version, $language, $product_name) = @_; my $path = installer::patch::InstallationSet::GetUnpackedMsiPath( $version, $language, "msi", $product_name); # Find the msi in the path. my $filename = undef; if ( -d $path) { my @msi_files = glob(File::Spec->catfile($path, "*.msi")); if (scalar @msi_files != 1) { printf STDERR ("there are %d msi files in %s, should be 1", scalar @msi_files, $filename); $filename = ""; } else { $filename = $msi_files[0]; } } else { installer::logger::PrintError("can not access path '%s' to find msi\n", $path); return undef; } if ( ! -f $filename) { installer::logger::PrintError("can not access MSI file at '%s'\n", $filename); return undef; } my $self = { 'filename' => $filename, 'path' => $path, 'version' => $version, 'language' => $language, 'package_format' => "msi", 'product_name' => $product_name, 'tmpdir' => File::Temp->newdir(CLEANUP => 1), 'is_valid' => -f $filename }; bless($self, $class); return $self; } sub IsValid ($) { my ($self) = @_; return $self->{'is_valid'}; } =head2 GetTable($seld, $table_name) Return an MsiTable object for $table_name. Table objects are kept alive for the life time of the Msi object. Therefore the second call for the same table is very cheap. =cut sub GetTable ($$) { my ($self, $table_name) = @_; my $table = $self->{'tables'}->{$table_name}; if ( ! defined $table) { my $table_filename = File::Spec->catfile($self->{'tmpdir'}, $table_name .".idt"); if ( ! -f $table_filename || ! EnsureAYoungerThanB($table_filename, $self->{'fullname'})) { # Extract table from database to text file on disk. my $truncated_table_name = length($table_name)>8 ? substr($table_name,0,8) : $table_name; my $command = join(" ", "msidb.exe", "-d", installer::patch::Tools::CygpathToWindows($self->{'filename'}), "-f", installer::patch::Tools::CygpathToWindows($self->{'tmpdir'}), "-e", $table_name); my $result = qx($command); print $result; } # Read table into memory. $table = new installer::patch::MsiTable($table_filename, $table_name); $self->{'tables'}->{$table_name} = $table; } return $table; } =head2 EnsureAYoungerThanB ($filename_a, $filename_b) Internal function (not a method) that compares to files according to their last modification times (mtime). =cut sub EnsureAYoungerThanB ($$) { my ($filename_a, $filename_b) = @_; die("file $filename_a does not exist") unless -f $filename_a; die("file $filename_b does not exist") unless -f $filename_b; my @stat_a = stat($filename_a); my @stat_b = stat($filename_b); if ($stat_a[9] <= $stat_b[9]) { return 0; } else { return 1; } } =head2 SplitLongShortName($name) Split $name (typically from the 'FileName' column in the 'File' table or 'DefaultDir' column in the 'Directory' table) at the '|' into short (8.3) and long names. If there is no '|' in $name then $name is returned as both short and long name. Returns long and short name (in this order) as array. =cut sub SplitLongShortName ($) { my ($name) = @_; if ($name =~ /^([^\|]*)\|(.*)$/) { return ($2,$1); } else { return ($name,$name); } } =head2 SplitTargetSourceLongShortName ($name) Split $name first at the ':' into target and source parts and each of those at the '|'s into long and short parts. Names that follow this pattern come from the 'DefaultDir' column in the 'Directory' table. =cut sub SplitTargetSourceLongShortName ($) { my ($name) = @_; if ($name =~ /^([^:]*):(.*)$/) { return (installer::patch::Msi::SplitLongShortName($1), installer::patch::Msi::SplitLongShortName($2)); } else { my ($long,$short) = installer::patch::Msi::SplitLongShortName($name); return ($long,$short,$long,$short); } } =head2 GetFileToDirectoryMap ($) Return a map (hash) that maps the unique name (column 'File' in the 'File' table) to its directory names. Each value is a reference to an array of two elements: the source path and the target path. The map is kept alive for the lifetime of the Msi object. All calls but the first are cheap. =cut sub GetFileToDirectoryMap ($) { my ($self) = @_; if (defined $self->{'FileToDirectoryMap'}) { return $self->{'FileToDirectoryMap'}; } my $file_table = $self->GetTable("File"); my $directory_table = $self->GetTable("Directory"); my $component_table = $self->GetTable("Component"); $installer::logger::Info->printf("got access to tables File, Directory, Component\n"); my %dir_map = (); foreach my $row (@{$directory_table->GetAllRows()}) { my ($target_name, undef, $source_name, undef) = installer::patch::Msi::SplitTargetSourceLongShortName($row->GetValue("DefaultDir")); $dir_map{$row->GetValue("Directory")} = { 'parent' => $row->GetValue("Directory_Parent"), 'source_name' => $source_name, 'target_name' => $target_name}; } # Set up full names for all directories. my @todo = map {$_} (keys %dir_map); my $process_count = 0; my $push_count = 0; while (scalar @todo > 0) { ++$process_count; my $key = shift @todo; my $item = $dir_map{$key}; next if defined $item->{'full_source_name'}; if ($item->{'parent'} eq "") { # Directory has no parent => full names are the same as the name. $item->{'full_source_name'} = $item->{'source_name'}; $item->{'full_target_name'} = $item->{'target_name'}; } else { my $parent = $dir_map{$item->{'parent'}}; if ( defined $parent->{'full_source_name'}) { # Parent aleady has full names => we can create the full name of the current item. $item->{'full_source_name'} = $parent->{'full_source_name'} . "/" . $item->{'source_name'}; $item->{'full_target_name'} = $parent->{'full_target_name'} . "/" . $item->{'target_name'}; } else { # Parent has to be processed before the current item can be processed. # Push both to the head of the list. unshift @todo, $key; unshift @todo, $item->{'parent'}; ++$push_count; } } } foreach my $key (keys %dir_map) { $dir_map{$key}->{'full_source_name'} =~ s/\/(\.\/)+/\//g; $dir_map{$key}->{'full_source_name'} =~ s/^SourceDir\///; $dir_map{$key}->{'full_target_name'} =~ s/\/(\.\/)+/\//g; $dir_map{$key}->{'full_target_name'} =~ s/^SourceDir\///; } $installer::logger::Info->printf("for %d directories there where %d processing steps and %d pushes\n", $directory_table->GetRowCount(), $process_count, $push_count); # Setup a map from component names to directory items. my %component_to_directory_map = map {$_->GetValue('Component') => $_->GetValue('Directory_')} @{$component_table->GetAllRows()}; # Finally, create the map from files to directories. my $map = {}; my $file_component_index = $file_table->GetColumnIndex("Component_"); my $file_file_index = $file_table->GetColumnIndex("File"); foreach my $file_row (@{$file_table->GetAllRows()}) { my $component_name = $file_row->GetValue($file_component_index); my $directory_name = $component_to_directory_map{$component_name}; my $dir_item = $dir_map{$directory_name}; my $unique_name = $file_row->GetValue($file_file_index); $map->{$unique_name} = [$dir_item->{'full_source_name'},$dir_item->{'full_target_name'}]; } $installer::logger::Info->printf("got full paths for %d files\n", $file_table->GetRowCount()); $self->{'FileToDirectoryMap'} = $map; return $map; } 1;