package Win32::MSI::DB; =head1 NAME Win32::MSI::DB - Modify MSI databases =head1 SYNOPSIS use Win32::MSI::DB; $database = Win32::MSI::DB::new("filename", $flags); $database->transform("filename", $flags); $table = $database->table("table"); $view = $database->view("SELECT * FROM File WHERE FileSize < ?", 100000); @rec = $table->records(); $rec4 = $table->record(4); $rec->set("field", "value"); # string $rec->set("field", 4); # int $rec->set("field", "file"); # streams $rec->get("field"); $rec->getintofile("field", "file"); $field = $rec->field("field"); $field->set(2); $data = $field->get(); $field->fromfile("autoexec.bat"); $field->intofile("tmp.aa"); $db->error(); $view->error(); $rec->error(); =head1 DESCRIPTION =head2 Obtaining a database object C returns a new database object, open in one of the following modes: =over 4 =item $Win32::MSI::MSIDBOPEN_READONLY This doesn't really open the file read-only, but changes will not be written to disk. =item $Win32::MSI::MSIDBOPEN_TRANSACT Open in transactional mode so that changes are written only on commit. This is the default. =item $Win32::MSI::MSIDBOPEN_DIRECT Opens read/write without transactional behaviour. =item $Win32::MSI::MSIDBOPEN_CREATE This creates a new database in transactional mode. =back A database object allows creation of Cs or Cs. If you simply need access to a table you can use the C
method; for a subset of records or even a SQL-query you can use the C method. =head2 Using transforms When you have got a handle to a database, you can successively apply transforms to it. You do this by using C, which needs the filename of the transform file (normally with extension F<.mst>) and optionally a flag specification. Most of the possible flag values specify which merge errors are to be suppressed. =over 4 =item $Win32::MSI::MSITR_IGNORE_ADDEXISTINGROW Ignores adding a row that already exists. =item $Win32::MSI::MSITR_IGNORE_ADDEXISTINGTABLE Ignores adding a table that already exists. =item $Win32::MSI::MSITR_IGNORE_DELMISSINGROW Ignores deleting a row that doesn't exist. =item $Win32::MSI::MSITR_IGNORE_DELMISSINGTABLE Ignores deleting a table that doesn't exist. =item $Win32::MSI::MSITR_IGNORE_UPDATEMISSINGROW Ignores updating a row that doesn't exist. =item $Win32::MSI::MSITR_IGNORE_CHANGECODEPAGE Ignores that the code pages in the MSI database and the transform file do not match and neither has a neutral code page. =item $Win32::MSI::MSITR_IGNORE_ALL This flag combines all of the above mentioned flags. This is the default. =item $Win32::MSI::MSITR_VIEWTRANSFORM This flag should not be used together with the other flags. It specifies that instead of merging the data, a table named C<_TransformView> is created in memory, which has the columns C
, C, C, C and C. This way the data in a transform file can be directly queried. For more information please see S. =back This doesn't open the file read-only, but changes will not be written to disk. A transform is a specification of changed values. So you get a MSI database from your favorite vendor, make a transform to overlay your own settings (the target installation directory, the features to be installed, etc.) and upon installation you can use these settings via a commandline similar to msiexec /i TRANSFORMS = F F /qb The changes in a transform are stored by a (table, row, cell, old value, new value) tuple. =head2 Fetch records from a table or view When you have obtained a C
or C object, you can use the C method to access individual records. It takes a number as parameter. Records are fetched as needed. Using C as parameter fetches all records and returns the first (index 0). Another possibility is to use the C method, which returns an array of all records in this table or view. =head2 A record has fields A record's fields can be queried or changed using the C object, as in $rec->set("field", "value"); # string $rec->set("field", 4); # int $rec->set("field", "file"); # streams $rec->get("field"); $rec->getintofile("field", "file"); or you can have separate C objects: $field = $rec->field("field"); $data = $field->get(); $field->set(2); Access to files (streams) is currently not finished. =head2 Errors Each object may access an C method, which gives a string or an array (depending on context) containing the error information. Help wanted: Is there a way to get a error string from the number which does not depend on the current MSI database? In particular, the developer errors (2000 and above) are not listed. =head1 REMARKS This module depends on C, which is used to import the functions out of the F. Currently the C is not used - patches are welcome. =head2 AUTHOR Please contact C for questions, suggestions, and patches (C please). A big thank you goes to DBH for various changes throughout the code. =head2 Further plans A C package is planned - which will allow to compare databases and give a diff, and similar tools. I have started to write a simple Tk visualization. =head1 SEE ALSO S =cut use strict; use warnings; use Win32::API; our $VERSION = "1.06"; ###### Constants and other definitions # Shorthand to define API call constants sub _def { return Win32::API->new("msi", @_, "I") || die $!; } my $MsiOpenDatabase = _def(MsiOpenDatabase => "PPP"); my $MsiOpenDatabasePIP = _def(MsiOpenDatabase => "PIP"); my $MsiCloseHandle = _def(MsiCloseHandle => "I"); my $MsiDataBaseCommit = _def(MsiDatabaseCommit => "I"); my $MsiDatabaseApplyTransform = _def(MsiDatabaseApplyTransform => "IPI"); my $MsiViewExecute = _def(MsiViewExecute => "II"); my $MsiDatabaseOpenView = _def(MsiDatabaseOpenView => "IPP"); my $MsiViewClose = _def(MsiViewClose => "I"); my $MsiViewFetch = _def(MsiViewFetch => "IP"); my $MsiRecordGetFieldCount = _def(MsiRecordGetFieldCount => "I"); my $MsiRecordGetInteger = _def(MsiRecordGetInteger => "II"); my $MsiRecordGetString = _def(MsiRecordGetString => "IIPP"); my $MsiRecordGetStringIIIP = _def(MsiRecordGetString => "IIIP"); my $MsiRecordSetInteger = _def(MsiRecordSetInteger => "III"); my $MsiRecordSetString = _def(MsiRecordSetString => "IIP"); my $MsiRecordSetStream = _def(MsiRecordSetStream => "IIP"); my $MsiCreateRecord = _def(MsiCreateRecord => "I"); my $MsiViewGetColumnInfo = _def(MsiViewGetColumnInfo => "IIP"); my $MsiGetLastErrorRecord = _def(MsiGetLastErrorRecord => ""); my $MsiFormatRecord = _def(MsiFormatRecord => "IIPP"); # External constants our $MSIDBOPEN_READONLY = 0; our $MSIDBOPEN_TRANSACT = 1; our $MSIDBOPEN_DIRECT = 2; our $MSIDBOPEN_CREATE = 3; our $MSICOLINFO_NAMES = 0; our $MSICOLINFO_TYPES = 1; my $_MSICOLINFO_INDEX = 21231231; # For own use, not defined by MS our $MSITR_IGNORE_ADDEXISTINGROW = 0x1; our $MSITR_IGNORE_DELMISSINGROW = 0x2; our $MSITR_IGNORE_ADDEXISTINGTABLE = 0x4; our $MSITR_IGNORE_DELMISSINGTABLE = 0x8; our $MSITR_IGNORE_UPDATEMISSINGROW = 0x10; our $MSITR_IGNORE_CHANGECODEPAGE = 0x20; our $MSITR_VIEWTRANSFORM = 0x100; our $MSITR_IGNORE_ALL = $MSITR_IGNORE_ADDEXISTINGROW | $MSITR_IGNORE_DELMISSINGROW | $MSITR_IGNORE_ADDEXISTINGTABLE | $MSITR_IGNORE_DELMISSINGTABLE | $MSITR_IGNORE_UPDATEMISSINGROW | $MSITR_IGNORE_CHANGECODEPAGE; my $MSI_NULL_INTEGER = -0x80000000; my $ERROR_NO_MORE_ITEMS = 259; my $ERROR_MORE_DATA = 234; my $COLTYPE_STREAM = 1; my $COLTYPE_INT = 2; my $COLTYPE_STRING = 3; my %COLTYPES = ( "i" => $COLTYPE_INT, "j" => $COLTYPE_INT, "s" => $COLTYPE_STRING, "g" => $COLTYPE_STRING, "l" => $COLTYPE_STRING, "v" => $COLTYPE_STREAM, ); my $INITIAL_EMPTY_STRING = "\0" x 1024; ##### Default Routines sub new { my ($file, $mode) = @_; return undef unless ($file); my $hdl = pack("l",0); $mode = $MSIDBOPEN_TRANSACT unless (defined($mode)); if ($mode =~ /^\d+$/) { # For special values of mode another call # is needed (integer instead of pointer) $MsiOpenDatabasePIP->Call($file, $mode, $hdl) and return undef; } else { $MsiOpenDatabase->Call($file, $mode, $hdl) and return undef; } my %a = (handle => unpack("l", $hdl)); return _bless_type(\%a, "db"); } sub DESTROY { my $self = shift; $self->_commit() if ($self->{""} eq "db"); if ($self->{"handle"}) { _close($self->{"handle"}) and return undef; } $self = {}; } ##### Public Routines # Database method to return the records in $table, optionally # qualified by SQL clause $where with parameters @param sub table { my ($self, $table, $where, @param) = @_; return undef unless (defined $table); $self->_check("db"); my $sql = "SELECT * FROM $table" . (defined $where && " WHERE $where"); $self->view($sql, @param); } # Database method to return the view obtained by executing $sql SELECT # statement with parameters @param. If $sql is not a SELECT then # return an object of "type" "sql". sub view { my ($self, $sql, @param) = @_; $self->_check("db"); my $hdl = pack("l",0); $MsiDatabaseOpenView->Call($self->{"handle"}, $sql, $hdl) and return undef; my %s = (handle => unpack("l", $hdl)); my $a = 0; if (@param) { $a = _newrecord(@param) or return undef; } $MsiViewExecute->Call($s{"handle"}, $a) and return undef; _close($a) if ($a); return _bless_type(\%s, "sql") unless ($sql =~ /^\s*SELECT\s/i); my $me = _bless_type(\%s, "v"); $me->get_info(undef); $me->{"coltypes"} = [ map($COLTYPES{lc(substr($_->{type}, 0, 1))}, @{$me->{"colinfo"}}) ]; return $me; } # Given a table or view, return record number $recnum. Fetch records # as necessary. If $recnum is undef, fetch all records and return the # first. sub record { my ($self, $recnum) = @_; $self->_check("v"); while (!defined($recnum) || $recnum > $self->{"fetched"}) { my $hdl = pack("l",0); last if ($MsiViewFetch->Call($self->{"handle"}, $hdl) == $ERROR_NO_MORE_ITEMS); $hdl = unpack("l", $hdl); $self->{"records"}[$self->{fetched} ++] = _bless_type({handle => $hdl, view => $self}, "r"); } return $self->{"records"}[$recnum || 0]; } sub records { my ($self) = @_; $self->_check("v"); $self->record(undef); return @{$self->{"records"}}; } sub fields { return field(@_); } # Return a record's fields with names @names, or the first such in a # scalar context sub field { my ($self, @names) = @_; my ($cn); $self->_check("r"); my @ret = (); for my $n (@names) { my $i = $self->{"view"}->get_info($_MSICOLINFO_INDEX, $n); if (defined $i) { push @ret, bless_type({rec => $self, cn => $i->{"index"}}, "f"); } else { push @ret, undef; } } return @names > 1 || wantarray() ? @ret : $ret[0]; } sub close { my $self = shift; $self->DESTROY(); } sub get { my ($self, $field) = @_; $self->_check("r", "f"); if ($self->_type() eq "f") # Get the value of a field { return $self->{"rec"}{data}[$self->{cn}]; } if (!$self->{"data"}) # Get $field from a record { $self->{"data"} = [_extract_fields($self->{handle}, @{$self->{"view"}{coltypes}} ) ]; } my $f = $self->{"view"}->get_info($_MSICOLINFO_INDEX, $field); return defined($f) ? $self->{"data"}[$f] : undef; } sub set { my ($self, $field, $value) = @_; my ($rec, $cn, $type); $self->_check("r", "f"); if ($self->_type() eq "r") # Set $field of this record { $rec = $self; $cn = $self->{"view"}->get_info($_MSICOLINFO_INDEX, $field); } else # Set this field { $rec = $self->{"rec"}; $cn = $self->{"cn"}; $value = $field; # $field not given } $type = $rec->{"view"}{coltypes}[$cn]; $cn++; # MSI numbers columns from 1 if ($type == $COLTYPE_INT) { $MsiRecordSetInteger->Call($rec->{"handle"}, $cn, $value) and return undef; } elsif ($type == $COLTYPE_STRING) { $MsiRecordSetString->Call($rec->{"handle"}, $cn, $value) and return undef; } elsif ($type == $COLTYPE_STREAM) { $MsiRecordSetStream->Call($rec->{"handle"}, $cn, $value) and return undef; } else { return undef; } return 1; } sub coltypes { my ($self) = @_; $self->get_info($MSICOLINFO_TYPES); } sub colnames { my ($self) = @_; $self->get_info($MSICOLINFO_NAMES); } # Return column names or types for this view # $which = # $MSICOLINFO_NAMES # $MSICOLINFO_TYPES # $_MSICOLINFO_INDEX => Return column index of $field # undef => return whole colinfo hash sub get_info { my ($self, $which, $field) = @_; $self->_check("v"); # Fetch and store my colinfo if absent if (!$self->{"colinfo"}) { my $hdl = pack("l",0); $MsiViewGetColumnInfo->Call($self->{"handle"}, $MSICOLINFO_NAMES, $hdl) and return undef; $hdl = unpack("l", $hdl); my @name = _extract_fields($hdl); _close($hdl); $hdl = pack("l",0); $MsiViewGetColumnInfo->Call($self->{"handle"}, $MSICOLINFO_TYPES, $hdl) and return undef; $hdl = unpack("l", $hdl); my @type = _extract_fields($hdl); _close($hdl); foreach my $i (0..$#name) { my $n = $name[$i]; $self->{"colinfo_hash"}{$n} = $self->{colinfo}[$i] = {name => $n, type => $type[$i], index => $i}; } } if (defined $which && $which == $_MSICOLINFO_INDEX) { return undef unless ($field); my $t = $self->{"colinfo_hash"}{$field}; return $t ? $t->{"index"} : undef; } return !defined($which) ? %{$self->{"colinfo_hash"}} : $which == $MSICOLINFO_NAMES ? map($_->{"name"}, @{$self->{colinfo}}) : $which == $MSICOLINFO_TYPES ? map($_->{"type"}, @{$self->{colinfo}}) : undef; } # die with $message followed by error info from $self sub db_die { my ($self, @msg) = @_; die join(" ", @msg), ": ", join("/", $self->error); } sub error { my ($self) = shift; my $e = $MsiGetLastErrorRecord->Call() or return undef; my @a = _extract_fields($e); _close($e); return wantarray ? @a : join("/", @a); } # XXX Errors 1000-1999 are install-time errors and their strings are # stored in the Error table but errors > 2000 are MSI authoring errors # and are not in the error table. # ms-help://MS.PSDKXPSP2.1033/msi/setup/windows_installer_error_messages.htm sub _error_string_to_do { my ($self, @a) = @_; print join("<>", @a), "\n"; my $q = $self->openview("SELECT Message FROM Error WHERE Error = ?", $a[0]) or die $!; push @a, $q->fetch(); _close($q); $q = newrecord(@a) or die $!; print "rec = $q\n"; my $s = " " x 1024; my $l = pack("l", length($s)); $MsiFormatRecord->Call($self, $q, $s, $l) or die $!; print "->$s\n"; substr($s, unpack("l", $l)) = ""; return $s; } sub transform { my ($self, $filename, $flags) = @_; $self->_check("db"); return undef unless ($filename); $flags = $MSITR_IGNORE_ALL if (!defined($flags)); my $r = $MsiDatabaseApplyTransform->Call( $self->{"handle"}, $filename, $flags); return $r; } ##### Internal Routines - not for use outside this module sub _commit { my $self = shift; $MsiDataBaseCommit->Call($self->{"handle"}) and return undef; } sub _close { my $hdl = shift; $MsiCloseHandle->Call($hdl) and return undef; } # Bless hash $ref into this package, setting its "type" in the hash # element with an empty string key. Ugh. sub _bless_type { my ($ref, $type, $class) = @_; my $me = bless $ref, $class || __PACKAGE__; $me->{""} = $type; return $me; } sub _type { my ($self) = @_; return $self->{""}; } sub _check { my ($self, @allowed) = @_; my $t = $self->_type(); die "$self is type '$t' instead of " . join(", ", @allowed) unless (grep($t eq $_, @allowed)); } # Return the handle for a new record containing @list sub _newrecord { my (@list) = @_; my $hdl = $MsiCreateRecord->Call(scalar(@list)) or return undef; for my $i (0..$#list) { # print "new rec. $i: ", $list[$i], " is "; if ($list[$i] =~ /^\d+$/) { # print "int\n"; $MsiRecordSetInteger->Call($hdl, $i+1, $list[$i]) and return undef; } else { # print "string\n"; $MsiRecordSetString->Call($hdl, $i+1, $list[$i]) and return undef; } } return $hdl; } sub _getI { my ($hdl, $num) = @_; my $i = $MsiRecordGetInteger->Call($hdl, $num); return $i == $MSI_NULL_INTEGER ? undef : $i; } sub _getS { my ($hdl, $num) = @_; my ($len); my $s = $INITIAL_EMPTY_STRING; my $p = pack("l", length($s)); # Initial size my $e = $MsiRecordGetString->Call($hdl, $num, $s, $p); if ($e == $ERROR_MORE_DATA) { $len = unpack("l", $p)*2; # Unicode? $s = "\0" x $len; $e = $MsiRecordGetString->Call($hdl, $num, $s, $len); } die $! if ($e); $len = unpack("l", $p); return "((too big))" if ($len > length($s)); # $l = index($s, "\0"); # $l = length($s) if $l<0; return substr($s, 0, $len); } # Get values of fields for $hdl. If present, @types gives the type, # $COLTYPE_INT or $COLTYPE_STRING of each field; otherwise try to # fetch it as an int and if that fails try string. sub _extract_fields { my ($hdl, @types) = @_; my $i = $MsiRecordGetFieldCount->Call($hdl) or die $!; my @a = (); for my $c (1..$i) { my $s; if (@types) { my $t = shift @types; $s = $t == $COLTYPE_INT ? _getI($hdl, $c) : $t == $COLTYPE_STRING ? _getS($hdl, $c) : undef; # STREAMS and other not processed here } else # Autodetect mode { $s = _getI($hdl, $c); $s = _getS($hdl, $c) unless (defined($s)); } push @a, $s; } return @a; } # vim: sw=2 ai