// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. namespace WixToolset.Data { using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using SimpleJson; /// /// Container class for an intermediate object. /// public sealed class Intermediate { private static readonly Version CurrentVersion = new Version("4.0.0.0"); private const string WixOutputStreamName = "wix-ir.json"; private readonly Dictionary localizationsByCulture; /// /// Instantiate a new Intermediate. /// public Intermediate() { this.Id = Convert.ToBase64String(Guid.NewGuid().ToByteArray()).TrimEnd('=').Replace('+', '.').Replace('/', '_'); this.localizationsByCulture = new Dictionary(StringComparer.OrdinalIgnoreCase); this.Sections = new List(); } public Intermediate(string id, IEnumerable sections, IDictionary localizationsByCulture) : this(id, level: null, sections, localizationsByCulture) { } public Intermediate(string id, string level, IEnumerable sections, IDictionary localizationsByCulture) { this.Id = id; this.Level = level; this.localizationsByCulture = (localizationsByCulture != null) ? new Dictionary(localizationsByCulture, StringComparer.OrdinalIgnoreCase) : new Dictionary(StringComparer.OrdinalIgnoreCase); this.Sections = (sections != null) ? new List(sections) : new List(); } /// /// Get the id for the intermediate. /// public string Id { get; } /// /// Get the id for the intermediate. /// public string Level { get; private set; } /// /// Get the localizations contained in this intermediate. /// public IEnumerable Localizations => this.localizationsByCulture.Values; /// /// Get the sections contained in this intermediate. /// public IList Sections { get; } /// /// Loads an intermediate from a path on disk. /// /// Path to intermediate file saved on disk. /// Suppress checking for wix.dll version mismatches. /// Returns the loaded intermediate. public static Intermediate Load(string path, bool suppressVersionCheck = false) { var creator = new SimpleTupleDefinitionCreator(); return Intermediate.Load(path, creator, suppressVersionCheck); } /// /// Loads an intermediate from a stream. /// /// Assembly with intermediate embedded in resource stream. /// Name of resource stream. /// Suppress checking for wix.dll version mismatches. /// Returns the loaded intermediate. public static Intermediate Load(Assembly assembly, string resourceName, bool suppressVersionCheck = false) { var creator = new SimpleTupleDefinitionCreator(); return Intermediate.Load(assembly, resourceName, creator, suppressVersionCheck); } /// /// Loads an intermediate from a stream. /// /// Assembly with intermediate embedded in resource stream. /// Name of resource stream. /// ITupleDefinitionCreator to use when reconstituting the intermediate. /// Suppress checking for wix.dll version mismatches. /// Returns the loaded intermediate. public static Intermediate Load(Assembly assembly, string resourceName, ITupleDefinitionCreator creator, bool suppressVersionCheck = false) { using (var wixout = WixOutput.Read(assembly, resourceName)) { return Intermediate.LoadIntermediate(wixout, creator, suppressVersionCheck); } } /// /// Loads an intermediate from a path on disk. /// /// Path to intermediate file saved on disk. /// ITupleDefinitionCreator to use when reconstituting the intermediate. /// Suppress checking for wix.dll version mismatches. /// Returns the loaded intermediate. public static Intermediate Load(string path, ITupleDefinitionCreator creator, bool suppressVersionCheck = false) { using (var wixout = WixOutput.Read(path)) { return Intermediate.LoadIntermediate(wixout, creator, suppressVersionCheck); } } /// /// Loads an intermediate from a WixOutput object. /// /// WixOutput object. /// ITupleDefinitionCreator to use when reconstituting the intermediate. /// Suppress checking for wix.dll version mismatches. /// Returns the loaded intermediate. public static Intermediate Load(WixOutput wixOutput, bool suppressVersionCheck = false) { var creator = new SimpleTupleDefinitionCreator(); return Intermediate.LoadIntermediate(wixOutput, creator, suppressVersionCheck); } /// /// Loads an intermediate from a WixOutput object. /// /// WixOutput object. /// ITupleDefinitionCreator to use when reconstituting the intermediate. /// Suppress checking for wix.dll version mismatches. /// Returns the loaded intermediate. public static Intermediate Load(WixOutput wixOutput, ITupleDefinitionCreator creator, bool suppressVersionCheck = false) { return Intermediate.LoadIntermediate(wixOutput, creator, suppressVersionCheck); } /// /// Loads several intermediates from paths on disk using the same definitions. /// /// Paths to intermediate files saved on disk. /// Suppress checking for wix.dll version mismatches. /// Returns the loaded intermediates public static IEnumerable Load(IEnumerable intermediateFiles) { var creator = new SimpleTupleDefinitionCreator(); return Intermediate.Load(intermediateFiles, creator); } /// /// Loads several intermediates from paths on disk using the same definitions. /// /// Paths to intermediate files saved on disk. /// ITupleDefinitionCreator to use when reconstituting the intermediates. /// Suppress checking for wix.dll version mismatches. /// Returns the loaded intermediates public static IEnumerable Load(IEnumerable intermediateFiles, ITupleDefinitionCreator creator, bool suppressVersionCheck = false) { var jsons = new Queue(); var intermediates = new List(); foreach (var path in intermediateFiles) { using (var wixout = WixOutput.Read(path)) { var data = wixout.GetData(WixOutputStreamName); var json = Intermediate.LoadJson(data, wixout.Uri, suppressVersionCheck); Intermediate.LoadDefinitions(json, creator); jsons.Enqueue(new JsonWithPath { Json = json, Path = wixout.Uri }); } } while (jsons.Count > 0) { var jsonWithPath = jsons.Dequeue(); var intermediate = Intermediate.FinalizeLoad(jsonWithPath.Json, jsonWithPath.Path, creator); intermediates.Add(intermediate); } return intermediates; } /// /// Updates the intermediate level to the specified level. /// /// Intermediate level. public void UpdateLevel(string level) { this.Level = String.IsNullOrEmpty(this.Level) ? level : String.Concat(this.Level, ";", level); } /// /// Returns whether a specifed intermediate level has been set for this intermediate. /// /// Intermediate level. /// True if the specifed intermediate level has been set for this intermediate. public bool HasLevel(string level) { return this.Level?.Contains(level) == true; } /// /// Saves an intermediate to a path on disk. /// /// Path to save intermediate file to disk. public void Save(string path) { Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))); using (var wixout = WixOutput.Create(path)) { this.Save(wixout); } } /// /// Saves an intermediate to a path on disk. /// /// Path to save intermediate file to disk. public void Save(WixOutput wixout) { this.SaveEmbedFiles(wixout); this.SaveIR(wixout); } /// /// Loads an intermediate from a path on disk. /// /// Stream to intermediate file. /// Path name of intermediate file. /// ITupleDefinitionCreator to use when reconstituting the intermediate. /// Suppress checking for wix.dll version mismatches. /// Returns the loaded intermediate. private static Intermediate LoadIntermediate(WixOutput wixout, ITupleDefinitionCreator creator, bool suppressVersionCheck = false) { var data = wixout.GetData(WixOutputStreamName); var json = Intermediate.LoadJson(data, wixout.Uri, suppressVersionCheck); Intermediate.LoadDefinitions(json, creator); return Intermediate.FinalizeLoad(json, wixout.Uri, creator); } /// /// Loads json form of intermedaite from stream. /// /// Stream to intermediate file. /// Path name of intermediate file. /// Suppress checking for wix.dll version mismatches. /// Returns the loaded json. private static JsonObject LoadJson(string json, Uri baseUri, bool suppressVersionCheck) { var jsonObject = SimpleJson.DeserializeObject(json) as JsonObject; if (!suppressVersionCheck) { var versionJson = jsonObject.GetValueOrDefault("version"); if (!Version.TryParse(versionJson, out var version) || !Intermediate.CurrentVersion.Equals(version)) { throw new WixException(ErrorMessages.VersionMismatch(SourceLineNumber.CreateFromUri(baseUri.AbsoluteUri), "intermediate", versionJson, Intermediate.CurrentVersion.ToString())); } } return jsonObject; } /// /// Loads custom definitions in intermediate json into the creator. /// /// Json version of intermediate. /// ITupleDefinitionCreator to use when reconstituting the intermediate. private static void LoadDefinitions(JsonObject json, ITupleDefinitionCreator creator) { var definitionsJson = json.GetValueOrDefault("definitions"); if (definitionsJson != null) { foreach (JsonObject definitionJson in definitionsJson) { var definition = IntermediateTupleDefinition.Deserialize(definitionJson); creator.AddCustomTupleDefinition(definition); } } } /// /// Loads the sections and localization for the intermediate. /// /// Json version of intermediate. /// Path to the intermediate. /// ITupleDefinitionCreator to use when reconstituting the intermediate. /// The finalized intermediate. private static Intermediate FinalizeLoad(JsonObject json, Uri baseUri, ITupleDefinitionCreator creator) { var id = json.GetValueOrDefault("id"); var level = json.GetValueOrDefault("level"); var sections = new List(); var sectionsJson = json.GetValueOrDefault("sections"); foreach (JsonObject sectionJson in sectionsJson) { var section = IntermediateSection.Deserialize(creator, baseUri, sectionJson); sections.Add(section); } var localizations = new Dictionary(StringComparer.OrdinalIgnoreCase); var localizationsJson = json.GetValueOrDefault("localizations") ?? new JsonArray(); foreach (JsonObject localizationJson in localizationsJson) { var localization = Localization.Deserialize(localizationJson); localizations.Add(localization.Culture, localization); } return new Intermediate(id, level, sections, localizations); } private void SaveEmbedFiles(WixOutput wixout) { var embeddedFields = this.Sections.SelectMany(s => s.Tuples) .SelectMany(t => t.Fields) .Where(f => f?.Type == IntermediateFieldType.Path) .Select(f => f.AsPath()) .Where(f => f.Embed) .ToList(); var savedEmbedFields = new Dictionary(StringComparer.OrdinalIgnoreCase); var uniqueEntryNames = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var embeddedField in embeddedFields) { var key = String.Concat(embeddedField.BaseUri?.AbsoluteUri, "?", embeddedField.Path); if (savedEmbedFields.TryGetValue(key, out var existing)) { embeddedField.Path = existing.Path; } else { var entryName = CalculateUniqueEntryName(uniqueEntryNames, embeddedField.Path); if (embeddedField.BaseUri == null) { wixout.ImportDataStream(entryName, embeddedField.Path); } else // open the container specified in baseUri and copy the correct stream out of it. { using (var otherWixout = WixOutput.Read(embeddedField.BaseUri)) using (var stream = otherWixout.GetDataStream(embeddedField.Path)) using (var target = wixout.CreateDataStream(entryName)) { stream.CopyTo(target); } } embeddedField.Path = entryName; savedEmbedFields.Add(key, embeddedField); } } } private void SaveIR(WixOutput wixout) { using (var writer = new StreamWriter(wixout.CreateDataStream(WixOutputStreamName))) { var jsonObject = new JsonObject { { "id", this.Id }, { "level", this.Level }, { "version", Intermediate.CurrentVersion.ToString() } }; var sectionsJson = new JsonArray(this.Sections.Count); foreach (var section in this.Sections) { var sectionJson = section.Serialize(); sectionsJson.Add(sectionJson); } jsonObject.Add("sections", sectionsJson); var customDefinitions = this.GetCustomDefinitionsInSections(); if (customDefinitions.Count > 0) { var customDefinitionsJson = new JsonArray(customDefinitions.Count); foreach (var kvp in customDefinitions.OrderBy(d => d.Key)) { var customDefinitionJson = kvp.Value.Serialize(); customDefinitionsJson.Add(customDefinitionJson); } jsonObject.Add("definitions", customDefinitionsJson); } if (this.Localizations.Any()) { var localizationsJson = new JsonArray(); foreach (var localization in this.Localizations) { var localizationJson = localization.Serialize(); localizationsJson.Add(localizationJson); } jsonObject.Add("localizations", localizationsJson); } var json = SimpleJson.SerializeObject(jsonObject); writer.Write(json); } } private static string CalculateUniqueEntryName(ISet entryNames, string path) { var filename = Path.GetFileName(path); var entryName = "wix-ir/" + filename; var i = 0; while (!entryNames.Add(entryName)) { entryName = $"wix-ir/{filename}-{++i}"; } return entryName; } private Dictionary GetCustomDefinitionsInSections() { var customDefinitions = new Dictionary(); foreach (var tuple in this.Sections.SelectMany(s => s.Tuples).Where(t => t.Definition.Type == TupleDefinitionType.MustBeFromAnExtension)) { if (!customDefinitions.ContainsKey(tuple.Definition.Name)) { customDefinitions.Add(tuple.Definition.Name, tuple.Definition); } } return customDefinitions; } private struct JsonWithPath { public JsonObject Json { get; set; } public Uri Path { get; set; } } } }