aboutsummaryrefslogtreecommitdiff
path: root/src/wixext/DependencyCompiler.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/wixext/DependencyCompiler.cs')
-rw-r--r--src/wixext/DependencyCompiler.cs542
1 files changed, 15 insertions, 527 deletions
diff --git a/src/wixext/DependencyCompiler.cs b/src/wixext/DependencyCompiler.cs
index 18bb89df..0405c324 100644
--- a/src/wixext/DependencyCompiler.cs
+++ b/src/wixext/DependencyCompiler.cs
@@ -2,13 +2,9 @@
2 2
3namespace WixToolset.Dependency 3namespace WixToolset.Dependency
4{ 4{
5 using System;
6 using System.Collections.Generic; 5 using System.Collections.Generic;
7 using System.Text;
8 using System.Xml.Linq; 6 using System.Xml.Linq;
9 using WixToolset.Data; 7 using WixToolset.Data;
10 using WixToolset.Data.Symbols;
11 using WixToolset.Dependency.Symbols;
12 using WixToolset.Extensibility; 8 using WixToolset.Extensibility;
13 using WixToolset.Extensibility.Data; 9 using WixToolset.Extensibility.Data;
14 10
@@ -17,18 +13,6 @@ namespace WixToolset.Dependency
17 /// </summary> 13 /// </summary>
18 public sealed class DependencyCompiler : BaseCompilerExtension 14 public sealed class DependencyCompiler : BaseCompilerExtension
19 { 15 {
20 /// <summary>
21 /// Package type when parsing the Provides element.
22 /// </summary>
23 private enum PackageType
24 {
25 None,
26 ExePackage,
27 MsiPackage,
28 MspPackage,
29 MsuPackage
30 }
31
32 public override XNamespace Namespace => "http://wixtoolset.org/schemas/v4/wxs/dependency"; 16 public override XNamespace Namespace => "http://wixtoolset.org/schemas/v4/wxs/dependency";
33 17
34 /// <summary> 18 /// <summary>
@@ -40,534 +24,38 @@ namespace WixToolset.Dependency
40 public override void ParseAttribute(Intermediate intermediate, IntermediateSection section, XElement parentElement, XAttribute attribute, IDictionary<string, string> context) 24 public override void ParseAttribute(Intermediate intermediate, IntermediateSection section, XElement parentElement, XAttribute attribute, IDictionary<string, string> context)
41 { 25 {
42 var sourceLineNumbers = this.ParseHelper.GetSourceLineNumbers(parentElement); 26 var sourceLineNumbers = this.ParseHelper.GetSourceLineNumbers(parentElement);
43 switch (parentElement.Name.LocalName) 27 var addCheck = YesNoType.NotSet;
44 { 28 var addRequire = YesNoType.NotSet;
45 case "Bundle":
46 switch (attribute.Name.LocalName)
47 {
48 case "ProviderKey":
49 this.ParseProviderKeyAttribute(section, sourceLineNumbers, parentElement, attribute);
50 break;
51 default:
52 this.ParseHelper.UnexpectedAttribute(parentElement, attribute);
53 break;
54 }
55 break;
56 default:
57 this.ParseHelper.UnexpectedAttribute(parentElement, attribute);
58 break;
59 }
60 }
61
62 /// <summary>
63 /// Processes an element for the Compiler.
64 /// </summary>
65 /// <param name="sourceLineNumbers">Source line number for the parent element.</param>
66 /// <param name="parentElement">Parent element of element to process.</param>
67 /// <param name="element">Element to process.</param>
68 /// <param name="contextValues">Extra information about the context in which this element is being parsed.</param>
69 public override void ParseElement(Intermediate intermediate, IntermediateSection section, XElement parentElement, XElement element, IDictionary<string, string> context)
70 {
71 var packageType = PackageType.None;
72 29
73 switch (parentElement.Name.LocalName) 30 switch (parentElement.Name.LocalName)
74 { 31 {
75 case "Bundle": 32 case "Provides":
76 case "Fragment": 33 if (attribute.Name.LocalName == "Check" && parentElement.Parent.Name.LocalName == "Component")
77 case "Module":
78 case "Package":
79 switch (element.Name.LocalName)
80 { 34 {
81 case "Requires": 35 addCheck = this.ParseHelper.GetAttributeYesNoValue(sourceLineNumbers, attribute);
82 this.ParseRequiresElement(intermediate, section, element, null, false);
83 break;
84 default:
85 this.ParseHelper.UnexpectedElement(parentElement, element);
86 break;
87 } 36 }
88 break; 37 break;
89 case "ExePackage": 38 case "Requires":
90 packageType = PackageType.ExePackage; 39 case "RequiresRef":
91 break; 40 if (attribute.Name.LocalName == "Enforce" && parentElement.Parent.Name.LocalName == "Component")
92 case "MsiPackage":
93 packageType = PackageType.MsiPackage;
94 break;
95 case "MspPackage":
96 packageType = PackageType.MspPackage;
97 break;
98 case "MsuPackage":
99 packageType = PackageType.MsuPackage;
100 break;
101 default:
102 this.ParseHelper.UnexpectedElement(parentElement, element);
103 break;
104 }
105
106 if (PackageType.None != packageType)
107 {
108 var packageId = context["PackageId"];
109
110 switch (element.Name.LocalName)
111 {
112 case "Provides":
113 this.ParseProvidesElement(intermediate, section, element, packageType, packageId);
114 break;
115 default:
116 this.ParseHelper.UnexpectedElement(parentElement, element);
117 break;
118 }
119 }
120 }
121
122 /// <summary>
123 /// Processes a child element of a Component for the Compiler.
124 /// </summary>
125 /// <param name="parentElement">Parent element of element to process.</param>
126 /// <param name="element">Element to process.</param>
127 /// <param name="context">Extra information about the context in which this element is being parsed.</param>
128 /// <returns>The component key path type if set.</returns>
129 public override IComponentKeyPath ParsePossibleKeyPathElement(Intermediate intermediate, IntermediateSection section, XElement parentElement, XElement element, IDictionary<string, string> context)
130 {
131 var sourceLineNumbers = this.ParseHelper.GetSourceLineNumbers(parentElement);
132 IComponentKeyPath keyPath = null;
133
134 switch (parentElement.Name.LocalName)
135 {
136 case "Component":
137 var componentId = context["ComponentId"];
138
139 // 64-bit components may cause issues downlevel.
140 Boolean.TryParse(context["Win64"], out var win64);
141
142 switch (element.Name.LocalName)
143 { 41 {
144 case "Provides": 42 addRequire = this.ParseHelper.GetAttributeYesNoValue(sourceLineNumbers, attribute);
145 if (win64)
146 {
147 this.Messaging.Write(DependencyWarnings.Win64Component(sourceLineNumbers, componentId));
148 }
149
150 keyPath = this.ParseProvidesElement(intermediate, section, element, PackageType.None, componentId);
151 break;
152 default:
153 this.ParseHelper.UnexpectedElement(parentElement, element);
154 break;
155 } 43 }
156 break; 44 break;
157 default:
158 this.ParseHelper.UnexpectedElement(parentElement, element);
159 break;
160 }
161
162 return keyPath;
163 }
164
165 /// <summary>
166 /// Processes the ProviderKey bundle attribute.
167 /// </summary>
168 /// <param name="sourceLineNumbers">Source line number for the parent element.</param>
169 /// <param name="parentElement">Parent element of attribute.</param>
170 /// <param name="attribute">The XML attribute for the ProviderKey attribute.</param>
171 private void ParseProviderKeyAttribute(IntermediateSection section, SourceLineNumber sourceLineNumbers, XElement parentElement, XAttribute attribute)
172 {
173 Identifier id = null;
174 string providerKey = null;
175 int illegalChar = -1;
176
177 switch (attribute.Name.LocalName)
178 {
179 case "ProviderKey":
180 providerKey = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attribute);
181 break;
182 default:
183 this.ParseHelper.UnexpectedAttribute(parentElement, attribute);
184 break;
185 }
186
187 // Make sure the key does not contain any illegal characters or values.
188 if (String.IsNullOrEmpty(providerKey))
189 {
190 this.Messaging.Write(ErrorMessages.IllegalEmptyAttributeValue(sourceLineNumbers, parentElement.Name.LocalName, attribute.Name.LocalName));
191 } 45 }
192 else if (0 <= (illegalChar = providerKey.IndexOfAny(DependencyCommon.InvalidCharacters)))
193 {
194 var sb = new StringBuilder(DependencyCommon.InvalidCharacters.Length * 2);
195 Array.ForEach<char>(DependencyCommon.InvalidCharacters, c => sb.Append(c).Append(" "));
196 46
197 this.Messaging.Write(DependencyErrors.IllegalCharactersInProvider(sourceLineNumbers, "ProviderKey", providerKey[illegalChar], sb.ToString())); 47 if (addCheck == YesNoType.NotSet && addRequire == YesNoType.NotSet)
198 }
199 else if ("ALL" == providerKey)
200 { 48 {
201 this.Messaging.Write(DependencyErrors.ReservedValue(sourceLineNumbers, parentElement.Name.LocalName, "ProviderKey", providerKey)); 49 this.ParseHelper.UnexpectedAttribute(parentElement, attribute);
202 } 50 }
203 51 else if (addCheck == YesNoType.Yes)
204 // Generate the primary key for the row.
205 id = this.ParseHelper.CreateIdentifier("dep", attribute.Name.LocalName, providerKey);
206
207 if (!this.Messaging.EncounteredError)
208 { 52 {
209 // Create the provider symbol for the bundle. The Component_ field is required 53 this.ParseHelper.CreateCustomActionReference(sourceLineNumbers, section, "Wix4DependencyCheck", this.Context.Platform, CustomActionPlatforms.X86 | CustomActionPlatforms.X64 | CustomActionPlatforms.ARM64);
210 // in the table definition but unused for bundles, so just set it to the valid ID.
211 section.AddSymbol(new WixDependencyProviderSymbol(sourceLineNumbers, id)
212 {
213 ComponentRef = id.Id,
214 ProviderKey = providerKey,
215 Attributes = WixDependencyProviderAttributes.ProvidesAttributesBundle,
216 });
217 } 54 }
218 } 55 else if (addRequire == YesNoType.Yes)
219
220 /// <summary>
221 /// Processes the Provides element.
222 /// </summary>
223 /// <param name="node">The XML node for the Provides element.</param>
224 /// <param name="packageType">The type of the package being chained into a bundle, or "None" if building an MSI package.</param>
225 /// <param name="keyPath">Explicit key path.</param>
226 /// <param name="parentId">The identifier of the parent component or package.</param>
227 /// <returns>The type of key path if set.</returns>
228 private IComponentKeyPath ParseProvidesElement(Intermediate intermediate, IntermediateSection section, XElement node, PackageType packageType, string parentId)
229 {
230 var sourceLineNumbers = this.ParseHelper.GetSourceLineNumbers(node);
231 IComponentKeyPath keyPath = null;
232 Identifier id = null;
233 string key = null;
234 string version = null;
235 string displayName = null;
236 int illegalChar = -1;
237
238 foreach (var attrib in node.Attributes())
239 { 56 {
240 if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || this.Namespace == attrib.Name.Namespace) 57 this.ParseHelper.CreateCustomActionReference(sourceLineNumbers, section, "Wix4DependencyRequire", this.Context.Platform, CustomActionPlatforms.X86 | CustomActionPlatforms.X64 | CustomActionPlatforms.ARM64);
241 {
242 switch (attrib.Name.LocalName)
243 {
244 case "Id":
245 id = this.ParseHelper.GetAttributeIdentifier(sourceLineNumbers, attrib);
246 break;
247 case "Key":
248 key = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
249 break;
250 case "Version":
251 version = this.ParseHelper.GetAttributeVersionValue(sourceLineNumbers, attrib);
252 break;
253 case "DisplayName":
254 displayName = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
255 break;
256 default:
257 this.ParseHelper.UnexpectedAttribute(node, attrib);
258 break;
259 }
260 }
261 else
262 {
263 this.ParseHelper.ParseExtensionAttribute(this.Context.Extensions, intermediate, section, node, attrib);
264 }
265 } 58 }
266
267 // Make sure the key is valid. The key will default to the ProductCode for MSI packages
268 // and the package code for MSP packages in the binder if not specified.
269 if (!String.IsNullOrEmpty(key))
270 {
271 // Make sure the key does not contain any illegal characters or values.
272 if (0 <= (illegalChar = key.IndexOfAny(DependencyCommon.InvalidCharacters)))
273 {
274 var sb = new StringBuilder(DependencyCommon.InvalidCharacters.Length * 2);
275 Array.ForEach<char>(DependencyCommon.InvalidCharacters, c => sb.Append(c).Append(" "));
276
277 this.Messaging.Write(DependencyErrors.IllegalCharactersInProvider(sourceLineNumbers, "Key", key[illegalChar], sb.ToString()));
278 }
279 else if ("ALL" == key)
280 {
281 this.Messaging.Write(DependencyErrors.ReservedValue(sourceLineNumbers, node.Name.LocalName, "Key", key));
282 }
283 }
284 else if (PackageType.ExePackage == packageType || PackageType.MsuPackage == packageType)
285 {
286 // Must specify the provider key when authored for a package.
287 this.Messaging.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, node.Name.LocalName, "Key"));
288 }
289 else if (PackageType.None == packageType)
290 {
291 // Make sure the ProductCode is authored and set the key.
292 this.ParseHelper.CreateSimpleReference(section, sourceLineNumbers, SymbolDefinitions.Property, "ProductCode");
293 key = "!(bind.property.ProductCode)";
294 }
295
296 // The Version attribute should not be authored in or for an MSI package.
297 if (!String.IsNullOrEmpty(version))
298 {
299 switch (packageType)
300 {
301 case PackageType.None:
302 this.Messaging.Write(DependencyWarnings.DiscouragedVersionAttribute(sourceLineNumbers));
303 break;
304 case PackageType.MsiPackage:
305 this.Messaging.Write(DependencyWarnings.DiscouragedVersionAttribute(sourceLineNumbers, parentId));
306 break;
307 }
308 }
309 else if (PackageType.MspPackage == packageType || PackageType.MsuPackage == packageType)
310 {
311 // Must specify the Version when authored for packages that do not contain a version.
312 this.Messaging.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, node.Name.LocalName, "Version"));
313 }
314
315 // Need the element ID for child element processing, so generate now if not authored.
316 if (null == id)
317 {
318 id = this.ParseHelper.CreateIdentifier("dep", node.Name.LocalName, parentId, key);
319 }
320
321 foreach (var child in node.Elements())
322 {
323 if (this.Namespace == child.Name.Namespace)
324 {
325 switch (child.Name.LocalName)
326 {
327 case "Requires":
328 this.ParseRequiresElement(intermediate, section, child, id.Id, PackageType.None == packageType);
329 break;
330 case "RequiresRef":
331 this.ParseRequiresRefElement(intermediate, section, child, id.Id, PackageType.None == packageType);
332 break;
333 default:
334 this.ParseHelper.UnexpectedElement(node, child);
335 break;
336 }
337 }
338 else
339 {
340 this.ParseHelper.ParseExtensionElement(this.Context.Extensions, intermediate, section, node, child);
341 }
342 }
343
344 if (!this.Messaging.EncounteredError)
345 {
346 var symbol = section.AddSymbol(new WixDependencyProviderSymbol(sourceLineNumbers, id)
347 {
348 ComponentRef = parentId,
349 ProviderKey = key,
350 });
351
352 if (!String.IsNullOrEmpty(version))
353 {
354 symbol.Version = version;
355 }
356
357 if (!String.IsNullOrEmpty(displayName))
358 {
359 symbol.DisplayName = displayName;
360 }
361
362 if (PackageType.None == packageType)
363 {
364 this.ParseHelper.CreateCustomActionReference(sourceLineNumbers, section, "DependencyCheck", this.Context.Platform, CustomActionPlatforms.X86 | CustomActionPlatforms.X64 | CustomActionPlatforms.ARM64);
365
366 // Generate registry rows for the provider using binder properties.
367 var keyProvides = String.Concat(DependencyCommon.RegistryRoot, key);
368 var root = RegistryRootType.MachineUser;
369
370 var value = "[ProductCode]";
371 this.ParseHelper.CreateRegistrySymbol(section, sourceLineNumbers, root, keyProvides, null, value, parentId, false);
372
373 value = !String.IsNullOrEmpty(version) ? version : "[ProductVersion]";
374 var versionRegistrySymbol =
375 this.ParseHelper.CreateRegistrySymbol(section, sourceLineNumbers, root, keyProvides, "Version", value, parentId, false);
376
377 value = !String.IsNullOrEmpty(displayName) ? displayName : "[ProductName]";
378 this.ParseHelper.CreateRegistrySymbol(section, sourceLineNumbers, root, keyProvides, "DisplayName", value, parentId, false);
379
380 // Use the Version registry value and use that as a potential key path.
381 keyPath = this.CreateComponentKeyPath();
382 keyPath.Id = versionRegistrySymbol.Id;
383 keyPath.Explicit = false;
384 keyPath.Type = PossibleKeyPathType.Registry;
385 }
386 }
387
388 return keyPath;
389 }
390
391 /// <summary>
392 /// Processes the Requires element.
393 /// </summary>
394 /// <param name="node">The XML node for the Requires element.</param>
395 /// <param name="providerId">The parent provider identifier.</param>
396 /// <param name="requiresAction">Whether the Requires custom action should be referenced.</param>
397 private void ParseRequiresElement(Intermediate intermediate, IntermediateSection section, XElement node, string providerId, bool requiresAction)
398 {
399 var sourceLineNumbers = this.ParseHelper.GetSourceLineNumbers(node);
400 Identifier id = null;
401 string providerKey = null;
402 string minVersion = null;
403 string maxVersion = null;
404 int attributes = 0;
405 int illegalChar = -1;
406
407 foreach (var attrib in node.Attributes())
408 {
409 if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || this.Namespace == attrib.Name.Namespace)
410 {
411 switch (attrib.Name.LocalName)
412 {
413 case "Id":
414 id = this.ParseHelper.GetAttributeIdentifier(sourceLineNumbers, attrib);
415 break;
416 case "ProviderKey":
417 providerKey = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
418 break;
419 case "Minimum":
420 minVersion = this.ParseHelper.GetAttributeVersionValue(sourceLineNumbers, attrib);
421 break;
422 case "Maximum":
423 maxVersion = this.ParseHelper.GetAttributeVersionValue(sourceLineNumbers, attrib);
424 break;
425 case "IncludeMinimum":
426 if (YesNoType.Yes == this.ParseHelper.GetAttributeYesNoValue(sourceLineNumbers, attrib))
427 {
428 attributes |= DependencyCommon.RequiresAttributesMinVersionInclusive;
429 }
430 break;
431 case "IncludeMaximum":
432 if (YesNoType.Yes == this.ParseHelper.GetAttributeYesNoValue(sourceLineNumbers, attrib))
433 {
434 attributes |= DependencyCommon.RequiresAttributesMaxVersionInclusive;
435 }
436 break;
437 default:
438 this.ParseHelper.UnexpectedAttribute(node, attrib);
439 break;
440 }
441 }
442 else
443 {
444 this.ParseHelper.ParseExtensionAttribute(this.Context.Extensions, intermediate, section, node, attrib);
445 }
446 }
447
448 this.ParseHelper.ParseForExtensionElements(this.Context.Extensions, intermediate, section, node);
449
450 if (null == id)
451 {
452 // Generate an ID only if this element is authored under a Provides element; otherwise, a RequiresRef
453 // element will be necessary and the Id attribute will be required.
454 if (!String.IsNullOrEmpty(providerId))
455 {
456 id = this.ParseHelper.CreateIdentifier("dep", node.Name.LocalName, providerKey);
457 }
458 else
459 {
460 this.Messaging.Write(ErrorMessages.ExpectedAttributeWhenElementNotUnderElement(sourceLineNumbers, node.Name.LocalName, "Id", "Provides"));
461 id = Identifier.Invalid;
462 }
463 }
464
465 if (String.IsNullOrEmpty(providerKey))
466 {
467 this.Messaging.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, node.Name.LocalName, "ProviderKey"));
468 }
469 // Make sure the key does not contain any illegal characters.
470 else if (0 <= (illegalChar = providerKey.IndexOfAny(DependencyCommon.InvalidCharacters)))
471 {
472 var sb = new StringBuilder(DependencyCommon.InvalidCharacters.Length * 2);
473 Array.ForEach<char>(DependencyCommon.InvalidCharacters, c => sb.Append(c).Append(" "));
474
475 this.Messaging.Write(DependencyErrors.IllegalCharactersInProvider(sourceLineNumbers, "ProviderKey", providerKey[illegalChar], sb.ToString()));
476 }
477
478 if (!this.Messaging.EncounteredError)
479 {
480 // Reference the Require custom action if required.
481 if (requiresAction)
482 {
483 this.AddReferenceToWixDependencyRequire(section, sourceLineNumbers);
484 }
485
486 var symbol = section.AddSymbol(new WixDependencySymbol(sourceLineNumbers, id)
487 {
488 ProviderKey = providerKey,
489 MinVersion = minVersion,
490 MaxVersion = maxVersion,
491 });
492
493 if (0 != attributes)
494 {
495 symbol.Attributes = attributes;
496 }
497
498 // Create the relationship between this WixDependency symbol and the WixDependencyProvider symbol.
499 if (!String.IsNullOrEmpty(providerId))
500 {
501 section.AddSymbol(new WixDependencyRefSymbol(sourceLineNumbers)
502 {
503 WixDependencyProviderRef = providerId,
504 WixDependencyRef = id.Id,
505 });
506 }
507 }
508 }
509
510 /// <summary>
511 /// Processes the RequiresRef element.
512 /// </summary>
513 /// <param name="node">The XML node for the RequiresRef element.</param>
514 /// <param name="providerId">The parent provider identifier.</param>
515 /// <param name="requiresAction">Whether the Requires custom action should be referenced.</param>
516 private void ParseRequiresRefElement(Intermediate intermediate, IntermediateSection section, XElement node, string providerId, bool requiresAction)
517 {
518 var sourceLineNumbers = this.ParseHelper.GetSourceLineNumbers(node);
519 string id = null;
520
521 foreach (var attrib in node.Attributes())
522 {
523 if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || this.Namespace == attrib.Name.Namespace)
524 {
525 switch (attrib.Name.LocalName)
526 {
527 case "Id":
528 id = this.ParseHelper.GetAttributeIdentifierValue(sourceLineNumbers, attrib);
529 break;
530 default:
531 this.ParseHelper.UnexpectedAttribute(node, attrib);
532 break;
533 }
534 }
535 else
536 {
537 this.ParseHelper.ParseExtensionAttribute(this.Context.Extensions, intermediate, section, node, attrib);
538 }
539 }
540
541 this.ParseHelper.ParseForExtensionElements(this.Context.Extensions, intermediate, section, node);
542
543 if (String.IsNullOrEmpty(id))
544 {
545 this.Messaging.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, node.Name.LocalName, "Id"));
546 }
547
548 if (!this.Messaging.EncounteredError)
549 {
550 // Reference the Require custom action if required.
551 if (requiresAction)
552 {
553 this.AddReferenceToWixDependencyRequire(section, sourceLineNumbers);
554 }
555
556 // Create a link dependency on the row that contains information we'll need during bind.
557 this.ParseHelper.CreateSimpleReference(section, sourceLineNumbers, DependencySymbolDefinitions.WixDependency, id);
558
559 // Create the relationship between the WixDependency row and the parent WixDependencyProvider row.
560 section.AddSymbol(new WixDependencyRefSymbol(sourceLineNumbers)
561 {
562 WixDependencyProviderRef = providerId,
563 WixDependencyRef = id,
564 });
565 }
566 }
567
568 private void AddReferenceToWixDependencyRequire(IntermediateSection section, SourceLineNumber sourceLineNumbers)
569 {
570 this.ParseHelper.CreateCustomActionReference(sourceLineNumbers, section, "DependencyRequire", this.Context.Platform, CustomActionPlatforms.X86 | CustomActionPlatforms.X64 | CustomActionPlatforms.ARM64);
571 } 59 }
572 } 60 }
573} 61}