 
            TODO: - make awstring version of MsiProvideComponent() and use that. - Actually implement MsiProvideAssemblyA() using the above. - Write conformance test?
Dependency: - Have ACTION_PublishAssemblies() record feature (https://gitlab.winehq.org/wine/wine/-/merge_requests/9037) - Have MsiEnumFeatures() fixed. Currently it seems to just look for features at the wrong place and thus is useless for this purpose. (https://gitlab.winehq.org/wine/wine/-/merge_requests/9041)
Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=58578
-- v4: [WIP] msi: implement MsiProvideAssembly[AW]()
 
            From: Ratchanan Srirattanamet peathot+winehq@hotmail.com
TODO: - make awstring version of MsiProvideComponent() and use that. - Actually implement MsiProvideAssemblyA() using the above. - Write conformance test?
Dependency: - Have ACTION_PublishAssemblies() record feature - Have MsiEnumFeatures() fixed. Currently it seems to just look for features at the wrong place and thus is useless for this purpose.
Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=58578 --- dlls/msi/assembly.c | 116 ++++++++++++++++++++++++++++++++++++++++++++ dlls/msi/msi.c | 56 ++++++++++++++++++++- dlls/msi/msipriv.h | 2 + include/msi.h | 5 ++ 4 files changed, 177 insertions(+), 2 deletions(-)
diff --git a/dlls/msi/assembly.c b/dlls/msi/assembly.c index 6f386d64a88..19dbedace05 100644 --- a/dlls/msi/assembly.c +++ b/dlls/msi/assembly.c @@ -685,3 +685,119 @@ UINT ACTION_MsiUnpublishAssemblies( MSIPACKAGE *package ) } return ERROR_SUCCESS; } + +UINT msi_lookup_published_assembly(UINT install_context, + const WCHAR * displayname, + const WCHAR * app_context, + BOOL win32, + WCHAR * out_product_id, + WCHAR * out_feature, + WCHAR * out_component_id) +{ + UINT rc = ERROR_FUNCTION_FAILED; + LONG res; + HRESULT hr; + HKEY hkey; + IAssemblyName * name; + + DWORD num_values; + DWORD max_value_name_len; + DWORD max_value_data_len; + WCHAR * value_name; + WCHAR * value_data; + DWORD index; + BOOL found; + + if (!init_assembly_caches() || !pCreateAssemblyNameObject) + return ERROR_FUNCTION_FAILED; + + hr = pCreateAssemblyNameObject( &name, displayname, CANOF_PARSE_DISPLAY_NAME, NULL ); + if (FAILED( hr )) return ERROR_INVALID_PARAMETER; + + if (app_context) + { + if ((res = open_local_assembly_key( install_context, win32, app_context, &hkey ))) + { + WARN( "failed to open local assembly key %ld\n", res ); + rc = ERROR_FUNCTION_FAILED; + goto out_name; + } + } + else + { + if ((res = open_global_assembly_key( install_context, win32, &hkey ))) + { + WARN( "failed to open global assembly key %ld\n", res ); + rc = ERROR_FUNCTION_FAILED; + goto out_name; + } + } + + rc = RegQueryInfoKeyW(hkey, NULL, NULL, NULL, NULL, NULL, NULL, + &num_values, &max_value_name_len, &max_value_data_len, + NULL, NULL); + if (rc != ERROR_SUCCESS) + goto out_reg; + if (num_values == 0) { + rc = ERROR_UNKNOWN_COMPONENT; + goto out_reg; + } + + value_name = malloc((max_value_name_len + 1) * sizeof(WCHAR)); + value_data = malloc((max_value_data_len + 1) * sizeof(WCHAR)); + found = FALSE; + + for (index = 0; index < num_values && !found; index++) + { + DWORD value_type; + DWORD len_value_name = max_value_name_len + 1; + DWORD len_value_data = max_value_data_len + 1; + IAssemblyName * value_asmname; + + rc = RegEnumValueW(hkey, index, value_name, &len_value_name, NULL, + &value_type, (BYTE *) value_data, &len_value_data); + if (rc != ERROR_SUCCESS) { + rc = ERROR_BAD_CONFIGURATION; + break; + } + + if (value_type != REG_MULTI_SZ) { + rc = ERROR_BAD_CONFIGURATION;; + break; + } + + hr = pCreateAssemblyNameObject( &value_asmname, value_name, CANOF_PARSE_DISPLAY_NAME, NULL ); + if (FAILED( hr )) { + rc = ERROR_BAD_CONFIGURATION; + break; + } + + /* IsEqual will do the right thing if `name` does not specify all + components of a display name by just skipping them. */ + hr = IAssemblyName_IsEqual(name, value_asmname, ASM_CMPF_IL_ALL); + + if (hr == S_OK) { + found = TRUE; + + rc = MsiDecomposeDescriptorW( + value_data, out_product_id, out_feature, out_component_id, NULL); + if (rc == ERROR_INVALID_PARAMETER) + rc = ERROR_BAD_CONFIGURATION; + } else { + index++; + } + + IAssemblyName_Release(value_asmname); + } + + if (!found && rc == ERROR_SUCCESS) + rc = ERROR_UNKNOWN_COMPONENT; + + free(value_name); + free(value_data); +out_reg: + RegCloseKey(hkey); +out_name: + IAssemblyName_Release(name); + return rc; +} diff --git a/dlls/msi/msi.c b/dlls/msi/msi.c index 6edee9c8185..f2f96978e61 100644 --- a/dlls/msi/msi.c +++ b/dlls/msi/msi.c @@ -2486,12 +2486,64 @@ UINT WINAPI MsiProvideAssemblyA( const char *szAssemblyName, const char *szAppCo return ERROR_CALL_NOT_IMPLEMENTED; }
+// TODO: make awstring version of MsiProvideComponent() and use that. UINT WINAPI MsiProvideAssemblyW( const WCHAR *szAssemblyName, const WCHAR *szAppContext, DWORD dwInstallMode, DWORD dwAssemblyInfo, WCHAR *lpPathBuf, DWORD *pcchPathBuf ) { - FIXME( "%s, %s, %#lx, %#lx, %p, %p\n", debugstr_w(szAssemblyName), debugstr_w(szAppContext), dwInstallMode, + UINT rc; + BOOL win32; + WCHAR product[MAX_FEATURE_CHARS+1]; + WCHAR feature[MAX_FEATURE_CHARS+1]; + WCHAR feature_parent[MAX_FEATURE_CHARS+1]; + WCHAR component[MAX_FEATURE_CHARS+1]; + + TRACE( "%s, %s, %#lx, %#lx, %p, %p\n", debugstr_w(szAssemblyName), debugstr_w(szAppContext), dwInstallMode, dwAssemblyInfo, lpPathBuf, pcchPathBuf ); - return ERROR_CALL_NOT_IMPLEMENTED; + + win32 = (dwAssemblyInfo == MSIASSEMBLYINFO_WIN32ASSEMBLY); + + rc = msi_lookup_published_assembly(MSIINSTALLCONTEXT_USERMANAGED, szAssemblyName, szAppContext, + win32, product, feature, component); + + if (rc != ERROR_SUCCESS) { + rc = msi_lookup_published_assembly(MSIINSTALLCONTEXT_MACHINE, szAssemblyName, szAppContext, + win32, product, feature, component); + } + + if (rc != ERROR_SUCCESS) { + rc = msi_lookup_published_assembly(MSIINSTALLCONTEXT_USERUNMANAGED, szAssemblyName, szAppContext, + win32, product, feature, component); + } + + if (rc != ERROR_SUCCESS) + return rc; + + /* If the product contain only 1 feature, it won't be recorded in assembly publication. + * Look it up to use as a parameter in MSIProvideComponentW(). + */ + if (feature[0] == '\0') { + /* First, verify that this product indeed has single feature. */ + rc = MsiEnumFeaturesW(product, 1, feature, feature_parent); + if (rc == ERROR_SUCCESS) { + ERR("Product for assembly '%ls' has > 1 feature, but the published assembly record " + "doesn't have feature. This can happen if package is installed in older version " + "of Wine. Try re-installing this package.\n", szAssemblyName); + return ERROR_BAD_CONFIGURATION; + } + + /* Now obtain the feature name to use with MsiProvideComponentW(). */ + rc = MsiEnumFeaturesW(product, 0, feature, feature_parent); + if (rc != ERROR_SUCCESS) + return ERROR_BAD_CONFIGURATION; + } + + if (dwInstallMode == INSTALLMODE_NODETECTION_ANY) { + FIXME("INSTALLMODE_NODETECTION_ANY currently behave the same way " + "as INSTALLMODE_NODETECTION\n"); + dwInstallMode = INSTALLMODE_NODETECTION; + } + + return MsiProvideComponentW(product, feature, component, dwInstallMode, lpPathBuf, pcchPathBuf); }
UINT WINAPI MsiProvideComponentFromDescriptorA( LPCSTR szDescriptor, diff --git a/dlls/msi/msipriv.h b/dlls/msi/msipriv.h index 0efc367c76d..4ffc14c30e9 100644 --- a/dlls/msi/msipriv.h +++ b/dlls/msi/msipriv.h @@ -1080,6 +1080,8 @@ extern void msi_destroy_assembly_caches(void); extern BOOL msi_is_global_assembly(MSICOMPONENT *); extern IAssemblyEnum *msi_create_assembly_enum(const WCHAR *); extern WCHAR *msi_get_assembly_path(const WCHAR *) __WINE_DEALLOC(free) __WINE_MALLOC; +extern UINT msi_lookup_published_assembly(UINT, const WCHAR *, const WCHAR *, + BOOL, WCHAR *, WCHAR *, WCHAR *); extern WCHAR **msi_split_string(const WCHAR *, WCHAR); extern UINT msi_set_original_database_property(MSIDATABASE *, const WCHAR *); extern WCHAR *msi_get_error_message(MSIDATABASE *, int) __WINE_DEALLOC(free) __WINE_MALLOC; diff --git a/include/msi.h b/include/msi.h index 4012d365579..a9d5790e389 100644 --- a/include/msi.h +++ b/include/msi.h @@ -252,6 +252,11 @@ typedef struct tagMSIPATCHSEQUENCEINFOW UINT uStatus; } MSIPATCHSEQUENCEINFOW, *PMSIPATCHSEQUENCEINFOW;
+typedef enum tagMSIASSEMBLYINFO { + MSIASSEMBLYINFO_NETASSEMBLY = 0, + MSIASSEMBLYINFO_WIN32ASSEMBLY = 1, +} MSIASSEMBLYINFO; + #define MAX_FEATURE_CHARS 38
#define ERROR_PATCH_TARGET_NOT_FOUND 1642
 
            Ratchanan Srirattanamet (@ratchanan.sr) commented about dlls/msi/msi.c:
win32, product, feature, component);- }
- if (rc != ERROR_SUCCESS)
return rc;- /* If the product contain only 1 feature, it won't be recorded in assembly publication.
* Look it up to use as a parameter in MSIProvideComponentW().
*/- if (feature[0] == '\0') {
/* First, verify that this product indeed has single feature. */
rc = MsiEnumFeaturesW(product, 1, feature, feature_parent);
if (rc == ERROR_SUCCESS) {
ERR("Product for assembly '%ls' has > 1 feature, but the published assembly record "
"doesn't have feature. This can happen if package is installed in older version "
"of Wine. Try re-installing this package.\n", szAssemblyName);
I need some help in wording this error. This error happens if the package is installed before !9037 is merged and the package indeed has 2 or more features. What I want is to direct users to re-install the package that provides the assembly in question. At it stands, I think it's too technical, but I can't think of a better wording at the moment.
 
            On Tue Sep 23 20:32:13 2025 +0000, Ratchanan Srirattanamet wrote:
I need some help in wording this error. This error happens if the package is installed before !9037 is merged and the package indeed has 2 or more features. What I want is to direct users to re-install the package that provides the assembly in question. At it stands, I think it's too technical, but I can't think of a better wording at the moment.
I'm not sure if it's worth adding a message for this. It's a case that users are unlikely to hit and they may miss the message anyway. It's also not clear what 'this package' means.
 
            On Tue Oct 7 07:36:42 2025 +0000, Hans Leidekker wrote:
I'm not sure if it's worth adding a message for this. It's a case that users are unlikely to hit and they may miss the message anyway. It's also not clear what 'this package' means.
Well... to be honest I'm not sure how likely a user will install an application (and its dependencies), found out that it doesn't work, and then try again with upgraded Wine but the same $WINEPREFIX. It's also possible that a user doesn't really know about $WINEPREFIX at all...
But you do have a point in that the user might miss it anyway. So the question now becomes "what should we do in this case?". Should we simply return `ERROR_BAD_CONFIGURATION` silently? Or should we simply pass the first feature? Should we issue a warning/error in this case? Should we cross-check this behavior with Windows?
 
            On Tue Oct 7 09:42:24 2025 +0000, Ratchanan Srirattanamet wrote:
Well... to be honest I'm not sure how likely a user will install an application (and its dependencies), found out that it doesn't work, and then try again with upgraded Wine but the same $WINEPREFIX. It's also possible that a user doesn't really know about $WINEPREFIX at all... But you do have a point in that the user might miss it anyway. So the question now becomes "what should we do in this case?". Should we simply return `ERROR_BAD_CONFIGURATION` silently? Or should we simply pass the first feature? Should we issue a warning/error in this case? Should we cross-check this behavior with Windows?
We can return ERROR_CALL_NOT_IMPLEMENTED here with a FIXME like `"Product has multiple features but the published assembly record doesn't have a feature"` which would let us know what's going on and help the user.
ERROR_CALL_NOT_IMPLEMENTED is what used to be returned so this shouldn't cause any change in behavior.


