blob: c59771208a25a5f1d6847c6af46feb9d3191654c [file] [log] [blame]
#**************************************************************
#
# 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 installer::patch::InstallationSet;
use File::Basename;
use File::Copy;
use strict;
=head1 NAME
package installer::patch::Msi - Class represents a single MSI file and gives access to its tables.
=cut
sub FindAndCreate($$$$$)
{
my ($class, $version, $is_current_version, $language, $product_name) = @_;
my $condensed_version = $version;
$condensed_version =~ s/\.//g;
# When $version is the current version we have to search the msi at a different place.
my $path;
my $filename;
my $is_current = 0;
$path = installer::patch::InstallationSet::GetUnpackedExePath(
$version,
$is_current_version,
installer::languages::get_normalized_language($language),
"msi",
$product_name);
# Find the msi in the path.ls .
$filename = File::Spec->catfile($path, "openoffice".$condensed_version.".msi");
$is_current = $is_current_version;
return $class->new($filename, $version, $is_current, $language, $product_name);
}
=head2 new($class, $filename, $version, $is_current_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, $filename, $version, $is_current_version, $language, $product_name) = @_;
if ( ! -f $filename)
{
installer::logger::PrintError("can not find the .msi file for version %s and language %s at '%s'\n",
$version,
$language,
$filename);
return undef;
}
my $self = {
'filename' => $filename,
'path' => dirname($filename),
'version' => $version,
'is_current_version' => $is_current_version,
'language' => $language,
'package_format' => "msi",
'product_name' => $product_name,
'tmpdir' => File::Temp->newdir(CLEANUP => 1),
'is_valid' => -f $filename
};
bless($self, $class);
# Fill in some missing values from the 'Properties' table.
if ( ! (defined $version && defined $language && defined $product_name))
{
my $property_table = $self->GetTable("Property");
$self->{'version'} = $property_table->GetValue("Property", "DEFINEDVERSION", "Value")
unless defined $self->{'version'};
$self->{'product_name'} = $property_table->GetValue("Property", "DEFINEDPRODUCT", "Value")
unless defined $self->{'product_name'};
my $language = $property_table->GetValue("Property", "ProductLanguage", "Value");
# TODO: Convert numerical language id to language name.
$self->{'language'} = $language
unless defined $self->{'language'};
}
return $self;
}
sub IsValid ($)
{
my ($self) = @_;
return $self->{'is_valid'};
}
=head2 Commit($self)
Write all modified tables back into the databse.
=cut
sub Commit ($)
{
my $self = shift;
my @tables_to_update = ();
foreach my $table (values %{$self->{'tables'}})
{
push @tables_to_update,$table if ($table->IsModified());
}
if (scalar @tables_to_update > 0)
{
$installer::logger::Info->printf("writing modified tables to database:\n");
foreach my $table (@tables_to_update)
{
$installer::logger::Info->printf(" %s\n", $table->GetName());
$self->PutTable($table);
}
foreach my $table (@tables_to_update)
{
$table->UpdateTimestamp();
$table->MarkAsUnmodified();
}
}
}
=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::ToEscapedWindowsPath($self->{'filename'}),
"-f", installer::patch::Tools::ToEscapedWindowsPath($self->{'tmpdir'}),
"-e", $table_name);
my $result = qx($command);
}
# Read table into memory.
$table = new installer::patch::MsiTable($table_filename, $table_name);
$self->{'tables'}->{$table_name} = $table;
}
return $table;
}
=head2 PutTable($self, $table)
Write the given table back to the databse.
=cut
sub PutTable ($$)
{
my ($self, $table) = @_;
# Create text file from the current table content.
$table->WriteFile();
my $table_name = $table->GetName();
# Store table from text file into database.
my $table_filename = $table->{'filename'};
if (length($table_name) > 8)
{
# The file name of the table data must not be longer than 8 characters (not counting the extension).
# The name passed as argument to the -i option may be longer.
my $truncated_table_name = substr($table_name,0,8);
my $table_truncated_filename = File::Spec->catfile(
dirname($table_filename),
$truncated_table_name.".idt");
File::Copy::copy($table_filename, $table_truncated_filename) || die("can not create table file with short name");
}
my $command = join(" ",
"msidb.exe",
"-d", installer::patch::Tools::ToEscapedWindowsPath($self->{'filename'}),
"-f", installer::patch::Tools::ToEscapedWindowsPath($self->{'tmpdir'}),
"-i", $table_name);
my $result = system($command);
if ($result != 0)
{
installer::logger::PrintError("writing table '%s' back to database failed", $table_name);
# For error messages see http://msdn.microsoft.com/en-us/library/windows/desktop/aa372835%28v=vs.85%29.aspx
}
}
=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);
}
}
sub SetupFullNames ($$);
sub SetupFullNames ($$)
{
my ($item, $directory_map) = @_;
# Don't process any item twice.
return if defined $item->{'full_source_name'};
my $parent = $item->{'parent'};
if (defined $parent)
{
# Process the parent first.
if ( ! defined $parent->{'full_source_long_name'})
{
SetupFullNames($parent, $directory_map);
}
# Prepend the full names of the parent to our names.
$item->{'full_source_long_name'}
= $parent->{'full_source_long_name'} . "/" . $item->{'source_long_name'};
$item->{'full_source_short_name'}
= $parent->{'full_source_short_name'} . "/" . $item->{'source_short_name'};
$item->{'full_target_long_name'}
= $parent->{'full_target_long_name'} . "/" . $item->{'target_long_name'};
$item->{'full_target_short_name'}
= $parent->{'full_target_short_name'} . "/" . $item->{'target_short_name'};
}
else
{
# Directory has no parent => full names are the same as the name.
$item->{'full_source_long_name'} = $item->{'source_long_name'};
$item->{'full_source_short_name'} = $item->{'source_short_name'};
$item->{'full_target_long_name'} = $item->{'target_long_name'};
$item->{'full_target_short_name'} = $item->{'target_short_name'};
}
}
=head2 GetDirectoryMap($self)
Return a map that maps directory unique names (column 'Directory' in table 'Directory')
to hashes that contains short and long source and target names.
=cut
sub GetDirectoryMap ($)
{
my ($self) = @_;
if (defined $self->{'DirectoryMap'})
{
return $self->{'DirectoryMap'};
}
# Initialize the directory map.
my $directory_table = $self->GetTable("Directory");
my $directory_map = ();
foreach my $row (@{$directory_table->GetAllRows()})
{
my ($target_long_name, $target_short_name, $source_long_name, $source_short_name)
= installer::patch::Msi::SplitTargetSourceLongShortName($row->GetValue("DefaultDir"));
my $unique_name = $row->GetValue("Directory");
$directory_map->{$unique_name} =
{
'unique_name' => $unique_name,
'parent_name' => $row->GetValue("Directory_Parent"),
'default_dir' => $row->GetValue("DefaultDir"),
'source_long_name' => $source_long_name,
'source_short_name' => $source_short_name,
'target_long_name' => $target_long_name,
'target_short_name' => $target_short_name
};
}
# Add references to parent directories.
foreach my $item (values %$directory_map)
{
$item->{'parent'} = $directory_map->{$item->{'parent_name'}};
}
# Set up full names for all directories.
foreach my $item (values %$directory_map)
{
SetupFullNames($item, $directory_map);
}
# Cleanup the names.
foreach my $item (values %$directory_map)
{
foreach my $id (
'full_source_long_name',
'full_source_short_name',
'full_target_long_name',
'full_target_short_name')
{
$item->{$id} =~ s/\/(\.\/)+/\//g;
$item->{$id} =~ s/^SourceDir\///;
$item->{$id} =~ s/^\.$//;
}
}
$self->{'DirectoryMap'} = $directory_map;
return $self->{'DirectoryMap'};
}
=head2 GetFileMap ($)
Return a map (hash) that maps the unique name (column 'File' in
the 'File' table) to data that is associated with that file, like
the directory or component.
The map is kept alive for the lifetime of the Msi object. All
calls but the first are cheap.
=cut
sub GetFileMap ($)
{
my ($self) = @_;
if (defined $self->{'FileMap'})
{
return $self->{'FileMap'};
}
my $file_table = $self->GetTable("File");
my $component_table = $self->GetTable("Component");
my $dir_map = $self->GetDirectoryMap();
# 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 $file_map = {};
my $file_component_index = $file_table->GetColumnIndex("Component_");
my $file_file_index = $file_table->GetColumnIndex("File");
my $file_filename_index = $file_table->GetColumnIndex("FileName");
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 $unique_name = $file_row->GetValue($file_file_index);
my $file_name = $file_row->GetValue($file_filename_index);
my ($long_name, $short_name) = SplitLongShortName($file_name);
$file_map->{$unique_name} = {
'directory' => $dir_map->{$directory_name},
'component_name' => $component_name,
'file_name' => $file_name,
'long_name' => $long_name,
'short_name' => $short_name
};
}
$self->{'FileMap'} = $file_map;
return $file_map;
}
1;