269 lines
7.5 KiB
Perl
269 lines
7.5 KiB
Perl
package MsiComp;
|
|
|
|
use strict;
|
|
use Carp;
|
|
use FileHandle;
|
|
use File::Temp;
|
|
use Win32::OLE qw(in);
|
|
use BinComp;
|
|
use CabComp;
|
|
|
|
sub new {
|
|
my $class = shift;
|
|
my $instance = {
|
|
DIFF => '',
|
|
ERR => ''
|
|
};
|
|
return bless $instance;
|
|
}
|
|
|
|
sub GetLastError {
|
|
my $self = shift;
|
|
if (!ref $self) {croak "Invalid object reference"}
|
|
|
|
return $self->{ERR};
|
|
}
|
|
|
|
sub GetLastDiff {
|
|
my $self = shift;
|
|
if (!ref $self) {croak "Invalid object reference"}
|
|
|
|
return $self->{DIFF};
|
|
}
|
|
|
|
# let's just pretend I am a macro
|
|
sub CheckCOMErrorWrapper(&;$) {
|
|
my $code = shift;
|
|
my $rerror = shift;
|
|
|
|
if (ref $code ne 'CODE') {confess "Invalid parameter to CheckCOMErrorWrapper"}
|
|
|
|
&$code;
|
|
if ( Win32::OLE->LastError() ) {
|
|
print "COM error: ".Win32::OLE->LastError(). "\n";
|
|
$$rerror = Win32::OLE->LastError() if $rerror;
|
|
return;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub OpenMsiDb {
|
|
my $self = shift;
|
|
my $msi_file = shift;
|
|
if (!ref $self) {confess "Invalid object reference"}
|
|
|
|
my $msi_object;
|
|
return if !CheckCOMErrorWrapper {$msi_object = Win32::OLE->new("WindowsInstaller.Installer")} \$self->{ERR};
|
|
|
|
my $msi_db;
|
|
return if !CheckCOMErrorWrapper {$msi_db = $msi_object->OpenDatabase( $msi_file, 0 )} \$self->{ERR};
|
|
|
|
return $msi_db;
|
|
}
|
|
|
|
sub StoreMsiStreamData {
|
|
my $self = shift;
|
|
my $msi_db = shift;
|
|
my $stream_name = shift;
|
|
my $scratch_dir = shift;
|
|
|
|
my ($view, $file, $size);
|
|
return if !CheckCOMErrorWrapper {$view = $msi_db->OpenView( "SELECT Data FROM _Streams WHERE Name = '$stream_name'" )} \$self->{ERR};
|
|
return if !CheckCOMErrorWrapper {$view->Execute()} \$self->{ERR};
|
|
return if !CheckCOMErrorWrapper {$file = $view->Fetch()} \$self->{ERR};
|
|
if (!$file) {
|
|
$self->{ERR} = "Cannot find stream '$stream_name' in DB";
|
|
return;
|
|
}
|
|
return if !CheckCOMErrorWrapper {$size = $file->DataSize(1)} \$self->{ERR};
|
|
|
|
my ($fh, $fname) = File::Temp::tempfile( DIR => $scratch_dir?$scratch_dir:$ENV{TEMP} );
|
|
if ( !$fh ) {
|
|
$self->{ERR} = "Unable to create temporary file: $!";
|
|
return;
|
|
}
|
|
|
|
binmode $fh;
|
|
my ($i, $data);
|
|
for ( $i = 0; $i < $size; $i+=2048 ) {
|
|
return if !CheckCOMErrorWrapper {$data = $file->ReadStream(1, 2048, 2)} \$self->{ERR};
|
|
$fh->write($data, 2048);
|
|
}
|
|
my $excess = $size%2048;
|
|
if ($excess) {
|
|
return if !CheckCOMErrorWrapper {$data = $file->ReadStream(1, $excess, 2)} \$self->{ERR};
|
|
$fh->write($data, $excess);
|
|
}
|
|
close $fh;
|
|
|
|
return $fname;
|
|
}
|
|
|
|
sub CompareMsiBinaries {
|
|
my $self = shift;
|
|
if (!ref $self) {confess "Invlid object reference"}
|
|
if (@_ != 6) {confess "Invalid number of parameters to CompareMsiBinaries"}
|
|
my $msi_a = shift;
|
|
my $msi_b = shift;
|
|
my $file_a = shift;
|
|
my $file_b = shift;
|
|
my $scratch_dir = shift;
|
|
my $rfsame = shift;
|
|
|
|
my $binary_a = $self->StoreMsiStreamData($msi_a, $file_a, $scratch_dir);
|
|
return if !$binary_a;
|
|
|
|
my $binary_b = $self->StoreMsiStreamData($msi_b, $file_b, $scratch_dir);
|
|
return if !$binary_b;
|
|
|
|
my $bin_comp = new BinComp;
|
|
$$rfsame = $bin_comp->compare( $binary_a, $binary_b );
|
|
if ( !defined $$rfsame ) {
|
|
$self->{ERR} = $bin_comp->GetLastError();
|
|
return;
|
|
}
|
|
elsif ( !$$rfsame ) {
|
|
$self->{DIFF} = $bin_comp->GetLastDiff();
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub CompareMsiCabs {
|
|
my $self = shift;
|
|
if (!ref $self) {confess "Invlid object reference"}
|
|
if (@_ != 6) {confess "Invalid number of parameters to CompareMsiCabs"}
|
|
my $msi_a = shift;
|
|
my $msi_b = shift;
|
|
my $file_a = shift;
|
|
my $file_b = shift;
|
|
my $scratch_dir = shift;
|
|
my $rfsame = shift;
|
|
|
|
my $cab_a = $self->StoreMsiStreamData($msi_a, $file_a, $scratch_dir);
|
|
return if !$cab_a;
|
|
|
|
my $cab_b = $self->StoreMsiStreamData($msi_b, $file_b, $scratch_dir);
|
|
return if !$cab_b;
|
|
|
|
my $cab_comp = new CabComp;
|
|
$$rfsame = $cab_comp->compare( $cab_a, $cab_b );
|
|
if ( !defined $$rfsame ) {
|
|
$self->{ERR} = $cab_comp->GetLastError();
|
|
return;
|
|
}
|
|
elsif ( !$$rfsame ) {
|
|
$self->{DIFF} = $cab_comp->GetLastDiff();
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
|
|
#
|
|
# 0 - different
|
|
# 1 - same
|
|
# undefined - error
|
|
#
|
|
sub compare {
|
|
my $self = shift;
|
|
my $base = shift;
|
|
my $upd = shift;
|
|
if (!ref $self) {croak "Invalid object reference"}
|
|
if (!$base||!$upd) {croak "Invalid function call -- missing required parameters"}
|
|
|
|
if ( ! -e $base ) {
|
|
$self->{ERR} = "Invalid file: $base";
|
|
return;
|
|
}
|
|
if ( ! -e $upd ) {
|
|
$self->{ERR} = "Invalid file: $upd";
|
|
return;
|
|
}
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
|
|
|
|
my $base_db = $self->OpenMsiDb( $base );
|
|
return if !$base_db;
|
|
|
|
my $upd_db = $self->OpenMsiDb( $upd );
|
|
return if !$upd_db;
|
|
|
|
my $scratch_dir = File::Temp::tempdir( DIR => $ENV{TEMP}, CLEANUP => 1 );
|
|
if ( !$scratch_dir ) {
|
|
$self->{ERR} = "Unable to create temporary directory: $!";
|
|
return;
|
|
}
|
|
|
|
my ($fh, $tempfile) = File::Temp::tempfile( DIR => $scratch_dir );
|
|
if ( !$tempfile ) {
|
|
$self->{ERR} = "Unable to create temporary file: $!";
|
|
return;
|
|
}
|
|
close $fh;
|
|
|
|
my $fDiff;
|
|
return if !CheckCOMErrorWrapper {$fDiff = $base_db->GenerateTransform( $upd_db, $tempfile )} \$self->{ERR};
|
|
|
|
if ( $fDiff ) {
|
|
# Don't actually apply the transform, but generate
|
|
# a temporary table that we can query for what updates
|
|
# are needed to make the DB's equal
|
|
return if !CheckCOMErrorWrapper {$upd_db->ApplyTransform( $tempfile, 0x0100 )} \$self->{ERR};
|
|
|
|
my $view;
|
|
return if !CheckCOMErrorWrapper {$view = $upd_db->OpenView( 'SELECT * FROM _TransformView' )} \$self->{ERR};
|
|
return if !CheckCOMErrorWrapper {$view->Execute()} \$self->{ERR};
|
|
|
|
my $record;
|
|
return if !CheckCOMErrorWrapper {$record = $view->Fetch()} \$self->{ERR};
|
|
|
|
my $fignore;
|
|
my ($table, $column, $row, $data, $current);
|
|
while ( $record ) {
|
|
undef $fignore;
|
|
return if !CheckCOMErrorWrapper {$table = $record->StringData(1)} \$self->{ERR};
|
|
return if !CheckCOMErrorWrapper {$column = $record->StringData(2)} \$self->{ERR};
|
|
return if !CheckCOMErrorWrapper {$row = $record->StringData(3)} \$self->{ERR};
|
|
return if !CheckCOMErrorWrapper {$data = $record->StringData(4)} \$self->{ERR};
|
|
return if !CheckCOMErrorWrapper {$current = $record->StringData(5)} \$self->{ERR};
|
|
|
|
if ( $table eq 'File' && $column eq 'Version' ) {
|
|
$fignore = 1;
|
|
}
|
|
elsif ( $table eq 'Product' && $column eq 'Version' ) {
|
|
$fignore = 1;
|
|
}
|
|
elsif ( $table eq 'Property' && $column eq 'Value' and $row eq 'ProductVersion' ) {
|
|
$fignore = 1;
|
|
}
|
|
elsif ( $table eq 'Binary' and $column eq 'Data' ) {
|
|
return if !$self->CompareMsiBinaries( $base_db, $upd_db, $current, $data, $scratch_dir, \$fignore );
|
|
return 0 if ( !$fignore );
|
|
}
|
|
elsif ( $table eq 'Cabs' and $column eq 'Data' ) {
|
|
return if !$self->CompareMsiCabs( $base_db, $upd_db, $current, $data, $scratch_dir, \$fignore );
|
|
return 0 if ( !$fignore );
|
|
}
|
|
|
|
if ( !$fignore ) {
|
|
$self->{DIFF} = "$table/$column ($row): $current vs $data";
|
|
return 0;
|
|
}
|
|
return if !CheckCOMErrorWrapper {$record = $view->Fetch()} \$self->{ERR};
|
|
}
|
|
|
|
}
|
|
|
|
# Compare equal
|
|
return 1;
|
|
}
|
|
|
|
1;
|
|
|