diff --git a/docs/design/features/host-runtime-information.md b/docs/design/features/host-runtime-information.md index d2d96ff6ada83a..b4210293081cb2 100644 --- a/docs/design/features/host-runtime-information.md +++ b/docs/design/features/host-runtime-information.md @@ -80,6 +80,10 @@ List of directory paths corresponding to shared store paths and additional probi ### Single-file +`BUNDLE_EXTRACTION_PATH` + +**Added in .NET 10** Path to extraction directory, if the single-file bundle extracted any files. This is used by the runtime to search for native libraries associated with bundled managed assemblies. + `BUNDLE_PROBE` Hex string representation of a function pointer. It is set when running a single-file application. The function is called by the runtime to look for assemblies bundled into the application. The expected signature is defined as `BundleProbeFn` in [`coreclrhost.h`](/src/coreclr/hosts/inc/coreclrhost.h) diff --git a/src/coreclr/binder/assemblybindercommon.cpp b/src/coreclr/binder/assemblybindercommon.cpp index fecd7538d1a407..b7989e9c47a44f 100644 --- a/src/coreclr/binder/assemblybindercommon.cpp +++ b/src/coreclr/binder/assemblybindercommon.cpp @@ -858,7 +858,10 @@ namespace BINDER_SPACE ProbeExtensionResult probeExtensionResult = AssemblyProbeExtension::Probe(assemblyFileName, /* pathIsBundleRelative */ true); if (probeExtensionResult.IsValid()) { - SString assemblyFilePath(Bundle::AppIsBundle() ? Bundle::AppBundle->BasePath() : SString::Empty()); + SString assemblyFilePath; + if (Bundle::AppIsBundle()) + assemblyFilePath.SetUTF8(Bundle::AppBundle->BasePath()); + assemblyFilePath.Append(assemblyFileName); hr = GetAssembly(assemblyFilePath, diff --git a/src/coreclr/inc/bundle.h b/src/coreclr/inc/bundle.h index 3cc887423b85fd..3aa76e3651dac0 100644 --- a/src/coreclr/inc/bundle.h +++ b/src/coreclr/inc/bundle.h @@ -43,8 +43,13 @@ class Bundle Bundle(LPCSTR bundlePath, BundleProbeFn *probe); BundleFileLocation Probe(const SString& path, bool pathIsBundleRelative = false) const; - const SString &Path() const { LIMITED_METHOD_CONTRACT; return m_path; } - const SString &BasePath() const { LIMITED_METHOD_CONTRACT; return m_basePath; } + // Paths do not change and should remain valid for the lifetime of the Bundle + const SString& Path() const { LIMITED_METHOD_CONTRACT; return m_path; } + const UTF8* BasePath() const { LIMITED_METHOD_CONTRACT; return m_basePath.GetUTF8(); } + + // Extraction path does not change and should remain valid for the lifetime of the Bundle + bool HasExtractedFiles() const { LIMITED_METHOD_CONTRACT; return !m_extractionPath.IsEmpty(); } + const WCHAR* ExtractionPath() const { LIMITED_METHOD_CONTRACT; return m_extractionPath.GetUnicode(); } static Bundle* AppBundle; // The BundleInfo for the current app, initialized by coreclr_initialize. static bool AppIsBundle() { LIMITED_METHOD_CONTRACT; return AppBundle != nullptr; } @@ -53,6 +58,7 @@ class Bundle private: SString m_path; // The path to single-file executable BundleProbeFn *m_probe; + SString m_extractionPath; // The path to the extraction location, if bundle extracted any files SString m_basePath; // The prefix to denote a path within the bundle COUNT_T m_basePathLength; diff --git a/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Unix.targets b/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Unix.targets index 3e8c0678afe0d5..38f810583102b1 100644 --- a/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Unix.targets +++ b/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.Unix.targets @@ -64,9 +64,6 @@ The .NET Foundation licenses this file to you under the MIT license. $(CrossCompileArch)-alpine-linux-$(CrossCompileAbi) $(CrossCompileArch)-unknown-freebsd12 - $ORIGIN - @executable_path - @rpath/$(TargetName)$(NativeBinaryExt) libeventpipe-disabled @@ -234,8 +231,6 @@ The .NET Foundation licenses this file to you under the MIT license. - - diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/CompilerHelpers/InteropHelpers.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/CompilerHelpers/InteropHelpers.cs index 195ea214189e3a..c0545550696594 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/CompilerHelpers/InteropHelpers.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/CompilerHelpers/InteropHelpers.cs @@ -292,7 +292,7 @@ internal static unsafe void FixupModuleCell(ModuleFixupCell* pCell) { string moduleName = GetModuleName(pCell); - uint dllImportSearchPath = 0; + uint dllImportSearchPath = (uint)DllImportSearchPath.AssemblyDirectory; bool hasDllImportSearchPath = (pCell->DllImportSearchPathAndCookie & InteropDataConstants.HasDllImportSearchPath) != 0; if (hasDllImportSearchPath) { diff --git a/src/coreclr/vm/CMakeLists.txt b/src/coreclr/vm/CMakeLists.txt index 306fa3fa229802..fa872bd0782901 100644 --- a/src/coreclr/vm/CMakeLists.txt +++ b/src/coreclr/vm/CMakeLists.txt @@ -53,7 +53,6 @@ set(VM_SOURCES_DAC_AND_WKS_COMMON assembly.cpp assemblybinder.cpp binder.cpp - bundle.cpp castcache.cpp callcounting.cpp cdacplatformmetadata.cpp @@ -296,6 +295,7 @@ set(VM_SOURCES_WKS assemblyprobeextension.cpp assemblyspec.cpp baseassemblyspec.cpp + bundle.cpp ${RUNTIME_DIR}/CachedInterfaceDispatch.cpp CachedInterfaceDispatchCoreclr.cpp cachelinealloc.cpp diff --git a/src/coreclr/vm/bundle.cpp b/src/coreclr/vm/bundle.cpp index 8e6c08482ad3de..e507144a658504 100644 --- a/src/coreclr/vm/bundle.cpp +++ b/src/coreclr/vm/bundle.cpp @@ -9,6 +9,7 @@ #include "common.h" #include "bundle.h" +#include "hostinformation.h" #include #include #include @@ -47,6 +48,10 @@ Bundle::Bundle(LPCSTR bundlePath, BundleProbeFn *probe) size_t baseLen = pos - bundlePath + 1; // Include DIRECTORY_SEPARATOR_CHAR_A in m_basePath m_basePath.SetUTF8(bundlePath, (COUNT_T)baseLen); m_basePathLength = (COUNT_T)baseLen; + + SString extractionPathMaybe; + if (HostInformation::GetProperty(HOST_PROPERTY_BUNDLE_EXTRACTION_PATH, extractionPathMaybe)) + m_extractionPath.Set(extractionPathMaybe.GetUnicode()); } BundleFileLocation Bundle::Probe(const SString& path, bool pathIsBundleRelative) const diff --git a/src/coreclr/vm/nativelibrary.cpp b/src/coreclr/vm/nativelibrary.cpp index f0029b57f79d1a..a4dde63aa3904a 100644 --- a/src/coreclr/vm/nativelibrary.cpp +++ b/src/coreclr/vm/nativelibrary.cpp @@ -472,12 +472,20 @@ namespace NATIVE_LIBRARY_HANDLE LoadFromPInvokeAssemblyDirectory(Assembly *pAssembly, LPCWSTR libName, DWORD flags, LoadLibErrorTracker *pErrorTracker) { STANDARD_VM_CONTRACT; + _ASSERTE(libName != NULL); - if (pAssembly->GetPEAssembly()->GetPath().IsEmpty()) + SString path{ pAssembly->GetPEAssembly()->GetPath() }; + + // Bundled assembly - path will be empty, path to load should point to the single-file bundle + bool isBundledAssembly = pAssembly->GetPEAssembly()->HasPEImage() && pAssembly->GetPEAssembly()->GetPEImage()->IsInBundle(); + _ASSERTE(!isBundledAssembly || Bundle::AppBundle != NULL); + if (isBundledAssembly) + path.Set(pAssembly->GetPEAssembly()->GetPEImage()->GetPathToLoad()); + + if (path.IsEmpty()) return NULL; NATIVE_LIBRARY_HANDLE hmod = NULL; - SString path{ pAssembly->GetPEAssembly()->GetPath() }; _ASSERTE(!Path::IsRelative(path)); SString::Iterator lastPathSeparatorIter = path.End(); @@ -490,6 +498,15 @@ namespace hmod = LocalLoadLibraryHelper(path, flags, pErrorTracker); } + // Bundle with additional files extracted - also treat the extraction path as the assembly directory for native library load + if (hmod == NULL && isBundledAssembly && Bundle::AppBundle->HasExtractedFiles()) + { + path.Set(Bundle::AppBundle->ExtractionPath()); + path.Append(DIRECTORY_SEPARATOR_CHAR_W); + path.Append(libName); + hmod = LocalLoadLibraryHelper(path, flags, pErrorTracker); + } + return hmod; } diff --git a/src/coreclr/vm/peimage.cpp b/src/coreclr/vm/peimage.cpp index 45d281e25ee455..de16e04c1fd2a3 100644 --- a/src/coreclr/vm/peimage.cpp +++ b/src/coreclr/vm/peimage.cpp @@ -758,8 +758,6 @@ PTR_PEImage PEImage::CreateFromHMODULE(HMODULE hMod) } #endif // !TARGET_UNIX -#endif //DACCESS_COMPILE - HANDLE PEImage::GetFileHandle() { CONTRACTL @@ -776,11 +774,7 @@ HANDLE PEImage::GetFileHandle() if (m_hFile == INVALID_HANDLE_VALUE) { -#if !defined(DACCESS_COMPILE) EEFileLoadException::Throw(GetPathToLoad(), hr); -#else // defined(DACCESS_COMPILE) - ThrowHR(hr); -#endif // !defined(DACCESS_COMPILE) } return m_hFile; @@ -819,6 +813,7 @@ HRESULT PEImage::TryOpenFile(bool takeLock) return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); } +#endif // !DACCESS_COMPILE BOOL PEImage::IsPtrInImage(PTR_CVOID data) { diff --git a/src/coreclr/vm/peimage.h b/src/coreclr/vm/peimage.h index 0222f672b72c90..45e7da2b9beb74 100644 --- a/src/coreclr/vm/peimage.h +++ b/src/coreclr/vm/peimage.h @@ -145,8 +145,10 @@ class PEImage final INT64 GetSize() const; BOOL IsCompressed(INT64* uncompressedSize = NULL) const; +#ifndef DACCESS_COMPILE HANDLE GetFileHandle(); HRESULT TryOpenFile(bool takeLock = false); +#endif void GetMVID(GUID *pMvid); BOOL HasV1Metadata(); diff --git a/src/installer/tests/AppHost.Bundle.Tests/NativeLibraries.cs b/src/installer/tests/AppHost.Bundle.Tests/NativeLibraries.cs index 5ffab890d9e6c9..621cab66bd1d69 100644 --- a/src/installer/tests/AppHost.Bundle.Tests/NativeLibraries.cs +++ b/src/installer/tests/AppHost.Bundle.Tests/NativeLibraries.cs @@ -50,8 +50,7 @@ private void PInvoke(bool selfContained, bool bundleNative) .Should().Pass() .And.CallPInvoke(null, true) .And.CallPInvoke(DllImportSearchPath.AssemblyDirectory, true) - // Single-file always looks in application directory, even when only System32 is specified - .And.CallPInvoke(DllImportSearchPath.System32, true); + .And.CallPInvoke(DllImportSearchPath.System32, false); } [Fact] @@ -84,8 +83,7 @@ private void TryLoad(bool selfContained, bool bundleNative) .Should().Pass() .And.TryLoadLibrary(null, true) .And.TryLoadLibrary(DllImportSearchPath.AssemblyDirectory, true) - // Single-file always looks in application directory, even when only System32 is specified - .And.TryLoadLibrary(DllImportSearchPath.System32, true); + .And.TryLoadLibrary(DllImportSearchPath.System32, false); } internal static string GetLibraryName(DllImportSearchPath? flags) diff --git a/src/installer/tests/AppHost.Bundle.Tests/SingleFileApiTests.cs b/src/installer/tests/AppHost.Bundle.Tests/SingleFileApiTests.cs index 9e483b8aacad73..cdaa7f962b2a30 100644 --- a/src/installer/tests/AppHost.Bundle.Tests/SingleFileApiTests.cs +++ b/src/installer/tests/AppHost.Bundle.Tests/SingleFileApiTests.cs @@ -64,45 +64,6 @@ public void SelfContained_BundleAllContent() .And.HaveStdOutContaining("System.Console location: " + extractionDir); // System.Console should be from extracted location } - [Fact] - public void NativeSearchDirectories() - { - string singleFile = sharedTestState.BundledAppPath; - string extractionRoot = sharedTestState.App.GetNewExtractionRootPath(); - string bundleDir = Directory.GetParent(singleFile).FullName; - - // If we don't extract anything to disk, the extraction dir shouldn't - // appear in the native search dirs. - Command.Create(singleFile, "native_search_dirs") - .CaptureStdErr() - .CaptureStdOut() - .EnvironmentVariable(Constants.BundleExtractBase.EnvironmentVariable, extractionRoot) - .Execute() - .Should().Pass() - .And.HaveStdOutContaining(bundleDir) - .And.NotHaveStdOutContaining(extractionRoot); - } - - [Fact] - public void NativeSearchDirectories_WithExtraction() - { - SingleFileTestApp app = sharedTestState.App; - string singleFile = app.Bundle(BundleOptions.BundleNativeBinaries, out Manifest manifest); - - string extractionRoot = app.GetNewExtractionRootPath(); - string extractionDir = app.GetExtractionDir(extractionRoot, manifest).FullName; - string bundleDir = Directory.GetParent(singleFile).FullName; - - Command.Create(singleFile, "native_search_dirs") - .CaptureStdErr() - .CaptureStdOut() - .EnvironmentVariable(Constants.BundleExtractBase.EnvironmentVariable, extractionRoot) - .Execute() - .Should().Pass() - .And.HaveStdOutContaining(extractionDir) - .And.HaveStdOutContaining(bundleDir); - } - public class SharedTestState : IDisposable { public SingleFileTestApp App { get; set; } diff --git a/src/installer/tests/Assets/Projects/SingleFileApiTests/Program.cs b/src/installer/tests/Assets/Projects/SingleFileApiTests/Program.cs index 8fc8f1228aed2b..56428b14cacad4 100644 --- a/src/installer/tests/Assets/Projects/SingleFileApiTests/Program.cs +++ b/src/installer/tests/Assets/Projects/SingleFileApiTests/Program.cs @@ -71,11 +71,6 @@ public static int Main(string[] args) Console.WriteLine("AppContext.BaseDirectory: " + AppContext.BaseDirectory); break; - case "native_search_dirs": - var native_search_dirs = AppContext.GetData("NATIVE_DLL_SEARCH_DIRECTORIES"); - Console.WriteLine("NATIVE_DLL_SEARCH_DIRECTORIES: " + native_search_dirs); - break; - default: Console.WriteLine("test failure"); return -1; diff --git a/src/native/corehost/host_runtime_contract.h b/src/native/corehost/host_runtime_contract.h index fa7d27fce49717..65bc3c7464f0b4 100644 --- a/src/native/corehost/host_runtime_contract.h +++ b/src/native/corehost/host_runtime_contract.h @@ -17,6 +17,7 @@ #define HOST_PROPERTY_RUNTIME_CONTRACT "HOST_RUNTIME_CONTRACT" #define HOST_PROPERTY_APP_PATHS "APP_PATHS" #define HOST_PROPERTY_BUNDLE_PROBE "BUNDLE_PROBE" +#define HOST_PROPERTY_BUNDLE_EXTRACTION_PATH "BUNDLE_EXTRACTION_PATH" #define HOST_PROPERTY_ENTRY_ASSEMBLY_NAME "ENTRY_ASSEMBLY_NAME" #define HOST_PROPERTY_NATIVE_DLL_SEARCH_DIRECTORIES "NATIVE_DLL_SEARCH_DIRECTORIES" #define HOST_PROPERTY_PINVOKE_OVERRIDE "PINVOKE_OVERRIDE" diff --git a/src/native/corehost/hostpolicy/deps_resolver.cpp b/src/native/corehost/hostpolicy/deps_resolver.cpp index ee8c6f1aca6424..d93763939407fa 100644 --- a/src/native/corehost/hostpolicy/deps_resolver.cpp +++ b/src/native/corehost/hostpolicy/deps_resolver.cpp @@ -854,19 +854,6 @@ bool deps_resolver_t::resolve_probe_dirs( } } - // If this is a single-file app, add the app's dir to the native search directories. - if (bundle::info_t::is_single_file_bundle() && !is_resources) - { - auto bundle = bundle::runner_t::app(); - add_unique_path(asset_type, bundle->base_path(), &items, output, &non_serviced, core_servicing); - - // Add the extraction path if it exists. - if (pal::directory_exists(bundle->extraction_path())) - { - add_unique_path(asset_type, bundle->extraction_path(), &items, output, &non_serviced, core_servicing); - } - } - output->append(non_serviced); return true; diff --git a/src/native/corehost/hostpolicy/hostpolicy_context.cpp b/src/native/corehost/hostpolicy/hostpolicy_context.cpp index 63ef9cdbc0d32f..b562d0e09ae408 100644 --- a/src/native/corehost/hostpolicy/hostpolicy_context.cpp +++ b/src/native/corehost/hostpolicy/hostpolicy_context.cpp @@ -120,6 +120,18 @@ namespace return pal::pal_utf8string(get_filename_without_ext(context->application), value_buffer, value_buffer_size); } + if (::strcmp(key, HOST_PROPERTY_BUNDLE_EXTRACTION_PATH) == 0) + { + if (!bundle::info_t::is_single_file_bundle()) + return -1; + + auto bundle = bundle::runner_t::app(); + if (bundle->extraction_path().empty()) + return -1; + + return pal::pal_utf8string(bundle->extraction_path(), value_buffer, value_buffer_size); + } + // Properties from runtime initialization pal::string_t key_str; if (pal::clr_palstring(key, &key_str)) diff --git a/src/tests/Directory.Build.targets b/src/tests/Directory.Build.targets index 1b05b1f097ed3d..23c3d94fe96b2c 100644 --- a/src/tests/Directory.Build.targets +++ b/src/tests/Directory.Build.targets @@ -527,11 +527,6 @@ variables is horribly slow (seeing 1.4 seconds on a 11th gen Intel Core i7) --> true - - @executable_path/.. - $ORIGIN/.. - true @@ -576,6 +571,11 @@ + + + + diff --git a/src/tests/Interop/DllImportSearchPaths/DllImportSearchPathsTest.cs b/src/tests/Interop/DllImportSearchPaths/DllImportSearchPathsTest.cs index dbfdc49815a929..0654aa8bd281e5 100644 --- a/src/tests/Interop/DllImportSearchPaths/DllImportSearchPathsTest.cs +++ b/src/tests/Interop/DllImportSearchPaths/DllImportSearchPathsTest.cs @@ -60,6 +60,19 @@ public static void AssemblyDirectoryAot_Found() Assert.Equal(3, sum); } + [ConditionalFact(typeof(TestLibrary.Utilities), nameof(TestLibrary.Utilities.IsNativeAot))] + public static void DefaultFlagsAot_Found() + { + int sum = NativeLibraryPInvokeAot.Sum_DefaultFlags(1, 2); + Assert.Equal(3, sum); + } + + [ConditionalFact(typeof(TestLibrary.Utilities), nameof(TestLibrary.Utilities.IsNativeAot))] + public static void System32Aot_NotFound() + { + Assert.Throws(() => NativeLibraryPInvokeAot.Sum_System32(1, 2)); + } + [Fact] [PlatformSpecific(TestPlatforms.Windows)] public static void AssemblyDirectory_NoFallback_NotFound() @@ -97,6 +110,23 @@ public static void DefaultFlags_Found() } } + [Fact] + public static void System32_NotFound() + { + string currentDirectory = Environment.CurrentDirectory; + try + { + Environment.CurrentDirectory = Subdirectory; + + // Library should not be found in System32 (Windows) or default OS search (non-Windows) + Assert.Throws(() => NativeLibraryPInvoke.Sum_Copy(1, 2)); + } + finally + { + Environment.CurrentDirectory = currentDirectory; + } + } + [ConditionalFact(nameof(CanLoadAssemblyInSubdirectory))] [PlatformSpecific(TestPlatforms.Windows)] public static void AssemblyDirectory_SearchFlags_WithDependency_Found() @@ -116,6 +146,7 @@ public class NativeLibraryPInvoke { internal const string CopyName = $"{NativeLibraryToLoad.Name}-copy"; internal const string DefaultFlagsName = $"{NativeLibraryToLoad.Name}-default-flags"; + internal const string System32Name = $"{NativeLibraryToLoad.Name}-system32"; [MethodImpl(MethodImplOptions.NoInlining)] public static int Sum(int a, int b) @@ -129,6 +160,10 @@ public static int Sum_Copy(int a, int b) public static int Sum_DefaultFlags(int a, int b) => NativeSum_DefaultFlags(a, b); + [MethodImpl(MethodImplOptions.NoInlining)] + public static int Sum_System32(int a, int b) + => NativeSum_System32(a, b); + [DllImport(NativeLibraryToLoad.Name)] [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)] static extern int NativeSum(int arg1, int arg2); @@ -139,6 +174,9 @@ public static int Sum_DefaultFlags(int a, int b) [DllImport(DefaultFlagsName, EntryPoint = nameof(NativeSum))] static extern int NativeSum_DefaultFlags(int arg1, int arg2); + + [DllImport(System32Name, EntryPoint = nameof(NativeSum))] + static extern int NativeSum_System32(int arg1, int arg2); } public class NativeLibraryPInvokeAot @@ -147,10 +185,25 @@ public class NativeLibraryPInvokeAot public static int Sum(int a, int b) => NativeSum(a, b); + [MethodImpl(MethodImplOptions.NoInlining)] + public static int Sum_DefaultFlags(int a, int b) + => NativeSum_DefaultFlags(a, b); + + [MethodImpl(MethodImplOptions.NoInlining)] + public static int Sum_System32(int a, int b) + => NativeSum_System32(a, b); + // For NativeAOT, validate the case where the native library is next to the AOT application. [DllImport(NativeLibraryToLoad.Name + "-in-native")] [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)] static extern int NativeSum(int arg1, int arg2); + + [DllImport(NativeLibraryToLoad.Name + "-in-native-default-flags", EntryPoint = nameof(NativeSum))] + static extern int NativeSum_DefaultFlags(int arg1, int arg2); + + [DllImport(NativeLibraryToLoad.Name + "-in-native-system32", EntryPoint = nameof(NativeSum))] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + static extern int NativeSum_System32(int arg1, int arg2); } public class NativeLibraryWithDependency diff --git a/src/tests/Interop/DllImportSearchPaths/DllImportSearchPathsTest.csproj b/src/tests/Interop/DllImportSearchPaths/DllImportSearchPathsTest.csproj index a7ad13fbff654e..46af61bb01901d 100644 --- a/src/tests/Interop/DllImportSearchPaths/DllImportSearchPathsTest.csproj +++ b/src/tests/Interop/DllImportSearchPaths/DllImportSearchPathsTest.csproj @@ -4,6 +4,7 @@ true true + false @@ -29,6 +30,7 @@ + @@ -45,5 +47,7 @@ + + diff --git a/src/tests/Interop/NativeLibrary/API/NativeLibraryTests.csproj b/src/tests/Interop/NativeLibrary/API/NativeLibraryTests.csproj index 750d40c045d3a3..2cf089e210b27c 100644 --- a/src/tests/Interop/NativeLibrary/API/NativeLibraryTests.csproj +++ b/src/tests/Interop/NativeLibrary/API/NativeLibraryTests.csproj @@ -6,6 +6,7 @@ true true + false @@ -44,7 +45,11 @@ + + + +