Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,33 @@ SQLEXTENSION_INTERFACE SQLRETURN CleanupSession(SQLGUID sessionId, SQLUSMALLINT
//
SQLEXTENSION_INTERFACE SQLRETURN Cleanup();

// Installs an external library to the specified directory.
// The library file is expected to be a zip. If it contains an inner zip,
// that zip is extracted to the install directory. Otherwise, all files
Comment on lines +146 to +147
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it mean that it will recursively unzip everything if a zip has a zip in it?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No -- only ONE level of nesting. The flow is: extract the outer ZIP into tempFolder; if a top-level .zip is found inside, extract THAT into tempFolder as well; then copy the resulting tree to installDir. We do not re-scan after the inner-zip extraction, so a zip-in-zip-in-zip would land as a nested .zip file on disk (treated as opaque content). Added a comment around the inner-zip discovery loop in commit e7ec844 documenting this.

// are copied directly.
//
SQLEXTENSION_INTERFACE SQLRETURN InstallExternalLibrary(
SQLGUID setupSessionId,
SQLCHAR *libraryName,
SQLINTEGER libraryNameLength,
SQLCHAR *libraryFile,
SQLINTEGER libraryFileLength,
SQLCHAR *libraryInstallDirectory,
SQLINTEGER libraryInstallDirectoryLength,
SQLCHAR **libraryError,
SQLINTEGER *libraryErrorLength);

// Uninstalls an external library from the specified directory.
//
SQLEXTENSION_INTERFACE SQLRETURN UninstallExternalLibrary(
SQLGUID setupSessionId,
SQLCHAR *libraryName,
SQLINTEGER libraryNameLength,
SQLCHAR *libraryInstallDirectory,
SQLINTEGER libraryInstallDirectoryLength,
SQLCHAR **libraryError,
SQLINTEGER *libraryErrorLength);

// Dotnet environment pointer
//
static DotnetEnvironment* g_dotnet_runtime = nullptr;
620 changes: 620 additions & 0 deletions language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,12 @@ public static List<string> CreateDllList(
}
else
{
if (!string.IsNullOrEmpty(privatePath))
{
dllList.AddRange(Directory.GetFiles(privatePath, userLibName));
}

if (!string.IsNullOrEmpty(publicPath))
{
dllList.AddRange(Directory.GetFiles(publicPath, userLibName));
}
// Callers may pass either a bare library name ("regex") or an explicit
// filename ("Foo.dll"). Try the exact name first so filenames with
// extensions resolve correctly; fall back to the "{name}.*" wildcard
// for bare names.
AddMatches(privatePath, userLibName, dllList);
AddMatches(publicPath, userLibName, dllList);
}

if (dllList.Count == 0)
Expand All @@ -110,6 +107,36 @@ public static List<string> CreateDllList(
return dllList;
}

/// <summary>
/// Adds DLL matches for <paramref name="userLibName"/> under
/// <paramref name="searchPath"/>. Tries the exact name first (so callers
/// that pass "Foo.dll" resolve correctly) and falls back to the
/// "{name}.*" wildcard for bare names (so callers that pass "Foo"
/// still match "Foo.dll", "Foo.runtimeconfig.json", etc.).
/// </summary>
private static void AddMatches(string searchPath, string userLibName, List<string> dllList)
{
if (string.IsNullOrEmpty(searchPath) || !Directory.Exists(searchPath))
{
return;
}

string exactPath = Path.Combine(searchPath, userLibName);
if (File.Exists(exactPath))
{
dllList.Add(exactPath);
return;
}

// The "{name}.*" wildcard matches non-DLL siblings too
// ("{name}.runtimeconfig.json", "{name}.deps.json", etc.). Only
// .dll files are valid Assembly.LoadFrom targets, so filter the
// wildcard match to avoid spamming the error log when the loader
// tries to load a JSON file as an assembly.
dllList.AddRange(Directory.GetFiles(searchPath, userLibName + ".*")
.Where(f => f.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)));
}

/// <summary>
/// This method finds the corresponding loaded dll for user dll's dependencies.
/// It searches for the corresponding loaded dll that matches args.Name.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,4 +340,139 @@ SQLRETURN Cleanup()
LOG("nativecsharpextension::Cleanup");
delete g_dotnet_runtime;
return SQL_SUCCESS;
}

//--------------------------------------------------------------------------------------------------
// Name: SetLibraryError
//
// Description:
// Helper to populate the library error output parameters.
//
static void SetLibraryError(
const std::string &errorString,
SQLCHAR **libraryError,
SQLINTEGER *libraryErrorLength)
{
// Guard against null out-parameters. The managed SetLibraryError
// (CSharpExtension.cs) does the same null-check; without it, a caller
// passing null libraryError / libraryErrorLength would dereference null
// and crash the host process.
if (libraryError == nullptr || libraryErrorLength == nullptr)
{
return;
}

if (!errorString.empty())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a null-pointer guard consistent with the managed version:

if (!errorString.empty() && libraryError != nullptr && libraryErrorLength != nullptr)

The managed SetLibraryError (CSharpExtension.cs) guards with if (libraryError != null && libraryErrorLength != null), but the native helper does not. If a caller ever passes null libraryError or libraryErrorLength, the native path dereferences null pointers (*libraryErrorLength = ...), causing an access violation / crash.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit e7ec844. Native SetLibraryError now early-returns when libraryError == nullptr || libraryErrorLength == nullptr, mirroring the managed guard. Also explicitly clears *libraryError = nullptr; *libraryErrorLength = 0; when errorString.empty() so callers that don't pre-initialize see a well-defined no-error state.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Problem: If errorString is empty, the function does nothing — *libraryError and *libraryErrorLength remain at whatever the caller initialized them to. ExtHost initializes them to nullptr / 0, so this is fine in the current call pattern. But a future caller could pass uninitialized values.

if (!errorString.empty())
{
    // Only set on non-empty — otherwise output params are untouched
}

Recommendation: Consider explicitly setting *libraryErrorLength = 0 in the else branch for robustness. This is a nit — not a bug in current usage.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit e7ec844. The else branch now sets *libraryError = nullptr; *libraryErrorLength = 0; so the no-error case is well-defined regardless of caller pre-initialization.

{
// Length excludes null terminator -- ExtHost adds +1 when copying
// (see Utf8ToNullTerminatedUtf16Le / strcpy_s in the host).
*libraryErrorLength = static_cast<SQLINTEGER>(errorString.length());
std::string *pError = new std::string(errorString);
*libraryError = const_cast<SQLCHAR*>(
reinterpret_cast<const SQLCHAR *>(pError->c_str()));
}
else
{
// Explicitly clear the out-parameters so callers that don't
// pre-initialize them see a well-defined "no error" state.
*libraryError = nullptr;
*libraryErrorLength = 0;
}
}

//--------------------------------------------------------------------------------------------------
// Name: InstallExternalLibrary
//
// Description:
// Installs an external library to the specified directory.
// The library file is expected to be a zip containing the library files.
// If it contains an inner zip, that zip is extracted to the install directory.
// Otherwise, all files are copied directly.
//
// Returns:
// SQL_SUCCESS on success, else SQL_ERROR
//
SQLRETURN InstallExternalLibrary(
SQLGUID setupSessionId,
SQLCHAR *libraryName,
SQLINTEGER libraryNameLength,
SQLCHAR *libraryFile,
SQLINTEGER libraryFileLength,
SQLCHAR *libraryInstallDirectory,
SQLINTEGER libraryInstallDirectoryLength,
SQLCHAR **libraryError,
SQLINTEGER *libraryErrorLength)
{
LOG("nativecsharpextension::InstallExternalLibrary");

SQLRETURN result = SQL_ERROR;

if (g_dotnet_runtime == nullptr)
{
SetLibraryError(
"Extension not initialized. Call Init before InstallExternalLibrary.",
libraryError,
libraryErrorLength);
}
else
{
result = g_dotnet_runtime->call_managed_method<decltype(&InstallExternalLibrary)>(
nameof(InstallExternalLibrary),
setupSessionId,
libraryName,
libraryNameLength,
libraryFile,
libraryFileLength,
libraryInstallDirectory,
libraryInstallDirectoryLength,
libraryError,
libraryErrorLength);
}

return result;
}

//--------------------------------------------------------------------------------------------------
// Name: UninstallExternalLibrary
//
// Description:
// Uninstalls an external library from the specified directory.
//
// Returns:
// SQL_SUCCESS on success, else SQL_ERROR
//
SQLRETURN UninstallExternalLibrary(
SQLGUID setupSessionId,
SQLCHAR *libraryName,
SQLINTEGER libraryNameLength,
SQLCHAR *libraryInstallDirectory,
SQLINTEGER libraryInstallDirectoryLength,
SQLCHAR **libraryError,
SQLINTEGER *libraryErrorLength)
{
LOG("nativecsharpextension::UninstallExternalLibrary");

SQLRETURN result = SQL_ERROR;

if (g_dotnet_runtime == nullptr)
{
SetLibraryError(
"Extension not initialized. Call Init before UninstallExternalLibrary.",
libraryError,
libraryErrorLength);
}
else
{
result = g_dotnet_runtime->call_managed_method<decltype(&UninstallExternalLibrary)>(
nameof(UninstallExternalLibrary),
setupSessionId,
libraryName,
libraryNameLength,
libraryInstallDirectory,
libraryInstallDirectoryLength,
libraryError,
libraryErrorLength);
}

return result;
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,26 @@ typedef SQLRETURN FN_cleanupSession(

typedef SQLRETURN FN_cleanup();

typedef SQLRETURN FN_installExternalLibrary(
SQLGUID, // setupSessionId
SQLCHAR *, // libraryName
SQLINTEGER, // libraryNameLength
SQLCHAR *, // libraryFile
SQLINTEGER, // libraryFileLength
SQLCHAR *, // libraryInstallDirectory
SQLINTEGER, // libraryInstallDirectoryLength
SQLCHAR **, // libraryError
SQLINTEGER *);// libraryErrorLength

typedef SQLRETURN FN_uninstallExternalLibrary(
SQLGUID, // setupSessionId
SQLCHAR *, // libraryName
SQLINTEGER, // libraryNameLength
SQLCHAR *, // libraryInstallDirectory
SQLINTEGER, // libraryInstallDirectoryLength
SQLCHAR **, // libraryError
SQLINTEGER *);// libraryErrorLength

namespace ExtensionApiTest
{
// Forward declaration
Expand Down Expand Up @@ -341,7 +361,7 @@ namespace ExtensionApiTest
// User library name and class full name
// The name of the library is same as the dll file name.
//
const std::string m_UserLibName = "Microsoft.SqlServer.CSharpExtensionTest.dll";;
const std::string m_UserLibName = "Microsoft.SqlServer.CSharpExtensionTest.dll";
const std::string m_UserClassFullName = "Microsoft.SqlServer.CSharpExtensionTest.CSharpTestExecutor";
const std::string m_Separator = ";";

Expand Down Expand Up @@ -451,6 +471,14 @@ namespace ExtensionApiTest
// Pointer to the Cleanup function
//
static FN_cleanup *sm_cleanupFuncPtr;

// Pointer to the InstallExternalLibrary function
//
static FN_installExternalLibrary *sm_installExternalLibraryFuncPtr;

// Pointer to the UninstallExternalLibrary function
//
static FN_uninstallExternalLibrary *sm_uninstallExternalLibraryFuncPtr;
};

// ColumnInfo template class to store information
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ namespace ExtensionApiTest
//
TEST_F(CSharpExtensionApiTests, ExecuteInvalidLibraryNameScriptTest)
{
// Unmatched library name with the dll file name.
// Use a library name that doesn't match any DLL file in the library path.
//
string userLibName = "Microsoft.SqlServer.CSharpExtensionTest";
string userLibName = "NonExistentLibrary";
string scriptString = userLibName + m_Separator + m_UserClassFullName;
InitializeSession(
0, // inputSchemaColumnsNumber
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ namespace ExtensionApiTest
FN_getOutputParam *CSharpExtensionApiTests::sm_getOutputParamFuncPtr = nullptr;
FN_cleanupSession *CSharpExtensionApiTests::sm_cleanupSessionFuncPtr = nullptr;
FN_cleanup *CSharpExtensionApiTests::sm_cleanupFuncPtr = nullptr;
FN_installExternalLibrary *CSharpExtensionApiTests::sm_installExternalLibraryFuncPtr = nullptr;
FN_uninstallExternalLibrary *CSharpExtensionApiTests::sm_uninstallExternalLibraryFuncPtr = nullptr;

//----------------------------------------------------------------------------------------------
// Name: CSharpExtensionApiTest::SetUpTestCase
Expand Down Expand Up @@ -250,6 +252,14 @@ namespace ExtensionApiTest

sm_cleanupFuncPtr = reinterpret_cast<FN_cleanup*>(GetProcAddress(sm_libHandle, "Cleanup"));
EXPECT_TRUE(sm_cleanupFuncPtr != nullptr);

sm_installExternalLibraryFuncPtr = reinterpret_cast<FN_installExternalLibrary*>(
GetProcAddress(sm_libHandle, "InstallExternalLibrary"));
EXPECT_TRUE(sm_installExternalLibraryFuncPtr != nullptr);

sm_uninstallExternalLibraryFuncPtr = reinterpret_cast<FN_uninstallExternalLibrary*>(
GetProcAddress(sm_libHandle, "UninstallExternalLibrary"));
EXPECT_TRUE(sm_uninstallExternalLibraryFuncPtr != nullptr);
}

//----------------------------------------------------------------------------------------------
Expand Down
Loading