1#
2# This file is part of the LibreOffice project.
3#
4# This Source Code Form is subject to the terms of the Mozilla Public
5# License, v. 2.0. If a copy of the MPL was not distributed with this
6# file, You can obtain one at http://mozilla.org/MPL/2.0/.
7#
8# This file incorporates work covered by the following license notice:
9#
10#   Licensed to the Apache Software Foundation (ASF) under one or more
11#   contributor license agreements. See the NOTICE file distributed
12#   with this work for additional information regarding copyright
13#   ownership. The ASF licenses this file to you under the Apache
14#   License, Version 2.0 (the "License"); you may not use this file
15#   except in compliance with the License. You may obtain a copy of
16#   the License at http://www.apache.org/licenses/LICENSE-2.0 .
17#
18
19package installer::windows::update;
20
21use installer::converter;
22use installer::exiter;
23use installer::files;
24use installer::globals;
25use installer::pathanalyzer;
26use installer::systemactions;
27
28#################################################################################
29# Extracting all tables from an msi database
30#################################################################################
31
32sub extract_all_tables_from_msidatabase
33{
34    my ($fulldatabasepath, $workdir) = @_;
35
36    my $msidb = "msidb.exe";    # Has to be in the path
37    my $infoline = "";
38    my $systemcall = "";
39    my $returnvalue = "";
40    my $extraslash = "";        # Has to be set for non-ActiveState perl
41
42    # Export of all tables by using "*"
43
44    if ( $^O =~ /cygwin/i ) {
45        # msidb.exe really wants backslashes. (And double escaping because system() expands the string.)
46        $fulldatabasepath =~ s/\//\\\\/g;
47        $workdir =~ s/\//\\\\/g;
48        $extraslash = "\\";
49    }
50    if ( $^O =~ /linux/i) {
51        $extraslash = "\\";
52    }
53
54    $systemcall = $msidb . " -d " . $fulldatabasepath . " -f " . $workdir . " -e " . $extraslash . "*";
55    $returnvalue = system($systemcall);
56
57    $infoline = "Systemcall: $systemcall\n";
58    push( @installer::globals::logfileinfo, $infoline);
59
60    if ($returnvalue)
61    {
62        $infoline = "ERROR: Could not execute $systemcall !\n";
63        push( @installer::globals::logfileinfo, $infoline);
64        installer::exiter::exit_program("ERROR: Could not exclude tables from msi database: $fulldatabasepath !", "extract_all_tables_from_msidatabase");
65    }
66    else
67    {
68        $infoline = "Success: Executed $systemcall successfully!\n";
69        push( @installer::globals::logfileinfo, $infoline);
70    }
71}
72
73#################################################################################
74# Collecting the keys from the first line of the idt file
75#################################################################################
76
77sub collect_all_keys
78{
79    my ($line) = @_;
80
81    my @allkeys = ();
82    my $rownumber = 0;
83    my $onekey = "";
84
85    while ( $line =~ /^\s*(\S+?)\t(.*)$/ )
86    {
87        $onekey = $1;
88        $line = $2;
89        $rownumber++;
90        push(@allkeys, $onekey);
91    }
92
93    # and the last key
94
95    $onekey = $line;
96    $onekey =~ s/^\s*//g;
97    $onekey =~ s/\s*$//g;
98
99    $rownumber++;
100    push(@allkeys, $onekey);
101
102    return (\@allkeys, $rownumber);
103}
104
105#################################################################################
106# Analyzing the content of one line of an idt file
107#################################################################################
108
109sub get_oneline_hash
110{
111    my ($line, $allkeys, $rownumber) = @_;
112
113    my $counter = 0;
114    my %linehash = ();
115
116    $line =~ s/^\s*//;
117    $line =~ s/\s*$//;
118
119    my $value = "";
120    my $onekey = "";
121
122    while ( $line =~ /^(.*?)\t(.*)$/ )
123    {
124        $value = $1;
125        $line = $2;
126        $onekey = ${$allkeys}[$counter];
127        $linehash{$onekey} = $value;
128        $counter++;
129    }
130
131    # the last column
132
133    $value = $line;
134    $onekey = ${$allkeys}[$counter];
135
136    $linehash{$onekey} = $value;
137
138    return \%linehash;
139}
140
141#################################################################################
142# Analyzing the content of an idt file
143#################################################################################
144
145sub analyze_idt_file
146{
147    my ($filecontent) = @_;
148
149    my %table = ();
150    # keys are written in first line
151    my ($allkeys, $rownumber) = collect_all_keys(${$filecontent}[0]);
152
153    for ( my $i = 0; $i <= $#{$filecontent}; $i++ )
154    {
155        if (( $i == 0 ) || ( $i == 1 ) || ( $i == 2 )) { next; }
156
157        my $onelinehash = get_oneline_hash(${$filecontent}[$i], $allkeys, $rownumber);
158        my $linekey = $i - 2;  # ! : The linenumber is the unique key !? Always decrease by two, because of removed first three lines.
159        $table{$linekey} = $onelinehash;
160    }
161
162    return \%table;
163}
164
165#################################################################################
166# Reading all idt files in a specified directory
167#################################################################################
168
169sub read_all_tables_from_msidatabase
170{
171    my ($workdir) = @_;
172
173    my %database = ();
174
175    my $ext = "idt";
176
177    my $allidtfiles = installer::systemactions::find_file_with_file_extension($ext, $workdir);
178
179    for ( my $i = 0; $i <= $#{$allidtfiles}; $i++ )
180    {
181        my $onefilename = ${$allidtfiles}[$i];
182        my $longonefilename = $workdir . $installer::globals::separator . $onefilename;
183        if ( ! -f $longonefilename ) { installer::exiter::exit_program("ERROR: Could not find idt file: $longonefilename!", "read_all_tables_from_msidatabase"); }
184        my $filecontent = installer::files::read_file($longonefilename);
185        my $idtcontent = analyze_idt_file($filecontent);
186        if ($onefilename eq "Directory.idt") {
187            collect_directories($filecontent);
188        }
189        my $key = $onefilename;
190        $key =~ s/\.idt\s*$//;
191        $database{$key} = $idtcontent;
192    }
193
194    return \%database;
195}
196
197#################################################################################
198# Checking, if this is the correct database.
199#################################################################################
200
201sub correct_database
202{
203    my ($product, $pro, $langs, $languagestringref) = @_;
204
205    my $correct_database = 0;
206
207    # Comparing $product with $installer::globals::product and
208    # $pro with $installer::globals::pro and
209    # $langs with $languagestringref
210
211    my $product_is_good = 0;
212
213    my $localproduct = $installer::globals::product;
214    if ( $installer::globals::languagepack ) { $localproduct = $localproduct . "LanguagePack"; }
215    elsif ( $installer::globals::helppack ) { $localproduct = $localproduct . "HelpPack"; }
216
217    if ( $product eq $localproduct ) { $product_is_good = 1; }
218
219    if ( $product_is_good )
220    {
221        my $pro_is_good = 0;
222
223        if ((( $pro eq "pro" ) && ( $installer::globals::pro )) || (( $pro eq "nonpro" ) && ( ! $installer::globals::pro ))) { $pro_is_good = 1; }
224
225        if ( $pro_is_good )
226        {
227            my $langlisthash = installer::converter::convert_stringlist_into_hash(\$langs, ",");
228            my $langstringhash = installer::converter::convert_stringlist_into_hash($languagestringref, "_");
229
230            my $not_included = 0;
231            foreach my $onelang ( keys %{$langlisthash} )
232            {
233                if ( ! exists($langstringhash->{$onelang}) )
234                {
235                    $not_included = 1;
236                    last;
237                }
238            }
239
240            if ( ! $not_included )
241            {
242                foreach my $onelanguage ( keys %{$langstringhash} )
243                {
244                    if ( ! exists($langlisthash->{$onelanguage}) )
245                    {
246                        $not_included = 1;
247                        last;
248                    }
249                }
250
251                if ( ! $not_included ) { $correct_database = 1; }
252            }
253        }
254    }
255
256    return $correct_database;
257}
258
259#################################################################################
260# Searching for the path to the reference database for this special product.
261#################################################################################
262
263sub get_databasename_from_list
264{
265    my ($filecontent, $languagestringref, $filename) = @_;
266
267    my $databasepath = "";
268
269    for ( my $i = 0; $i <= $#{$filecontent}; $i++ )
270    {
271        my $line = ${$filecontent}[$i];
272        if ( $line =~ /^\s*$/ ) { next; } # empty line
273        if ( $line =~ /^\s*\#/ ) { next; } # comment line
274
275        if ( $line =~ /^\s*(.+?)\s*\t+\s*(.+?)\s*\t+\s*(.+?)\s*\t+\s*(.+?)\s*$/ )
276        {
277            my $product = $1;
278            my $pro = $2;
279            my $langs = $3;
280            my $path = $4;
281
282            if (( $pro ne "pro" ) && ( $pro ne "nonpro" )) { installer::exiter::exit_program("ERROR: Wrong syntax in file: $filename. Only \"pro\" or \"nonpro\" allowed in column 1! Line: \"$line\"", "get_databasename_from_list"); }
283
284            if ( correct_database($product, $pro, $langs, $languagestringref) )
285            {
286                $databasepath = $path;
287                last;
288            }
289        }
290        else
291        {
292            installer::exiter::exit_program("ERROR: Wrong syntax in file: $filename! Line: \"$line\"", "get_databasename_from_list");
293        }
294    }
295
296    return $databasepath;
297}
298
299#################################################################################
300# Reading an existing database completely
301#################################################################################
302
303sub readdatabase
304{
305    my ($allvariables, $languagestringref, $includepatharrayref) = @_;
306
307    my $database = "";
308    my $infoline = "";
309
310    if ( ! $allvariables->{'UPDATE_DATABASE_LISTNAME'} ) { installer::exiter::exit_program("ERROR: If \"UPDATE_DATABASE\" is set, \"UPDATE_DATABASE_LISTNAME\" is required.", "Main"); }
311    my $listfilename = $allvariables->{'UPDATE_DATABASE_LISTNAME'};
312
313    # Searching the list in the include paths
314    my $listname = installer::scriptitems::get_sourcepath_from_filename_and_includepath(\$listfilename, $includepatharrayref, 1);
315    if ( $$listname eq "" ) { installer::exiter::exit_program("ERROR: List file not found: $listfilename !", "readdatabase"); }
316    my $completelistname = $$listname;
317
318    # Reading list file
319    my $listfile = installer::files::read_file($completelistname);
320
321    # Get name and path of reference database
322    my $databasename = get_databasename_from_list($listfile, $languagestringref, $completelistname);
323
324    # If the correct database was not found, this is not necessarily an error. But in this case, this is not an update packaging process!
325    if (( $databasename ) && ( $databasename ne "" )) # This is an update packaging process!
326    {
327        $installer::globals::updatedatabase = 1;
328        installer::logger::print_message( "... update process, using database $databasename ...\n" );
329        $infoline = "\nDatabase found in $completelistname: \"$databasename\"\n\n";
330        # Saving in global variable
331        $installer::globals::updatedatabasepath = $databasename;
332    }
333    else
334    {
335        $infoline = "\nNo database found in $completelistname. This is no update process!\n\n";
336    }
337    push( @installer::globals::logfileinfo, $infoline);
338
339    if ( $installer::globals::updatedatabase )
340    {
341        if ( ! -f $databasename ) { installer::exiter::exit_program("ERROR: Could not find reference database: $databasename!", "readdatabase"); }
342
343        my $msifilename = $databasename;
344        installer::pathanalyzer::make_absolute_filename_to_relative_filename(\$msifilename);
345
346        installer::logger::include_timestamp_into_logfile("Performance Info: readdatabase start");
347
348        # create directory for unpacking
349        my $databasedir = installer::systemactions::create_directories("database", $languagestringref);
350
351        # copy database
352        my $fulldatabasepath = $databasedir . $installer::globals::separator . $msifilename;
353        installer::systemactions::copy_one_file($databasename, $fulldatabasepath);
354
355        installer::logger::include_timestamp_into_logfile("Performance Info: readdatabase: before extracting tables");
356
357        # extract all tables from database
358        extract_all_tables_from_msidatabase($fulldatabasepath, $databasedir);
359
360        installer::logger::include_timestamp_into_logfile("Performance Info: readdatabase: before reading tables");
361
362        # read all tables
363        $database = read_all_tables_from_msidatabase($databasedir);
364
365        # Test output:
366
367        #   foreach my $key1 ( keys %{$database} )
368        #   {
369        #       print "Test1: $key1\n";
370        #       foreach my $key2 ( keys %{$database->{$key1}} )
371        #       {
372        #           print "\tTest2: $key2\n";
373        #           foreach my $key3 ( keys %{$database->{$key1}->{$key2}} )
374        #           {
375        #               print "\t\tTest3: $key3: $database->{$key1}->{$key2}->{$key3}\n";
376        #           }
377        #       }
378        #   }
379
380        # Example: File table
381
382        # my $filetable = $database->{'File'};
383        # foreach my $linenumber ( keys  %{$filetable} )
384        # {
385        #   print "Test Filenumber: $linenumber\n";
386        #   foreach my $key ( keys %{$filetable->{$linenumber}} )
387        #   {
388        #       print "\t\tTest: $key: $filetable->{$linenumber}->{$key}\n";
389        #   }
390        # }
391
392        # Example: Searching for ProductCode in table Property
393
394        # my $column1 = "Property";
395        # my $column2 = "Value";
396        # my $searchkey = "ProductCode";
397        # my $propertytable = $database->{'Property'};
398        # foreach my $linenumber ( keys  %{$propertytable} )
399        # {
400        #   if ( $propertytable->{$linenumber}->{$column1} eq $searchkey )
401        #   {
402        #       print("Test: $searchkey : $propertytable->{$linenumber}->{$column2}\n");
403        #   }
404        # }
405
406        installer::logger::include_timestamp_into_logfile("Performance Info: readdatabase end");
407    }
408
409    return $database;
410}
411
412#########################################################################
413# Reading the file "Directory.idt".
414#########################################################################
415
416sub collect_directories
417{
418    my ($filecontent) = @_;
419
420    for ( my $i = 0; $i <= $#{$filecontent}; $i++ )
421    {
422        if ( $i <= 2 ) { next; }                        # ignoring first three lines
423        if ( ${$filecontent}[$i] =~ /^\s*$/ ) { next; } # ignoring empty lines
424        # Format: Directory Directory_Parent    DefaultDir
425        if ( ${$filecontent}[$i] =~ /^\s*(.+?)\t(.*?)\t(.*?)\s*$/ )
426        {
427            $installer::globals::merge_directory_hash{$1} = 1;
428        }
429        else
430        {
431            my $linecount = $i + 1;
432            installer::exiter::exit_program("ERROR: Unknown line format in table \"$idtfilename\" (line $linecount) !", "collect_directories");
433        }
434    }
435}
436
437#################################################################################
438# Files can be included in merge modules. This is also important for update.
439#################################################################################
440
441sub readmergedatabase
442{
443    my ( $mergemodules, $languagestringref, $includepatharrayref ) = @_;
444
445    installer::logger::include_timestamp_into_logfile("Performance Info: readmergedatabase start");
446
447    my $mergemoduledir = installer::systemactions::create_directories("mergedatabase", $languagestringref);
448
449    my %allmergefiles = ();
450
451    foreach my $mergemodule ( @{$mergemodules} )
452    {
453        my $filename = $mergemodule->{'Name'};
454        my $mergefile = $ENV{'MSM_PATH'} . $filename;
455
456        if ( ! -f $mergefile ) { installer::exiter::exit_program("ERROR: msm file not found: $filename !", "readmergedatabase"); }
457        my $completesource = $mergefile;
458
459        my $mergegid = $mergemodule->{'gid'};
460        my $workdir = $mergemoduledir . $installer::globals::separator . $mergegid;
461        if ( ! -d $workdir ) { installer::systemactions::create_directory($workdir); }
462
463        my $completedest = $workdir . $installer::globals::separator . $filename;
464        installer::systemactions::copy_one_file($completesource, $completedest);
465        if ( ! -f $completedest ) { installer::exiter::exit_program("ERROR: msm file not found: $completedest !", "readmergedatabase"); }
466
467        # extract all tables from database
468        extract_all_tables_from_msidatabase($completedest, $workdir);
469
470        # read all tables
471        my $onemergefile = read_all_tables_from_msidatabase($workdir);
472
473        $allmergefiles{$mergegid} = $onemergefile;
474    }
475
476    foreach my $mergefilegid ( keys %allmergefiles )
477    {
478        my $onemergefile = $allmergefiles{$mergefilegid};
479        my $filetable = $onemergefile->{'File'};
480
481        foreach my $linenumber ( keys %{$filetable} )
482        {
483            # Collecting all files from merge modules in global hash
484            $installer::globals::mergemodulefiles{$filetable->{$linenumber}->{'File'}} = 1;
485        }
486    }
487
488    installer::logger::include_timestamp_into_logfile("Performance Info: readmergedatabase end");
489}
490
491#################################################################################
492# Creating several useful hashes from old database
493#################################################################################
494
495sub create_database_hashes
496{
497    my ( $database ) = @_;
498
499    # 1. Hash ( Component -> UniqueFileName ), required in File table.
500    # Read from File table.
501
502    my %uniquefilename = ();
503    my %allupdatesequences = ();
504    my %allupdatecomponents = ();
505    my %allupdatefileorder = ();
506    my %allupdatecomponentorder = ();
507    my %revuniquefilename = ();
508    my %revshortfilename = ();
509    my %shortdirname = ();
510    my %componentid = ();
511    my %componentidkeypath = ();
512    my %alloldproperties = ();
513    my %allupdatelastsequences = ();
514    my %allupdatediskids = ();
515
516    my $filetable = $database->{'File'};
517
518    foreach my $linenumber ( keys  %{$filetable} )
519    {
520        my $comp = $filetable->{$linenumber}->{'Component_'};
521        my $uniquename = $filetable->{$linenumber}->{'File'};
522        my $filename = $filetable->{$linenumber}->{'FileName'};
523        my $sequence = $filetable->{$linenumber}->{'Sequence'};
524
525        my $shortname = "";
526        if ( $filename =~ /^\s*(.*?)\|\s*(.*?)\s*$/ )
527        {
528            $shortname = $1;
529            $filename = $2;
530        }
531
532        # unique is the combination of $component and $filename
533        my $key = "$comp/$filename";
534
535        if ( exists($uniquefilename{$key}) ) { installer::exiter::exit_program("ERROR: Component/FileName \"$key\" is not unique in table \"File\" !", "create_database_hashes"); }
536
537        my $value = $uniquename;
538        if ( $shortname ne "" ) { $value = "$uniquename;$shortname"; }
539        $uniquefilename{$key} = $value; # saving the unique keys and short names in hash
540
541        # Saving reverse keys too
542        $revuniquefilename{$uniquename} = $key;
543        if ( $shortname ne "" ) { $revshortfilename{$shortname} = $key; }
544
545        # Saving Sequences for unique names (and also components)
546        $allupdatesequences{$uniquename} = $sequence;
547        $allupdatecomponents{$uniquename} = $comp;
548
549        # Saving unique names and components for sequences
550        $allupdatefileorder{$sequence} = $uniquename;
551        $allupdatecomponentorder{$sequence} = $comp;
552    }
553
554    # 2. Hash, required in Directory table.
555
556    my $dirtable = $database->{'Directory'};
557
558    foreach my $linenumber ( keys  %{$dirtable} )
559    {
560        my $dir = $dirtable->{$linenumber}->{'Directory'}; # this is a unique name
561        my $defaultdir = $dirtable->{$linenumber}->{'DefaultDir'};
562
563        my $shortname = "";
564        if ( $defaultdir =~ /^\s*(.*?)\|\s*(.*?)\s*$/ )
565        {
566            $shortname = $1;
567            $shortdirname{$dir} = $shortname;   # collecting only the short names
568        }
569    }
570
571    # 3. Hash, collecting info from Component table.
572    # ComponentID and KeyPath have to be reused.
573
574    my $comptable = $database->{'Component'};
575
576    foreach my $linenumber ( keys  %{$comptable} )
577    {
578        my $comp = $comptable->{$linenumber}->{'Component'};
579        my $compid = $comptable->{$linenumber}->{'ComponentId'};
580        my $keypath = $comptable->{$linenumber}->{'KeyPath'};
581
582        $componentid{$comp} = $compid;
583        $componentidkeypath{$comp} = $keypath;
584    }
585
586    # 4. Hash, property table, required for ProductCode and Installlocation.
587
588    my $proptable = $database->{'Property'};
589
590    foreach my $linenumber ( keys  %{$proptable} )
591    {
592        my $prop = $proptable->{$linenumber}->{'Property'};
593        my $value = $proptable->{$linenumber}->{'Value'};
594
595        $alloldproperties{$prop} = $value;
596    }
597
598    # 5. Media table, getting last sequence
599
600    my $mediatable = $database->{'Media'};
601    $installer::globals::updatelastsequence = 0;
602
603    foreach my $linenumber ( keys  %{$mediatable} )
604    {
605        my $cabname = $mediatable->{$linenumber}->{'Cabinet'};
606        my $lastsequence = $mediatable->{$linenumber}->{'LastSequence'};
607        my $diskid = $mediatable->{$linenumber}->{'DiskId'};
608        $allupdatelastsequences{$cabname} = $lastsequence;
609        $allupdatediskids{$cabname} = $diskid;
610
611        if ( $lastsequence > $installer::globals::updatelastsequence ) { $installer::globals::updatelastsequence = $lastsequence; }
612    }
613
614    $installer::globals::updatesequencecounter = $installer::globals::updatelastsequence;
615
616    return (\%uniquefilename, \%revuniquefilename, \%revshortfilename, \%allupdatesequences, \%allupdatecomponents, \%allupdatefileorder, \%allupdatecomponentorder, \%shortdirname, \%componentid, \%componentidkeypath, \%alloldproperties, \%allupdatelastsequences, \%allupdatediskids);
617}
618
619
6201;
621