From df953b31c2d8474bf121d66b292b64e61b681a8c Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Thu, 18 May 2023 16:00:34 +0100 Subject: [PATCH] Refactor VFS. --- error.go | 81 +----------- internal/util/const.go | 116 ++++++++++++++++ internal/util/error.go | 72 ++++++++++ internal/vfs/const.go | 80 +++++------ internal/vfs/vfs.go | 243 +++++++++++----------------------- internal/vfs/vfs_api.go | 13 +- internal/vfs/vfs_file.go | 160 ++++++++++++++++++++-- internal/vfs/vfs_lock.go | 41 ++---- internal/vfs/vfs_lock_test.go | 4 +- sqlite3vfs/vfs.go | 2 +- sqlite3vfs/vfs_api_test.go | 8 +- 11 files changed, 475 insertions(+), 345 deletions(-) create mode 100644 internal/util/const.go diff --git a/error.go b/error.go index 74bf248..957a744 100644 --- a/error.go +++ b/error.go @@ -3,6 +3,8 @@ package sqlite3 import ( "strconv" "strings" + + "github.com/ncruces/go-sqlite3/internal/util" ) // Error wraps an SQLite Error Code. @@ -83,72 +85,7 @@ func (e *Error) SQL() string { // Error implements the error interface. func (e ErrorCode) Error() string { - switch e { - case _OK: - return "sqlite3: not an error" - case _ROW: - return "sqlite3: another row available" - case _DONE: - return "sqlite3: no more rows available" - - case ERROR: - return "sqlite3: SQL logic error" - case INTERNAL: - break - case PERM: - return "sqlite3: access permission denied" - case ABORT: - return "sqlite3: query aborted" - case BUSY: - return "sqlite3: database is locked" - case LOCKED: - return "sqlite3: database table is locked" - case NOMEM: - return "sqlite3: out of memory" - case READONLY: - return "sqlite3: attempt to write a readonly database" - case INTERRUPT: - return "sqlite3: interrupted" - case IOERR: - return "sqlite3: disk I/O error" - case CORRUPT: - return "sqlite3: database disk image is malformed" - case NOTFOUND: - return "sqlite3: unknown operation" - case FULL: - return "sqlite3: database or disk is full" - case CANTOPEN: - return "sqlite3: unable to open database file" - case PROTOCOL: - return "sqlite3: locking protocol" - case FORMAT: - break - case SCHEMA: - return "sqlite3: database schema has changed" - case TOOBIG: - return "sqlite3: string or blob too big" - case CONSTRAINT: - return "sqlite3: constraint failed" - case MISMATCH: - return "sqlite3: datatype mismatch" - case MISUSE: - return "sqlite3: bad parameter or other API misuse" - case NOLFS: - break - case AUTH: - return "sqlite3: authorization denied" - case EMPTY: - break - case RANGE: - return "sqlite3: column index out of range" - case NOTADB: - return "sqlite3: file is not a database" - case NOTICE: - return "sqlite3: notification message" - case WARNING: - return "sqlite3: warning message" - } - return "sqlite3: unknown error" + return util.ErrorCodeString(uint32(e)) } // Temporary returns true for [BUSY] errors. @@ -158,17 +95,7 @@ func (e ErrorCode) Temporary() bool { // Error implements the error interface. func (e ExtendedErrorCode) Error() string { - switch x := ErrorCode(e); { - case e == ABORT_ROLLBACK: - return "sqlite3: abort due to ROLLBACK" - case x < _ROW: - return x.Error() - case e == _ROW: - return "sqlite3: another row available" - case e == _DONE: - return "sqlite3: no more rows available" - } - return "sqlite3: unknown error" + return util.ErrorCodeString(uint32(e)) } // Is tests whether this error matches a given [ErrorCode]. diff --git a/internal/util/const.go b/internal/util/const.go new file mode 100644 index 0000000..6398bc1 --- /dev/null +++ b/internal/util/const.go @@ -0,0 +1,116 @@ +package util + +// https://sqlite.com/matrix/rescode.html +const ( + OK = 0 /* Successful result */ + + ERROR = 1 /* Generic error */ + INTERNAL = 2 /* Internal logic error in SQLite */ + PERM = 3 /* Access permission denied */ + ABORT = 4 /* Callback routine requested an abort */ + BUSY = 5 /* The database file is locked */ + LOCKED = 6 /* A table in the database is locked */ + NOMEM = 7 /* A malloc() failed */ + READONLY = 8 /* Attempt to write a readonly database */ + INTERRUPT = 9 /* Operation terminated by sqlite3_interrupt() */ + IOERR = 10 /* Some kind of disk I/O error occurred */ + CORRUPT = 11 /* The database disk image is malformed */ + NOTFOUND = 12 /* Unknown opcode in sqlite3_file_control() */ + FULL = 13 /* Insertion failed because database is full */ + CANTOPEN = 14 /* Unable to open the database file */ + PROTOCOL = 15 /* Database lock protocol error */ + EMPTY = 16 /* Internal use only */ + SCHEMA = 17 /* The database schema changed */ + TOOBIG = 18 /* String or BLOB exceeds size limit */ + CONSTRAINT = 19 /* Abort due to constraint violation */ + MISMATCH = 20 /* Data type mismatch */ + MISUSE = 21 /* Library used incorrectly */ + NOLFS = 22 /* Uses OS features not supported on host */ + AUTH = 23 /* Authorization denied */ + FORMAT = 24 /* Not used */ + RANGE = 25 /* 2nd parameter to sqlite3_bind out of range */ + NOTADB = 26 /* File opened that is not a database file */ + NOTICE = 27 /* Notifications from sqlite3_log() */ + WARNING = 28 /* Warnings from sqlite3_log() */ + + ROW = 100 /* sqlite3_step() has another row ready */ + DONE = 101 /* sqlite3_step() has finished executing */ + + ERROR_MISSING_COLLSEQ = ERROR | (1 << 8) + ERROR_RETRY = ERROR | (2 << 8) + ERROR_SNAPSHOT = ERROR | (3 << 8) + IOERR_READ = IOERR | (1 << 8) + IOERR_SHORT_READ = IOERR | (2 << 8) + IOERR_WRITE = IOERR | (3 << 8) + IOERR_FSYNC = IOERR | (4 << 8) + IOERR_DIR_FSYNC = IOERR | (5 << 8) + IOERR_TRUNCATE = IOERR | (6 << 8) + IOERR_FSTAT = IOERR | (7 << 8) + IOERR_UNLOCK = IOERR | (8 << 8) + IOERR_RDLOCK = IOERR | (9 << 8) + IOERR_DELETE = IOERR | (10 << 8) + IOERR_BLOCKED = IOERR | (11 << 8) + IOERR_NOMEM = IOERR | (12 << 8) + IOERR_ACCESS = IOERR | (13 << 8) + IOERR_CHECKRESERVEDLOCK = IOERR | (14 << 8) + IOERR_LOCK = IOERR | (15 << 8) + IOERR_CLOSE = IOERR | (16 << 8) + IOERR_DIR_CLOSE = IOERR | (17 << 8) + IOERR_SHMOPEN = IOERR | (18 << 8) + IOERR_SHMSIZE = IOERR | (19 << 8) + IOERR_SHMLOCK = IOERR | (20 << 8) + IOERR_SHMMAP = IOERR | (21 << 8) + IOERR_SEEK = IOERR | (22 << 8) + IOERR_DELETE_NOENT = IOERR | (23 << 8) + IOERR_MMAP = IOERR | (24 << 8) + IOERR_GETTEMPPATH = IOERR | (25 << 8) + IOERR_CONVPATH = IOERR | (26 << 8) + IOERR_VNODE = IOERR | (27 << 8) + IOERR_AUTH = IOERR | (28 << 8) + IOERR_BEGIN_ATOMIC = IOERR | (29 << 8) + IOERR_COMMIT_ATOMIC = IOERR | (30 << 8) + IOERR_ROLLBACK_ATOMIC = IOERR | (31 << 8) + IOERR_DATA = IOERR | (32 << 8) + IOERR_CORRUPTFS = IOERR | (33 << 8) + LOCKED_SHAREDCACHE = LOCKED | (1 << 8) + LOCKED_VTAB = LOCKED | (2 << 8) + BUSY_RECOVERY = BUSY | (1 << 8) + BUSY_SNAPSHOT = BUSY | (2 << 8) + BUSY_TIMEOUT = BUSY | (3 << 8) + CANTOPEN_NOTEMPDIR = CANTOPEN | (1 << 8) + CANTOPEN_ISDIR = CANTOPEN | (2 << 8) + CANTOPEN_FULLPATH = CANTOPEN | (3 << 8) + CANTOPEN_CONVPATH = CANTOPEN | (4 << 8) + CANTOPEN_DIRTYWAL = CANTOPEN | (5 << 8) /* Not Used */ + CANTOPEN_SYMLINK = CANTOPEN | (6 << 8) + CORRUPT_VTAB = CORRUPT | (1 << 8) + CORRUPT_SEQUENCE = CORRUPT | (2 << 8) + CORRUPT_INDEX = CORRUPT | (3 << 8) + READONLY_RECOVERY = READONLY | (1 << 8) + READONLY_CANTLOCK = READONLY | (2 << 8) + READONLY_ROLLBACK = READONLY | (3 << 8) + READONLY_DBMOVED = READONLY | (4 << 8) + READONLY_CANTINIT = READONLY | (5 << 8) + READONLY_DIRECTORY = READONLY | (6 << 8) + ABORT_ROLLBACK = ABORT | (2 << 8) + CONSTRAINT_CHECK = CONSTRAINT | (1 << 8) + CONSTRAINT_COMMITHOOK = CONSTRAINT | (2 << 8) + CONSTRAINT_FOREIGNKEY = CONSTRAINT | (3 << 8) + CONSTRAINT_FUNCTION = CONSTRAINT | (4 << 8) + CONSTRAINT_NOTNULL = CONSTRAINT | (5 << 8) + CONSTRAINT_PRIMARYKEY = CONSTRAINT | (6 << 8) + CONSTRAINT_TRIGGER = CONSTRAINT | (7 << 8) + CONSTRAINT_UNIQUE = CONSTRAINT | (8 << 8) + CONSTRAINT_VTAB = CONSTRAINT | (9 << 8) + CONSTRAINT_ROWID = CONSTRAINT | (10 << 8) + CONSTRAINT_PINNED = CONSTRAINT | (11 << 8) + CONSTRAINT_DATATYPE = CONSTRAINT | (12 << 8) + NOTICE_RECOVER_WAL = NOTICE | (1 << 8) + NOTICE_RECOVER_ROLLBACK = NOTICE | (2 << 8) + NOTICE_RBU = NOTICE | (3 << 8) + WARNING_AUTOINDEX = WARNING | (1 << 8) + AUTH_USER = AUTH | (1 << 8) + + OK_LOAD_PERMANENTLY = OK | (1 << 8) + OK_SYMLINK = OK | (2 << 8) /* internal use only */ +) diff --git a/internal/util/error.go b/internal/util/error.go index 351aa3f..a0682bb 100644 --- a/internal/util/error.go +++ b/internal/util/error.go @@ -40,3 +40,75 @@ func Finalizer[T any](skip int) func(*T) { } return func(*T) { panic(ErrorString(msg)) } } + +func ErrorCodeString(rc uint32) string { + switch rc { + case ABORT_ROLLBACK: + return "sqlite3: abort due to ROLLBACK" + case ROW: + return "sqlite3: another row available" + case DONE: + return "sqlite3: no more rows available" + } + switch rc & 0xff { + case OK: + return "sqlite3: not an error" + case ERROR: + return "sqlite3: SQL logic error" + case INTERNAL: + break + case PERM: + return "sqlite3: access permission denied" + case ABORT: + return "sqlite3: query aborted" + case BUSY: + return "sqlite3: database is locked" + case LOCKED: + return "sqlite3: database table is locked" + case NOMEM: + return "sqlite3: out of memory" + case READONLY: + return "sqlite3: attempt to write a readonly database" + case INTERRUPT: + return "sqlite3: interrupted" + case IOERR: + return "sqlite3: disk I/O error" + case CORRUPT: + return "sqlite3: database disk image is malformed" + case NOTFOUND: + return "sqlite3: unknown operation" + case FULL: + return "sqlite3: database or disk is full" + case CANTOPEN: + return "sqlite3: unable to open database file" + case PROTOCOL: + return "sqlite3: locking protocol" + case FORMAT: + break + case SCHEMA: + return "sqlite3: database schema has changed" + case TOOBIG: + return "sqlite3: string or blob too big" + case CONSTRAINT: + return "sqlite3: constraint failed" + case MISMATCH: + return "sqlite3: datatype mismatch" + case MISUSE: + return "sqlite3: bad parameter or other API misuse" + case NOLFS: + break + case AUTH: + return "sqlite3: authorization denied" + case EMPTY: + break + case RANGE: + return "sqlite3: column index out of range" + case NOTADB: + return "sqlite3: file is not a database" + case NOTICE: + return "sqlite3: notification message" + case WARNING: + return "sqlite3: warning message" + } + return "sqlite3: unknown error" +} diff --git a/internal/vfs/const.go b/internal/vfs/const.go index 1d7684d..4dcc591 100644 --- a/internal/vfs/const.go +++ b/internal/vfs/const.go @@ -1,6 +1,9 @@ package vfs -import "github.com/ncruces/go-sqlite3/sqlite3vfs" +import ( + "github.com/ncruces/go-sqlite3/internal/util" + "github.com/ncruces/go-sqlite3/sqlite3vfs" +) const ( _MAX_STRING = 512 // Used for short strings: names, error messages… @@ -11,54 +14,35 @@ const ( // https://www.sqlite.org/rescode.html type _ErrorCode uint32 -const ( - _OK _ErrorCode = 0 /* Successful result */ - _PERM _ErrorCode = 3 /* Access permission denied */ - _BUSY _ErrorCode = 5 /* The database file is locked */ - _IOERR _ErrorCode = 10 /* Some kind of disk I/O error occurred */ - _NOTFOUND _ErrorCode = 12 /* Unknown opcode in sqlite3_file_control() */ - _CANTOPEN _ErrorCode = 14 /* Unable to open the database file */ +func (e _ErrorCode) Error() string { + return util.ErrorCodeString(uint32(e)) +} - _IOERR_READ = _IOERR | (1 << 8) - _IOERR_SHORT_READ = _IOERR | (2 << 8) - _IOERR_WRITE = _IOERR | (3 << 8) - _IOERR_FSYNC = _IOERR | (4 << 8) - _IOERR_DIR_FSYNC = _IOERR | (5 << 8) - _IOERR_TRUNCATE = _IOERR | (6 << 8) - _IOERR_FSTAT = _IOERR | (7 << 8) - _IOERR_UNLOCK = _IOERR | (8 << 8) - _IOERR_RDLOCK = _IOERR | (9 << 8) - _IOERR_DELETE = _IOERR | (10 << 8) - _IOERR_BLOCKED = _IOERR | (11 << 8) - _IOERR_NOMEM = _IOERR | (12 << 8) - _IOERR_ACCESS = _IOERR | (13 << 8) - _IOERR_CHECKRESERVEDLOCK = _IOERR | (14 << 8) - _IOERR_LOCK = _IOERR | (15 << 8) - _IOERR_CLOSE = _IOERR | (16 << 8) - _IOERR_DIR_CLOSE = _IOERR | (17 << 8) - _IOERR_SHMOPEN = _IOERR | (18 << 8) - _IOERR_SHMSIZE = _IOERR | (19 << 8) - _IOERR_SHMLOCK = _IOERR | (20 << 8) - _IOERR_SHMMAP = _IOERR | (21 << 8) - _IOERR_SEEK = _IOERR | (22 << 8) - _IOERR_DELETE_NOENT = _IOERR | (23 << 8) - _IOERR_MMAP = _IOERR | (24 << 8) - _IOERR_GETTEMPPATH = _IOERR | (25 << 8) - _IOERR_CONVPATH = _IOERR | (26 << 8) - _IOERR_VNODE = _IOERR | (27 << 8) - _IOERR_AUTH = _IOERR | (28 << 8) - _IOERR_BEGIN_ATOMIC = _IOERR | (29 << 8) - _IOERR_COMMIT_ATOMIC = _IOERR | (30 << 8) - _IOERR_ROLLBACK_ATOMIC = _IOERR | (31 << 8) - _IOERR_DATA = _IOERR | (32 << 8) - _IOERR_CORRUPTFS = _IOERR | (33 << 8) - _CANTOPEN_NOTEMPDIR = _CANTOPEN | (1 << 8) - _CANTOPEN_ISDIR = _CANTOPEN | (2 << 8) - _CANTOPEN_FULLPATH = _CANTOPEN | (3 << 8) - _CANTOPEN_CONVPATH = _CANTOPEN | (4 << 8) - _CANTOPEN_DIRTYWAL = _CANTOPEN | (5 << 8) /* Not Used */ - _CANTOPEN_SYMLINK = _CANTOPEN | (6 << 8) - _OK_SYMLINK = _OK | (2 << 8) /* internal use only */ +const ( + _OK _ErrorCode = util.OK + _PERM _ErrorCode = util.PERM + _BUSY _ErrorCode = util.BUSY + _IOERR _ErrorCode = util.IOERR + _NOTFOUND _ErrorCode = util.NOTFOUND + _CANTOPEN _ErrorCode = util.CANTOPEN + _IOERR_READ _ErrorCode = util.IOERR_READ + _IOERR_SHORT_READ _ErrorCode = util.IOERR_SHORT_READ + _IOERR_WRITE _ErrorCode = util.IOERR_WRITE + _IOERR_FSYNC _ErrorCode = util.IOERR_FSYNC + _IOERR_DIR_FSYNC _ErrorCode = util.IOERR_DIR_FSYNC + _IOERR_TRUNCATE _ErrorCode = util.IOERR_TRUNCATE + _IOERR_FSTAT _ErrorCode = util.IOERR_FSTAT + _IOERR_UNLOCK _ErrorCode = util.IOERR_UNLOCK + _IOERR_RDLOCK _ErrorCode = util.IOERR_RDLOCK + _IOERR_DELETE _ErrorCode = util.IOERR_DELETE + _IOERR_ACCESS _ErrorCode = util.IOERR_ACCESS + _IOERR_CHECKRESERVEDLOCK _ErrorCode = util.IOERR_CHECKRESERVEDLOCK + _IOERR_LOCK _ErrorCode = util.IOERR_LOCK + _IOERR_CLOSE _ErrorCode = util.IOERR_CLOSE + _IOERR_SEEK _ErrorCode = util.IOERR_SEEK + _IOERR_DELETE_NOENT _ErrorCode = util.IOERR_DELETE_NOENT + _CANTOPEN_FULLPATH _ErrorCode = util.CANTOPEN_FULLPATH + _OK_SYMLINK _ErrorCode = util.OK_SYMLINK ) // https://www.sqlite.org/c3ref/c_open_autoproxy.html diff --git a/internal/vfs/vfs.go b/internal/vfs/vfs.go index f1dd1fe..9e8257f 100644 --- a/internal/vfs/vfs.go +++ b/internal/vfs/vfs.go @@ -7,8 +7,6 @@ import ( "io" "io/fs" "os" - "path/filepath" - "runtime" "time" "github.com/ncruces/go-sqlite3/internal/util" @@ -46,7 +44,7 @@ func Export(env wazero.HostModuleBuilder) wazero.HostModuleBuilder { type vfsKey struct{} type vfsState struct { - files []vfsFile + files []sqlite3vfs.File } func Context(ctx context.Context) (context.Context, io.Closer) { @@ -56,7 +54,7 @@ func Context(ctx context.Context) (context.Context, io.Closer) { func (vfs *vfsState) Close() error { for _, f := range vfs.files { - if f.File != nil { + if f != nil { f.Close() } } @@ -119,161 +117,57 @@ func vfsCurrentTime64(ctx context.Context, mod api.Module, pVfs, piNow uint32) _ func vfsFullPathname(ctx context.Context, mod api.Module, pVfs, zRelative, nFull, zFull uint32) _ErrorCode { vfs := vfsAPIGet(mod, pVfs) - rel := util.ReadString(mod, zRelative, _MAX_PATHNAME) + path := util.ReadString(mod, zRelative, _MAX_PATHNAME) - var abs string - var symlink bool - if vfs != nil { - p, err := vfs.FullPathname(rel) - if err != nil { - return vfsAPIErrorCode(err, _CANTOPEN_FULLPATH) - } - abs = p - } else { - p, err := filepath.Abs(rel) - if err != nil { - return _CANTOPEN_FULLPATH - } - s, err := os.Lstat(p) - if err == nil { - symlink = s.Mode()&fs.ModeSymlink != 0 - } else if !errors.Is(err, fs.ErrNotExist) { - return _CANTOPEN_FULLPATH - } - abs = p - } + path, err := vfs.FullPathname(path) - size := uint64(len(abs) + 1) + size := uint64(len(path) + 1) if size > uint64(nFull) { return _CANTOPEN_FULLPATH } mem := util.View(mod, zFull, size) - mem[len(abs)] = 0 - copy(mem, abs) + mem[len(path)] = 0 + copy(mem, path) - if symlink { - return _OK_SYMLINK - } - return _OK + return vfsAPIErrorCode(err, _CANTOPEN_FULLPATH) } func vfsDelete(ctx context.Context, mod api.Module, pVfs, zPath, syncDir uint32) _ErrorCode { vfs := vfsAPIGet(mod, pVfs) path := util.ReadString(mod, zPath, _MAX_PATHNAME) - if vfs != nil { - err := vfs.Delete(path, syncDir != 0) - return vfsAPIErrorCode(err, _IOERR_DELETE) - } - - err := os.Remove(path) - if errors.Is(err, fs.ErrNotExist) { - return _IOERR_DELETE_NOENT - } - if err != nil { - return _IOERR_DELETE - } - if runtime.GOOS != "windows" && syncDir != 0 { - f, err := os.Open(filepath.Dir(path)) - if err != nil { - return _OK - } - defer f.Close() - err = osSync(f, false, false) - if err != nil { - return _IOERR_DIR_FSYNC - } - } - return _OK + err := vfs.Delete(path, syncDir != 0) + return vfsAPIErrorCode(err, _IOERR_DELETE) } func vfsAccess(ctx context.Context, mod api.Module, pVfs, zPath uint32, flags _AccessFlag, pResOut uint32) _ErrorCode { vfs := vfsAPIGet(mod, pVfs) path := util.ReadString(mod, zPath, _MAX_PATHNAME) + ok, err := vfs.Access(path, flags) var res uint32 - var rc _ErrorCode - if vfs != nil { - ok, err := vfs.Access(path, flags) - if ok { - res = 1 - } else { - res = 0 - } - rc = vfsAPIErrorCode(err, _IOERR_ACCESS) - } else { - err := osAccess(path, flags) - if flags == _ACCESS_EXISTS { - switch { - case err == nil: - res = 1 - case errors.Is(err, fs.ErrNotExist): - res = 0 - default: - rc = _IOERR_ACCESS - } - } else { - switch { - case err == nil: - res = 1 - case errors.Is(err, fs.ErrPermission): - res = 0 - default: - rc = _IOERR_ACCESS - } - } + if ok { + res = 1 } util.WriteUint32(mod, pResOut, res) - return rc + return vfsAPIErrorCode(err, _IOERR_ACCESS) } -func vfsOpen(ctx context.Context, mod api.Module, pVfs, zName, pFile uint32, flags _OpenFlag, pOutFlags uint32) _ErrorCode { +func vfsOpen(ctx context.Context, mod api.Module, pVfs, zPath, pFile uint32, flags _OpenFlag, pOutFlags uint32) _ErrorCode { vfs := vfsAPIGet(mod, pVfs) - var oflags int - if flags&_OPEN_EXCLUSIVE != 0 { - oflags |= os.O_EXCL - } - if flags&_OPEN_CREATE != 0 { - oflags |= os.O_CREATE - } - if flags&_OPEN_READONLY != 0 { - oflags |= os.O_RDONLY - } - if flags&_OPEN_READWRITE != 0 { - oflags |= os.O_RDWR + var path string + if zPath != 0 { + path = util.ReadString(mod, zPath, _MAX_PATHNAME) } - var err error - var name string - var f *os.File - if zName != 0 { - name = util.ReadString(mod, zName, _MAX_PATHNAME) - } - switch { - case vfs != nil: - _, flags, err = vfs.Open(name, flags) - case name == "": - f, err = os.CreateTemp("", "*.db") - default: - f, err = osOpenFile(name, oflags, 0666) - } + file, flags, err := vfs.Open(path, flags) if err != nil { - return _CANTOPEN + return vfsAPIErrorCode(err, _CANTOPEN) } - if flags&_OPEN_DELETEONCLOSE != 0 { - os.Remove(f.Name()) - } - - file := vfsFileOpen(ctx, mod, pFile, f) - file.psow = true - file.readOnly = flags&_OPEN_READONLY != 0 - file.syncDir = runtime.GOOS != "windows" && - flags&(_OPEN_CREATE) != 0 && - flags&(_OPEN_MAIN_JOURNAL|_OPEN_SUPER_JOURNAL|_OPEN_WAL) != 0 - + vfsFileRegister(ctx, mod, pFile, file) if pOutFlags != 0 { util.WriteUint32(mod, pOutFlags, uint32(flags)) } @@ -289,9 +183,9 @@ func vfsClose(ctx context.Context, mod api.Module, pFile uint32) _ErrorCode { } func vfsRead(ctx context.Context, mod api.Module, pFile, zBuf, iAmt uint32, iOfst int64) _ErrorCode { + file := vfsFileGet(ctx, mod, pFile) buf := util.View(mod, zBuf, uint64(iAmt)) - file := vfsFileGet(ctx, mod, pFile) n, err := file.ReadAt(buf, iOfst) if n == int(iAmt) { return _OK @@ -306,9 +200,9 @@ func vfsRead(ctx context.Context, mod api.Module, pFile, zBuf, iAmt uint32, iOfs } func vfsWrite(ctx context.Context, mod api.Module, pFile, zBuf, iAmt uint32, iOfst int64) _ErrorCode { + file := vfsFileGet(ctx, mod, pFile) buf := util.View(mod, zBuf, uint64(iAmt)) - file := vfsFileGet(ctx, mod, pFile) _, err := file.WriteAt(buf, iOfst) if err != nil { return _IOERR_WRITE @@ -319,60 +213,70 @@ func vfsWrite(ctx context.Context, mod api.Module, pFile, zBuf, iAmt uint32, iOf func vfsTruncate(ctx context.Context, mod api.Module, pFile uint32, nByte int64) _ErrorCode { file := vfsFileGet(ctx, mod, pFile) err := file.Truncate(nByte) - if err != nil { - return _IOERR_TRUNCATE - } - return _OK + return vfsAPIErrorCode(err, _IOERR_TRUNCATE) } func vfsSync(ctx context.Context, mod api.Module, pFile uint32, flags _SyncFlag) _ErrorCode { - dataonly := (flags & _SYNC_DATAONLY) != 0 - fullsync := (flags & 0x0f) == _SYNC_FULL - file := vfsFileGet(ctx, mod, pFile) - err := osSync(file.File, fullsync, dataonly) - if err != nil { - return _IOERR_FSYNC - } - if runtime.GOOS != "windows" && file.syncDir { - file.syncDir = false - f, err := os.Open(filepath.Dir(file.Name())) - if err != nil { - return _OK - } - defer f.Close() - err = osSync(f, false, false) - if err != nil { - return _IOERR_DIR_FSYNC - } - } - return _OK + err := file.Sync(flags) + return vfsAPIErrorCode(err, _IOERR_FSYNC) } func vfsFileSize(ctx context.Context, mod api.Module, pFile, pSize uint32) _ErrorCode { file := vfsFileGet(ctx, mod, pFile) - off, err := file.Seek(0, io.SeekEnd) - if err != nil { - return _IOERR_SEEK + size, err := file.FileSize() + util.WriteUint64(mod, pSize, uint64(size)) + return vfsAPIErrorCode(err, _IOERR_SEEK) +} + +func vfsLock(ctx context.Context, mod api.Module, pFile uint32, eLock _LockLevel) _ErrorCode { + file := vfsFileGet(ctx, mod, pFile) + err := file.Lock(eLock) + return vfsAPIErrorCode(err, _IOERR_LOCK) +} + +func vfsUnlock(ctx context.Context, mod api.Module, pFile uint32, eLock _LockLevel) _ErrorCode { + file := vfsFileGet(ctx, mod, pFile) + err := file.Unlock(eLock) + return vfsAPIErrorCode(err, _IOERR_UNLOCK) +} + +func vfsCheckReservedLock(ctx context.Context, mod api.Module, pFile, pResOut uint32) _ErrorCode { + file := vfsFileGet(ctx, mod, pFile) + locked, err := file.CheckReservedLock() + + var res uint32 + if locked { + res = 1 } - util.WriteUint64(mod, pSize, uint64(off)) - return _OK + util.WriteUint32(mod, pResOut, res) + return vfsAPIErrorCode(err, _IOERR_CHECKRESERVEDLOCK) } func vfsFileControl(ctx context.Context, mod api.Module, pFile uint32, op _FcntlOpcode, pArg uint32) _ErrorCode { switch op { case _FCNTL_LOCKSTATE: - util.WriteUint32(mod, pArg, uint32(vfsFileGet(ctx, mod, pFile).lock)) + file, ok := vfsFileGet(ctx, mod, pFile).(*vfsFile) + if !ok { + return _NOTFOUND + } + util.WriteUint32(mod, pArg, uint32(file.lock)) return _OK case _FCNTL_LOCK_TIMEOUT: - file := vfsFileGet(ctx, mod, pFile) + file, ok := vfsFileGet(ctx, mod, pFile).(*vfsFile) + if !ok { + return _NOTFOUND + } millis := file.lockTimeout.Milliseconds() file.lockTimeout = time.Duration(util.ReadUint32(mod, pArg)) * time.Millisecond util.WriteUint32(mod, pArg, uint32(millis)) return _OK case _FCNTL_POWERSAFE_OVERWRITE: - file := vfsFileGet(ctx, mod, pFile) + file, ok := vfsFileGet(ctx, mod, pFile).(*vfsFile) + if !ok { + return _NOTFOUND + } switch util.ReadUint32(mod, pArg) { case 0: file.psow = false @@ -400,19 +304,23 @@ func vfsFileControl(ctx context.Context, mod api.Module, pFile uint32, op _Fcntl } func vfsSectorSize(ctx context.Context, mod api.Module, pFile uint32) uint32 { - return _DEFAULT_SECTOR_SIZE + file := vfsFileGet(ctx, mod, pFile) + return uint32(file.SectorSize()) } func vfsDeviceCharacteristics(ctx context.Context, mod api.Module, pFile uint32) _DeviceCharacteristic { - file := vfsFileGet(ctx, mod, pFile) - if file.psow { + file, ok := vfsFileGet(ctx, mod, pFile).(*vfsFile) + if ok && file.psow { return _IOCAP_POWERSAFE_OVERWRITE } return 0 } func vfsSizeHint(ctx context.Context, mod api.Module, pFile, pArg uint32) _ErrorCode { - file := vfsFileGet(ctx, mod, pFile) + file, ok := vfsFileGet(ctx, mod, pFile).(*vfsFile) + if !ok { + return _NOTFOUND + } size := util.ReadUint64(mod, pArg) err := osAllocate(file.File, int64(size)) if err != nil { @@ -422,7 +330,10 @@ func vfsSizeHint(ctx context.Context, mod api.Module, pFile, pArg uint32) _Error } func vfsFileMoved(ctx context.Context, mod api.Module, pFile, pResOut uint32) _ErrorCode { - file := vfsFileGet(ctx, mod, pFile) + file, ok := vfsFileGet(ctx, mod, pFile).(*vfsFile) + if !ok { + return _NOTFOUND + } fi, err := file.Stat() if err != nil { return _IOERR_FSTAT diff --git a/internal/vfs/vfs_api.go b/internal/vfs/vfs_api.go index 01e3f88..06c0be1 100644 --- a/internal/vfs/vfs_api.go +++ b/internal/vfs/vfs_api.go @@ -8,12 +8,15 @@ import ( "github.com/tetratelabs/wazero/api" ) -func vfsAPIGet(mod api.Module, pVfs uint32) sqlite3vfs.VFS { - if pVfs == 0 { - return nil +func vfsAPIGet(mod api.Module, pVfs uint32) (vfs sqlite3vfs.VFS) { + if pVfs != 0 { + name := util.ReadString(mod, util.ReadUint32(mod, pVfs+16), _MAX_STRING) + vfs = sqlite3vfs.Find(name) } - name := util.ReadString(mod, util.ReadUint32(mod, pVfs+16), _MAX_STRING) - return sqlite3vfs.Find(name) + if vfs == nil { + vfs = vfsOS + } + return } func vfsAPIErrorCode(err error, def _ErrorCode) _ErrorCode { diff --git a/internal/vfs/vfs_file.go b/internal/vfs/vfs_file.go index a8510b6..07d1aba 100644 --- a/internal/vfs/vfs_file.go +++ b/internal/vfs/vfs_file.go @@ -2,13 +2,118 @@ package vfs import ( "context" + "errors" + "io" + "io/fs" "os" + "path/filepath" + "runtime" "time" "github.com/ncruces/go-sqlite3/internal/util" + "github.com/ncruces/go-sqlite3/sqlite3vfs" "github.com/tetratelabs/wazero/api" ) +const vfsOS vfsOSAPI = false + +type vfsOSAPI bool + +func (vfsOSAPI) FullPathname(path string) (string, error) { + path, err := filepath.Abs(path) + if err != nil { + return "", err + } + fi, err := os.Lstat(path) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return path, nil + } + return "", err + } + if fi.Mode()&fs.ModeSymlink != 0 { + err = _OK_SYMLINK + } + return path, err +} + +func (vfsOSAPI) Delete(path string, syncDir bool) error { + err := os.Remove(path) + if errors.Is(err, fs.ErrNotExist) { + return _IOERR_DELETE_NOENT + } + if err != nil { + return err + } + if runtime.GOOS != "windows" && syncDir { + f, err := os.Open(filepath.Dir(path)) + if err != nil { + return _OK + } + defer f.Close() + err = osSync(f, false, false) + if err != nil { + return _IOERR_DIR_FSYNC + } + } + return nil +} + +func (vfsOSAPI) Access(name string, flags sqlite3vfs.AccessFlag) (bool, error) { + err := osAccess(name, flags) + if flags == _ACCESS_EXISTS { + if errors.Is(err, fs.ErrNotExist) { + return false, nil + } + } else { + if errors.Is(err, fs.ErrPermission) { + return false, nil + } + } + return err == nil, err +} + +func (vfsOSAPI) Open(name string, flags sqlite3vfs.OpenFlag) (sqlite3vfs.File, sqlite3vfs.OpenFlag, error) { + var oflags int + if flags&_OPEN_EXCLUSIVE != 0 { + oflags |= os.O_EXCL + } + if flags&_OPEN_CREATE != 0 { + oflags |= os.O_CREATE + } + if flags&_OPEN_READONLY != 0 { + oflags |= os.O_RDONLY + } + if flags&_OPEN_READWRITE != 0 { + oflags |= os.O_RDWR + } + + var err error + var f *os.File + if name == "" { + f, err = os.CreateTemp("", "*.db") + } else { + f, err = osOpenFile(name, oflags, 0666) + } + if err != nil { + return nil, flags, err + } + + if flags&_OPEN_DELETEONCLOSE != 0 { + os.Remove(f.Name()) + } + + file := vfsFile{ + File: f, + psow: true, + readOnly: flags&_OPEN_READONLY != 0, + syncDir: runtime.GOOS != "windows" && + flags&(_OPEN_CREATE) != 0 && + flags&(_OPEN_MAIN_JOURNAL|_OPEN_SUPER_JOURNAL|_OPEN_WAL) != 0, + } + return &file, flags, nil +} + type vfsFile struct { *os.File lockTimeout time.Duration @@ -18,37 +123,66 @@ type vfsFile struct { readOnly bool } -func vfsFileNew(vfs *vfsState, file *os.File) uint32 { +func vfsFileNew(vfs *vfsState, file sqlite3vfs.File) uint32 { // Find an empty slot. for id, f := range vfs.files { - if f.File == nil { - vfs.files[id] = vfsFile{File: file} + if f == nil { + vfs.files[id] = file return uint32(id) } } // Add a new slot. - vfs.files = append(vfs.files, vfsFile{File: file}) + vfs.files = append(vfs.files, file) return uint32(len(vfs.files) - 1) } -func vfsFileGet(ctx context.Context, mod api.Module, pFile uint32) *vfsFile { - vfs := ctx.Value(vfsKey{}).(*vfsState) - id := util.ReadUint32(mod, pFile+4) - return &vfs.files[id] +func vfsFileRegister(ctx context.Context, mod api.Module, pFile uint32, file sqlite3vfs.File) { + id := vfsFileNew(ctx.Value(vfsKey{}).(*vfsState), file) + util.WriteUint32(mod, pFile+4, id) } -func vfsFileOpen(ctx context.Context, mod api.Module, pFile uint32, file *os.File) *vfsFile { +func vfsFileGet(ctx context.Context, mod api.Module, pFile uint32) sqlite3vfs.File { vfs := ctx.Value(vfsKey{}).(*vfsState) - id := vfsFileNew(vfs, file) - util.WriteUint32(mod, pFile+4, id) - return &vfs.files[id] + id := util.ReadUint32(mod, pFile+4) + return vfs.files[id] } func vfsFileClose(ctx context.Context, mod api.Module, pFile uint32) error { vfs := ctx.Value(vfsKey{}).(*vfsState) id := util.ReadUint32(mod, pFile+4) file := vfs.files[id] - vfs.files[id] = vfsFile{} + vfs.files[id] = nil return file.Close() } + +func (f *vfsFile) Sync(flags sqlite3vfs.SyncFlag) error { + dataonly := (flags & _SYNC_DATAONLY) != 0 + fullsync := (flags & 0x0f) == _SYNC_FULL + + err := osSync(f.File, fullsync, dataonly) + if err != nil { + return err + } + if runtime.GOOS != "windows" && f.syncDir { + f.syncDir = false + d, err := os.Open(filepath.Dir(f.File.Name())) + if err != nil { + return nil + } + defer d.Close() + err = osSync(d, false, false) + if err != nil { + return _IOERR_DIR_FSYNC + } + } + return nil +} + +func (f *vfsFile) FileSize() (int64, error) { + return f.Seek(0, io.SeekEnd) +} + +func (*vfsFile) SectorSize() int { + return _DEFAULT_SECTOR_SIZE +} diff --git a/internal/vfs/vfs_lock.go b/internal/vfs/vfs_lock.go index ad5df28..f5140cc 100644 --- a/internal/vfs/vfs_lock.go +++ b/internal/vfs/vfs_lock.go @@ -1,12 +1,11 @@ package vfs import ( - "context" "os" "time" "github.com/ncruces/go-sqlite3/internal/util" - "github.com/tetratelabs/wazero/api" + "github.com/ncruces/go-sqlite3/sqlite3vfs" ) const ( @@ -16,14 +15,12 @@ const ( _SHARED_SIZE = 510 ) -func vfsLock(ctx context.Context, mod api.Module, pFile uint32, eLock _LockLevel) _ErrorCode { +func (file *vfsFile) Lock(eLock sqlite3vfs.LockLevel) error { // Argument check. SQLite never explicitly requests a pending lock. if eLock != _LOCK_SHARED && eLock != _LOCK_RESERVED && eLock != _LOCK_EXCLUSIVE { panic(util.AssertErr()) } - file := vfsFileGet(ctx, mod, pFile) - switch { case file.lock < _LOCK_NONE || file.lock > _LOCK_EXCLUSIVE: // Connection state check. @@ -38,7 +35,7 @@ func vfsLock(ctx context.Context, mod api.Module, pFile uint32, eLock _LockLevel // If we already have an equal or more restrictive lock, do nothing. if file.lock >= eLock { - return _OK + return nil } // Do not allow any kind of write-lock on a read-only database. @@ -56,7 +53,7 @@ func vfsLock(ctx context.Context, mod api.Module, pFile uint32, eLock _LockLevel return rc } file.lock = _LOCK_SHARED - return _OK + return nil case _LOCK_RESERVED: // Must be SHARED to get RESERVED. @@ -67,7 +64,7 @@ func vfsLock(ctx context.Context, mod api.Module, pFile uint32, eLock _LockLevel return rc } file.lock = _LOCK_RESERVED - return _OK + return nil case _LOCK_EXCLUSIVE: // Must be SHARED, RESERVED or PENDING to get EXCLUSIVE. @@ -85,21 +82,19 @@ func vfsLock(ctx context.Context, mod api.Module, pFile uint32, eLock _LockLevel return rc } file.lock = _LOCK_EXCLUSIVE - return _OK + return nil default: panic(util.AssertErr()) } } -func vfsUnlock(ctx context.Context, mod api.Module, pFile uint32, eLock _LockLevel) _ErrorCode { +func (file *vfsFile) Unlock(eLock sqlite3vfs.LockLevel) error { // Argument check. if eLock != _LOCK_NONE && eLock != _LOCK_SHARED { panic(util.AssertErr()) } - file := vfsFileGet(ctx, mod, pFile) - // Connection state check. if file.lock < _LOCK_NONE || file.lock > _LOCK_EXCLUSIVE { panic(util.AssertErr()) @@ -107,7 +102,7 @@ func vfsUnlock(ctx context.Context, mod api.Module, pFile uint32, eLock _LockLev // If we don't have a more restrictive lock, do nothing. if file.lock <= eLock { - return _OK + return nil } switch eLock { @@ -116,7 +111,7 @@ func vfsUnlock(ctx context.Context, mod api.Module, pFile uint32, eLock _LockLev return rc } file.lock = _LOCK_SHARED - return _OK + return nil case _LOCK_NONE: rc := osReleaseLock(file.File, file.lock) @@ -128,28 +123,16 @@ func vfsUnlock(ctx context.Context, mod api.Module, pFile uint32, eLock _LockLev } } -func vfsCheckReservedLock(ctx context.Context, mod api.Module, pFile, pResOut uint32) _ErrorCode { - file := vfsFileGet(ctx, mod, pFile) - +func (file *vfsFile) CheckReservedLock() (bool, error) { // Connection state check. if file.lock < _LOCK_NONE || file.lock > _LOCK_EXCLUSIVE { panic(util.AssertErr()) } - var locked bool - var rc _ErrorCode if file.lock >= _LOCK_RESERVED { - locked = true - } else { - locked, rc = osCheckReservedLock(file.File) + return true, nil } - - var res uint32 - if locked { - res = 1 - } - util.WriteUint32(mod, pResOut, res) - return rc + return osCheckReservedLock(file.File) } func osGetReservedLock(file *os.File, timeout time.Duration) _ErrorCode { diff --git a/internal/vfs/vfs_lock_test.go b/internal/vfs/vfs_lock_test.go index ae0f0f6..b78ff78 100644 --- a/internal/vfs/vfs_lock_test.go +++ b/internal/vfs/vfs_lock_test.go @@ -43,8 +43,8 @@ func Test_vfsLock(t *testing.T) { ctx, vfs := Context(context.TODO()) defer vfs.Close() - vfsFileOpen(ctx, mod, pFile1, file1) - vfsFileOpen(ctx, mod, pFile2, file2) + vfsFileRegister(ctx, mod, pFile1, &vfsFile{File: file1}) + vfsFileRegister(ctx, mod, pFile2, &vfsFile{File: file2}) rc := vfsCheckReservedLock(ctx, mod, pFile1, pOutput) if rc != _OK { diff --git a/sqlite3vfs/vfs.go b/sqlite3vfs/vfs.go index 5fce853..80c1eb2 100644 --- a/sqlite3vfs/vfs.go +++ b/sqlite3vfs/vfs.go @@ -19,7 +19,7 @@ type File interface { Lock(lock LockLevel) error Unlock(lock LockLevel) error CheckReservedLock() (bool, error) - SectorSize() int64 + SectorSize() int } var ( diff --git a/sqlite3vfs/vfs_api_test.go b/sqlite3vfs/vfs_api_test.go index e38a7f1..97bc8f8 100644 --- a/sqlite3vfs/vfs_api_test.go +++ b/sqlite3vfs/vfs_api_test.go @@ -18,8 +18,8 @@ func (t testVFS) Open(name string, flags sqlite3vfs.OpenFlag) (sqlite3vfs.File, return nil, flags, nil } -func (t testVFS) Delete(name string, dirSync bool) error { - t.Log("Delete", name, dirSync) +func (t testVFS) Delete(name string, syncDir bool) error { + t.Log("Delete", name, syncDir) return nil } @@ -38,11 +38,11 @@ func TestRegister(t *testing.T) { sqlite3vfs.Register("foo", vfs) defer sqlite3vfs.Unregister("foo") - defer func() { _ = recover() }() - conn, err := sqlite3.Open("file:file.db?vfs=foo") if err != nil { t.Fatal(err) } defer conn.Close() + + t.Error("want skip") }