#**************************************************************
#  
#  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::windows::sign;

use Cwd;
use installer::converter;
use installer::existence;
use installer::files;
use installer::globals;
use installer::scriptitems;
use installer::worker;
use installer::windows::admin;

########################################################
# Copying an existing Windows installation set.
########################################################

sub copy_install_set
{
	my ( $installsetpath ) = @_;

	installer::logger::include_header_into_logfile("Start: Copying installation set $installsetpath");

	my $infoline = "";

	my $dirname = $installsetpath;
	installer::pathanalyzer::make_absolute_filename_to_relative_filename(\$dirname);

	my $path = $installsetpath;
	installer::pathanalyzer::get_path_from_fullqualifiedname(\$path);

	$path =~ s/\Q$installer::globals::separator\E\s*$//;

	if ( $dirname =~ /\./ ) { $dirname =~ s/\./_signed_inprogress./; }
	else { $dirname = $dirname . "_signed_inprogress"; }
	
	my $newpath = $path . $installer::globals::separator . $dirname;
	my $removepath = $newpath;
	$removepath =~ s/_inprogress/_witherror/;
	
	if ( -d $newpath ) { installer::systemactions::remove_complete_directory($newpath, 1); }
	if ( -d $removepath ) { installer::systemactions::remove_complete_directory($removepath, 1); }

	$infoline = "Copy installation set from $installsetpath to $newpath\n";
	$installer::logger::Lang->print($infoline);

	$installsetpath = installer::systemactions::copy_complete_directory($installsetpath, $newpath);

	installer::logger::include_header_into_logfile("End: Copying installation set $installsetpath");

	return $newpath;
}

########################################################
# Renaming an existing Windows installation set.
########################################################

sub rename_install_set
{
	my ( $installsetpath ) = @_;

	my $infoline = "";

	my $dirname = $installsetpath;
	installer::pathanalyzer::make_absolute_filename_to_relative_filename(\$dirname);

	my $path = $installsetpath;
	installer::pathanalyzer::get_path_from_fullqualifiedname(\$path);

	$path =~ s/\Q$installer::globals::separator\E\s*$//;

	if ( $dirname =~ /\./ ) { $dirname =~ s/\./_inprogress./; }
	else { $dirname = $dirname . "_inprogress"; }
	
	my $newpath = $path . $installer::globals::separator . $dirname;
	my $removepath = $newpath;
	$removepath =~ s/_inprogress/_witherror/;
	
	if ( -d $newpath ) { installer::systemactions::remove_complete_directory($newpath, 1); }
	if ( -d $removepath ) { installer::systemactions::remove_complete_directory($removepath, 1); }

	$installsetpath = installer::systemactions::rename_directory($installsetpath, $newpath);

	return $newpath;
}

#########################################################
# Checking the local system
# Checking existence of needed files in include path
#########################################################

sub check_system_path
{
	# The following files have to be found in the environment variable PATH
	# Only, if \"-sign\" is used.
	# Windows : "msicert.exe", "diff.exe", "msidb.exe", "signtool.exe"

	my @needed_files_in_path = ("msicert.exe", "msidb.exe", "signtool.exe", "diff.exe");
	if ( $installer::globals::internal_cabinet_signing )
	{
		push(@needed_files_in_path, "cabarc.exe");
		push(@needed_files_in_path, "makecab.exe");
	}

	my $onefile;	
	my $error = 0;
	my $pathvariable = $ENV{'PATH'};
	my $local_pathseparator = $installer::globals::pathseparator;
	
	if( $^O =~ /cygwin/i )
	{	# When using cygwin's perl the PATH variable is POSIX style and ...
		$pathvariable = qx{cygpath -mp "$pathvariable"} ;
		# has to be converted to DOS style for further use.
		$local_pathseparator = ';';
	}
	
	my $patharrayref = installer::converter::convert_stringlist_into_array(\$pathvariable, $local_pathseparator);
	
	$installer::globals::patharray = $patharrayref;
		
	foreach my $onefile ( @needed_files_in_path )
	{

        $installer::logger::Info->printf("...... searching %s ...\n", $onefile);

		my $fileref = installer::scriptitems::get_sourcepath_from_filename_and_includepath_classic(\$onefile, $patharrayref , 0);

		if ( $$fileref eq "" )
		{
			$error = 1;
			installer::logger::print_error( "$onefile not found\n" );
		}
		else
		{
            $installer::logger::Info->printf("\tFound: %s\n", $$fileref);
		}		
	}

	$installer::globals::signfiles_checked = 1;

	if ( $error ) { installer::exiter::exit_program("ERROR: Could not find all needed files in path!", "check_system_path"); }
}

######################################################
# Making systemcall
######################################################

sub make_systemcall
{
	my ($systemcall, $displaysystemcall) = @_;

    $installer::logger::Info->printf("... %s ...\n", $displaysystemcall);

	my $success = 1;
	my $returnvalue = system($systemcall);

	my $infoline = "Systemcall: $displaysystemcall\n";
	$installer::logger::Lang->print($infoline);

	if ($returnvalue)
	{
		$infoline = "ERROR: Could not execute \"$displaysystemcall\"!\n";
		$installer::logger::Lang->print($infoline);
		$success = 0;
	}
	else
	{
		$infoline = "Success: Executed \"$displaysystemcall\" successfully!\n";
		$installer::logger::Lang->print($infoline);
	}
	
	return $success;
}	

######################################################
# Making systemcall with warning
######################################################

sub make_systemcall_with_warning
{
	my ($systemcall, $displaysystemcall) = @_;

    $installer::logger::Info->printf("... %s ...\n", $displaysystemcall);

	my $success = 1;
	my $returnvalue = system($systemcall);

	my $infoline = "Systemcall: $displaysystemcall\n";
	$installer::logger::Lang->print($infoline);

	if ($returnvalue)
	{
		$infoline = "WARNING: Could not execute \"$displaysystemcall\"!\n";
		$installer::logger::Lang->print($infoline);
		$success = 0;
	}
	else
	{
		$infoline = "Success: Executed \"$displaysystemcall\" successfully!\n";
		$installer::logger::Lang->print($infoline);
	}
	
	return $success;
}	

######################################################
# Making systemcall with more return data
######################################################

sub execute_open_system_call
{
	my ( $systemcall ) = @_;

	my @openoutput = ();
	my $success = 1;

	my $comspec = $ENV{COMSPEC};
	$comspec = $comspec . " -c ";

	if( $^O =~ /cygwin/i )
	{
		# $comspec =~ s/\\/\\\\/g;
		# $comspec = qx{cygpath -u "$comspec"};
		# $comspec =~ s/\s*$//g;
		$comspec = "";
	}

	my $localsystemcall = "$comspec $systemcall 2>&1 |";

	open( OPN, "$localsystemcall") or warn "Can't execute $localsystemcall\n";
	while (<OPN>) { push(@openoutput, $_); }
	close (OPN);

	my $returnvalue = $?;	# $? contains the return value of the systemcall

	if ($returnvalue)
	{
		$infoline = "ERROR: Could not execute \"$systemcall\"!\n";
		$installer::logger::Lang->print($infoline);
		$success = 0;
	}
	else
	{
		$infoline = "Success: Executed \"$systemcall\" successfully!\n";
		$installer::logger::Lang->print($infoline);
	}

	return ($success, \@openoutput);
}

########################################################
# Reading first line of pw file.
########################################################

sub get_pw
{
	my ( $file ) = @_;
	
	my $filecontent = installer::files::read_file($file);

	my $pw = ${$filecontent}[0];
	$pw =~ s/^\s*//;	
	$pw =~ s/\s*$//;
	
	return $pw;	
}

########################################################
# Counting the keys of a hash.
########################################################

sub get_hash_count
{
	my ($hashref) = @_;
	
	my $counter = 0;
	
	foreach my $key ( keys %{$hashref} ) { $counter++; }
	
	return $counter;
}

############################################################
# Collect all last files in a cabinet file. This is 
# necessary to control, if the cabinet file was damaged
# by calling signtool.exe.
############################################################

sub analyze_file_file
{
	my ($filecontent) = @_;

	my %filenamehash = ();

	for ( my $i = 0; $i <= $#{$filecontent}; $i++ )
	{
		if ( $i < 3 ) { next; }

		if ( ${$filecontent}[$i] =~ /^\s*(.*?)\t(.*?)\t(.*?)\t(.*?)\t(.*?)\t(.*?)\t(.*?)\t(.*?)\s*$/ )
		{
			my $name = $1;
			my $sequence = $8;

			$filenamehash{$sequence} = $name;
		}
	}

	return ( \%filenamehash );
}

############################################################
# Collect all DiskIds to the corresponding cabinet files.
############################################################

sub analyze_media_file
{
	my ($filecontent) = @_;
	
	my %diskidhash = ();
	my %lastsequencehash = ();

	for ( my $i = 0; $i <= $#{$filecontent}; $i++ )
	{
		if ( $i < 3 ) { next; }

		if ( ${$filecontent}[$i] =~ /^\s*(.*?)\t(.*?)\t(.*?)\t(.*?)\t(.*?)\t(.*?)\s*$/ )
		{
			my $diskid = $1;
			my $lastsequence = $2;
			my $cabfile = $4;

			$diskidhash{$cabfile} = $diskid;
			$lastsequencehash{$cabfile} = $lastsequence;
		}
	}

	return ( \%diskidhash, \%lastsequencehash );
}

########################################################
# Collect all DiskIds from database table "Media".
########################################################

sub collect_diskid_from_media_table
{
	my ($msidatabase, $languagestring) = @_;
	
	# creating working directory
	my $workdir = installer::systemactions::create_directories("media", \$languagestring);
	installer::windows::admin::extract_tables_from_pcpfile($msidatabase, $workdir, "Media File");

	# Reading tables
	my $filename = $workdir . $installer::globals::separator . "Media.idt";
	if ( ! -f $filename ) { installer::exiter::exit_program("ERROR: Could not find required file: $filename !", "collect_diskid_from_media_table"); }
	my $filecontent = installer::files::read_file($filename);
	my ( $diskidhash, $lastsequencehash ) = analyze_media_file($filecontent);

	$filename = $workdir . $installer::globals::separator . "File.idt";
	if ( ! -f $filename ) { installer::exiter::exit_program("ERROR: Could not find required file: $filename !", "collect_diskid_from_media_table"); }
	$filecontent = installer::files::read_file($filename);
	my $filenamehash = analyze_file_file($filecontent);

	return ( $diskidhash, $filenamehash, $lastsequencehash );
}

########################################################
# Check, if this installation set contains
# internal cabinet files included into the msi
# database.
########################################################

sub check_for_internal_cabfiles
{
	my ($cabfilehash) = @_;
	
	my $contains_internal_cabfiles = 0;
	my %allcabfileshash = ();
	
	foreach my $filename ( keys %{$cabfilehash} )
	{
		if ( $filename =~ /^\s*\#/ )	 # starting with a hash
		{
			$contains_internal_cabfiles = 1;
			# setting real filename without hash as key and name with hash as value
			my $realfilename = $filename;
			$realfilename =~ s/^\s*\#//;
			$allcabfileshash{$realfilename} = $filename;
		}
	}
	
	return ( $contains_internal_cabfiles, \%allcabfileshash );
}

########################################################
# Collecting all files in an installation set.
########################################################

sub analyze_installset_content
{
	my ( $installsetpath ) = @_;

	my @sourcefiles = ();
	my $pathstring = "";
	installer::systemactions::read_complete_directory($installsetpath, $pathstring, \@sourcefiles);

	if ( ! ( $#sourcefiles > -1 )) { installer::exiter::exit_program("ERROR: No file in installation set. Path: $installsetpath !", "analyze_installset_content"); }

	my %allcabfileshash = ();
	my %allmsidatabaseshash = ();
	my %allfileshash = ();
	my $contains_external_cabfiles = 0;
	my $msidatabase = "";
	my $contains_msidatabase = 0;

	for ( my $j = 0; $j <= $#sourcefiles; $j++ )
	{
		if ( $sourcefiles[$j] =~ /\.cab\s*$/ ) { $allcabfileshash{$sourcefiles[$j]} = 1; }
		else
		{
			if ( $sourcefiles[$j] =~ /\.txt\s*$/ ) { next; }
			if ( $sourcefiles[$j] =~ /\.html\s*$/ ) { next; }
			if ( $sourcefiles[$j] =~ /\.ini\s*$/ ) { next; }
			if ( $sourcefiles[$j] =~ /\.bmp\s*$/ ) { next; }
			if ( $sourcefiles[$j] =~ /\.msi\s*$/ )
			{
				if ( $msidatabase eq "" ) { $msidatabase = $sourcefiles[$j]; }
				else { installer::exiter::exit_program("ERROR: There is more than one msi database in installation set. Path: $installsetpath !", "analyze_installset_content"); }
			}
			$allfileshash{$sourcefiles[$j]} = 1;
		}
	}
		
	# Is there at least one cab file in the installation set?
	my $cabcounter = get_hash_count(\%allcabfileshash);
	if ( $cabcounter > 0 ) { $contains_external_cabfiles = 1; }
	
	# How about a cab file without a msi database?
	if (( $cabcounter > 0 ) && ( $msidatabase eq "" )) { installer::exiter::exit_program("ERROR: There is no msi database in the installation set, but an external cabinet file. Path: $installsetpath !", "collect_installset_content"); }

	if ( $msidatabase ne "" ) { $contains_msidatabase = 1; }

	return (\%allcabfileshash, \%allfileshash, $msidatabase, $contains_external_cabfiles, $contains_msidatabase, \@sourcefiles);
}

########################################################
# Adding content of external cabinet files into the
# msi database
########################################################

sub msicert_database
{
	my ($msidatabase, $allcabfiles, $cabfilehash, $internalcabfile) = @_;

	my $fullsuccess = 1;

	foreach my $cabfile ( keys %{$allcabfiles} )
	{
		my $origfilesize = -s $cabfile;
		
		my $mediacabfilename = $cabfile;
		if ( $internalcabfile ) { $mediacabfilename = "\#" . $mediacabfilename; }
		if ( ! exists($cabfilehash->{$mediacabfilename}) ) { installer::exiter::exit_program("ERROR: Could not determine DiskId from media table for cabinet file \"$cabfile\" !", "msicert_database"); }
		my $diskid = $cabfilehash->{$mediacabfilename};

		my $systemcall = "msicert.exe -d $msidatabase -m $diskid -c $cabfile -h";
 		$success = make_systemcall($systemcall, $systemcall);
		if ( ! $success ) { $fullsuccess = 0; }

		# size of cabinet file must not change		
		my $finalfilesize = -s $cabfile;
		
		if ( $origfilesize != $finalfilesize ) { installer::exiter::exit_program("ERROR: msicert.exe changed size of cabinet file !", "msicert_database"); }
	}
	
	return $fullsuccess;
}

########################################################
# Checking if cabinet file was broken by signtool.
########################################################

sub cabinet_cosistency_check
{
	my ( $onefile, $followmeinfohash, $filenamehash, $lastsequencehash, $temppath ) = @_;
	
	my $infoline = "Making consistency check of $onefile\n"; 
	$installer::logger::Lang->print($infoline);
	my $expandfile = "expand.exe";	# Has to be in the path

	if ( $^O =~ /cygwin/i )
	{
		$expandfile = qx(cygpath -u "$ENV{WINDIR}"/System32/expand.exe);
		chomp $expandfile;
	}
	 		
	if ( $filenamehash == 0 )
	{
		$infoline = "Warning: Stopping consistency check: Important hash of filenames is empty!\n";
		$installer::logger::Lang->print($infoline);
	}
	elsif  ( $lastsequencehash == 0 )
	{
		$infoline = "Warning: Stopping consistency check; Important hash of last sequences is empty!\n";
		$installer::logger::Lang->print($infoline);	 			
	}
	else # both hashes are available
	{
		# $onefile contains only the name of the cabinet file without path
	 	my $sequence = $lastsequencehash->{$onefile};
	 	my $lastfile = $filenamehash->{$sequence};
		$infoline = "Check of $onefile: Sequence: $sequence is file: $lastfile\n";
		$installer::logger::Lang->print($infoline);
	 			
	 	# Therefore the file $lastfile need to be binary compared.
	 	# It has to be expanded from the cabinet file
	 	# of the original installation set and from the
	 	# newly signed cabinet file.
	 	
		# How about cabinet files extracted from msi database?
		my $finalinstalldir = $followmeinfohash->{'finalinstalldir'};
		
		$finalinstalldir =~ s/\\\s*$//;
		$finalinstalldir =~ s/\/\s*$//;
		my $sourcecabfile = $finalinstalldir . $installer::globals::separator . $onefile;
		my $currentpath = cwd();
		my $destcabfile = $currentpath . $installer::globals::separator . $onefile;
		# my $destcabfile = $onefile;
		
		if ( $^O =~ /cygwin/i )
		{
			chomp( $destcabfile = qx{cygpath -w "$destcabfile"} );
			$destcabfile =~ s/\\/\//g;
		}

		if ( ! -f $sourcecabfile ) 
		{
			$infoline = "WARNING: Check of cab file cannot happen, because source cabinet file was not found: $sourcecabfile\n";
			$installer::logger::Lang->print($infoline);	
		}
		elsif ( ! -f $destcabfile ) 
		{
			$infoline = "WARNING: Check of cab file cannot happen, because destination cabinet file was not found: $sourcecabfile\n";
			$installer::logger::Lang->print($infoline);				
		}
		else # everything is okay for the check
		{
			my $diffpath = get_diff_path($temppath);
		
			my $origdiffpath = $diffpath . $installer::globals::separator . "orig";
			my $newdiffpath = $diffpath . $installer::globals::separator . "new";
		
			if ( ! -d $origdiffpath ) { mkdir($origdiffpath); }
			if ( ! -d $newdiffpath ) { mkdir($newdiffpath); }

			my $systemcall = "$expandfile $sourcecabfile $origdiffpath -f:$lastfile ";
			$infoline = $systemcall . "\n";
			$installer::logger::Lang->print($infoline);

			my $success = make_systemcall($systemcall, $systemcall);
			if ( ! $success ) { installer::exiter::exit_program("ERROR: Could not successfully execute: $systemcall !", "cabinet_cosistency_check"); }
				
			$systemcall = "$expandfile $destcabfile $newdiffpath -f:$lastfile ";
			$infoline = $systemcall . "\n";
			$installer::logger::Lang->print($infoline);
		
			$success = make_systemcall($systemcall, $systemcall);
			if ( ! $success ) { installer::exiter::exit_program("ERROR: Could not successfully execute: $systemcall !", "cabinet_cosistency_check"); }

			# and finally the two files can be diffed.
			my $origfile = $origdiffpath . $installer::globals::separator . $lastfile; 
			my $newfile = $newdiffpath . $installer::globals::separator . $lastfile;
		
			if ( ! -f $origfile ) { installer::exiter::exit_program("ERROR: Unpacked original file not found: $origfile !", "cabinet_cosistency_check"); }
			if ( ! -f $newfile ) { installer::exiter::exit_program("ERROR: Unpacked new file not found: $newfile !", "cabinet_cosistency_check"); }
		
			my $origsize = -s $origfile;
			my $newsize = -s $newfile;
		
			if ( $origsize != $newsize ) # This shows an error!
			{			
				$infoline = "ERROR: Different filesize after signtool.exe was used. Original: $origsize Bytes, new: $newsize. File: $lastfile\n";
				$installer::logger::Lang->print($infoline);
				installer::exiter::exit_program("ERROR: The cabinet file $destcabfile is broken after signtool.exe signed this file !", "cabinet_cosistency_check");
			}
			else
			{
				$infoline = "Same size of last file in cabinet file after usage of signtool.exe: $newsize (File: $lastfile)\n";
				$installer::logger::Lang->print($infoline);

				# Also making a binary diff?

				my $difffile = "diff.exe";  # has to be in the path
				# $systemcall = "$difffile $sourcecabfile $destcabfile";  # Test for differences
				$systemcall = "$difffile $origfile $newfile";
				$infoline = $systemcall . "\n";
				$returnvalue = make_systemcall($systemcall, $systemcall);

				my $success = $?;

				if ( $success == 0 )
				{
					$infoline = "Last files are identical after signing cabinet file (File: $lastfile)\n";
					$installer::logger::Lang->print($infoline);
				}
				elsif ( $success == 1 )
				{
					$infoline = "ERROR: Last files are different after signing cabinet file (File: $lastfile)\n";
					$installer::logger::Lang->print($infoline);				
					installer::exiter::exit_program("ERROR: Last files are different after signing cabinet file (File: $lastfile)!", "cabinet_cosistency_check");
				}		
				else
				{
					$infoline = "ERROR: Problem occured calling diff.exe (File: $lastfile)\n";
					$installer::logger::Lang->print($infoline);								
					installer::exiter::exit_program("ERROR: Problem occured calling diff.exe (File: $lastfile) !", "cabinet_cosistency_check");
				}
			}
		}		
	}	

}

########################################################
# Signing a list of files
########################################################

sub sign_files
{
	my ( $followmeinfohash, $allfiles, $pw, $cabinternal, $filenamehash, $lastsequencehash, $temppath ) = @_;
	
	my $infoline = "";
	my $fullsuccess = 1;
	my $maxcounter = 3;
	
	my $productname = "";
	if ( $followmeinfohash->{'allvariableshash'}->{'PRODUCTNAME'} ) { $productname = "/d " . "\"$followmeinfohash->{'allvariableshash'}->{'PRODUCTNAME'}\""; }
	my $url = "/du " . "\"http://www.openoffice.org\"";
	my $timestampurl = "http://timestamp.verisign.com/scripts/timestamp.dll";
	
	my $pfxfilepath = $installer::globals::pfxfile;
	
	if( $^O =~ /cygwin/i )
	{
		$pfxfilepath = qx{cygpath -w "$pfxfilepath"};
		$pfxfilepath =~ s/\\/\\\\/g;
		$pfxfilepath =~ s/\s*$//g;
	}
	
	foreach my $onefile ( reverse sort keys %{$allfiles} )
	{
		if ( already_certified($onefile) )
		{
			$infoline = "Already certified: Skipping file $onefile\n";
			$installer::logger::Lang->print($infoline);
			next;
		}
		
		my $counter = 1;
		my $success = 0;
		
		while (( $counter <= $maxcounter ) && ( ! $success ))
		{
			if ( $counter > 1 )
            {
                $installer::logger::Info->printf("\n");
                $installer::logger::Info->printf("\n");
                $installer::logger::Info->printf("... repeating file %s ...\n", $onefile);
            }
			if ( $cabinternal )
            {
                $installer::logger::Info->printf("    Signing: %s\n", $onefile);
            }
			my $systemcall = "signtool.exe sign /f \"$pfxfilepath\" /p $pw $productname $url /t \"$timestampurl\" \"$onefile\"";
			my $displaysystemcall = "signtool.exe sign /f \"$pfxfilepath\" /p ***** $productname $url /t \"$timestampurl\" \"$onefile\"";
	 		$success = make_systemcall_with_warning($systemcall, $displaysystemcall);
	 		$counter++;
	 	}
	 	
	 	# Special check for cabinet files, that sometimes get damaged by signtool.exe
	 	if (( $success ) && ( $onefile =~ /\.cab\s*$/ ) && ( ! $cabinternal ))
	 	{
	 		cabinet_cosistency_check($onefile, $followmeinfohash, $filenamehash, $lastsequencehash, $temppath);
		}

 		if ( ! $success )
 		{
 			$fullsuccess = 0;
			installer::exiter::exit_program("ERROR: Could not sign file: $onefile!", "sign_files");
		}
	}
	
	return $fullsuccess;
}

##########################################################################
# Lines in ddf files must not contain more than 256 characters
##########################################################################

sub check_ddf_file
{
	my ( $ddffile, $ddffilename ) = @_;

	my $maxlength = 0;
	my $maxline = 0;
	my $linelength = 0;
	my $linenumber = 0;

	for ( my $i = 0; $i <= $#{$ddffile}; $i++ )
	{
		my $oneline = ${$ddffile}[$i];
		
		$linelength = length($oneline);
		$linenumber = $i + 1;
		
		if ( $linelength > 256 )
		{
			installer::exiter::exit_program("ERROR \"$ddffilename\" line $linenumber: Lines in ddf files must not contain more than 256 characters!", "check_ddf_file");
		}
		
		if ( $linelength > $maxlength )
		{
			$maxlength = $linelength;
			$maxline = $linenumber;
		}
	}
	
	my $infoline = "Check of ddf file \"$ddffilename\": Maximum length \"$maxlength\" in line \"$maxline\" (allowed line length: 256 characters)\n"; 
	$installer::logger::Lang->print($infoline);
}

#################################################################
# Setting the path, where the cab files are unpacked.
#################################################################

sub get_cab_path
{
	my ($temppath) = @_;

	my $cabpath = "cabs_" . $$;
	$cabpath = $temppath . $installer::globals::separator . $cabpath;
	if ( ! -d $cabpath ) { installer::systemactions::create_directory($cabpath); }

	return $cabpath;	
}

#################################################################
# Setting the path, where the diff can happen.
#################################################################

sub get_diff_path
{
	my ($temppath) = @_;

	my $diffpath = "diff_" . $$;
	$diffpath = $temppath . $installer::globals::separator . $diffpath;
	if ( ! -d $diffpath ) { installer::systemactions::create_directory($diffpath); }

	return $diffpath;	
}

#################################################################
# Exclude all cab files from the msi database.
#################################################################

sub extract_cabs_from_database
{
	my ($msidatabase, $allcabfiles) = @_;

	installer::logger::include_header_into_logfile("Extracting cabs from msi database");

	my $infoline = "";
	my $fullsuccess = 1;
	my $msidb = "msidb.exe";	# Has to be in the path

	# msidb.exe really wants backslashes. (And double escaping because system() expands the string.)
	$msidatabase =~ s/\//\\\\/g;

	foreach my $onefile ( keys %{$allcabfiles} )
	{
		my $systemcall = $msidb . " -d " . $msidatabase . " -x " . $onefile;
 		my $success = make_systemcall($systemcall, $systemcall);
		if ( ! $success ) { $fullsuccess = 0; }
		
		# and removing the stream from the database
		$systemcall = $msidb . " -d " . $msidatabase . " -k " . $onefile;
 		$success = make_systemcall($systemcall, $systemcall);
		if ( ! $success ) { $fullsuccess = 0; }
	}
	
	return $fullsuccess;
}

#################################################################
# Include cab files into the msi database.
#################################################################

sub include_cabs_into_database
{
	my ($msidatabase, $allcabfiles) = @_;

	installer::logger::include_header_into_logfile("Including cabs into msi database");

	my $infoline = "";
	my $fullsuccess = 1;
	my $msidb = "msidb.exe";	# Has to be in the path

	# msidb.exe really wants backslashes. (And double escaping because system() expands the string.)
	$msidatabase =~ s/\//\\\\/g;

	foreach my $onefile ( keys %{$allcabfiles} )
	{
		my $systemcall = $msidb . " -d " . $msidatabase . " -a " . $onefile;
 		my $success = make_systemcall($systemcall, $systemcall);
		if ( ! $success ) { $fullsuccess = 0; }
	}
	
	return $fullsuccess;
}

########################################################
# Reading the order of the files inside the
# cabinet files.
########################################################

sub read_cab_file
{
	my ($cabfilename) = @_;

    $installer::logger::Info->printf("\n");
    $installer::logger::Info->printf("... reading cabinet file %s ...\n", $cabfilename);
	my $infoline = "Reading cabinet file $cabfilename\n";
	$installer::logger::Lang->print($infoline);

	my $systemcall = "cabarc.exe" . " L " . $cabfilename;
	push(@logfile, "$systemcall\n");
	
	my ($success, $fileorder) = execute_open_system_call($systemcall);

	my @allfiles = ();
	
	for ( my $i = 0; $i <= $#{$fileorder}; $i++ )
	{
		my $line = ${$fileorder}[$i];
		if ( $line =~ /^\s*(.*?)\s+\d+\s+\d+\/\d+\/\d+\s+\d+\:\d+\:\d+\s+[\w-]+\s*$/ )
		{
			my $filename = $1;
			push(@allfiles, $filename);
		}
	}
		
	return \@allfiles;
}

########################################################
# Unpacking a cabinet file.
########################################################

sub unpack_cab_file
{
	my ($cabfilename, $temppath) = @_;

    $installer::logger::Info->printf("\n");
    $installer::logger::Info->printf("... unpacking cabinet file %s ...\n", $cabfilename);
	my $infoline = "Unpacking cabinet file $cabfilename\n";
	$installer::logger::Lang->print($infoline);
	
	my $dirname = $cabfilename;
	$dirname =~ s/\.cab\s*$//;
	my $workingpath = $temppath . $installer::globals::separator . "unpack_". $dirname . "_" . $$;	
	if ( ! -d $workingpath ) { installer::systemactions::create_directory($workingpath); }

	# changing into unpack directory
	my $from = cwd();
	chdir($workingpath);

	my $fullcabfilename = $from . $installer::globals::separator . $cabfilename;

	if( $^O =~ /cygwin/i )
	{
		$fullcabfilename = qx{cygpath -w "$fullcabfilename"};
		$fullcabfilename =~ s/\\/\\\\/g;
		$fullcabfilename =~ s/\s*$//g;
	}

	my $systemcall = "cabarc.exe" . " -p X " . $fullcabfilename;
	$success = make_systemcall($systemcall, $systemcall);
	if ( ! $success ) { installer::exiter::exit_program("ERROR: Could not unpack cabinet file: $fullcabfilename!", "unpack_cab_file"); }

	# returning to directory
	chdir($from);

	return $workingpath;
}

########################################################
# Returning the header of a ddf file.
########################################################

sub get_ddf_file_header
{
	my ($ddffileref, $cabinetfile, $installdir) = @_;
	
	my $oneline;
	my $compressionlevel = 2;

	if( $^O =~ /cygwin/i )
	{
		$installdir = qx{cygpath -w "$installdir"};
		$installdir =~ s/\s*$//g;
	}
	
	$oneline = ".Set CabinetName1=" . $cabinetfile . "\n";
	push(@{$ddffileref} ,$oneline);
	$oneline = ".Set ReservePerCabinetSize=128\n";	# This reserves space for a digital signature.
	push(@{$ddffileref} ,$oneline);
	$oneline = ".Set MaxDiskSize=2147483648\n";		# This allows the .cab file to get a size of 2 GB.
	push(@{$ddffileref} ,$oneline);
	$oneline = ".Set CompressionType=LZX\n";
	push(@{$ddffileref} ,$oneline);
	$oneline = ".Set Compress=ON\n";
	push(@{$ddffileref} ,$oneline);
	$oneline = ".Set CompressionLevel=$compressionlevel\n";
	push(@{$ddffileref} ,$oneline);
	$oneline = ".Set Cabinet=ON\n";
	push(@{$ddffileref} ,$oneline);
	$oneline = ".Set DiskDirectoryTemplate=" . $installdir . "\n";
	push(@{$ddffileref} ,$oneline);
}

########################################################
# Writing content into ddf file.
########################################################

sub put_all_files_into_ddffile
{
	my ($ddffile, $allfiles, $workingpath) = @_;

	$workingpath =~ s/\//\\/g;

	for ( my $i = 0; $i <= $#{$allfiles}; $i++ )
	{
		my $filename = ${$allfiles}[$i];
		if( $^O =~ /cygwin/i ) { $filename =~ s/\//\\/g; } # Backslash for Cygwin!
		if ( ! -f $filename ) { installer::exiter::exit_program("ERROR: Could not find file: $filename!", "put_all_files_into_ddffile"); }
		my $infoline = "\"" . $filename . "\"" . " " . ${$allfiles}[$i] . "\n";
		push( @{$ddffile}, $infoline);
	}
}

########################################################
# Packing a cabinet file.
########################################################

sub do_pack_cab_file
{
	my ($cabfilename, $allfiles, $workingpath, $temppath) = @_;

    $installer::logger::Info->print("\n");
    $installer::logger::Info->printf("... packing cabinet file %s ...\n", $cabfilename);
	my $infoline = "Packing cabinet file $cabfilename\n";
	$installer::logger::Lang->print($infoline);

	if ( -f $cabfilename ) { unlink($cabfilename); } # removing cab file
	if ( -f $cabfilename ) { installer::exiter::exit_program("ERROR: Failed to remove file: $cabfilename!", "do_pack_cab_file"); }

	# generate ddf file for makecab.exe
	my @ddffile = ();

	my $dirname = $cabfilename;
	$dirname =~ s/\.cab\s*$//;
	my $ddfpath = $temppath . $installer::globals::separator . "ddf_". $dirname . "_" . $$;	

	my $ddffilename = $cabfilename;
	$ddffilename =~ s/.cab/.ddf/;
	$ddffilename = $ddfpath . $installer::globals::separator . $ddffilename;
		
	if ( ! -d $ddfpath ) { installer::systemactions::create_directory($ddfpath); }

	my $from = cwd();

	chdir($workingpath); # changing into the directory with the unpacked files

	get_ddf_file_header(\@ddffile, $cabfilename, $from);
	put_all_files_into_ddffile(\@ddffile, $allfiles, $workingpath);
	# lines in ddf files must not be longer than 256 characters
	check_ddf_file(\@ddffile, $ddffilename);
	
	installer::files::save_file($ddffilename, \@ddffile);

	if( $^O =~ /cygwin/i )
	{
		$ddffilename = qx{cygpath -w "$ddffilename"};
		$ddffilename =~ s/\\/\\\\/g;
		$ddffilename =~ s/\s*$//g;
	}

	my $systemcall = "makecab.exe /V1 /F " . $ddffilename;
	my $success = make_systemcall($systemcall, $systemcall);
	if ( ! $success ) { installer::exiter::exit_program("ERROR: Could not pack cabinet file!", "do_pack_cab_file"); }

	chdir($from);

	return ($success);	
}

########################################################
# Extraction the file extension from a file
########################################################

sub get_extension
{
	my ( $file ) = @_;
	
	my $extension = "";
	
	if ( $file =~ /^\s*(.*)\.(\w+?)\s*$/ ) { $extension = $2; }
	
	return $extension;	
}

########################################################
# Checking, if a file already contains a certificate.
# This must not be overwritten.
########################################################

sub already_certified
{
	my ( $filename ) = @_;
	
	my $success = 1;	
	my $is_certified = 0;
	
	my $systemcall = "signtool.exe verify /q /pa \"$filename\"";
	my $returnvalue = system($systemcall);
	
	if ( $returnvalue ) { $success = 0; }

	# my $success = make_systemcall($systemcall, $systemcall);

 	if ( $success )
 	{
 		$is_certified = 1;
        $installer::logger::Info->printf("... already certified -> skipping %s ...\n", $filename);
	}

	return $is_certified;
}

########################################################
# Signing the files, that are included into
# cabinet files.
########################################################

sub sign_files_in_cabinet_files
{
	my ( $followmeinfohash, $allcabfiles, $pw, $temppath ) = @_;

	my $complete_success = 1;
	my $from = cwd();

	foreach my $cabfilename ( keys %{$allcabfiles} )
	{
		my $success = 1;
		
		# saving order of files in cab file
		my $fileorder = read_cab_file($cabfilename);

		# unpack into $working path
		my $workingpath = unpack_cab_file($cabfilename, $temppath);

		chdir($workingpath);

		# sign files
		my %allfileshash = ();
		foreach my $onefile ( @{$fileorder} )
		{
			my $extension = get_extension($onefile);
			if ( exists( $installer::globals::sign_extensions{$extension} ) )
			{
				$allfileshash{$onefile} = 1;
			}
		}
 		$success = sign_files($followmeinfohash, \%allfileshash, $pw, 1, 0, 0, $temppath);
		if ( ! $success ) { $complete_success = 0; }

		chdir($from);
		
		# pack into new directory
		do_pack_cab_file($cabfilename, $fileorder, $workingpath, $temppath);
	}
	
	return $complete_success;
}

########################################################
# Comparing the content of two directories.
# Only filesize is compared.
########################################################

sub compare_directories
{
	my ( $dir1, $dir2, $files ) = @_;
	
	$dir1 =~ s/\\\s*//;
	$dir2 =~ s/\\\s*//;
	$dir1 =~ s/\/\s*//;
	$dir2 =~ s/\/\s*//;

	my $infoline = "Comparing directories: $dir1 and $dir2\n";
	$installer::logger::Lang->print($infoline);
	
	foreach my $onefile ( @{$files} )
	{
		my $file1 = $dir1 . $installer::globals::separator . $onefile;
		my $file2 = $dir2 . $installer::globals::separator . $onefile;
		
		if ( ! -f $file1 ) { installer::exiter::exit_program("ERROR: Missing file : $file1!", "compare_directories"); }
		if ( ! -f $file2 ) { installer::exiter::exit_program("ERROR: Missing file : $file2!", "compare_directories"); }

		my $size1 = -s $file1;
		my $size2 = -s $file2;

		$infoline = "Comparing files: $file1 ($size1) and $file2 ($size2)\n";
		$installer::logger::Lang->print($infoline);
		
		if ( $size1 != $size2 )
		{
			installer::exiter::exit_program("ERROR: File defect after copy (different size) $file1 ($size1 bytes) and $file2 ($size2 bytes)!", "compare_directories");
		}
	}
}

########################################################
# Signing an existing Windows installation set.
########################################################

sub sign_install_set
{
	my ($followmeinfohash, $make_copy, $temppath) = @_;

	my $installsetpath = $followmeinfohash->{'finalinstalldir'};

	installer::logger::include_header_into_logfile("Start: Signing installation set $installsetpath");

	my $complete_success = 1;
	my $success = 1;
	
	my $infoline = "Signing installation set in $installsetpath\n";
	$installer::logger::Lang->print($infoline);

	# check required files.
	if ( ! $installer::globals::signfiles_checked ) { check_system_path(); }
	
	# get cerficate information
	my $pw = get_pw($installer::globals::pwfile);

	# making a copy of the installation set, if required
	if ( $make_copy ) { $installsetpath = copy_install_set($installsetpath); }
	else { $installsetpath = rename_install_set($installsetpath); }
	
	# collecting all files in the installation set 
	my ($allcabfiles, $allfiles, $msidatabase, $contains_external_cabfiles, $contains_msidatabase, $sourcefiles) = analyze_installset_content($installsetpath);

	if ( $make_copy ) { compare_directories($installsetpath, $followmeinfohash->{'finalinstalldir'}, $sourcefiles); }

	# changing into installation set
	my $from = cwd();
	my $fullmsidatabase = $installsetpath . $installer::globals::separator . $msidatabase;

	if( $^O =~ /cygwin/i )
	{
		$fullmsidatabase = qx{cygpath -w "$fullmsidatabase"};
		$fullmsidatabase =~ s/\\/\\\\/g;
		$fullmsidatabase =~ s/\s*$//g;
	}

	chdir($installsetpath);

	if ( $contains_msidatabase )
	{
		# exclude media table from msi database and get all diskids.
		my ( $cabfilehash, $filenamehash, $lastsequencehash ) = collect_diskid_from_media_table($msidatabase, $followmeinfohash->{'languagestring'});

		# Check, if there are internal cab files 
		my ( $contains_internal_cabfiles, $all_internal_cab_files) = check_for_internal_cabfiles($cabfilehash);

		if ( $contains_internal_cabfiles )
		{
			my $cabpath = get_cab_path($temppath);
			chdir($cabpath);

			# Exclude all cabinet files from database
			$success = extract_cabs_from_database($fullmsidatabase, $all_internal_cab_files);
			if ( ! $success ) { $complete_success = 0; }

			if ( $installer::globals::internal_cabinet_signing ) { sign_files_in_cabinet_files($followmeinfohash, $all_internal_cab_files, $pw, $temppath); }

			$success = sign_files($followmeinfohash, $all_internal_cab_files, $pw, 0, $filenamehash, $lastsequencehash, $temppath);
			if ( ! $success ) { $complete_success = 0; }
			$success = msicert_database($fullmsidatabase, $all_internal_cab_files, $cabfilehash, 1);
			if ( ! $success ) { $complete_success = 0; }

			# Include all cabinet files into database
			$success = include_cabs_into_database($fullmsidatabase, $all_internal_cab_files);
			if ( ! $success ) { $complete_success = 0; }
			chdir($installsetpath);
		}

		# Warning: There might be a problem with very big cabinet files
		# signing all external cab files first
		if ( $contains_external_cabfiles )
		{
			if ( $installer::globals::internal_cabinet_signing ) { sign_files_in_cabinet_files($followmeinfohash, $allcabfiles, $pw, $temppath); }

			$success = sign_files($followmeinfohash, $allcabfiles, $pw, 0, $filenamehash, $lastsequencehash, $temppath);
			if ( ! $success ) { $complete_success = 0; }
			$success = msicert_database($msidatabase, $allcabfiles, $cabfilehash, 0);
			if ( ! $success ) { $complete_success = 0; }
		}
	}

	# finally all other files can be signed
	$success = sign_files($followmeinfohash, $allfiles, $pw, 0, 0, 0, $temppath);
	if ( ! $success ) { $complete_success = 0; }
	
	# and changing back
	chdir($from);

	installer::logger::include_header_into_logfile("End: Signing installation set $installsetpath");
	
	return ($installsetpath);
}

1;
