diff --git a/.github/workflows/test-sqlite.yml b/.github/workflows/test-sqlite.yml index a79dcf1..834e8cf 100644 --- a/.github/workflows/test-sqlite.yml +++ b/.github/workflows/test-sqlite.yml @@ -29,5 +29,8 @@ jobs: - name: Race run: go test -v -race ./... + - name: Race with sqlite_enable_api_armor + run: go test -v -race -tags sqlite_enable_api_armor + - name: No CGO build run: CGO_ENABLED=0 go install ./... diff --git a/cgosqlite/apiarmor_disabled_test.go b/cgosqlite/apiarmor_disabled_test.go new file mode 100644 index 0000000..4034808 --- /dev/null +++ b/cgosqlite/apiarmor_disabled_test.go @@ -0,0 +1,11 @@ +//go:build !sqlite_enable_api_armor + +package cgosqlite + +import ( + "testing" +) + +func TestAPIArmorDisabled(t *testing.T) { + testAPIArmor(t, false) +} diff --git a/cgosqlite/apiarmor_enabled_test.go b/cgosqlite/apiarmor_enabled_test.go new file mode 100644 index 0000000..1f5b27d --- /dev/null +++ b/cgosqlite/apiarmor_enabled_test.go @@ -0,0 +1,11 @@ +//go:build sqlite_enable_api_armor + +package cgosqlite + +import ( + "testing" +) + +func TestAPIArmorEnabled(t *testing.T) { + testAPIArmor(t, true) +} diff --git a/cgosqlite/apiarmor_test.go b/cgosqlite/apiarmor_test.go new file mode 100644 index 0000000..76193c2 --- /dev/null +++ b/cgosqlite/apiarmor_test.go @@ -0,0 +1,58 @@ +package cgosqlite + +import ( + "path/filepath" + "sync/atomic" + "testing" + + "github.com/tailscale/sqlite/sqliteh" +) + +// testAPIArmor provides a common function for testing SQLITE_ENABLE_API_ARMOR. +func testAPIArmor(t *testing.T, wantMisuseError bool) { + if wantMisuseError && !APIArmorEnabled() { + t.Fatal("APIArmor is not enabled") + } else if !wantMisuseError && APIArmorEnabled() { + t.Fatal("APIArmor is enabled") + } + + var gotMisuseLog atomic.Bool + err := SetLogCallback(func(code sqliteh.Code, msg string) { + if code == sqliteh.SQLITE_MISUSE { + gotMisuseLog.Store(true) + } + }) + if err != nil { + t.Fatal(err) + } + + db, err := Open(filepath.Join(t.TempDir(), "test.db"), sqliteh.OpenFlagsDefault, "") + if err != nil { + t.Fatal(err) + } + err = db.Close() + if err != nil { + t.Fatal(err) + } + + // Configuring AutoCheckpoint on a closed database should result in a misuse error + // if and only if [APIArmorEnabled] reports true. + got := db.AutoCheckpoint(1) + if wantMisuseError { + if got == nil || got.Error() != "SQLITE_MISUSE" { + t.Fatalf("want SQLITE_MISUSE, got %s", got) + } + + if !gotMisuseLog.Load() { + t.Fatal("did not get SQLITE_MISUSE in LogCallback") + } + } else { + if got != nil && got.Error() == "SQLITE_MISUSE" { + t.Fatalf("want no error, got %s", got) + } + + if gotMisuseLog.Load() { + t.Fatal("got SQLITE_MISUSE in LogCallback") + } + } +} diff --git a/cgosqlite/cgosqlite.go b/cgosqlite/cgosqlite.go index e5f5d9a..145e370 100644 --- a/cgosqlite/cgosqlite.go +++ b/cgosqlite/cgosqlite.go @@ -46,6 +46,15 @@ package cgosqlite // libm is required by the FTS5 extension, on Linux. #cgo linux LDFLAGS: -lm +// Enable API armor. +#cgo sqlite_enable_api_armor CFLAGS: -DSQLITE_ENABLE_API_ARMOR + +#ifdef SQLITE_ENABLE_API_ARMOR +int api_armor_enabled=1; +#else +int api_armor_enabled=0; +#endif + #include "cgosqlite.h" */ import "C" @@ -502,3 +511,8 @@ func stringFromBytes(b []byte) string { internCache.Store(s, s) return s } + +// APIArmorEnabled reports whether or not sqlite was compiled with SQLITE_ENABLE_API_ARMOR. +func APIArmorEnabled() bool { + return C.api_armor_enabled == 1 +}