<?xml version="1.0" encoding="utf-8"?>
<!--
    Copyright 1991-2020 Amebis
    Copyright 2016 GÉANT

    This file is part of MSIBuild.

    MSIBuild is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    MSIBuild is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with MSIBuild. If not, see <http://www.gnu.org/licenses/>.
-->
<package>
	<job id="MakeDDF">
		<runtime>
			<description>Compile DDF file for shared CAB archive from input MSI files.</description>
			<unnamed name="&lt;output.ddf&gt;" helpstring="Output DDF file" required="true"/>
			<unnamed name="&lt;input.msi ...&gt;" helpstring="Input MSI files" required="true"/>
			<named name="I" helpstring="Input files folder" type="string" required="false"/>
			<named name="O" helpstring="CAB archive base name (default &quot;Dat&quot;)" type="string" required="false"/>
			<named name="C" helpstring="Compression (default &quot;MSZIP&quot;)" type="string" required="false"/>
		</runtime>
		<script language="JScript" src="MSI.js"/>
		<script language="JScript" src="String.js"/>
		<script language="JScript"><![CDATA[
			if (WScript.Arguments.Unnamed.Length < 2) {
				WScript.Arguments.ShowUsage();
				WScript.Quit(1);
			}

			var
				fso      = WScript.CreateObject("Scripting.FileSystemObject"),
				ddf_path = WScript.Arguments.Unnamed(0);

			try {
				var
					installer   = WScript.CreateObject("WindowsInstaller.Installer"),
					input_path  = WScript.Arguments.Named.Exists("I") ? WScript.Arguments.Named("I") : null,
					output_base = WScript.Arguments.Named.Exists("O") ? WScript.Arguments.Named("O") : "Dat",
					compression = WScript.Arguments.Named.Exists("C") ? WScript.Arguments.Named("C") : "MSZIP",
					files       = new Array(),
					i, n = WScript.Arguments.Unnamed.length;

				// No user interaction is desired.
				installer.UILevel = msiUILevelNone;

				for (i = 1; i < n; i++) {
					var
						msi_path = WScript.Arguments.Unnamed(i),
						d;

					try {
						d = MSIGetFiles(installer, msi_path, input_path);
						MSIMergeFiles(files, d);
					} catch (err) {
						throw new Error(err.number, "" + msi_path + ": " + err.description);
					}

					// Force JScript engine to free database and session objects, otherwise opening of another session will fail.
					CollectGarbage();
				}

				// Create DDF file and write header properties.
				var
					dat = fso.CreateTextFile(ddf_path, true, false);

				try {
					dat.WriteLine(".Set CabinetNameTemplate=" + output_base + "*.cab");
					dat.WriteLine(".Set CabinetName1=" + output_base + ".cab");
					dat.WriteLine(".Set InfFileName=" + output_base + ".inf");
					dat.WriteLine(".Set RptFileName=" + output_base + ".rpt");
					dat.WriteLine(".Set ReservePerCabinetSize=8");
					dat.WriteLine(".Set MaxDiskSize=0");
					dat.WriteLine(".Set CompressionType=" + compression);
					dat.WriteLine(".Set InfFileLineFormat=(*disk#*) *file#*: *file* = *Size*");
					dat.WriteLine(".Set InfHeader=");
					dat.WriteLine(".Set InfFooter=");
					dat.WriteLine(".Set InfDateFormat=yyyy-mm-dd");
					dat.WriteLine(".Set DiskDirectoryTemplate=");
					dat.WriteLine(".Set Compress=ON");
					dat.WriteLine(".Set Cabinet=ON");
					//if (input_path)
					//	dat.WriteLine(".Set SourceDir=" + input_path);
					for (fileKey in files) {
						var
							f = fso.GetFile(files[fileKey]),
							d = new Date(f.DateLastModified);

						// Seconds should be even. This is MAKECAB limitation.
						d.setSeconds(Math.round(d.getSeconds() / 2) * 2);

						dat.WriteLine("\"" + files[fileKey] + "\" /Date=" + d.getFullYear() + "-" + (d.getMonth() + 1) + "-" + d.getDate() + " /Time=" + Time2Str(d) + " " + fileKey);
					}
				} finally {
					dat.Close();
				}
			} catch (err) {
				// In case of error, delete output file.
				if (fso.FileExists(ddf_path))
					fso.DeleteFile(ddf_path);

				throw err;
			}

			WScript.Quit(0);
		]]></script>
	</job>
	<job id="MakeDEP">
		<runtime>
			<description>Compile DEP dependency file from input MSI files.</description>
			<unnamed name="&lt;output.dep&gt;" helpstring="Output DEP file" required="true"/>
			<unnamed name="&lt;target file&gt;" helpstring="Target file dependant of input" required="true"/>
			<unnamed name="&lt;input.msi ...&gt;" helpstring="Input MSI files" required="true"/>
			<named name="I" helpstring="Input files folder" type="string" required="false"/>
		</runtime>
		<script language="JScript" src="MSI.js"/>
		<script language="JScript"><![CDATA[
			if (WScript.Arguments.Unnamed.Length < 3) {
				WScript.Arguments.ShowUsage();
				WScript.Quit(1);
			}

			var
				fso      = WScript.CreateObject("Scripting.FileSystemObject"),
				dep_path = WScript.Arguments.Unnamed(0);

			try {
				var
					installer   = WScript.CreateObject("WindowsInstaller.Installer"),
					input_path  = WScript.Arguments.Named.Exists("I") ? WScript.Arguments.Named("I") : null,
					target_path = WScript.Arguments.Unnamed(1),
					files       = new Array(),
					i, n = WScript.Arguments.Unnamed.length;

				// No user interaction is desired.
				installer.UILevel = msiUILevelNone;

				for (i = 2; i < n; i++) {
					var
						msi_path = WScript.Arguments.Unnamed(i),
						d;

					d = MSIGetFiles(installer, msi_path, input_path);
					MSIMergeFiles(files, d);

					// Force JScript engine to free database and session objects, otherwise opening of another session will fail.
					CollectGarbage();
				}

				var
					dat = fso.CreateTextFile(dep_path, true, false);

				try {
					dat.Write("\"" + target_path + "\" :");
					for (fileKey in files)
						dat.Write(" \\\r\n\t\"" + files[fileKey] + "\"");
					dat.WriteLine();
					dat.WriteLine();
				} finally {
					dat.Close();
				}
			} catch (err) {
				// In case of error, delete output file.
				if (fso.FileExists(dep_path))
					fso.DeleteFile(dep_path);

				throw err;
			}

			WScript.Quit(0);
		]]></script>
	</job>
	<job id="SetCAB">
		<runtime>
			<description>Configures MSI package to use external CAB file.</description>
			<unnamed name="&lt;database.msi&gt;" helpstring="Output MSI file" required="true"/>
			<unnamed name="&lt;input.inf&gt;" helpstring="Input INF file" required="true"/>
			<named name="I" helpstring="Input files folder" type="string" required="false"/>
			<named name="E" helpstring="Embed CAB file" type="simple" required="false"/>
		</runtime>
		<script language="JScript" src="MSI.js"/>
		<script language="JScript" src="String.js"/>
		<script language="JScript"><![CDATA[
			if (WScript.Arguments.Unnamed.Length < 2) {
				WScript.Arguments.ShowUsage();
				WScript.Quit(1);
			}

			var
				installer  = WScript.CreateObject("WindowsInstaller.Installer"),
				fso        = WScript.CreateObject("Scripting.FileSystemObject"),
				input_path = WScript.Arguments.Named.Exists("I") ? WScript.Arguments.Named("I") : "",
				embed      = WScript.Arguments.Named.Exists("E") ? true : false,
				msi_path   = WScript.Arguments.Unnamed(0),
				inf_path   = WScript.Arguments.Unnamed(1),
				cab_path   = null,
				cab_name;

			// No user interaction is desired.
			installer.UILevel = msiUILevelNone;

			var
				database     = installer.OpenDatabase(msi_path, msiOpenDatabaseModeTransact),
				session      = installer.OpenPackage(database, 1),
				shortNames   = session.Mode(msiRunModeSourceShortNames),
				lastSequence = 0,
				stat,
				view,
				record,
				updateMode,
				sumInfo,
				today = new Date();

			if (input_path)
				session.Property("OriginalDatabase") = input_path;

			stat = session.DoAction("CostInitialize");
			if (stat != 1)
				throw new Error("Error calling CostInitialize() (code " + stat + ").");

			var
				dat = fso.OpenTextFile(inf_path, 1);

			try {
				var
					section         = null,
					re_section      = new RegExp("^\\s*\\[([^\\]]*)\\]\\s*$", "i"),
					re_file_list    = new RegExp("^\\s*\\((\\d+)\\)\\s*(\\d+)\\s*[:]([^=]+)[=]\\s*(\\d+)\\s*$", "i"),
					re_cabinet_list = new RegExp("^\\s*(\\d+)\\s*[,]\\s*(\\d+)\\s*[,](.*)$", "i");

				while (!dat.AtEndOfStream) {
					var
						s = new String(dat.ReadLine()),
						m;

					if (m = s.match(re_section)) {
						// We found a section.
						section = new String(m[1]);
					} else if (section.toLowerCase() == "file list") {
						if (m = s.match(re_file_list)) {
							var
								fileNum = parseInt(m[2], 10),
								fileId  = Trim(m[3]);

							// Set file sequence in database.
							view = database.OpenView("SELECT Sequence,Attributes FROM File WHERE File='" + _S(fileId) + "'");
							view.Execute();
							record = view.Fetch();
							if (record) {
								record.IntegerData(1) = fileNum;
								// record.IntegerData(2) = (record.IntegerData(2) & ~msidbFileAttributesNoncompressed) | msidbFileAttributesCompressed;
								view.Modify(msiViewModifyUpdate, record);
							}
							view.Close();

							if (fileNum > lastSequence)
								lastSequence = fileNum;
						}
					} else if (section.toLowerCase() == "cabinet list") {
						if (m = s.match(re_cabinet_list)) {
							if (cab_path)
								throw new Error("INF file can contain one CAB archive only.");

							// Read archive file name.
							cab_path = Trim(m[3]);
							cab_name = cab_path.split("\\");
							cab_name = cab_name[cab_name.length - 1];
						}
					}
				}
			} finally {
				dat.Close();
			}

			var cab_id = embed ? _MSI(cab_name) : cab_name;

			view = database.OpenView("SELECT DiskId,LastSequence,Cabinet FROM Media ORDER BY DiskId");
			view.Execute();
			record = view.Fetch();
			updateMode = msiViewModifyUpdate;
			if (!record) {
				// Media table is empty.
				record = installer.CreateRecord(3);
				record.IntegerData(1) = 1;
				updateMode = msiViewModifyInsert;
			}
			record.IntegerData(2) = lastSequence;
			record.StringData(3)  = (embed ? "#" : "") + cab_id;
			view.Modify(updateMode, record);

			sumInfo = database.SummaryInformation(3);
			sumInfo.Property(PID_LASTPRINTED)  = today.getVarDate();
			sumInfo.Property(PID_LASTSAVE_DTM) = today.getVarDate();
			sumInfo.Property(PID_WORDCOUNT)    = (shortNames ? 1 : 0) | 2;
			sumInfo.Persist();

			if (embed) {
				view = database.OpenView("SELECT Name,Data FROM _Streams");
				view.Execute();
				record = installer.CreateRecord(2);
				record.StringData(1) = cab_id;
				record.SetStream(2, cab_path);
				view.Modify(msiViewModifyAssign, record);
			}

			// Commit database
			database.Commit();

			WScript.Quit(0);
		]]></script>
	</job>
	<job id="MakeMST">
		<runtime>
			<description>Build MSI language from differences between two MSI files.</description>
			<unnamed name="&lt;source.msi&gt;" helpstring="Original MSI file" required="true"/>
			<unnamed name="&lt;destination.msi&gt;" helpstring="Updated MSI file" required="true"/>
			<unnamed name="&lt;diff.mst&gt;" helpstring="Output MST file" required="true"/>
		</runtime>
		<script language="JScript" src="MSI.js"/>
		<script language="JScript" src="String.js"/>
		<script language="JScript"><![CDATA[
			if (WScript.Arguments.Unnamed.Length < 3) {
				WScript.Arguments.ShowUsage();
				WScript.Quit(1);
			}

			var
				fso      = WScript.CreateObject("Scripting.FileSystemObject"),
				mst_path = WScript.Arguments.Unnamed(2);

			try {
				var
					installer = WScript.CreateObject("WindowsInstaller.Installer"),
					db_src    = installer.OpenDatabase(WScript.Arguments.Unnamed(0), msiOpenDatabaseModeReadOnly),
					db_dst    = installer.OpenDatabase(WScript.Arguments.Unnamed(1), msiOpenDatabaseModeReadOnly);

				db_dst.GenerateTransform(db_src, mst_path);
				db_dst.CreateTransformSummaryInfo(db_src, mst_path, msiTransformErrorNone, msiTransformValidationNone);
			} catch (err) {
				// In case of error, delete output file.
				if (fso.FileExists(mst_path))
					fso.DeleteFile(mst_path);

				throw err;
			}

			WScript.Quit(0);
		]]></script>
	</job>
	<job id="AddStorage">
		<runtime>
			<description>Adds sub-storage to MSI file.</description>
			<unnamed name="&lt;database.msi&gt;" helpstring="MSI database file" required="true"/>
			<unnamed name="&lt;transform.mst&gt;" helpstring="MST file to add as a sub-storage" required="true"/>
			<unnamed name="&lt;name&gt;" helpstring="Sub-storage name" required="true"/>
			<named name="L" helpstring="Add transform to the list of language transforms" type="boolean" required="false"/>
		</runtime>
		<script language="JScript" src="MSI.js"/>
		<script language="JScript" src="String.js"/>
		<script language="JScript"><![CDATA[
			if (WScript.Arguments.Unnamed.Length < 3) {
				WScript.Arguments.ShowUsage();
				WScript.Quit(1);
			}

			var
				fso      = WScript.CreateObject("Scripting.FileSystemObject"),
				msi_path = WScript.Arguments.Unnamed(0);

			try {
				var
					installer   = WScript.CreateObject("WindowsInstaller.Installer"),
					database    = installer.OpenDatabase(msi_path, msiOpenDatabaseModeTransact),
					view        = database.OpenView("SELECT Name,Data FROM _Storages"),
					record      = installer.CreateRecord(2),
					stream_path = WScript.Arguments.Unnamed(1),
					stream_name = WScript.Arguments.Unnamed(2);

				try {
					// Inserts or updates sub-storage.
					record.StringData(1) = stream_name;
					view.Execute(record);
					record.SetStream(2, stream_path);
					view.Modify(msiViewModifyAssign, record);
				} catch (err) {
					throw new Error(err.number, "" + stream_path + ": " + err.description);
				}

				if (WScript.Arguments.Named.Exists("L") && WScript.Arguments.Named("L") != false) {
					// Get platform.
					var
						sumInfo   = database.SummaryInformation(1),
						template  = sumInfo.Property(PID_TEMPLATE),
						idx_delim = template.indexOf(";"),
						platform  = idx_delim >= 0 ? template.substring(0, idx_delim + 1) : ";",
						languages = idx_delim >= 0 ? template.substring(idx_delim + 1).split(new RegExp("\\s*,\\s*")) : new Array();

					languages.push(stream_name);
					
					// Set template property.
					sumInfo.Property(PID_TEMPLATE) = platform + languages.join(",");
					sumInfo.Persist();
				}

				// Commit database
				database.Commit();
			} catch (err) {
				// In case of error, delete output file.
				if (fso.FileExists(msi_path))
					fso.DeleteFile(msi_path);

				throw err;
			}

			WScript.Quit(0);
		]]></script>
	</job>
	<job id="IDTDiff">
		<runtime>
			<description>Extract strings to translate by comparing two IDT files.</description>
			<unnamed name="&lt;output.po&gt;" helpstring="Output PO file" required="true"/>
			<unnamed name="&lt;source.idt&gt;" helpstring="Original IDT file" required="true"/>
			<unnamed name="&lt;destination.idt&gt;" helpstring="Translated IDT file" required="true"/>
			<named name="L" helpstring="Language identifier" type="string" required="false"/>
		</runtime>
		<reference object="ADODB.Stream"/>
		<reference object="Scripting.FileSystemObject"/>
		<script language="JScript" src="IDT.js"/>
		<script language="JScript" src="PO.js"/>
		<script language="JScript" src="String.js"/>
		<script language="JScript"><![CDATA[
			if (WScript.Arguments.Unnamed.Length < 3) {
				WScript.Arguments.ShowUsage();
				WScript.Quit(1);
			}

			var
				fso     = WScript.CreateObject("Scripting.FileSystemObject"),
				po_path = WScript.Arguments.Unnamed(0);

			try {
				// Open and parse IDT file(s).
				var
					idt_src_path = WScript.Arguments.Unnamed(1),
					idt_src      = new IDT(idt_src_path),
					idt_dst      = new IDT(WScript.Arguments.Unnamed(2));

				// IDT files must represent identical table.
				if (idt_src.columns.toString() != idt_dst.columns.toString() ||
					idt_src.types  .toString() != idt_dst.types  .toString() ||
					idt_src.table              != idt_dst.table              ||
					idt_src.key    .toString() != idt_dst.key    .toString())
					throw new Error("IDT files are not compatible.");

				var po = new POCatalog();

				// Add header.
				po.push("",
					"MIME-Version: 1.0\n" +
					"Content-Type: text/plain; charset=" + po.charset + "\n" +
					"Content-Transfer-Encoding: 8bit\n" +
					(WScript.Arguments.Named.Exists("L") ? "Language: "+ WScript.Arguments.Named("L") +"\n" : ""), false);

				// Build translations.
				for (var key in idt_src.data) {
					if (key in idt_dst.data) {
						// We found a record in both IDT files.
						for (var col in idt_src.types)
							if (idt_src.isLocalizable(col) && idt_src.data[key][col] != "")
								po.push(idt_src.data[key][col], idt_dst.data[key][col], false, "#: " + idt_src_path + ":" + idt_src.linenum[key]);
					} else {
						// The record is available in the source IDT file only.
						for (var col in idt_src.types)
							if (idt_src.isLocalizable(col) && idt_src.data[key][col] != "")
								po.push(idt_src.data[key][col], "", false, "#: " + idt_src_path + ":" + idt_src.linenum[key]);
					}
				}

				// Save to file.
				po.save(po_path);
			} catch (err) {
				// In case of error, delete output file.
				if (fso.FileExists(po_path))
					fso.DeleteFile(po_path);

				throw err;
			}

			WScript.Quit(0);
		]]></script>
	</job>
	<job id="IDTExtract">
		<runtime>
			<description>Extract strings to translate.</description>
			<unnamed name="&lt;output.po&gt;" helpstring="Output PO file" required="true"/>
			<unnamed name="&lt;source.idt&gt;..." helpstring="Source IDT files" required="true"/>
		</runtime>
		<reference object="ADODB.Stream"/>
		<reference object="Scripting.FileSystemObject"/>
		<script language="JScript" src="IDT.js"/>
		<script language="JScript" src="PO.js"/>
		<script language="JScript" src="String.js"/>
		<script language="JScript"><![CDATA[
			if (WScript.Arguments.Unnamed.Length < 2) {
				WScript.Arguments.ShowUsage();
				WScript.Quit(1);
			}

			var
				fso     = WScript.CreateObject("Scripting.FileSystemObject"),
				po_path = WScript.Arguments.Unnamed(0);

			try {
				var po = new POCatalog();

				// Add header.
				po.push("",
					"MIME-Version: 1.0\n" +
					"Content-Type: text/plain; charset=" + po.charset + "\n" +
					"Content-Transfer-Encoding: 8bit\n", false);

				// Build translations.
				for (var i = 1, argc = WScript.Arguments.Unnamed.Length; i < argc; i++) {
					// Open and parse IDT file(s).
					var
						idt_src_path = WScript.Arguments.Unnamed(i),
						idt_src      = new IDT(idt_src_path);

					for (var key in idt_src.data) {
						for (var col in idt_src.types)
							if (idt_src.isLocalizable(col) && idt_src.data[key][col] != "") {
								var src = idt_src.data[key][col];
								var t = po.search(src);
								if (t) {
									// Text-to-translate already present. Add source to the list.
									t["#"] += " " + idt_src_path + ":" + idt_src.linenum[key];
								} else {
									// Add text-to-translate.
									po.push(src, "", false, "#: " + idt_src_path + ":" + idt_src.linenum[key]);
								}
							}
					}
				}

				// Save to file.
				po.save(po_path);
			} catch (err) {
				// In case of error, delete output file.
				if (fso.FileExists(po_path))
					fso.DeleteFile(po_path);

				throw err;
			}

			WScript.Quit(0);
		]]></script>
	</job>
	<job id="IDTTranslate">
		<runtime>
			<description>Translate IDT file.</description>
			<unnamed name="&lt;destination.idt&gt;" helpstring="Translated IDT file" required="true"/>
			<unnamed name="&lt;source.idt&gt;" helpstring="Original IDT file" required="true"/>
			<unnamed name="&lt;catalogue.po&gt;" helpstring="PO catalog file with translations" required="true"/>
			<named name="CP" helpstring="Output code page" type="string" required="false"/>
		</runtime>
		<reference object="ADODB.Stream"/>
		<reference object="Scripting.FileSystemObject"/>
		<script language="JScript" src="IDT.js"/>
		<script language="JScript" src="PO.js"/>
		<script language="JScript" src="String.js"/>
		<script language="JScript"><![CDATA[
			if (WScript.Arguments.Unnamed.Length < 3) {
				WScript.Arguments.ShowUsage();
				WScript.Quit(1);
			}

			var
				fso          = WScript.CreateObject("Scripting.FileSystemObject"),
				idt_dst_path = WScript.Arguments.Unnamed(0);

			try {
				// Open and parse source IDT file and PO catalogue.
				var
					idt_src = new IDT(WScript.Arguments.Unnamed(1)),
					po      = new POCatalog(WScript.Arguments.Unnamed(2));

				// Translate records.
				for (var key in idt_src.data) {
					for (var col in idt_src.types) {
						if (idt_src.isLocalizable(col) && idt_src.data[key][col] != "")
							idt_src.data[key][col] = po.translate(idt_src.data[key][col]);
					}
				}

				if (WScript.Arguments.Named.Exists("CP"))
					idt_src.codepage = parseInt(WScript.Arguments.Named("CP"), 10);
				idt_src.save(idt_dst_path);
			} catch (err) {
				// In case of error, delete output file.
				if (fso.FileExists(idt_dst_path))
					fso.DeleteFile(idt_dst_path);

				throw err;
			}

			WScript.Quit(0);
		]]></script>
	</job>
	<job id="MakeGUID">
		<runtime>
			<description>Creates a new GUID and displays it on output.</description>
			<named name="M" helpstring="Makefile macro name" type="string" required="false"/>
		</runtime>
		<reference object="Scriptlet.TypeLib"/>
		<script language="JScript"><![CDATA[
			var
					tlib = WScript.CreateObject("Scriptlet.TypeLib"),
					guid = tlib.Guid;

			WScript.Echo(
				WScript.Arguments.Named.Exists("M") ? WScript.Arguments.Named("M") + "=" + guid.toUpperCase() :
				guid.toUpperCase());

			WScript.Quit(0);
		]]></script>
	</job>
</package>
