diff --git a/core/moonshark.go b/core/moonshark.go index d40d68b..26ef03b 100644 --- a/core/moonshark.go +++ b/core/moonshark.go @@ -176,6 +176,7 @@ func (s *Moonshark) initRunner() error { runnerOpts := []runner.RunnerOption{ runner.WithPoolSize(s.Config.Runner.PoolSize), runner.WithLibDirs(s.Config.Dirs.Libs...), + runner.WithDataDir(s.Config.Dirs.Data), } var err error diff --git a/core/runner/embed.go b/core/runner/embed.go index 780158d..98df9a9 100644 --- a/core/runner/embed.go +++ b/core/runner/embed.go @@ -16,12 +16,17 @@ var sandboxLuaCode string //go:embed json.lua var jsonLuaCode string +//go:embed sqlite.lua +var sqliteLuaCode string + // Global bytecode cache to improve performance var ( - sandboxBytecode atomic.Pointer[[]byte] - jsonBytecode atomic.Pointer[[]byte] - bytecodeOnce sync.Once - jsonBytecodeOnce sync.Once + sandboxBytecode atomic.Pointer[[]byte] + jsonBytecode atomic.Pointer[[]byte] + sqliteBytecode atomic.Pointer[[]byte] + bytecodeOnce sync.Once + jsonBytecodeOnce sync.Once + sqliteBytecodeOnce sync.Once ) // precompileSandboxCode compiles the sandbox.lua code to bytecode once @@ -68,10 +73,33 @@ func precompileJsonModule() { logger.Debug("Successfully precompiled json.lua to bytecode (%d bytes)", len(code)) } +// precompileSqliteModule compiles the sqlite.lua code to bytecode once +func precompileSqliteModule() { + tempState := luajit.New() + if tempState == nil { + logger.Fatal("Failed to create temp Lua state for SQLite module compilation") + } + defer tempState.Close() + defer tempState.Cleanup() + + code, err := tempState.CompileBytecode(sqliteLuaCode, "sqlite.lua") + if err != nil { + logger.Error("Failed to compile SQLite module: %v", err) + return + } + + bytecode := make([]byte, len(code)) + copy(bytecode, code) + sqliteBytecode.Store(&bytecode) + + logger.Debug("Successfully precompiled sqlite.lua to bytecode (%d bytes)", len(code)) +} + // loadSandboxIntoState loads the sandbox code into a Lua state func loadSandboxIntoState(state *luajit.State, verbose bool) error { bytecodeOnce.Do(precompileSandboxCode) jsonBytecodeOnce.Do(precompileJsonModule) + sqliteBytecodeOnce.Do(precompileSqliteModule) // First load and execute the JSON module jsBytecode := jsonBytecode.Load() @@ -80,20 +108,16 @@ func loadSandboxIntoState(state *luajit.State, verbose bool) error { logger.Debug("Loading json.lua from precompiled bytecode") } - // Execute JSON module bytecode and store result to _G.json if err := state.LoadBytecode(*jsBytecode, "json.lua"); err != nil { return err } - // Execute with 1 result and store to _G.json if err := state.RunBytecodeWithResults(1); err != nil { return err } - // The module table is now on top of stack, set it to _G.json state.SetGlobal("json") } else { - // Fallback - compile and execute JSON module directly if verbose { logger.Warning("Using non-precompiled json.lua") } @@ -103,7 +127,37 @@ func loadSandboxIntoState(state *luajit.State, verbose bool) error { } } - // Then load sandbox + // Initialize active connections tracking + if err := state.DoString(`__active_sqlite_connections = {}`); err != nil { + return err + } + + // Load SQLite module + sqlBytecode := sqliteBytecode.Load() + if sqlBytecode != nil && len(*sqlBytecode) > 0 { + if verbose { + logger.Debug("Loading sqlite.lua from precompiled bytecode") + } + + if err := state.LoadBytecode(*sqlBytecode, "sqlite.lua"); err != nil { + return err + } + + if err := state.RunBytecodeWithResults(1); err != nil { + return err + } + + state.SetGlobal("sqlite") + } else { + if verbose { + logger.Warning("Using non-precompiled sqlite.lua") + } + + if err := state.DoString(sqliteLuaCode); err != nil { + return err + } + } + bytecode := sandboxBytecode.Load() if bytecode != nil && len(*bytecode) > 0 { if verbose { @@ -112,7 +166,6 @@ func loadSandboxIntoState(state *luajit.State, verbose bool) error { return state.LoadAndRunBytecode(*bytecode, "sandbox.lua") } - // Fallback if verbose { logger.Warning("Using non-precompiled sandbox.lua") } diff --git a/core/runner/runner.go b/core/runner/runner.go index 96a5eb1..b2b5a24 100644 --- a/core/runner/runner.go +++ b/core/runner/runner.go @@ -38,6 +38,7 @@ type Runner struct { statePool chan int // Pool of available state indexes poolSize int // Size of the state pool moduleLoader *ModuleLoader // Module loader + dataDir string // Data directory for SQLite databases isRunning atomic.Bool // Whether the runner is active mu sync.RWMutex // Mutex for thread safety scriptDir string // Current script directory @@ -65,11 +66,21 @@ func WithLibDirs(dirs ...string) RunnerOption { } } +// WithDataDir sets the data directory for SQLite databases +func WithDataDir(dataDir string) RunnerOption { + return func(r *Runner) { + if dataDir != "" { + r.dataDir = dataDir + } + } +} + // NewRunner creates a new Runner with a pool of states func NewRunner(options ...RunnerOption) (*Runner, error) { // Default configuration runner := &Runner{ poolSize: runtime.GOMAXPROCS(0), + dataDir: "data", } // Apply options @@ -86,13 +97,17 @@ func NewRunner(options ...RunnerOption) (*Runner, error) { runner.moduleLoader = NewModuleLoader(config) } + // Initialize SQLite + InitSQLite(runner.dataDir) + // Initialize states and pool runner.states = make([]*State, runner.poolSize) runner.statePool = make(chan int, runner.poolSize) // Create and initialize all states if err := runner.initializeStates(); err != nil { - runner.Close() // Clean up already created states + CleanupSQLite() // Clean up SQLite connections + runner.Close() // Clean up already created states return nil, err } @@ -258,6 +273,9 @@ cleanup: } } + // Clean up SQLite + CleanupSQLite() + logger.Debug("Runner closed") return nil } diff --git a/core/runner/sandbox.go b/core/runner/sandbox.go index d3bdc41..f55606b 100644 --- a/core/runner/sandbox.go +++ b/core/runner/sandbox.go @@ -101,19 +101,21 @@ func (s *Sandbox) registerCoreFunctions(state *luajit.State) error { return err } + if err := RegisterSQLiteFunctions(state); err != nil { + return err + } + return nil } // Execute runs a Lua script in the sandbox with the given context func (s *Sandbox) Execute(state *luajit.State, bytecode []byte, ctx *Context) (*Response, error) { - // Get the execution function first state.GetGlobal("__execute_script") if !state.IsFunction(-1) { state.Pop(1) return nil, ErrSandboxNotInitialized } - // Load bytecode if err := state.LoadBytecode(bytecode, "script"); err != nil { state.Pop(1) // Pop the __execute_script function return nil, fmt.Errorf("failed to load script: %w", err) @@ -127,10 +129,10 @@ func (s *Sandbox) Execute(state *luajit.State, bytecode []byte, ctx *Context) (* // Execute with 2 args, 1 result if err := state.Call(2, 1); err != nil { + ReleaseActiveConnections(state) return nil, fmt.Errorf("script execution failed: %w", err) } - // Get result value body, err := state.ToValue(-1) state.Pop(1) @@ -141,6 +143,8 @@ func (s *Sandbox) Execute(state *luajit.State, bytecode []byte, ctx *Context) (* extractHTTPResponseData(state, response) + ReleaseActiveConnections(state) + return response, nil } diff --git a/core/runner/sqlite.go b/core/runner/sqlite.go new file mode 100644 index 0000000..20af1ff --- /dev/null +++ b/core/runner/sqlite.go @@ -0,0 +1,393 @@ +package runner + +import ( + "errors" + "fmt" + "path/filepath" + "strings" + "sync" + + sqlite "zombiezen.com/go/sqlite" + "zombiezen.com/go/sqlite/sqlitex" + + "Moonshark/core/utils/logger" + + luajit "git.sharkk.net/Sky/LuaJIT-to-Go" +) + +// SQLiteConnection tracks an active connection +type SQLiteConnection struct { + DbName string + Conn *sqlite.Conn + Pool *sqlitex.Pool +} + +// SQLiteManager handles database connections +type SQLiteManager struct { + mu sync.RWMutex + pools map[string]*sqlitex.Pool + activeConns map[string]*SQLiteConnection + dataDir string +} + +// Global manager +var sqliteManager *SQLiteManager + +// InitSQLite initializes the SQLite manager +func InitSQLite(dataDir string) { + sqliteManager = &SQLiteManager{ + pools: make(map[string]*sqlitex.Pool), + activeConns: make(map[string]*SQLiteConnection), + dataDir: dataDir, + } + logger.Server("SQLite initialized with data directory: %s", dataDir) +} + +// CleanupSQLite closes all database connections +func CleanupSQLite() { + if sqliteManager == nil { + return + } + + sqliteManager.mu.Lock() + defer sqliteManager.mu.Unlock() + + // Release all active connections + for id, conn := range sqliteManager.activeConns { + if conn.Pool != nil { + conn.Pool.Put(conn.Conn) + } + delete(sqliteManager.activeConns, id) + } + + // Close all pools + for name, pool := range sqliteManager.pools { + if err := pool.Close(); err != nil { + logger.Error("Failed to close database %s: %v", name, err) + } + } + + sqliteManager.pools = nil + sqliteManager.activeConns = nil + logger.Debug("SQLite connections closed") +} + +// ReleaseActiveConnections returns all active connections to their pools +func ReleaseActiveConnections(state *luajit.State) { + if sqliteManager == nil { + return + } + + sqliteManager.mu.Lock() + defer sqliteManager.mu.Unlock() + + // Get active connections table from Lua + state.GetGlobal("__active_sqlite_connections") + if !state.IsTable(-1) { + state.Pop(1) + return + } + + // Iterate through active connections + state.PushNil() // Start iteration + for state.Next(-2) { + // Stack now has key at -2 and value at -1 + if state.IsTable(-1) { + state.GetField(-1, "id") + if state.IsString(-1) { + connID := state.ToString(-1) + + // Release connection from Go side + if conn, exists := sqliteManager.activeConns[connID]; exists { + if conn.Pool != nil { + conn.Pool.Put(conn.Conn) + } + delete(sqliteManager.activeConns, connID) + } + } + state.Pop(1) // Pop connection id + } + state.Pop(1) // Pop value, leave key for next iteration + } + + // Clear the active connections table + state.PushNil() + state.SetGlobal("__active_sqlite_connections") +} + +// getPool returns a connection pool for the specified database +func getPool(dbName string) (*sqlitex.Pool, error) { + if sqliteManager == nil { + return nil, errors.New("SQLite not initialized") + } + + // Validate database name + dbName = filepath.Base(dbName) + if dbName == "" || dbName[0] == '.' { + return nil, errors.New("invalid database name") + } + + // Check for existing pool + sqliteManager.mu.RLock() + pool, exists := sqliteManager.pools[dbName] + sqliteManager.mu.RUnlock() + + if exists { + return pool, nil + } + + // Create new pool + sqliteManager.mu.Lock() + defer sqliteManager.mu.Unlock() + + // Double check if another goroutine created it + if pool, exists = sqliteManager.pools[dbName]; exists { + return pool, nil + } + + // Create database file path + dbPath := filepath.Join(sqliteManager.dataDir, dbName+".db") + + // Create the pool + pool, err := sqlitex.NewPool(dbPath, sqlitex.PoolOptions{}) + + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + + sqliteManager.pools[dbName] = pool + return pool, nil +} + +// getConnection returns a connection from the pool +func getConnection(dbName string, connID string) (*sqlite.Conn, *sqlitex.Pool, error) { + // Check for existing connection first + sqliteManager.mu.RLock() + conn, exists := sqliteManager.activeConns[connID] + sqliteManager.mu.RUnlock() + + if exists { + return conn.Conn, conn.Pool, nil + } + + // Get the pool + pool, err := getPool(dbName) + if err != nil { + return nil, nil, err + } + + // Get a connection + dbConn := pool.Get(nil) + if dbConn == nil { + return nil, nil, errors.New("failed to get connection from pool") + } + + // Store connection + sqliteManager.mu.Lock() + sqliteManager.activeConns[connID] = &SQLiteConnection{ + DbName: dbName, + Conn: dbConn, + Pool: pool, + } + sqliteManager.mu.Unlock() + + return dbConn, pool, nil +} + +// luaSQLQuery executes a SQL query and returns results to Lua +func luaSQLQuery(state *luajit.State) int { + // Get database name + if !state.IsString(1) { + state.PushString("sqlite.query: database name must be a string") + return -1 + } + dbName := state.ToString(1) + + // Get query + if !state.IsString(2) { + state.PushString("sqlite.query: query must be a string") + return -1 + } + query := state.ToString(2) + + // Get connection ID (optional for compatibility) + var connID string + if state.GetTop() >= 4 && !state.IsNil(4) && state.IsString(4) { + connID = state.ToString(4) + } else { + // Generate a temporary connection ID + connID = fmt.Sprintf("temp_%p", &query) + } + + // Get parameters (optional) + var params map[string]any + if state.GetTop() >= 3 && !state.IsNil(3) && state.IsTable(3) { + var err error + params, err = state.ToTable(3) + if err != nil { + state.PushString("sqlite.query: failed to parse parameters: " + err.Error()) + return -1 + } + } + + // Get connection + conn, pool, err := getConnection(dbName, connID) + if err != nil { + state.PushString("sqlite.query: " + err.Error()) + return -1 + } + + // For temporary connections, defer release + if !strings.HasPrefix(connID, "temp_") { + defer pool.Put(conn) + + // Remove from active connections + sqliteManager.mu.Lock() + delete(sqliteManager.activeConns, connID) + sqliteManager.mu.Unlock() + } + + // Execute query and collect results + var rows []map[string]any + + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Named: params, // Using Named for named parameters + ResultFunc: func(stmt *sqlite.Stmt) error { + row := make(map[string]any) + columnCount := stmt.ColumnCount() + + for i := 0; i < columnCount; i++ { + columnName := stmt.ColumnName(i) + columnType := stmt.ColumnType(i) + + switch columnType { + case sqlite.TypeInteger: + row[columnName] = stmt.ColumnInt64(i) + case sqlite.TypeFloat: + row[columnName] = stmt.ColumnFloat(i) + case sqlite.TypeText: + row[columnName] = stmt.ColumnText(i) + case sqlite.TypeBlob: + blobSize := stmt.ColumnLen(i) + buf := make([]byte, blobSize) + blob := stmt.ColumnBytes(i, buf) + row[columnName] = blob + case sqlite.TypeNull: + row[columnName] = nil + } + } + + // Add row copy to results + rowCopy := make(map[string]any, len(row)) + for k, v := range row { + rowCopy[k] = v + } + rows = append(rows, rowCopy) + return nil + }, + }) + + if err != nil { + state.PushString("sqlite.query: " + err.Error()) + return -1 + } + + // Create result table + state.NewTable() + + // Add results to the table + for i, row := range rows { + state.PushNumber(float64(i + 1)) + if err := state.PushTable(row); err != nil { + state.PushString("sqlite.query: " + err.Error()) + return -1 + } + state.SetTable(-3) + } + + return 1 +} + +// luaSQLExec executes a SQL statement without returning results +func luaSQLExec(state *luajit.State) int { + // Get database name and query + if !state.IsString(1) { + state.PushString("sqlite.exec: database name must be a string") + return -1 + } + dbName := state.ToString(1) + + if !state.IsString(2) { + state.PushString("sqlite.exec: query must be a string") + return -1 + } + query := state.ToString(2) + + // Get connection ID (optional for compatibility) + var connID string + if state.GetTop() >= 4 && !state.IsNil(4) && state.IsString(4) { + connID = state.ToString(4) + } else { + // Generate a temporary connection ID + connID = fmt.Sprintf("temp_%p", &query) + } + + // Get parameters (optional) + var params map[string]any + if state.GetTop() >= 3 && !state.IsNil(3) && state.IsTable(3) { + var err error + params, err = state.ToTable(3) + if err != nil { + state.PushString("sqlite.exec: failed to parse parameters: " + err.Error()) + return -1 + } + } + + // Get connection + conn, pool, err := getConnection(dbName, connID) + if err != nil { + state.PushString("sqlite.exec: " + err.Error()) + return -1 + } + + // For temporary connections, defer release + if !strings.HasPrefix(connID, "temp_") { + defer pool.Put(conn) + + // Remove from active connections + sqliteManager.mu.Lock() + delete(sqliteManager.activeConns, connID) + sqliteManager.mu.Unlock() + } + + // Execute statement + if params != nil { + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Named: params, // Using Named for named parameters + }) + } else { + err = sqlitex.ExecScript(conn, query) + } + + if err != nil { + state.PushString("sqlite.exec: " + err.Error()) + return -1 + } + + // Return number of affected rows + state.PushNumber(float64(conn.Changes())) + return 1 +} + +// RegisterSQLiteFunctions registers SQLite functions with the Lua state +func RegisterSQLiteFunctions(state *luajit.State) error { + if err := state.RegisterGoFunction("__sqlite_query", luaSQLQuery); err != nil { + return err + } + + if err := state.RegisterGoFunction("__sqlite_exec", luaSQLExec); err != nil { + return err + } + + return nil +} diff --git a/core/runner/sqlite.lua b/core/runner/sqlite.lua new file mode 100644 index 0000000..a0f08b1 --- /dev/null +++ b/core/runner/sqlite.lua @@ -0,0 +1,180 @@ +__active_sqlite_connections = {} + +-- Connection metatable +local connection_mt = { + __index = { + -- Execute a query and return results as a table + query = function(self, query, params) + if type(query) ~= "string" then + error("connection:query: query must be a string", 2) + end + return __sqlite_query(self.db_name, query, params) + end, + + -- Execute a statement and return affected rows + exec = function(self, query, params) + if type(query) ~= "string" then + error("connection:exec: query must be a string", 2) + end + return __sqlite_exec(self.db_name, query, params) + end, + + -- Create a new table + create_table = function(self, table_name, schema) + if type(schema) ~= "table" then + error("connection:create_table: schema must be a table", 2) + end + + local columns = {} + for name, definition in pairs(schema) do + table.insert(columns, name .. " " .. definition) + end + + local query = string.format("CREATE TABLE IF NOT EXISTS %s (%s)", + table_name, table.concat(columns, ", ")) + + return self:exec(query) + end, + + -- Insert a row or multiple rows + insert = function(self, table_name, data) + if type(data) ~= "table" then + error("connection:insert: data must be a table", 2) + end + + -- Single row + if data[1] == nil and next(data) ~= nil then + local columns = {} + local placeholders = {} + local params = {} + + for col, val in pairs(data) do + table.insert(columns, col) + table.insert(placeholders, ":" .. col) + params[col] = val + end + + local query = string.format( + "INSERT INTO %s (%s) VALUES (%s)", + table_name, + table.concat(columns, ", "), + table.concat(placeholders, ", ") + ) + + return self:exec(query, params) + end + + -- Multiple rows + if #data > 0 and type(data[1]) == "table" then + local affected = 0 + + for _, row in ipairs(data) do + local result = self:insert(table_name, row) + affected = affected + result + end + + return affected + end + + error("connection:insert: invalid data format", 2) + end, + + -- Update rows + update = function(self, table_name, data, where, where_params) + if type(data) ~= "table" then + error("connection:update: data must be a table", 2) + end + + local sets = {} + local params = {} + + for col, val in pairs(data) do + table.insert(sets, col .. " = :" .. col) + params[col] = val + end + + local query = string.format( + "UPDATE %s SET %s", + table_name, + table.concat(sets, ", ") + ) + + if where then + query = query .. " WHERE " .. where + + if where_params then + for k, v in pairs(where_params) do + params[k] = v + end + end + end + + return self:exec(query, params) + end, + + -- Delete rows + delete = function(self, table_name, where, params) + local query = "DELETE FROM " .. table_name + + if where then + query = query .. " WHERE " .. where + end + + return self:exec(query, params) + end, + + -- Get one row + get_one = function(self, query, params) + local results = self:query(query, params) + return results[1] + end, + + -- Begin transaction + begin = function(self) + return self:exec("BEGIN TRANSACTION") + end, + + -- Commit transaction + commit = function(self) + return self:exec("COMMIT") + end, + + -- Rollback transaction + rollback = function(self) + return self:exec("ROLLBACK") + end, + + -- Transaction wrapper function + transaction = function(self, callback) + self:begin() + + local success, result = pcall(function() + return callback(self) + end) + + if success then + self:commit() + return result + else + self:rollback() + error(result, 2) + end + end + } +} + +-- Create sqlite() function that returns a connection object +return function(db_name) + if type(db_name) ~= "string" then + error("sqlite: database name must be a string", 2) + end + + local conn = { + db_name = db_name, + id = tostring({}):match("table: (.*)") -- unique ID based on table address + } + + __active_sqlite_connections[conn.id] = conn + + return setmetatable(conn, connection_mt) +end diff --git a/go.mod b/go.mod index e0eab7c..2b13fe5 100644 --- a/go.mod +++ b/go.mod @@ -10,15 +10,25 @@ require ( github.com/matoous/go-nanoid/v2 v2.1.0 github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/fasthttp v1.61.0 + zombiezen.com/go/sqlite v1.4.0 ) require ( github.com/andybalholm/brotli v1.1.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/golang/snappy v1.0.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/sys v0.32.0 // indirect + modernc.org/libc v1.65.0 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.10.0 // indirect + modernc.org/sqlite v1.37.0 // indirect ) replace git.sharkk.net/Sky/LuaJIT-to-Go => ./luajit diff --git a/go.sum b/go.sum index 342e6f3..00e2e0b 100644 --- a/go.sum +++ b/go.sum @@ -12,17 +12,29 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deneonet/benc v1.1.7 h1:0XPxTTVJZq/ulxXvMn2Mzjx5XquekVky3wX6eTgA0vA= github.com/deneonet/benc v1.1.7/go.mod h1:UCfkM5Od0B2huwv/ZItvtUb7QnALFt9YXtX8NXX4Lts= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE= github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= @@ -35,8 +47,52 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= +modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= +modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= +modernc.org/libc v1.65.0 h1:e183gLDnAp9VJh6gWKdTy0CThL9Pt7MfcR/0bgb7Y1Y= +modernc.org/libc v1.65.0/go.mod h1:7m9VzGq7APssBTydds2zBcxGREwvIGpuUBaKTXdm2Qs= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4= +modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= +modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= +modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= +modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +zombiezen.com/go/sqlite v1.4.0 h1:N1s3RIljwtp4541Y8rM880qgGIgq3fTD2yks1xftnKU= +zombiezen.com/go/sqlite v1.4.0/go.mod h1:0w9F1DN9IZj9AcLS9YDKMboubCACkwYCGkzoy3eG5ik=