InitWinSqlite3Native.ps1
|
# --------------------------------------------------------------------------- # InitWinSqlite3Native.ps1 # P/Invoke bridge targeting Windows native winsqlite3.dll # --------------------------------------------------------------------------- # Runs as ScriptsToProcess in the caller's scope; the script block keeps its variables out of that scope. & { # Platform pre-flight check: ensure running on Windows if ($PSVersionTable.PSEdition -eq 'Core') { $isWin = if (Get-Variable -Name 'IsWindows' -ErrorAction SilentlyContinue) { $IsWindows } else { [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows) } if (-not $isWin) { throw [System.PlatformNotSupportedException]::new("PSLocalBase relies on Windows native winsqlite3.dll and is supported exclusively on Windows operating systems.") } } # WinSqlite3 P/Invoke definition loaded before module classes are parsed if (-not ('WinSqlite3' -as [type])) { $csharpCode = @' using System; using System.Runtime.InteropServices; using System.Management.Automation; [assembly: DefaultDllImportSearchPaths(DllImportSearchPath.System32)] // Owns one sqlite3* connection. The finalizer closes connections whose wrapper was never disposed. public sealed class PSLocalBaseConnectionHandle : SafeHandle { public PSLocalBaseConnectionHandle(IntPtr db) : base(IntPtr.Zero, true) { SetHandle(db); } public override bool IsInvalid { get { return handle == IntPtr.Zero; } } protected override bool ReleaseHandle() { return WinSqlite3.CloseV2(handle) == WinSqlite3.SQLITE_OK; } } public class WinSqlite3 { public const int BridgeVersion = 4; public const int SQLITE_OK = 0; public const int SQLITE_BUSY = 5; public const int SQLITE_ROW = 100; public const int SQLITE_DONE = 101; public const int SQLITE_INTEGER = 1; public const int SQLITE_FLOAT = 2; public const int SQLITE_TEXT = 3; public const int SQLITE_BLOB = 4; public const int SQLITE_NULL = 5; public static readonly IntPtr SQLITE_TRANSIENT = new IntPtr(-1); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void ScalarFunction(IntPtr context, int argc, IntPtr argv); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void AuxDestructor(IntPtr data); // Static roots keep callbacks alive while any connection/statement uses them. private static readonly ScalarFunction PsLikeCallback = EvaluatePsLike; private static readonly AuxDestructor PatternDestructor = ReleasePattern; [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_create_function16", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)] private static extern int CreateFunction(IntPtr db, string name, int argc, int encoding, IntPtr data, ScalarFunction callback, IntPtr step, IntPtr final); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_value_type", CallingConvention = CallingConvention.Cdecl)] private static extern int ValueType(IntPtr value); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_value_text16", CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr ValueText(IntPtr value); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_value_bytes16", CallingConvention = CallingConvention.Cdecl)] private static extern int ValueBytes(IntPtr value); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_result_int", CallingConvention = CallingConvention.Cdecl)] private static extern void ResultInt(IntPtr context, int value); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_result_null", CallingConvention = CallingConvention.Cdecl)] private static extern void ResultNull(IntPtr context); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_result_error16", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)] private static extern void ResultError(IntPtr context, string message, int bytes); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_get_auxdata", CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr GetAuxData(IntPtr context, int argument); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_set_auxdata", CallingConvention = CallingConvention.Cdecl)] private static extern void SetAuxData(IntPtr context, int argument, IntPtr data, AuxDestructor destructor); private const int SQLITE_UTF16 = 4; private const int SQLITE_DIRECTONLY = 0x000080000; public static int RegisterPsLike(IntPtr db) { // DIRECTONLY keeps triggers and views stored in an untrusted database file from invoking the callback. return CreateFunction(db, "pslocalbase_like", 2, SQLITE_UTF16 | SQLITE_DIRECTONLY, IntPtr.Zero, PsLikeCallback, IntPtr.Zero, IntPtr.Zero); } // Returns the deepest object/array nesting level, ignoring brackets inside double-quoted strings. // Input that this scanner cannot measure reliably (JSON5 quotes or comments, unbalanced // brackets, an unterminated string) reports int.MaxValue so callers reject it. public static int JsonNestingDepth(string json) { if (json == null) return 0; int depth = 0, max = 0; bool inString = false; for (int i = 0; i < json.Length; i++) { char c = json[i]; if (inString) { if (c == '\\') { i++; } else if (c == '"') { inString = false; } } else if (c == '"') { inString = true; } else if (c == '{' || c == '[') { depth++; if (depth > max) max = depth; } else if (c == '}' || c == ']') { depth--; if (depth < 0) return int.MaxValue; } else if (c == '\'' || c == '/') { return int.MaxValue; } } if (inString || depth != 0) return int.MaxValue; return max; } // Variadic sqlite3_db_config with one int argument and an optional int* result. [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_db_config", CallingConvention = CallingConvention.Cdecl)] private static extern int NativeDbConfig(IntPtr db, int op, int value, IntPtr result); public const int SQLITE_DBCONFIG_ENABLE_TRIGGER = 1003; public const int SQLITE_DBCONFIG_DEFENSIVE = 1010; public const int SQLITE_DBCONFIG_ENABLE_VIEW = 1015; public static int DbConfig(IntPtr db, int op, int value) { return NativeDbConfig(db, op, value, IntPtr.Zero); } [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_limit", CallingConvention = CallingConvention.Cdecl)] public static extern int Limit(IntPtr db, int id, int newValue); public const int SQLITE_LIMIT_LENGTH = 0; private static string ReadValue(IntPtr value) { IntPtr text = ValueText(value); return text == IntPtr.Zero ? string.Empty : Marshal.PtrToStringUni(text, ValueBytes(value) / 2); } private static void EvaluatePsLike(IntPtr context, int argc, IntPtr argv) { try { IntPtr value = Marshal.ReadIntPtr(argv, 0); IntPtr patternValue = Marshal.ReadIntPtr(argv, IntPtr.Size); // Preserve SQL NULL behavior for both positive and negated predicates. if (ValueType(value) == SQLITE_NULL || ValueType(patternValue) == SQLITE_NULL) { ResultNull(context); return; } WildcardPattern matcher; IntPtr cached = GetAuxData(context, 1); if (cached != IntPtr.Zero) { matcher = (WildcardPattern)GCHandle.FromIntPtr(cached).Target; } else { matcher = new WildcardPattern(ReadValue(patternValue), WildcardOptions.IgnoreCase); GCHandle handle = GCHandle.Alloc(matcher); // SQLite releases this handle when the argument changes or the statement ends. SetAuxData(context, 1, GCHandle.ToIntPtr(handle), PatternDestructor); } ResultInt(context, matcher.IsMatch(ReadValue(value)) ? 1 : 0); } catch (Exception) { // Never let managed exceptions cross the unmanaged callback boundary. ResultError(context, "PSLocalBase -like pattern evaluation failed.", -1); } } private static void ReleasePattern(IntPtr data) { try { if (data != IntPtr.Zero) GCHandle.FromIntPtr(data).Free(); } catch (Exception) { // Never let managed exceptions cross the unmanaged callback boundary. } } // Connection lifecycle [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_open16", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)] public static extern int Open(string filename, out IntPtr db); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_close", CallingConvention = CallingConvention.Cdecl)] public static extern int Close(IntPtr db); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_close_v2", CallingConvention = CallingConvention.Cdecl)] public static extern int CloseV2(IntPtr db); // Command lifecycle [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_prepare16_v2", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)] public static extern int Prepare(IntPtr db, string sql, int numBytes, out IntPtr stmt, IntPtr pzTail); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_prepare16_v2", CallingConvention = CallingConvention.Cdecl)] private static extern int PrepareRaw(IntPtr db, IntPtr sql, int numBytes, out IntPtr stmt, out IntPtr pzTail); // Pure SQL text recognition, including internal CREATE TRIGGER semicolons. [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_complete16", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)] private static extern int Complete(string sql); private static int SkipSqlTrivia(string sql, int offset) { while (offset < sql.Length) { char c = sql[offset]; if (c == ' ' || (c >= '\t' && c <= '\r') || c == '\uFEFF' || c == ';') { offset++; } else if (c == '-' && offset + 1 < sql.Length && sql[offset + 1] == '-') { offset += 2; while (offset < sql.Length && sql[offset] != '\n') offset++; } else if (c == '/' && offset + 1 < sql.Length && sql[offset + 1] == '*') { int end = sql.IndexOf("*/", offset + 2, StringComparison.Ordinal); offset = end < 0 ? sql.Length : end + 2; } else { break; } } return offset; } // Reject compound SQL before preparing even the first statement: PRAGMA // can change connection state during preparation, before sqlite3_step. public static int PrepareSingle(IntPtr db, string sql, out IntPtr stmt, out bool hasTrailingStatement) { stmt = IntPtr.Zero; hasTrailingStatement = false; // SQLite stops reading at NUL, which would silently drop the rest of the text. if (sql != null && sql.IndexOf('\0') >= 0) { hasTrailingStatement = true; return SQLITE_OK; } sql = sql ?? string.Empty; int start = SkipSqlTrivia(sql, 0); for (int i = start; i < sql.Length; i++) { if (sql[i] != ';') continue; int complete = Complete(sql.Substring(start, i - start + 1)); if (complete != 0 && complete != 1) return complete; if (complete == 0) continue; hasTrailingStatement = SkipSqlTrivia(sql, i + 1) < sql.Length; if (hasTrailingStatement) return SQLITE_OK; break; } IntPtr buffer = Marshal.StringToHGlobalUni(sql); try { IntPtr tail; return PrepareRaw(db, buffer, -1, out stmt, out tail); } finally { Marshal.FreeHGlobal(buffer); } } [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_db_filename", CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr NativeDbFilename(IntPtr db, byte[] dbName); public static string DbFilename(IntPtr db) { IntPtr ptr = NativeDbFilename(db, System.Text.Encoding.UTF8.GetBytes("main\0")); return GetUtf8String(ptr); } [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_step", CallingConvention = CallingConvention.Cdecl)] public static extern int Step(IntPtr stmt); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_finalize", CallingConvention = CallingConvention.Cdecl)] public static extern int Finalize(IntPtr stmt); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_reset", CallingConvention = CallingConvention.Cdecl)] public static extern int Reset(IntPtr stmt); // Parameter binding [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_bind_text16", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)] public static extern int BindText(IntPtr stmt, int index, string val, int nLength, IntPtr destructor); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_bind_null", CallingConvention = CallingConvention.Cdecl)] public static extern int BindNull(IntPtr stmt, int index); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_bind_int64", CallingConvention = CallingConvention.Cdecl)] public static extern int BindInt64(IntPtr stmt, int index, long val); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_bind_double", CallingConvention = CallingConvention.Cdecl)] public static extern int BindDouble(IntPtr stmt, int index, double val); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_bind_parameter_count", CallingConvention = CallingConvention.Cdecl)] public static extern int BindParameterCount(IntPtr stmt); // Data extraction [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_column_type", CallingConvention = CallingConvention.Cdecl)] public static extern int ColumnType(IntPtr stmt, int colIndex); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_column_bytes16", CallingConvention = CallingConvention.Cdecl)] public static extern int ColumnBytes16(IntPtr stmt, int colIndex); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_column_int64", CallingConvention = CallingConvention.Cdecl)] public static extern long ColumnInt64(IntPtr stmt, int colIndex); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_column_double", CallingConvention = CallingConvention.Cdecl)] public static extern double ColumnDouble(IntPtr stmt, int colIndex); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_column_text16", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr ColumnText(IntPtr stmt, int colIndex); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_column_name16", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr ColumnName(IntPtr stmt, int colIndex); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_column_count", CallingConvention = CallingConvention.Cdecl)] public static extern int ColumnCount(IntPtr stmt); // Transaction autocommit status [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_get_autocommit", CallingConvention = CallingConvention.Cdecl)] public static extern int GetAutocommit(IntPtr db); // Error and execution helpers [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_errmsg16", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr Errmsg(IntPtr db); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_busy_timeout", CallingConvention = CallingConvention.Cdecl)] public static extern int BusyTimeout(IntPtr db, int ms); [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_exec", CallingConvention = CallingConvention.Cdecl)] private static extern int NativeExec(IntPtr db, byte[] sqlUtf8, IntPtr callback, IntPtr arg, out IntPtr errmsg); public static int Exec(IntPtr db, string sql, IntPtr callback, IntPtr arg, out IntPtr errmsg) { string text = sql ?? string.Empty; int byteCount = System.Text.Encoding.UTF8.GetByteCount(text); byte[] utf8Bytes = new byte[byteCount + 1]; System.Text.Encoding.UTF8.GetBytes(text, 0, text.Length, utf8Bytes, 0); return NativeExec(db, utf8Bytes, callback, arg, out errmsg); } // Memory reclamation [DllImport("winsqlite3.dll", EntryPoint = "sqlite3_free", CallingConvention = CallingConvention.Cdecl)] public static extern void Free(IntPtr ptr); // UTF-16 string conversion from unmanaged memory (sqlite3_*16 APIs) public static string GetString(IntPtr ptr) { if (ptr == IntPtr.Zero) return null; return Marshal.PtrToStringUni(ptr); } public static string GetString(IntPtr ptr, int charLen) { if (ptr == IntPtr.Zero) return null; return Marshal.PtrToStringUni(ptr, charLen); } // UTF-8 string conversion from unmanaged memory (sqlite3_exec errmsg) public static string GetUtf8String(IntPtr ptr) { if (ptr == IntPtr.Zero) return null; int len = 0; while (Marshal.ReadByte(ptr, len) != 0) { len++; } if (len == 0) return string.Empty; byte[] buffer = new byte[len]; Marshal.Copy(ptr, buffer, 0, len); return System.Text.Encoding.UTF8.GetString(buffer); } public static string GetErrorMessage(IntPtr db) { if (db == IntPtr.Zero) return "Invalid SQLite database pointer (IntPtr.Zero)"; IntPtr err = Errmsg(db); return GetString(err); } } '@ Add-Type -TypeDefinition $csharpCode -ReferencedAssemblies ([System.Management.Automation.PSObject].Assembly.Location) } } |