mirror of
https://github.com/ncruces/go-sqlite3.git
synced 2026-01-11 21:49:13 +00:00
Checksums in default VFS. (#177)
This commit is contained in:
44
config.go
44
config.go
@@ -2,6 +2,7 @@ package sqlite3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
|
||||
@@ -327,3 +328,46 @@ func (c *Conn) SoftHeapLimit(n int64) int64 {
|
||||
func (c *Conn) HardHeapLimit(n int64) int64 {
|
||||
return int64(c.call("sqlite3_hard_heap_limit64", uint64(n)))
|
||||
}
|
||||
|
||||
// EnableChecksums enables checksums on a database.
|
||||
//
|
||||
// https://sqlite.org/cksumvfs.html
|
||||
func (c *Conn) EnableChecksums(schema string) error {
|
||||
r, err := c.FileControl(schema, FCNTL_RESERVE_BYTES)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r == 8 {
|
||||
// Correct value, enabled.
|
||||
return nil
|
||||
}
|
||||
if r == 0 {
|
||||
// Default value, enable.
|
||||
_, err = c.FileControl(schema, FCNTL_RESERVE_BYTES, 8)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r, err = c.FileControl(schema, FCNTL_RESERVE_BYTES)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if r != 8 {
|
||||
// Invalid value.
|
||||
return util.ErrorString("sqlite3: reserve bytes must be 8, is: " + strconv.Itoa(r.(int)))
|
||||
}
|
||||
|
||||
// VACUUM the database.
|
||||
if schema != "" {
|
||||
err = c.Exec(`VACUUM ` + QuoteIdentifier(schema))
|
||||
} else {
|
||||
err = c.Exec(`VACUUM`)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Checkpoint the WAL.
|
||||
_, _, err = c.WALCheckpoint(schema, CHECKPOINT_RESTART)
|
||||
return err
|
||||
}
|
||||
|
||||
75
tests/cksm_test.go
Normal file
75
tests/cksm_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
"github.com/ncruces/go-sqlite3/util/ioutil"
|
||||
"github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
"github.com/ncruces/go-sqlite3/vfs/readervfs"
|
||||
)
|
||||
|
||||
//go:embed testdata/cksm.db
|
||||
var cksmDB string
|
||||
|
||||
func Test_fileformat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
readervfs.Create("test.db", ioutil.NewSizeReaderAt(strings.NewReader(cksmDB)))
|
||||
|
||||
db, err := driver.Open("file:test.db?vfs=reader")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var enabled bool
|
||||
err = db.QueryRow(`PRAGMA checksum_verification`).Scan(&enabled)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !enabled {
|
||||
t.Error("want true")
|
||||
}
|
||||
|
||||
db.SetMaxIdleConns(0) // Clears the page cache.
|
||||
|
||||
_, err = db.Exec(`PRAGMA integrity_check`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_enable(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := driver.Open(memdb.TestDB(t),
|
||||
func(db *sqlite3.Conn) error {
|
||||
return db.EnableChecksums("main")
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var enabled bool
|
||||
err = db.QueryRow(`PRAGMA checksum_verification`).Scan(&enabled)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !enabled {
|
||||
t.Error("want true")
|
||||
}
|
||||
|
||||
db.SetMaxIdleConns(0) // Clears the page cache.
|
||||
|
||||
_, err = db.Exec(`PRAGMA integrity_check`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -16,12 +16,12 @@ The main differences are [file locking](#file-locking) and [WAL mode](#write-ahe
|
||||
POSIX advisory locks, which SQLite uses on Unix, are
|
||||
[broken by design](https://github.com/sqlite/sqlite/blob/b74eb0/src/os_unix.c#L1073-L1161).
|
||||
|
||||
On Linux and macOS, this module uses
|
||||
On Linux and macOS, this package uses
|
||||
[OFD locks](https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html)
|
||||
to synchronize access to database files.
|
||||
OFD locks are fully compatible with POSIX advisory locks.
|
||||
|
||||
This module can also use
|
||||
This package can also use
|
||||
[BSD locks](https://man.freebsd.org/cgi/man.cgi?query=flock&sektion=2),
|
||||
albeit with reduced concurrency (`BEGIN IMMEDIATE` behaves like `BEGIN EXCLUSIVE`).
|
||||
On BSD, macOS, and illumos, BSD locks are fully compatible with POSIX advisory locks;
|
||||
@@ -30,7 +30,7 @@ elsewhere, they are very likely broken.
|
||||
BSD locks are the default on BSD and illumos,
|
||||
but you can opt into them with the `sqlite3_flock` build tag.
|
||||
|
||||
On Windows, this module uses `LockFileEx` and `UnlockFileEx`,
|
||||
On Windows, this package uses `LockFileEx` and `UnlockFileEx`,
|
||||
like SQLite.
|
||||
|
||||
Otherwise, file locking is not supported, and you must use
|
||||
@@ -46,7 +46,7 @@ to check if your build supports file locking.
|
||||
|
||||
### Write-Ahead Logging
|
||||
|
||||
On little-endian Unix, this module uses `mmap` to implement
|
||||
On little-endian Unix, this package uses `mmap` to implement
|
||||
[shared-memory for the WAL-index](https://sqlite.org/wal.html#implementation_of_shared_memory_for_the_wal_index),
|
||||
like SQLite.
|
||||
|
||||
@@ -67,9 +67,22 @@ to check if your build supports shared memory.
|
||||
|
||||
### Batch-Atomic Write
|
||||
|
||||
On 64-bit Linux, this module supports [batch-atomic writes](https://sqlite.org/cgi/src/technote/714)
|
||||
On 64-bit Linux, this package supports
|
||||
[batch-atomic writes](https://sqlite.org/cgi/src/technote/714)
|
||||
on the F2FS filesystem.
|
||||
|
||||
### Checksums
|
||||
|
||||
This package can be [configured](https://pkg.go.dev/github.com/ncruces/go-sqlite3#Conn.EnableChecksums)
|
||||
to add an 8-byte checksum to the end of every page in an SQLite database.
|
||||
The checksum is added as each page is written
|
||||
and verified as each page is read.\
|
||||
The checksum is intended to help detect database corruption
|
||||
caused by random bit-flips in the mass storage device.
|
||||
|
||||
The implementation is compatible with SQLite's
|
||||
[Checksum VFS Shim](https://sqlite.org/cksumvfs.html).
|
||||
|
||||
### Build Tags
|
||||
|
||||
The VFS can be customized with a few build tags:
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
var testDB string
|
||||
|
||||
func Test_fileformat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
readervfs.Create("test.db", ioutil.NewSizeReaderAt(strings.NewReader(testDB)))
|
||||
vfs.Register("radiantum", adiantum.Wrap(vfs.Find("reader"), nil))
|
||||
|
||||
|
||||
@@ -186,3 +186,8 @@ type blockingSharedMemory interface {
|
||||
SharedMemory
|
||||
shmEnableBlocking(block bool)
|
||||
}
|
||||
|
||||
type fileControl interface {
|
||||
File
|
||||
fileControl(ctx context.Context, mod api.Module, op _FcntlOpcode, pArg uint32) _ErrorCode
|
||||
}
|
||||
|
||||
149
vfs/cksm.go
Normal file
149
vfs/cksm.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package vfs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/binary"
|
||||
"strconv"
|
||||
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/ncruces/go-sqlite3/util/sql3util"
|
||||
)
|
||||
|
||||
func cksmWrapFile(name *Filename, flags OpenFlag, file File) File {
|
||||
// Checksum only main databases and WALs.
|
||||
if flags&(OPEN_MAIN_DB|OPEN_WAL) == 0 {
|
||||
return file
|
||||
}
|
||||
|
||||
cksm := cksmFile{File: file}
|
||||
|
||||
if flags&OPEN_WAL != 0 {
|
||||
main, _ := name.DatabaseFile().(cksmFile)
|
||||
cksm.cksmFlags = main.cksmFlags
|
||||
} else {
|
||||
cksm.cksmFlags = new(cksmFlags)
|
||||
cksm.isDB = true
|
||||
}
|
||||
|
||||
return cksm
|
||||
}
|
||||
|
||||
type cksmFile struct {
|
||||
File
|
||||
*cksmFlags
|
||||
isDB bool
|
||||
}
|
||||
|
||||
type cksmFlags struct {
|
||||
computeCksm bool
|
||||
verifyCksm bool
|
||||
inCkpt bool
|
||||
pageSize int
|
||||
}
|
||||
|
||||
func (c cksmFile) ReadAt(p []byte, off int64) (n int, err error) {
|
||||
n, err = c.File.ReadAt(p, off)
|
||||
|
||||
// SQLite is reading the header of a database file.
|
||||
if c.isDB && off == 0 && len(p) >= 100 &&
|
||||
bytes.HasPrefix(p, []byte("SQLite format 3\000")) {
|
||||
c.init(p)
|
||||
}
|
||||
|
||||
// Verify checksums.
|
||||
if c.verifyCksm && !c.inCkpt && len(p) == c.pageSize {
|
||||
cksm1 := cksmCompute(p[:len(p)-8])
|
||||
cksm2 := *(*[8]byte)(p[len(p)-8:])
|
||||
if cksm1 != cksm2 {
|
||||
return 0, _IOERR_DATA
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (c cksmFile) WriteAt(p []byte, off int64) (n int, err error) {
|
||||
// SQLite is writing the first page of a database file.
|
||||
if c.isDB && off == 0 && len(p) >= 100 &&
|
||||
bytes.HasPrefix(p, []byte("SQLite format 3\000")) {
|
||||
c.init(p)
|
||||
}
|
||||
|
||||
// Compute checksums.
|
||||
if c.computeCksm && !c.inCkpt && len(p) == c.pageSize {
|
||||
*(*[8]byte)(p[len(p)-8:]) = cksmCompute(p[:len(p)-8])
|
||||
}
|
||||
|
||||
return c.File.WriteAt(p, off)
|
||||
}
|
||||
|
||||
func (c cksmFile) Pragma(name string, value string) (string, error) {
|
||||
switch name {
|
||||
case "checksum_verification":
|
||||
b, ok := sql3util.ParseBool(value)
|
||||
if ok {
|
||||
c.verifyCksm = b && c.computeCksm
|
||||
}
|
||||
if !c.verifyCksm {
|
||||
return "0", nil
|
||||
}
|
||||
return "1", nil
|
||||
|
||||
case "page_size":
|
||||
if c.computeCksm {
|
||||
// Do not allow page size changes on a checksum database.
|
||||
return strconv.Itoa(c.pageSize), nil
|
||||
}
|
||||
}
|
||||
return "", _NOTFOUND
|
||||
}
|
||||
|
||||
func (c cksmFile) fileControl(ctx context.Context, mod api.Module, op _FcntlOpcode, pArg uint32) _ErrorCode {
|
||||
switch op {
|
||||
case _FCNTL_CKPT_START:
|
||||
c.inCkpt = true
|
||||
case _FCNTL_CKPT_DONE:
|
||||
c.inCkpt = false
|
||||
}
|
||||
if rc := vfsFileControlImpl(ctx, mod, c, op, pArg); rc != _NOTFOUND {
|
||||
return rc
|
||||
}
|
||||
return vfsFileControlImpl(ctx, mod, c.File, op, pArg)
|
||||
}
|
||||
|
||||
func (f *cksmFlags) init(header []byte) {
|
||||
f.pageSize = 256 * int(binary.LittleEndian.Uint16(header[16:18]))
|
||||
if r := header[20] == 8; r != f.computeCksm {
|
||||
f.computeCksm = r
|
||||
f.verifyCksm = r
|
||||
}
|
||||
}
|
||||
|
||||
func cksmCompute(a []byte) (cksm [8]byte) {
|
||||
var s1, s2 uint32
|
||||
for len(a) >= 8 {
|
||||
s1 += binary.LittleEndian.Uint32(a[0:4]) + s2
|
||||
s2 += binary.LittleEndian.Uint32(a[4:8]) + s1
|
||||
a = a[8:]
|
||||
}
|
||||
if len(a) != 0 {
|
||||
panic(util.AssertErr())
|
||||
}
|
||||
binary.LittleEndian.PutUint32(cksm[0:4], s1)
|
||||
binary.LittleEndian.PutUint32(cksm[4:8], s2)
|
||||
return
|
||||
}
|
||||
|
||||
func (c cksmFile) SharedMemory() SharedMemory {
|
||||
if f, ok := c.File.(FileSharedMemory); ok {
|
||||
return f.SharedMemory()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c cksmFile) Unwrap() File {
|
||||
return c.File
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
# Go `cksmvfs` SQLite VFS
|
||||
|
||||
This package wraps an SQLite VFS to help detect database corruption.
|
||||
|
||||
The `"cksmvfs"` VFS wraps the default SQLite VFS adding an 8-byte checksum
|
||||
to the end of every page in an SQLite database.\
|
||||
The checksum is added as each page is written
|
||||
and verified as each page is read.\
|
||||
The checksum is intended to help detect database corruption
|
||||
caused by random bit-flips in the mass storage device.
|
||||
|
||||
This implementation is compatible with SQLite's
|
||||
[Checksum VFS Shim](https://sqlite.org/cksumvfs.html).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> [Checksums](https://en.wikipedia.org/wiki/Checksum)
|
||||
> are meant to protect against _silent data corruption_ (bit rot).
|
||||
> They do not offer _authenticity_ (i.e. protect against _forgery_),
|
||||
> nor prevent _silent loss of durability_.
|
||||
> Checkpoint WAL mode databases to improve durabiliy.
|
||||
@@ -1,75 +0,0 @@
|
||||
// Package cksmvfs wraps an SQLite VFS to help detect database corruption.
|
||||
//
|
||||
// The "cksmvfs" [vfs.VFS] wraps the default VFS adding an 8-byte checksum
|
||||
// to the end of every page in an SQLite database.
|
||||
// The checksum is added as each page is written
|
||||
// and verified as each page is read.
|
||||
// The checksum is intended to help detect database corruption
|
||||
// caused by random bit-flips in the mass storage device.
|
||||
//
|
||||
// This implementation is compatible with SQLite's
|
||||
// [Checksum VFS Shim].
|
||||
//
|
||||
// [Checksum VFS Shim]: https://sqlite.org/cksumvfs.html
|
||||
package cksmvfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/vfs"
|
||||
)
|
||||
|
||||
func init() {
|
||||
vfs.Register("cksmvfs", Wrap(vfs.Find("")))
|
||||
}
|
||||
|
||||
// Wrap wraps a base VFS to create a checksumming VFS.
|
||||
func Wrap(base vfs.VFS) vfs.VFS {
|
||||
return &cksmVFS{VFS: base}
|
||||
}
|
||||
|
||||
// EnableChecksums enables checksums on a database.
|
||||
func EnableChecksums(db *sqlite3.Conn, schema string) error {
|
||||
if f, ok := db.Filename("").DatabaseFile().(*cksmFile); !ok {
|
||||
return fmt.Errorf("cksmvfs: incorrect type: %T", f)
|
||||
}
|
||||
|
||||
r, err := db.FileControl(schema, sqlite3.FCNTL_RESERVE_BYTES)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r == 8 {
|
||||
// Correct value, enabled.
|
||||
return nil
|
||||
}
|
||||
if r == 0 {
|
||||
// Default value, enable.
|
||||
_, err = db.FileControl(schema, sqlite3.FCNTL_RESERVE_BYTES, 8)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r, err = db.FileControl(schema, sqlite3.FCNTL_RESERVE_BYTES)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if r != 8 {
|
||||
// Invalid value.
|
||||
return fmt.Errorf("cksmvfs: reserve bytes must be 8, is: %d", r)
|
||||
}
|
||||
|
||||
// VACUUM the database.
|
||||
if schema != "" {
|
||||
err = db.Exec(`VACUUM ` + sqlite3.QuoteIdentifier(schema))
|
||||
} else {
|
||||
err = db.Exec(`VACUUM`)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Checkpoint the WAL.
|
||||
_, _, err = db.WALCheckpoint(schema, sqlite3.CHECKPOINT_RESTART)
|
||||
return err
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package cksmvfs_test
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
"github.com/ncruces/go-sqlite3/util/ioutil"
|
||||
"github.com/ncruces/go-sqlite3/vfs"
|
||||
"github.com/ncruces/go-sqlite3/vfs/cksmvfs"
|
||||
"github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
"github.com/ncruces/go-sqlite3/vfs/readervfs"
|
||||
)
|
||||
|
||||
//go:embed testdata/cksm.db
|
||||
var cksmDB string
|
||||
|
||||
func Test_fileformat(t *testing.T) {
|
||||
readervfs.Create("test.db", ioutil.NewSizeReaderAt(strings.NewReader(cksmDB)))
|
||||
vfs.Register("rcksm", cksmvfs.Wrap(vfs.Find("reader")))
|
||||
|
||||
db, err := driver.Open("file:test.db?vfs=rcksm")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var enabled bool
|
||||
err = db.QueryRow(`PRAGMA checksum_verification`).Scan(&enabled)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !enabled {
|
||||
t.Error("want true")
|
||||
}
|
||||
|
||||
db.SetMaxIdleConns(0) // Clears the page cache.
|
||||
|
||||
_, err = db.Exec(`PRAGMA integrity_check`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
//go:embed testdata/test.db
|
||||
var testDB []byte
|
||||
|
||||
func Test_enable(t *testing.T) {
|
||||
memdb.Create("nockpt.db", testDB)
|
||||
vfs.Register("mcksm", cksmvfs.Wrap(vfs.Find("memdb")))
|
||||
|
||||
db, err := driver.Open("file:/nockpt.db?vfs=mcksm",
|
||||
func(db *sqlite3.Conn) error {
|
||||
return cksmvfs.EnableChecksums(db, "")
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var enabled bool
|
||||
err = db.QueryRow(`PRAGMA checksum_verification`).Scan(&enabled)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !enabled {
|
||||
t.Error("want true")
|
||||
}
|
||||
|
||||
db.SetMaxIdleConns(0) // Clears the page cache.
|
||||
|
||||
_, err = db.Exec(`PRAGMA integrity_check`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_new(t *testing.T) {
|
||||
if !vfs.SupportsFileLocking {
|
||||
t.Skip("skipping without locks")
|
||||
}
|
||||
|
||||
name := "file:" +
|
||||
filepath.ToSlash(filepath.Join(t.TempDir(), "test.db")) +
|
||||
"?vfs=cksmvfs&_pragma=journal_mode(wal)"
|
||||
|
||||
db, err := driver.Open(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var enabled bool
|
||||
err = db.QueryRow(`PRAGMA checksum_verification`).Scan(&enabled)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !enabled {
|
||||
t.Error("want true")
|
||||
}
|
||||
|
||||
var size int
|
||||
err = db.QueryRow(`PRAGMA page_size=1024`).Scan(&size)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if size != 4096 {
|
||||
t.Errorf("got %d, want 4096", size)
|
||||
}
|
||||
|
||||
_, err = db.Exec(`CREATE TABLE users (id INT, name VARCHAR(10))`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = db.Exec(`INSERT INTO users (id, name) VALUES (0, 'go'), (1, 'zig'), (2, 'whatever')`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
db.SetMaxIdleConns(0) // Clears the page cache.
|
||||
|
||||
_, err = db.Exec(`PRAGMA integrity_check`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
package cksmvfs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"github.com/ncruces/go-sqlite3"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
"github.com/ncruces/go-sqlite3/util/sql3util"
|
||||
"github.com/ncruces/go-sqlite3/util/vfsutil"
|
||||
"github.com/ncruces/go-sqlite3/vfs"
|
||||
)
|
||||
|
||||
type cksmVFS struct {
|
||||
vfs.VFS
|
||||
}
|
||||
|
||||
func (c *cksmVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) {
|
||||
// notest // OpenFilename is called instead
|
||||
return nil, 0, sqlite3.CANTOPEN
|
||||
}
|
||||
|
||||
func (c *cksmVFS) OpenFilename(name *vfs.Filename, flags vfs.OpenFlag) (file vfs.File, _ vfs.OpenFlag, err error) {
|
||||
// Prevent accidental wrapping.
|
||||
if pc, _, _, ok := runtime.Caller(1); ok {
|
||||
if fn := runtime.FuncForPC(pc); fn != nil {
|
||||
if fn.Name() != "github.com/ncruces/go-sqlite3/vfs.vfsOpen" {
|
||||
return nil, 0, sqlite3.CANTOPEN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file, flags, err = vfsutil.WrapOpenFilename(c.VFS, name, flags)
|
||||
|
||||
// Checksum only main databases and WALs.
|
||||
if err != nil || flags&(vfs.OPEN_MAIN_DB|vfs.OPEN_WAL) == 0 {
|
||||
return file, flags, err
|
||||
}
|
||||
|
||||
cksm := cksmFile{File: file}
|
||||
|
||||
if flags&vfs.OPEN_WAL != 0 {
|
||||
main, _ := name.DatabaseFile().(*cksmFile)
|
||||
cksm.cksmFlags = main.cksmFlags
|
||||
} else {
|
||||
cksm.isDB = true
|
||||
cksm.cksmFlags = new(cksmFlags)
|
||||
}
|
||||
const createDB = vfs.OPEN_CREATE | vfs.OPEN_READWRITE | vfs.OPEN_MAIN_DB
|
||||
cksm.createDB = flags&createDB == createDB
|
||||
|
||||
return &cksm, flags, err
|
||||
}
|
||||
|
||||
type cksmFile struct {
|
||||
vfs.File
|
||||
*cksmFlags
|
||||
isDB bool
|
||||
createDB bool
|
||||
}
|
||||
|
||||
type cksmFlags struct {
|
||||
computeCksm bool
|
||||
verifyCksm bool
|
||||
inCkpt bool
|
||||
pageSize int
|
||||
}
|
||||
|
||||
//go:embed empty.db
|
||||
var empty string
|
||||
|
||||
func (c *cksmFile) ReadAt(p []byte, off int64) (n int, err error) {
|
||||
n, err = c.File.ReadAt(p, off)
|
||||
|
||||
// SQLite is trying to read from the first page of an empty database file.
|
||||
// Instead, read from an empty database that had checksums enabled,
|
||||
// so checksums are enabled by default.
|
||||
if c.createDB && n == 0 && err == io.EOF && off < 100 {
|
||||
n = copy(p, empty[off:])
|
||||
if n < len(p) {
|
||||
clear(p[n:])
|
||||
}
|
||||
err = nil
|
||||
}
|
||||
|
||||
// SQLite is reading the header of a database file.
|
||||
if c.isDB && off == 0 && len(p) >= 100 &&
|
||||
bytes.HasPrefix(p, []byte("SQLite format 3\000")) {
|
||||
c.updateFlags(p)
|
||||
}
|
||||
|
||||
// Verify checksums.
|
||||
if c.verifyCksm && !c.inCkpt && len(p) == c.pageSize {
|
||||
cksm1 := cksmCompute(p[:len(p)-8])
|
||||
cksm2 := *(*[8]byte)(p[len(p)-8:])
|
||||
if cksm1 != cksm2 {
|
||||
return 0, sqlite3.IOERR_DATA
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (c *cksmFile) WriteAt(p []byte, off int64) (n int, err error) {
|
||||
// SQLite is writing the first page of a database file.
|
||||
if c.isDB && off == 0 && len(p) >= 100 &&
|
||||
bytes.HasPrefix(p, []byte("SQLite format 3\000")) {
|
||||
c.updateFlags(p)
|
||||
}
|
||||
|
||||
// Compute checksums.
|
||||
if c.computeCksm && !c.inCkpt && len(p) == c.pageSize {
|
||||
*(*[8]byte)(p[len(p)-8:]) = cksmCompute(p[:len(p)-8])
|
||||
}
|
||||
|
||||
return c.File.WriteAt(p, off)
|
||||
}
|
||||
|
||||
func (c *cksmFile) updateFlags(header []byte) {
|
||||
c.pageSize = 256 * int(binary.LittleEndian.Uint16(header[16:18]))
|
||||
if r := header[20] == 8; r != c.computeCksm {
|
||||
c.computeCksm = r
|
||||
c.verifyCksm = r
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cksmFile) CheckpointStart() {
|
||||
c.inCkpt = true
|
||||
}
|
||||
|
||||
func (c *cksmFile) CheckpointDone() {
|
||||
c.inCkpt = false
|
||||
}
|
||||
|
||||
func (c *cksmFile) Pragma(name string, value string) (string, error) {
|
||||
switch name {
|
||||
case "checksum_verification":
|
||||
b, ok := sql3util.ParseBool(value)
|
||||
if ok {
|
||||
c.verifyCksm = b && c.computeCksm
|
||||
}
|
||||
if !c.verifyCksm {
|
||||
return "0", nil
|
||||
}
|
||||
return "1", nil
|
||||
|
||||
case "page_size":
|
||||
if c.computeCksm {
|
||||
// Do not allow page size changes on a checksum database.
|
||||
return strconv.Itoa(c.pageSize), nil
|
||||
}
|
||||
}
|
||||
return vfsutil.WrapPragma(c.File, name, value)
|
||||
}
|
||||
|
||||
func cksmCompute(a []byte) (cksm [8]byte) {
|
||||
var s1, s2 uint32
|
||||
for len(a) >= 8 {
|
||||
s1 += binary.LittleEndian.Uint32(a[0:4]) + s2
|
||||
s2 += binary.LittleEndian.Uint32(a[4:8]) + s1
|
||||
a = a[8:]
|
||||
}
|
||||
if len(a) != 0 {
|
||||
panic(util.AssertErr())
|
||||
}
|
||||
binary.LittleEndian.PutUint32(cksm[0:4], s1)
|
||||
binary.LittleEndian.PutUint32(cksm[4:8], s2)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cksmFile) Unwrap() vfs.File {
|
||||
return c.File
|
||||
}
|
||||
|
||||
func (c *cksmFile) SharedMemory() vfs.SharedMemory {
|
||||
return vfsutil.WrapSharedMemory(c.File)
|
||||
}
|
||||
|
||||
// Wrap optional methods.
|
||||
|
||||
func (c *cksmFile) LockState() vfs.LockLevel {
|
||||
return vfsutil.WrapLockState(c.File) // notest
|
||||
}
|
||||
|
||||
func (c *cksmFile) PersistentWAL() bool {
|
||||
return vfsutil.WrapPersistentWAL(c.File) // notest
|
||||
}
|
||||
|
||||
func (c *cksmFile) SetPersistentWAL(keepWAL bool) {
|
||||
vfsutil.WrapSetPersistentWAL(c.File, keepWAL) // notest
|
||||
}
|
||||
|
||||
func (c *cksmFile) PowersafeOverwrite() bool {
|
||||
return vfsutil.WrapPowersafeOverwrite(c.File) // notest
|
||||
}
|
||||
|
||||
func (c *cksmFile) SetPowersafeOverwrite(psow bool) {
|
||||
vfsutil.WrapSetPowersafeOverwrite(c.File, psow) // notest
|
||||
}
|
||||
|
||||
func (c *cksmFile) ChunkSize(size int) {
|
||||
vfsutil.WrapChunkSize(c.File, size) // notest
|
||||
}
|
||||
|
||||
func (c *cksmFile) SizeHint(size int64) error {
|
||||
return vfsutil.WrapSizeHint(c.File, size) // notest
|
||||
}
|
||||
|
||||
func (c *cksmFile) HasMoved() (bool, error) {
|
||||
return vfsutil.WrapHasMoved(c.File) // notest
|
||||
}
|
||||
|
||||
func (c *cksmFile) Overwrite() error {
|
||||
return vfsutil.WrapOverwrite(c.File) // notest
|
||||
}
|
||||
|
||||
func (c *cksmFile) CommitPhaseTwo() error {
|
||||
return vfsutil.WrapCommitPhaseTwo(c.File) // notest
|
||||
}
|
||||
|
||||
func (c *cksmFile) BeginAtomicWrite() error {
|
||||
return vfsutil.WrapBeginAtomicWrite(c.File) // notest
|
||||
}
|
||||
|
||||
func (c *cksmFile) CommitAtomicWrite() error {
|
||||
return vfsutil.WrapCommitAtomicWrite(c.File) // notest
|
||||
}
|
||||
|
||||
func (c *cksmFile) RollbackAtomicWrite() error {
|
||||
return vfsutil.WrapRollbackAtomicWrite(c.File) // notest
|
||||
}
|
||||
Binary file not shown.
BIN
vfs/cksmvfs/testdata/test.db
vendored
BIN
vfs/cksmvfs/testdata/test.db
vendored
Binary file not shown.
@@ -51,6 +51,7 @@ const (
|
||||
_IOERR_BEGIN_ATOMIC _ErrorCode = util.IOERR_BEGIN_ATOMIC
|
||||
_IOERR_COMMIT_ATOMIC _ErrorCode = util.IOERR_COMMIT_ATOMIC
|
||||
_IOERR_ROLLBACK_ATOMIC _ErrorCode = util.IOERR_ROLLBACK_ATOMIC
|
||||
_IOERR_DATA _ErrorCode = util.IOERR_DATA
|
||||
_BUSY_SNAPSHOT _ErrorCode = util.BUSY_SNAPSHOT
|
||||
_CANTOPEN_FULLPATH _ErrorCode = util.CANTOPEN_FULLPATH
|
||||
_CANTOPEN_ISDIR _ErrorCode = util.CANTOPEN_ISDIR
|
||||
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
var walDB []byte
|
||||
|
||||
func Test_wal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
Create("test.db", walDB)
|
||||
|
||||
db, err := sqlite3.Open("file:/test.db?vfs=memdb")
|
||||
|
||||
@@ -159,6 +159,7 @@ func vfsOpen(ctx context.Context, mod api.Module, pVfs, zPath, pFile uint32, fla
|
||||
if pOutFlags != 0 {
|
||||
util.WriteUint32(mod, pOutFlags, uint32(flags))
|
||||
}
|
||||
file = cksmWrapFile(name, flags, file)
|
||||
vfsFileRegister(ctx, mod, pFile, file)
|
||||
return _OK
|
||||
}
|
||||
@@ -237,7 +238,13 @@ func vfsCheckReservedLock(ctx context.Context, mod api.Module, pFile, pResOut ui
|
||||
|
||||
func vfsFileControl(ctx context.Context, mod api.Module, pFile uint32, op _FcntlOpcode, pArg uint32) _ErrorCode {
|
||||
file := vfsFileGet(ctx, mod, pFile).(File)
|
||||
if file, ok := file.(fileControl); ok {
|
||||
return file.fileControl(ctx, mod, op, pArg)
|
||||
}
|
||||
return vfsFileControlImpl(ctx, mod, file, op, pArg)
|
||||
}
|
||||
|
||||
func vfsFileControlImpl(ctx context.Context, mod api.Module, file File, op _FcntlOpcode, pArg uint32) _ErrorCode {
|
||||
switch op {
|
||||
case _FCNTL_LOCKSTATE:
|
||||
if file, ok := file.(FileLockState); ok {
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
var testDB string
|
||||
|
||||
func Test_fileformat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
readervfs.Create("test.db", ioutil.NewSizeReaderAt(strings.NewReader(testDB)))
|
||||
vfs.Register("rxts", xts.Wrap(vfs.Find("reader"), nil))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user