main
cs 500 lines 20.9 KB
Raw
1 // 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.
2
3 namespace WixToolset.Data
4 {
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8 using System.Linq;
9 using System.Reflection;
10 using SimpleJson;
11
12 /// <summary>
13 /// Container class for an intermediate object.
14 /// </summary>
15 public sealed class Intermediate
16 {
17 private static readonly Version CurrentVersion = new Version("4.0.0.0");
18 private const string WixOutputStreamName = "wix-ir.json";
19
20 private readonly Dictionary<string, Localization> localizationsByCulture;
21 private readonly List<IntermediateSection> sections;
22
23 /// <summary>
24 /// Instantiate a new Intermediate.
25 /// </summary>
26 public Intermediate()
27 {
28 this.Id = Convert.ToBase64String(Guid.NewGuid().ToByteArray()).TrimEnd('=').Replace('+', '.').Replace('/', '_');
29 this.localizationsByCulture = new Dictionary<string, Localization>(StringComparer.OrdinalIgnoreCase);
30 this.sections = new List<IntermediateSection>();
31 }
32
33 public Intermediate(string id, IEnumerable<IntermediateSection> sections, IDictionary<string, Localization> localizationsByCulture) : this(id, level: null, sections, localizationsByCulture)
34 {
35 }
36
37 public Intermediate(string id, string level, IEnumerable<IntermediateSection> sections, IDictionary<string, Localization> localizationsByCulture)
38 {
39 this.Id = id;
40 this.Level = level;
41 this.localizationsByCulture = (localizationsByCulture != null) ? new Dictionary<string, Localization>(localizationsByCulture, StringComparer.OrdinalIgnoreCase) : new Dictionary<string, Localization>(StringComparer.OrdinalIgnoreCase);
42 this.sections = (sections != null) ? new List<IntermediateSection>(sections) : new List<IntermediateSection>();
43 }
44
45 /// <summary>
46 /// Get the id for the intermediate.
47 /// </summary>
48 public string Id { get; }
49
50 /// <summary>
51 /// Get the level of the intermediate.
52 /// </summary>
53 public string Level { get; private set; }
54
55 /// <summary>
56 /// Get the localizations contained in this intermediate.
57 /// </summary>
58 public IReadOnlyCollection<Localization> Localizations => this.localizationsByCulture.Values;
59
60 /// <summary>
61 /// Get the sections contained in this intermediate.
62 /// </summary>
63 public IReadOnlyCollection<IntermediateSection> Sections => this.sections;
64
65 /// <summary>
66 /// Loads an intermediate from a path on disk.
67 /// </summary>
68 /// <param name="path">Path to intermediate file saved on disk.</param>
69 /// <param name="suppressVersionCheck">Suppress checking for wix.dll version mismatches.</param>
70 /// <returns>Returns the loaded intermediate.</returns>
71 public static Intermediate Load(string path, bool suppressVersionCheck = false)
72 {
73 var creator = new SimpleSymbolDefinitionCreator();
74 return Intermediate.Load(path, creator, suppressVersionCheck);
75 }
76
77 /// <summary>
78 /// Loads an intermediate from a stream.
79 /// </summary>
80 /// <param name="assembly">Assembly with intermediate embedded in resource stream.</param>
81 /// <param name="resourceName">Name of resource stream.</param>
82 /// <param name="suppressVersionCheck">Suppress checking for wix.dll version mismatches.</param>
83 /// <returns>Returns the loaded intermediate.</returns>
84 public static Intermediate Load(Assembly assembly, string resourceName, bool suppressVersionCheck = false)
85 {
86 var creator = new SimpleSymbolDefinitionCreator();
87 return Intermediate.Load(assembly, resourceName, creator, suppressVersionCheck);
88 }
89
90 /// <summary>
91 /// Loads an intermediate from a stream.
92 /// </summary>
93 /// <param name="assembly">Assembly with intermediate embedded in resource stream.</param>
94 /// <param name="resourceName">Name of resource stream.</param>
95 /// <param name="creator">ISymbolDefinitionCreator to use when reconstituting the intermediate.</param>
96 /// <param name="suppressVersionCheck">Suppress checking for wix.dll version mismatches.</param>
97 /// <returns>Returns the loaded intermediate.</returns>
98 public static Intermediate Load(Assembly assembly, string resourceName, ISymbolDefinitionCreator creator, bool suppressVersionCheck = false)
99 {
100 using (var wixout = WixOutput.Read(assembly, resourceName))
101 {
102 return Intermediate.LoadIntermediate(wixout, creator, suppressVersionCheck);
103 }
104 }
105
106 /// <summary>
107 /// Loads an intermediate from a path on disk.
108 /// </summary>
109 /// <param name="path">Path to intermediate file saved on disk.</param>
110 /// <param name="creator">ISymbolDefinitionCreator to use when reconstituting the intermediate.</param>
111 /// <param name="suppressVersionCheck">Suppress checking for wix.dll version mismatches.</param>
112 /// <returns>Returns the loaded intermediate.</returns>
113 public static Intermediate Load(string path, ISymbolDefinitionCreator creator, bool suppressVersionCheck = false)
114 {
115 using (var wixout = WixOutput.Read(path))
116 {
117 return Intermediate.LoadIntermediate(wixout, creator, suppressVersionCheck);
118 }
119 }
120
121 /// <summary>
122 /// Loads an intermediate from a WixOutput object.
123 /// </summary>
124 /// <param name="wixOutput">WixOutput object.</param>
125 /// <param name="suppressVersionCheck">Suppress checking for wix.dll version mismatches.</param>
126 /// <returns>Returns the loaded intermediate.</returns>
127 public static Intermediate Load(WixOutput wixOutput, bool suppressVersionCheck = false)
128 {
129 var creator = new SimpleSymbolDefinitionCreator();
130 return Intermediate.LoadIntermediate(wixOutput, creator, suppressVersionCheck);
131 }
132
133 /// <summary>
134 /// Loads an intermediate from a WixOutput object.
135 /// </summary>
136 /// <param name="wixOutput">WixOutput object.</param>
137 /// <param name="creator">ISymbolDefinitionCreator to use when reconstituting the intermediate.</param>
138 /// <param name="suppressVersionCheck">Suppress checking for wix.dll version mismatches.</param>
139 /// <returns>Returns the loaded intermediate.</returns>
140 public static Intermediate Load(WixOutput wixOutput, ISymbolDefinitionCreator creator, bool suppressVersionCheck = false)
141 {
142 return Intermediate.LoadIntermediate(wixOutput, creator, suppressVersionCheck);
143 }
144
145 /// <summary>
146 /// Loads several intermediates from paths on disk using the same definitions.
147 /// </summary>
148 /// <param name="intermediateFiles">Paths to intermediate files saved on disk.</param>
149 /// <returns>Returns the loaded intermediates</returns>
150 public static IReadOnlyList<Intermediate> Load(IEnumerable<string> intermediateFiles)
151 {
152 var creator = new SimpleSymbolDefinitionCreator();
153 return Intermediate.Load(intermediateFiles, creator);
154 }
155
156 /// <summary>
157 /// Loads several intermediates from paths on disk using the same definitions.
158 /// </summary>
159 /// <param name="intermediateFiles">Paths to intermediate files saved on disk.</param>
160 /// <param name="creator">ISymbolDefinitionCreator to use when reconstituting the intermediates.</param>
161 /// <param name="suppressVersionCheck">Suppress checking for wix.dll version mismatches.</param>
162 /// <returns>Returns the loaded intermediates</returns>
163 public static IReadOnlyList<Intermediate> Load(IEnumerable<string> intermediateFiles, ISymbolDefinitionCreator creator, bool suppressVersionCheck = false)
164 {
165 var jsons = new Queue<JsonWithPath>();
166 var intermediates = new List<Intermediate>();
167
168 foreach (var path in intermediateFiles)
169 {
170 using (var wixout = WixOutput.Read(path))
171 {
172 var data = wixout.GetData(WixOutputStreamName);
173 var json = Intermediate.LoadJson(data, wixout.Uri, suppressVersionCheck);
174
175 Intermediate.LoadDefinitions(json, creator);
176
177 jsons.Enqueue(new JsonWithPath { Json = json, Path = wixout.Uri });
178 }
179 }
180
181 while (jsons.Count > 0)
182 {
183 var jsonWithPath = jsons.Dequeue();
184
185 var intermediate = Intermediate.FinalizeLoad(jsonWithPath.Json, jsonWithPath.Path, creator);
186
187 intermediates.Add(intermediate);
188 }
189
190 return intermediates;
191 }
192
193 /// <summary>
194 /// Adds a section to the intermedaite.
195 /// </summary>
196 /// <param name="section">Section to add to the intermediate.</param>
197 /// <returns>Section added to the intermediate.</returns>
198 public IntermediateSection AddSection(IntermediateSection section)
199 {
200 this.sections.Add(section);
201 return section;
202 }
203
204 /// <summary>
205 /// Removes a section from the intermediate.
206 /// </summary>
207 /// <param name="section">Section to remove.</param>
208 /// <returns>True if the section was removed; otherwise false.</returns>
209 public bool RemoveSection(IntermediateSection section)
210 {
211 return this.sections.Remove(section);
212 }
213
214 /// <summary>
215 /// Updates the intermediate level to the specified level.
216 /// </summary>
217 /// <param name="level">Intermediate level.</param>
218 public Intermediate UpdateLevel(string level)
219 {
220 this.Level = String.IsNullOrEmpty(this.Level) ? level : String.Concat(this.Level, ";", level);
221
222 return this;
223 }
224
225 /// <summary>
226 /// Returns whether a specifed intermediate level has been set for this intermediate.
227 /// </summary>
228 /// <param name="level">Intermediate level.</param>
229 /// <returns>True if the specifed intermediate level has been set for this intermediate.</returns>
230 public bool HasLevel(string level)
231 {
232 return this.Level?.Contains(level) == true;
233 }
234
235 /// <summary>
236 /// Saves an intermediate to a path on disk.
237 /// </summary>
238 /// <param name="path">Path to save intermediate file to disk.</param>
239 public void Save(string path)
240 {
241 Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path)));
242
243 using (var wixout = WixOutput.Create(path))
244 {
245 this.Save(wixout);
246 }
247 }
248
249 /// <summary>
250 /// Saves an intermediate that can only be written to to a path on disk.
251 /// </summary>
252 /// <param name="path">Path to save intermediate file to disk.</param>
253 public void SaveNew(string path)
254 {
255 Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path)));
256
257 using (var wixout = WixOutput.CreateNew(path))
258 {
259 this.Save(wixout);
260 }
261 }
262
263 /// <summary>
264 /// Saves an intermediate to a WixOutput.
265 /// </summary>
266 /// <param name="wixout">Destination to save.</param>
267 public void Save(WixOutput wixout)
268 {
269 this.SaveEmbedFiles(wixout);
270
271 this.SaveIR(wixout);
272 }
273
274 /// <summary>
275 /// Loads an intermediate from a WixOutput.
276 /// </summary>
277 /// <param name="wixout">Source to load from.</param>
278 /// <param name="creator">ISymbolDefinitionCreator to use when reconstituting the intermediate.</param>
279 /// <param name="suppressVersionCheck">Suppress checking for wix.dll version mismatches.</param>
280 /// <returns>Returns the loaded intermediate.</returns>
281 private static Intermediate LoadIntermediate(WixOutput wixout, ISymbolDefinitionCreator creator, bool suppressVersionCheck = false)
282 {
283 var data = wixout.GetData(WixOutputStreamName);
284 var json = Intermediate.LoadJson(data, wixout.Uri, suppressVersionCheck);
285
286 Intermediate.LoadDefinitions(json, creator);
287
288 return Intermediate.FinalizeLoad(json, wixout.Uri, creator);
289 }
290
291 /// <summary>
292 /// Loads json form of intermediate.
293 /// </summary>
294 /// <param name="json">Source to load from.</param>
295 /// <param name="baseUri">Path name of intermediate file.</param>
296 /// <param name="suppressVersionCheck">Suppress checking for wix.dll version mismatches.</param>
297 /// <returns>Returns the loaded json.</returns>
298 private static JsonObject LoadJson(string json, Uri baseUri, bool suppressVersionCheck)
299 {
300 var jsonObject = SimpleJson.DeserializeObject(json) as JsonObject;
301
302 if (!suppressVersionCheck)
303 {
304 var versionJson = jsonObject.GetValueOrDefault<string>("version");
305
306 if (!Version.TryParse(versionJson, out var version) || !Intermediate.CurrentVersion.Equals(version))
307 {
308 throw new WixException(ErrorMessages.VersionMismatch(SourceLineNumber.CreateFromUri(baseUri.AbsoluteUri), "intermediate", versionJson, Intermediate.CurrentVersion.ToString()));
309 }
310 }
311
312 return jsonObject;
313 }
314
315 /// <summary>
316 /// Loads custom definitions in intermediate json into the creator.
317 /// </summary>
318 /// <param name="json">Json version of intermediate.</param>
319 /// <param name="creator">ISymbolDefinitionCreator to use when reconstituting the intermediate.</param>
320 private static void LoadDefinitions(JsonObject json, ISymbolDefinitionCreator creator)
321 {
322 var definitionsJson = json.GetValueOrDefault<JsonArray>("definitions");
323
324 if (definitionsJson != null)
325 {
326 foreach (JsonObject definitionJson in definitionsJson)
327 {
328 var definition = IntermediateSymbolDefinition.Deserialize(definitionJson);
329 creator.AddCustomSymbolDefinition(definition);
330 }
331 }
332 }
333
334 /// <summary>
335 /// Loads the sections and localization for the intermediate.
336 /// </summary>
337 /// <param name="json">Json version of intermediate.</param>
338 /// <param name="baseUri">Path to the intermediate.</param>
339 /// <param name="creator">ISymbolDefinitionCreator to use when reconstituting the intermediate.</param>
340 /// <returns>The finalized intermediate.</returns>
341 private static Intermediate FinalizeLoad(JsonObject json, Uri baseUri, ISymbolDefinitionCreator creator)
342 {
343 var id = json.GetValueOrDefault<string>("id");
344 var level = json.GetValueOrDefault<string>("level");
345
346 var sections = new List<IntermediateSection>();
347
348 var sectionsJson = json.GetValueOrDefault<JsonArray>("sections");
349 foreach (JsonObject sectionJson in sectionsJson)
350 {
351 var section = IntermediateSection.Deserialize(creator, baseUri, sectionJson);
352 sections.Add(section);
353 }
354
355 var localizations = new Dictionary<string, Localization>(StringComparer.OrdinalIgnoreCase);
356
357 var localizationsJson = json.GetValueOrDefault<JsonArray>("localizations") ?? new JsonArray();
358 foreach (JsonObject localizationJson in localizationsJson)
359 {
360 var localization = Localization.Deserialize(localizationJson);
361 localizations.Add(localization.Culture, localization);
362 }
363
364 return new Intermediate(id, level, sections, localizations);
365 }
366
367 private void SaveEmbedFiles(WixOutput wixout)
368 {
369 var embeddedFields = this.Sections.SelectMany(s => s.Symbols)
370 .SelectMany(t => t.Fields)
371 .Where(f => f?.Type == IntermediateFieldType.Path)
372 .Select(f => f.AsPath())
373 .Where(f => f.Embed)
374 .ToList();
375
376 var savedEmbedFields = new Dictionary<string, IntermediateFieldPathValue>(StringComparer.OrdinalIgnoreCase);
377 var uniqueEntryNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
378
379 foreach (var embeddedField in embeddedFields)
380 {
381 var key = String.Concat(embeddedField.BaseUri?.AbsoluteUri, "?", embeddedField.Path);
382
383 if (savedEmbedFields.TryGetValue(key, out var existing))
384 {
385 embeddedField.Path = existing.Path;
386 }
387 else
388 {
389 var entryName = CalculateUniqueEntryName(uniqueEntryNames, embeddedField.Path);
390
391 if (embeddedField.BaseUri == null)
392 {
393 wixout.ImportDataStream(entryName, embeddedField.Path);
394 }
395 else // open the container specified in baseUri and copy the correct stream out of it.
396 {
397 using (var otherWixout = WixOutput.Read(embeddedField.BaseUri))
398 using (var stream = otherWixout.GetDataStream(embeddedField.Path))
399 using (var target = wixout.CreateDataStream(entryName))
400 {
401 stream.CopyTo(target);
402 }
403 }
404
405 embeddedField.Path = entryName;
406
407 savedEmbedFields.Add(key, embeddedField);
408 }
409 }
410 }
411
412 private void SaveIR(WixOutput wixout)
413 {
414 using (var writer = new StreamWriter(wixout.CreateDataStream(WixOutputStreamName)))
415 {
416 var jsonObject = new JsonObject
417 {
418 { "id", this.Id },
419 { "level", this.Level },
420 { "version", Intermediate.CurrentVersion.ToString() }
421 };
422
423 var sectionsJson = new JsonArray(this.Sections.Count);
424 foreach (var section in this.Sections)
425 {
426 var sectionJson = section.Serialize();
427 sectionsJson.Add(sectionJson);
428 }
429
430 jsonObject.Add("sections", sectionsJson);
431
432 var customDefinitions = this.GetCustomDefinitionsInSections();
433
434 if (customDefinitions.Count > 0)
435 {
436 var customDefinitionsJson = new JsonArray(customDefinitions.Count);
437
438 foreach (var kvp in customDefinitions.OrderBy(d => d.Key))
439 {
440 var customDefinitionJson = kvp.Value.Serialize();
441 customDefinitionsJson.Add(customDefinitionJson);
442 }
443
444 jsonObject.Add("definitions", customDefinitionsJson);
445 }
446
447 if (this.Localizations.Any())
448 {
449 var localizationsJson = new JsonArray();
450 foreach (var localization in this.Localizations)
451 {
452 var localizationJson = localization.Serialize();
453 localizationsJson.Add(localizationJson);
454 }
455
456 jsonObject.Add("localizations", localizationsJson);
457 }
458
459 var json = SimpleJson.SerializeObject(jsonObject);
460 writer.Write(json);
461 }
462 }
463
464 private static string CalculateUniqueEntryName(ISet<string> entryNames, string path)
465 {
466 var filename = Path.GetFileName(path);
467 var entryName = "wix-ir/" + filename;
468 var i = 0;
469
470 while (!entryNames.Add(entryName))
471 {
472 entryName = $"wix-ir/{filename}-{++i}";
473 }
474
475 return entryName;
476 }
477
478 private Dictionary<string, IntermediateSymbolDefinition> GetCustomDefinitionsInSections()
479 {
480 var customDefinitions = new Dictionary<string, IntermediateSymbolDefinition>();
481
482 foreach (var symbol in this.Sections.SelectMany(s => s.Symbols).Where(t => t.Definition.Type == SymbolDefinitionType.MustBeFromAnExtension))
483 {
484 if (!customDefinitions.ContainsKey(symbol.Definition.Name))
485 {
486 customDefinitions.Add(symbol.Definition.Name, symbol.Definition);
487 }
488 }
489
490 return customDefinitions;
491 }
492
493 private struct JsonWithPath
494 {
495 public JsonObject Json { get; set; }
496
497 public Uri Path { get; set; }
498 }
499 }
500 }