@joebigelow / wix / commits / ae7e9817

Add RemoveRegistryKey

Rob Mensching committed Apr 11, 2021 at 12:23 UTC ae7e9817bb10d635e031e51496f2e529595a9cfe
11 files changed +366 -6
src/ca/RemoveRegistryKeysEx.cpp new
+114
@@ -0,0 +1,114 @@
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 +#include "precomp.h"
4 +
5 +LPCWSTR vcsRemoveRegistryKeyExQuery =
6 + L"SELECT `Wix4RemoveRegistryKeyEx`, `Component_`, `Root`, `Key`, `InstallMode`, `Condition` FROM `Wix4RemoveRegistryKeyEx`";
7 +enum eRemoveRegistryKeyExQuery { rrxqId = 1, rrxqComponent, rrxqRoot, rrxqKey, rrxqMode, rrxqCondition };
8 +
9 +extern "C" UINT WINAPI WixRemoveRegistryKeysEx(
10 + __in MSIHANDLE hInstall
11 +)
12 +{
13 + //AssertSz(FALSE, "debug WixRemoveRegistryKeyEx");
14 +
15 + HRESULT hr = S_OK;
16 + PMSIHANDLE hView;
17 + PMSIHANDLE hRec;
18 + LPWSTR sczId = NULL;
19 + LPWSTR sczComponent = NULL;
20 + LPWSTR sczCondition = NULL;
21 + LPWSTR sczKey = NULL;
22 + int iRoot = 0;
23 + int iMode = 0;
24 + MSIHANDLE hTable = NULL;
25 + MSIHANDLE hColumns = NULL;
26 +
27 + hr = WcaInitialize(hInstall, __FUNCTION__);
28 + ExitOnFailure(hr, "Failed to initialize " __FUNCTION__);
29 +
30 + // anything to do?
31 + if (S_OK != WcaTableExists(L"Wix4RemoveRegistryKeyEx"))
32 + {
33 + WcaLog(LOGMSG_STANDARD, "Wix4RemoveRegistryKeyEx table doesn't exist, so there are no registry keys to remove.");
34 + ExitFunction();
35 + }
36 +
37 + hr = WcaOpenExecuteView(vcsRemoveRegistryKeyExQuery, &hView);
38 + ExitOnFailure(hr, "Failed to open view on Wix4RemoveRegistryKeyEx table");
39 +
40 + while (S_OK == (hr = WcaFetchRecord(hView, &hRec)))
41 + {
42 + hr = WcaGetRecordString(hRec, rrxqId, &sczId);
43 + ExitOnFailure(hr, "Failed to get Wix4RemoveRegistryKeyEx identity.");
44 +
45 + hr = WcaGetRecordString(hRec, rrxqCondition, &sczCondition);
46 + ExitOnFailure(hr, "Failed to get Wix4RemoveRegistryKeyEx condition.");
47 +
48 + if (sczCondition && *sczCondition)
49 + {
50 + MSICONDITION condition = ::MsiEvaluateConditionW(hInstall, sczCondition);
51 + if (MSICONDITION_TRUE == condition)
52 + {
53 + WcaLog(LOGMSG_STANDARD, "True condition for row %S: %S; processing.", sczId, sczCondition);
54 + }
55 + else
56 + {
57 + WcaLog(LOGMSG_STANDARD, "False or invalid condition for row %S: %S; skipping.", sczId, sczCondition);
58 + continue;
59 + }
60 + }
61 +
62 + hr = WcaGetRecordString(hRec, rrxqComponent, &sczComponent);
63 + ExitOnFailure(hr, "Failed to get Wix4RemoveRegistryKeyEx component.");
64 +
65 + hr = WcaGetRecordInteger(hRec, rrxqRoot, &iRoot);
66 + ExitOnFailure(hr, "Failed to get Wix4RemoveRegistryKeyEx root.");
67 +
68 + hr = WcaGetRecordString(hRec, rrxqKey, &sczKey);
69 + ExitOnFailure(hr, "Failed to get Wix4RemoveRegistryKeyEx key.");
70 +
71 + hr = WcaGetRecordInteger(hRec, rrxqMode, &iMode);
72 + ExitOnFailure(hr, "Failed to get Wix4RemoveRegistryKeyEx mode.");
73 +
74 + switch (iMode)
75 + {
76 + case 1: // remove on install
77 + WcaLog(LOGMSG_STANDARD, "Adding RemoveRegistry row: %ls/%d/%ls/-/%ls", sczId, iRoot, sczKey, sczComponent);
78 + hr = WcaAddTempRecord(&hTable, &hColumns, L"RemoveRegistry", NULL, 0, 5, sczId, iRoot, sczKey, L"-", sczComponent);
79 + ExitOnFailure(hr, "Failed to add RemoveRegistry row for remove-on-install Wix4RemoveRegistryKeyEx row: %ls:", sczId);
80 + break;
81 + case 2: // remove on uninstall
82 + WcaLog(LOGMSG_STANDARD, "Adding Registry row: %ls/%d/%ls/-/null/%ls", sczId, iRoot, sczKey, sczComponent);
83 + hr = WcaAddTempRecord(&hTable, &hColumns, L"Registry", NULL, 0, 6, sczId, iRoot, sczKey, L"-", NULL, sczComponent);
84 + ExitOnFailure(hr, "Failed to add Registry row for remove-on-uninstall Wix4RemoveRegistryKeyEx row: %ls:", sczId);
85 + break;
86 + }
87 + }
88 +
89 + // reaching the end of the list is actually a good thing, not an error
90 + if (E_NOMOREITEMS == hr)
91 + {
92 + hr = S_OK;
93 + }
94 + ExitOnFailure(hr, "Failure occured while processing Wix4RemoveRegistryKeyEx table.");
95 +
96 +LExit:
97 + if (hColumns)
98 + {
99 + ::MsiCloseHandle(hColumns);
100 + }
101 +
102 + if (hTable)
103 + {
104 + ::MsiCloseHandle(hTable);
105 + }
106 +
107 + ReleaseStr(sczKey);
108 + ReleaseStr(sczComponent);
109 + ReleaseStr(sczCondition);
110 + ReleaseStr(sczId);
111 +
112 + DWORD er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
113 + return WcaFinalize(er);
114 +}
src/ca/utilca.def
+2
@@ -35,6 +35,8 @@ EXPORTS
35 WixSilentExec64
36 ; RemoveFoldersEx.cpp
37 WixRemoveFoldersEx
38 +; RemoveRegistryKeysEx.cpp
39 + WixRemoveRegistryKeysEx
40 ;scaexec.cpp
41 RegisterPerfCounterData
42 UnregisterPerfCounterData
src/ca/utilca.vcxproj
+1
@@ -59,6 +59,7 @@
59 <ClCompile Include="OsInfo.cpp" />
60 <ClCompile Include="qtexecca.cpp" />
61 <ClCompile Include="RemoveFoldersEx.cpp" />
62 + <ClCompile Include="RemoveRegistryKeysEx.cpp" />
63 <ClCompile Include="RestartManager.cpp" />
64 <ClCompile Include="scaexec.cpp" />
65 <ClCompile Include="scamanifest.cpp" />
src/test/WixToolsetTest.Util/TestData/RemoveRegistryKeyEx/Module.wxs new
+13
@@ -0,0 +1,13 @@
1 +<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
2 + <Module Language="1033" Version="1.0.0.0" Id="InternetShortcutModule" Guid="047730a5-30fe-4a62-a520-da9381b8226a">
3 + <SummaryInformation Manufacturer="Example Corporation" />
4 +
5 + <ComponentGroupRef Id="ModuleComponents" />
6 + </Module>
7 +
8 + <Fragment>
9 + <StandardDirectory Id="ProgramFilesFolder">
10 + <Directory Id="INSTALLFOLDER" Name="MergeModule" />
11 + </StandardDirectory>
12 + </Fragment>
13 +</Wix>
src/test/WixToolsetTest.Util/TestData/RemoveRegistryKeyEx/ModuleComponents.wxs new
+10
@@ -0,0 +1,10 @@
1 +<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
2 + <Fragment>
3 + <ComponentGroup Id="ModuleComponents" Directory="INSTALLFOLDER">
4 + <Component>
5 + <File Source="ModuleComponents.wxs" />
6 + <util:RemoveRegistryKey Root="HKLM" Key="SOFTWARE\Example" On="install" />
7 + </Component>
8 + </ComponentGroup>
9 + </Fragment>
10 +</Wix>
src/test/WixToolsetTest.Util/UtilExtensionFixture.cs
+15
@@ -129,6 +129,21 @@ namespace WixToolsetTest.Util
129 }, results.OrderBy(s => s).ToArray());
130 }
131
132 + [Fact]
133 + public void CanBuildRemoveRegistryKeyExInMergeModule()
134 + {
135 + var folder = TestData.Get(@"TestData", "RemoveRegistryKeyEx");
136 + var build = new Builder(folder, typeof(UtilExtensionFactory), new[] { folder }, "test.msm");
137 +
138 + var results = build.BuildAndQuery(BuildX64, "Binary", "CustomAction", "RemoveRegistry", "Wix4RemoveRegistryKeyEx");
139 + WixAssert.CompareLineByLine(new[]
140 + {
141 + "Binary:Wix4UtilCA_X64.047730A5_30FE_4A62_A520_DA9381B8226A\t[Binary data]",
142 + "CustomAction:Wix4RemoveRegistryKeysEx_X64.047730A5_30FE_4A62_A520_DA9381B8226A\t65\tWix4UtilCA_X64.047730A5_30FE_4A62_A520_DA9381B8226A\tWixRemoveRegistryKeysEx\t",
143 + "Wix4RemoveRegistryKeyEx:rrxfcDhR4HhE3v3rYiQcNtQjyahQNg.047730A5_30FE_4A62_A520_DA9381B8226A\tfilh4juyUVjoUcWWtcQmd5L07FoON4.047730A5_30FE_4A62_A520_DA9381B8226A\t2\tSOFTWARE\\Example\t1\t",
144 + }, results.OrderBy(s => s).ToArray());
145 + }
146 +
147 [Fact]
148 public void CanBuildRemoveFolderExInMergeModule()
149 {
src/wixext/Symbols/UtilSymbolDefinitions.cs
+4
@@ -23,6 +23,7 @@ namespace WixToolset.Util
23 WixFormatFiles,
24 WixInternetShortcut,
25 WixRemoveFolderEx,
26 + WixRemoveRegistryKeyEx,
27 WixRestartResource,
28 WixTouchFile,
29 WixWindowsFeatureSearch,
@@ -93,6 +94,9 @@ namespace WixToolset.Util
94 case UtilSymbolDefinitionType.WixRemoveFolderEx:
95 return UtilSymbolDefinitions.WixRemoveFolderEx;
96
97 + case UtilSymbolDefinitionType.WixRemoveRegistryKeyEx:
98 + return UtilSymbolDefinitions.WixRemoveRegistryKeyEx;
99 +
100 case UtilSymbolDefinitionType.WixRestartResource:
101 return UtilSymbolDefinitions.WixRestartResource;
102
src/wixext/Symbols/WixRemoveRegistryKeyExSymbol.cs new
+86
@@ -0,0 +1,86 @@
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.Util
4 +{
5 + using WixToolset.Data;
6 + using WixToolset.Util.Symbols;
7 +
8 + public static partial class UtilSymbolDefinitions
9 + {
10 + public static readonly IntermediateSymbolDefinition WixRemoveRegistryKeyEx = new IntermediateSymbolDefinition(
11 + UtilSymbolDefinitionType.WixRemoveRegistryKeyEx.ToString(),
12 + new[]
13 + {
14 + new IntermediateFieldDefinition(nameof(WixRemoveRegistryKeyExSymbolFields.ComponentRef), IntermediateFieldType.String),
15 + new IntermediateFieldDefinition(nameof(WixRemoveRegistryKeyExSymbolFields.Root), IntermediateFieldType.Number),
16 + new IntermediateFieldDefinition(nameof(WixRemoveRegistryKeyExSymbolFields.Key), IntermediateFieldType.String),
17 + new IntermediateFieldDefinition(nameof(WixRemoveRegistryKeyExSymbolFields.InstallMode), IntermediateFieldType.Number),
18 + new IntermediateFieldDefinition(nameof(WixRemoveRegistryKeyExSymbolFields.Condition), IntermediateFieldType.String),
19 + },
20 + typeof(WixRemoveRegistryKeyExSymbol));
21 + }
22 +}
23 +
24 +namespace WixToolset.Util.Symbols
25 +{
26 + using WixToolset.Data;
27 + using WixToolset.Data.Symbols;
28 +
29 + public enum WixRemoveRegistryKeyExSymbolFields
30 + {
31 + ComponentRef,
32 + Root,
33 + Key,
34 + InstallMode,
35 + Condition,
36 + }
37 +
38 + public enum WixRemoveRegistryKeyExInstallMode
39 + {
40 + Install = 1,
41 + Uninstall = 2,
42 + }
43 +
44 + public class WixRemoveRegistryKeyExSymbol : IntermediateSymbol
45 + {
46 + public WixRemoveRegistryKeyExSymbol() : base(UtilSymbolDefinitions.WixRemoveRegistryKeyEx, null, null)
47 + {
48 + }
49 +
50 + public WixRemoveRegistryKeyExSymbol(SourceLineNumber sourceLineNumber, Identifier id = null) : base(UtilSymbolDefinitions.WixRemoveRegistryKeyEx, sourceLineNumber, id)
51 + {
52 + }
53 +
54 + public IntermediateField this[WixRemoveRegistryKeyExSymbolFields index] => this.Fields[(int)index];
55 +
56 + public string ComponentRef
57 + {
58 + get => this.Fields[(int)WixRemoveRegistryKeyExSymbolFields.ComponentRef].AsString();
59 + set => this.Set((int)WixRemoveRegistryKeyExSymbolFields.ComponentRef, value);
60 + }
61 +
62 + public RegistryRootType Root
63 + {
64 + get => (RegistryRootType)this.Fields[(int)WixRemoveRegistryKeyExSymbolFields.Root].AsNumber();
65 + set => this.Set((int)WixRemoveRegistryKeyExSymbolFields.Root, (int)value);
66 + }
67 +
68 + public string Key
69 + {
70 + get => (string)this.Fields[(int)WixRemoveRegistryKeyExSymbolFields.Key];
71 + set => this.Set((int)WixRemoveRegistryKeyExSymbolFields.Key, value);
72 + }
73 +
74 + public WixRemoveRegistryKeyExInstallMode InstallMode
75 + {
76 + get => (WixRemoveRegistryKeyExInstallMode)this.Fields[(int)WixRemoveRegistryKeyExSymbolFields.InstallMode].AsNumber();
77 + set => this.Set((int)WixRemoveRegistryKeyExSymbolFields.InstallMode, (int)value);
78 + }
79 +
80 + public string Condition
81 + {
82 + get => this.Fields[(int)WixRemoveRegistryKeyExSymbolFields.Condition].AsString();
83 + set => this.Set((int)WixRemoveRegistryKeyExSymbolFields.Condition, value);
84 + }
85 + }
86 +}
src/wixext/UtilCompiler.cs
+97 -6
@@ -35,12 +35,6 @@ namespace WixToolset.Util
35 internal const int UserDontCreateUser = 0x00000200;
36 internal const int UserNonVital = 0x00000400;
37
38 - internal enum WixRegistrySearchFormat
39 - {
40 - Raw,
41 - Compatible,
42 - }
43 -
38 private static readonly Regex FindPropertyBrackets = new Regex(@"\[(?!\\|\])|(?<!\[\\\]|\[\\|\\\[)\]", RegexOptions.ExplicitCapture | RegexOptions.Compiled);
39
40 public override XNamespace Namespace => "http://wixtoolset.org/schemas/v4/wxs/util";
@@ -135,6 +129,9 @@ namespace WixToolset.Util
129 case "RemoveFolderEx":
130 this.ParseRemoveFolderExElement(intermediate, section, element, componentId);
131 break;
132 + case "RemoveRegistryKey":
133 + this.ParseRemoveRegistryKeyExElement(intermediate, section, element, componentId);
134 + break;
135 case "RestartResource":
136 this.ParseRestartResourceElement(intermediate, section, element, componentId);
137 break;
@@ -2887,6 +2884,100 @@ namespace WixToolset.Util
2884 }
2885 }
2886
2887 + /// <summary>
2888 + /// Parses a RemoveRegistryKeyEx element.
2889 + /// </summary>
2890 + /// <param name="node">Element to parse.</param>
2891 + /// <param name="componentId">Identifier of parent component.</param>
2892 + private void ParseRemoveRegistryKeyExElement(Intermediate intermediate, IntermediateSection section, XElement element, string componentId)
2893 + {
2894 + var sourceLineNumbers = this.ParseHelper.GetSourceLineNumbers(element);
2895 + Identifier id = null;
2896 + var mode = WixRemoveRegistryKeyExInstallMode.Uninstall;
2897 + string condition = null;
2898 + RegistryRootType? root = null;
2899 + string key = null;
2900 +
2901 + foreach (var attrib in element.Attributes())
2902 + {
2903 + if (String.IsNullOrEmpty(attrib.Name.NamespaceName) || this.Namespace == attrib.Name.Namespace)
2904 + {
2905 + switch (attrib.Name.LocalName)
2906 + {
2907 + case "Condition":
2908 + condition = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
2909 + break;
2910 + case "Id":
2911 + id = this.ParseHelper.GetAttributeIdentifier(sourceLineNumbers, attrib);
2912 + break;
2913 + case "On":
2914 + var actionValue = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
2915 + switch (actionValue)
2916 + {
2917 + case "":
2918 + break;
2919 + case "install":
2920 + mode = WixRemoveRegistryKeyExInstallMode.Install;
2921 + break;
2922 + case "uninstall":
2923 + mode = WixRemoveRegistryKeyExInstallMode.Uninstall;
2924 + break;
2925 + default:
2926 + this.Messaging.Write(ErrorMessages.IllegalAttributeValue(sourceLineNumbers, element.Name.LocalName, "On", actionValue, "install", "uninstall"));
2927 + break;
2928 + }
2929 + break;
2930 + case "Root":
2931 + root = this.ParseHelper.GetAttributeRegistryRootValue(sourceLineNumbers, attrib, false);
2932 + break;
2933 + case "Key":
2934 + key = this.ParseHelper.GetAttributeValue(sourceLineNumbers, attrib);
2935 + break;
2936 + default:
2937 + this.ParseHelper.UnexpectedAttribute(element, attrib);
2938 + break;
2939 + }
2940 + }
2941 + else
2942 + {
2943 + this.ParseHelper.ParseExtensionAttribute(this.Context.Extensions, intermediate, section, element, attrib);
2944 + }
2945 + }
2946 +
2947 + if (!root.HasValue)
2948 + {
2949 + this.Messaging.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, element.Name.LocalName, "Root"));
2950 + }
2951 +
2952 + if (key == null)
2953 + {
2954 + this.Messaging.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, element.Name.LocalName, "Key"));
2955 + }
2956 +
2957 + if (id == null)
2958 + {
2959 + id = this.ParseHelper.CreateIdentifier("rrx", componentId, condition, root.ToString(), key, mode.ToString());
2960 + }
2961 +
2962 + this.ParseHelper.ParseForExtensionElements(this.Context.Extensions, intermediate, section, element);
2963 +
2964 + if (!this.Messaging.EncounteredError)
2965 + {
2966 + this.ParseHelper.EnsureTable(section, sourceLineNumbers, "Registry");
2967 + this.ParseHelper.EnsureTable(section, sourceLineNumbers, "RemoveRegistry");
2968 + this.ParseHelper.CreateCustomActionReference(sourceLineNumbers, section, "Wix4RemoveRegistryKeysEx", this.Context.Platform, CustomActionPlatforms.X86 | CustomActionPlatforms.X64 | CustomActionPlatforms.ARM64);
2969 +
2970 + section.AddSymbol(new WixRemoveRegistryKeyExSymbol(sourceLineNumbers, id)
2971 + {
2972 + ComponentRef = componentId,
2973 + Root = root.Value,
2974 + Key = key,
2975 + InstallMode = mode,
2976 + Condition = condition
2977 + });
2978 + }
2979 + }
2980 +
2981 /// <summary>
2982 /// Parses a RestartResource element.
2983 /// </summary>
src/wixext/UtilTableDefinitions.cs
+16
@@ -38,6 +38,21 @@ namespace WixToolset.Util
38 symbolIdIsPrimaryKey: true
39 );
40
41 + public static readonly TableDefinition Wix4RemoveRegistryKeyEx = new TableDefinition(
42 + "Wix4RemoveRegistryKeyEx",
43 + UtilSymbolDefinitions.WixRemoveRegistryKeyEx,
44 + new[]
45 + {
46 + new ColumnDefinition("Wix4RemoveRegistryKeyEx", ColumnType.String, 72, primaryKey: true, nullable: false, ColumnCategory.Identifier, description: "Identifier for the Wix4RemoveRegistryKeyEx row in the package.", modularizeType: ColumnModularizeType.Column),
47 + new ColumnDefinition("Component_", ColumnType.String, 72, primaryKey: false, nullable: false, ColumnCategory.Identifier, keyTable: "Component", keyColumn: 1, description: "Foreign key into the Component table used to determine install state", modularizeType: ColumnModularizeType.Column),
48 + new ColumnDefinition("Root", ColumnType.Number, 2, primaryKey: false, nullable: false, ColumnCategory.Unknown, minValue: -1, maxValue: 3, description: "The predefined root key for the registry value, one of rrkEnum."),
49 + new ColumnDefinition("Key", ColumnType.Localized, 255, primaryKey: false, nullable: false, ColumnCategory.RegPath, description: "The key for the registry value.", modularizeType: ColumnModularizeType.Property),
50 + new ColumnDefinition("InstallMode", ColumnType.Number, 2, primaryKey: false, nullable: false, ColumnCategory.Unknown, minValue: 1, maxValue: 3, description: "1 == Remove only when the associated component is being installed (msiInstallStateLocal or msiInstallStateSource), 2 == Remove only when the associated component is being removed (msiInstallStateAbsent), 3 = Remove in either of the above cases."),
51 + new ColumnDefinition("Condition", ColumnType.String, 0, primaryKey: false, nullable: true, ColumnCategory.Condition, description: "Optional expression to control whether the registry key is removed.", modularizeType: ColumnModularizeType.Condition, forceLocalizable: true),
52 + },
53 + symbolIdIsPrimaryKey: true
54 + );
55 +
56 public static readonly TableDefinition Wix4RestartResource = new TableDefinition(
57 "Wix4RestartResource",
58 UtilSymbolDefinitions.WixRestartResource,
@@ -281,6 +296,7 @@ namespace WixToolset.Util
296 {
297 Wix4CloseApplication,
298 Wix4RemoveFolderEx,
299 + Wix4RemoveRegistryKeyEx,
300 Wix4RestartResource,
301 Wix4FileShare,
302 Wix4FileSharePermissions,
src/wixlib/UtilExtension_Platform.wxi
+8
@@ -44,6 +44,14 @@
44 </InstallExecuteSequence>
45 </Fragment>
46
47 + <Fragment>
48 + <CustomAction Id="$(var.Prefix)RemoveRegistryKeysEx$(var.Suffix)" DllEntry="WixRemoveRegistryKeysEx" Execute="immediate" Return="ignore" BinaryRef="$(var.Prefix)UtilCA$(var.Suffix)" />
49 +
50 + <InstallExecuteSequence>
51 + <Custom Action="$(var.Prefix)RemoveRegistryKeysEx$(var.Suffix)" Before="RemoveRegistryValues" />
52 + </InstallExecuteSequence>
53 + </Fragment>
54 +
55 <Fragment>
56 <CustomAction Id="$(var.Prefix)BroadcastSettingChange$(var.Suffix)" DllEntry="WixBroadcastSettingChange" Execute="immediate" Return="ignore" SuppressModularization="yes" BinaryRef="$(var.Prefix)UtilCA$(var.Suffix)" />
57