mirror of
https://github.com/ncruces/go-sqlite3.git
synced 2026-01-19 09:04:16 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bc39c5b91 | ||
|
|
12193cedea | ||
|
|
71d95bf9d5 | ||
|
|
7e23100ff7 | ||
|
|
e32d8401fb | ||
|
|
503db60927 | ||
|
|
1227fa7a04 | ||
|
|
e455b5f729 | ||
|
|
2bb1c8c795 | ||
|
|
844fab4167 | ||
|
|
5ed4a6cb9d | ||
|
|
37f2145588 | ||
|
|
e17b3ef2c8 | ||
|
|
a75b8887db |
12
.github/workflows/repro.sh
vendored
12
.github/workflows/repro.sh
vendored
@@ -2,14 +2,14 @@
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$OSTYPE" == "linux"* ]]; then
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/wasi-sdk-24.0-x86_64-linux.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_120/binaryen-version_120-x86_64-linux.tar.gz"
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-linux.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_121/binaryen-version_121-x86_64-linux.tar.gz"
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/wasi-sdk-24.0-arm64-macos.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_120/binaryen-version_120-arm64-macos.tar.gz"
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-arm64-macos.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_121/binaryen-version_121-arm64-macos.tar.gz"
|
||||
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/wasi-sdk-24.0-x86_64-windows.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_120/binaryen-version_120-x86_64-windows.tar.gz"
|
||||
WASI_SDK="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-windows.tar.gz"
|
||||
BINARYEN="https://github.com/WebAssembly/binaryen/releases/download/version_121/binaryen-version_121-x86_64-windows.tar.gz"
|
||||
fi
|
||||
|
||||
# Download tools
|
||||
|
||||
2
.github/workflows/repro.yml
vendored
2
.github/workflows/repro.yml
vendored
@@ -18,8 +18,6 @@ jobs:
|
||||
steps:
|
||||
- uses: ilammy/msvc-dev-cmd@v1
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with: { go-version: stable }
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
|
||||
@@ -74,7 +74,7 @@ This project aims for [high test coverage](https://github.com/ncruces/go-sqlite3
|
||||
It also benefits greatly from [SQLite's](https://sqlite.org/testing.html) and
|
||||
[wazero's](https://tetrate.io/blog/introducing-wazero-from-tetrate/#:~:text=Rock%2Dsolid%20test%20approach) thorough testing.
|
||||
|
||||
Every commit is [tested](https://github.com/ncruces/go-sqlite3/wiki/Test-matrix) on
|
||||
Every commit is [tested](https://github.com/ncruces/go-sqlite3/wiki/Support-matrix) on
|
||||
Linux (amd64/arm64/386/riscv64/ppc64le/s390x), macOS (amd64/arm64),
|
||||
Windows (amd64), FreeBSD (amd64), OpenBSD (amd64), NetBSD (amd64),
|
||||
DragonFly BSD (amd64), illumos (amd64), and Solaris (amd64).
|
||||
|
||||
46
conn.go
46
conn.go
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -24,7 +25,6 @@ type Conn struct {
|
||||
interrupt context.Context
|
||||
pending *Stmt
|
||||
stmts []*Stmt
|
||||
timer *time.Timer
|
||||
busy func(context.Context, int) bool
|
||||
log func(xErrorCode, string)
|
||||
collation func(*Conn, string)
|
||||
@@ -36,7 +36,9 @@ type Conn struct {
|
||||
rollback func()
|
||||
arena arena
|
||||
|
||||
handle uint32
|
||||
busy1st time.Time
|
||||
busylst time.Time
|
||||
handle uint32
|
||||
}
|
||||
|
||||
// Open calls [OpenFlags] with [OPEN_READWRITE], [OPEN_CREATE] and [OPEN_URI].
|
||||
@@ -389,38 +391,20 @@ func (c *Conn) BusyTimeout(timeout time.Duration) error {
|
||||
}
|
||||
|
||||
func timeoutCallback(ctx context.Context, mod api.Module, count, tmout int32) (retry uint32) {
|
||||
// https://fractaledmind.github.io/2024/04/15/sqlite-on-rails-the-how-and-why-of-optimal-performance/
|
||||
if c, ok := ctx.Value(connKey{}).(*Conn); ok && c.interrupt.Err() == nil {
|
||||
const delays = "\x01\x02\x05\x0a\x0f\x14\x19\x19\x19\x32\x32\x64"
|
||||
const totals = "\x00\x01\x03\x08\x12\x21\x35\x4e\x67\x80\xb2\xe4"
|
||||
const ndelay = int32(len(delays) - 1)
|
||||
|
||||
var delay, prior int32
|
||||
if count <= ndelay {
|
||||
delay = int32(delays[count])
|
||||
prior = int32(totals[count])
|
||||
} else {
|
||||
delay = int32(delays[ndelay])
|
||||
prior = int32(totals[ndelay]) + delay*(count-ndelay)
|
||||
switch {
|
||||
case count == 0:
|
||||
c.busy1st = time.Now()
|
||||
case time.Since(c.busy1st) >= time.Duration(tmout)*time.Millisecond:
|
||||
return 0
|
||||
}
|
||||
|
||||
if delay = min(delay, tmout-prior); delay > 0 {
|
||||
delay := time.Duration(delay) * time.Millisecond
|
||||
if c.interrupt.Done() == nil {
|
||||
time.Sleep(delay)
|
||||
return 1
|
||||
}
|
||||
if c.timer == nil {
|
||||
c.timer = time.NewTimer(delay)
|
||||
} else {
|
||||
c.timer.Reset(delay)
|
||||
}
|
||||
select {
|
||||
case <-c.interrupt.Done():
|
||||
c.timer.Stop()
|
||||
case <-c.timer.C:
|
||||
return 1
|
||||
}
|
||||
if time.Since(c.busylst) < time.Millisecond {
|
||||
const sleepIncrement = 2*1024*1024 - 1 // power of two, ~2ms
|
||||
time.Sleep(time.Duration(rand.Int63() & sleepIncrement))
|
||||
}
|
||||
c.busylst = time.Now()
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -379,7 +379,7 @@ func (c *conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, e
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tail != "" {
|
||||
if notWhitespace(tail) {
|
||||
s.Close()
|
||||
return nil, util.TailErr
|
||||
}
|
||||
|
||||
@@ -225,8 +225,8 @@ func Test_Prepare(t *testing.T) {
|
||||
}
|
||||
|
||||
_, err = db.Prepare(`SELECT 1; `)
|
||||
if err.Error() != string(util.TailErr) {
|
||||
t.Error("want tailErr")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err = db.Prepare(`SELECT 1; SELECT`)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
|
||||
)
|
||||
|
||||
|
||||
@@ -27,12 +27,12 @@ func Fuzz_stringOrTime_1(f *testing.F) {
|
||||
// Make sure times round-trip to the same string:
|
||||
// https://pkg.go.dev/database/sql#Rows.Scan
|
||||
if v.Format(time.RFC3339Nano) != str {
|
||||
t.Fatalf("did not round-trip: %q", str)
|
||||
t.Errorf("did not round-trip: %q", str)
|
||||
}
|
||||
} else {
|
||||
date, err := time.Parse(time.RFC3339Nano, str)
|
||||
if err == nil && date.Format(time.RFC3339Nano) == str {
|
||||
t.Fatalf("would round-trip: %q", str)
|
||||
t.Errorf("would round-trip: %q", str)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
61
driver/whitespace.go
Normal file
61
driver/whitespace.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package driver
|
||||
|
||||
func notWhitespace(sql string) bool {
|
||||
const (
|
||||
code = iota
|
||||
slash
|
||||
minus
|
||||
ccomment
|
||||
sqlcomment
|
||||
endcomment
|
||||
)
|
||||
|
||||
state := code
|
||||
for _, b := range ([]byte)(sql) {
|
||||
if b == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
switch state {
|
||||
case code:
|
||||
switch b {
|
||||
case '/':
|
||||
state = slash
|
||||
case '-':
|
||||
state = minus
|
||||
case ' ', ';', '\t', '\n', '\v', '\f', '\r':
|
||||
continue
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case slash:
|
||||
if b != '*' {
|
||||
return true
|
||||
}
|
||||
state = ccomment
|
||||
case minus:
|
||||
if b != '-' {
|
||||
return true
|
||||
}
|
||||
state = sqlcomment
|
||||
case ccomment:
|
||||
if b == '*' {
|
||||
state = endcomment
|
||||
}
|
||||
case sqlcomment:
|
||||
if b == '\n' {
|
||||
state = code
|
||||
}
|
||||
case endcomment:
|
||||
switch b {
|
||||
case '/':
|
||||
state = code
|
||||
case '*':
|
||||
state = endcomment
|
||||
default:
|
||||
state = ccomment
|
||||
}
|
||||
}
|
||||
}
|
||||
return state == slash || state == minus
|
||||
}
|
||||
73
driver/whitespace_test.go
Normal file
73
driver/whitespace_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
_ "github.com/ncruces/go-sqlite3/internal/testcfg"
|
||||
)
|
||||
|
||||
func Fuzz_notWhitespace(f *testing.F) {
|
||||
f.Add("")
|
||||
f.Add(" ")
|
||||
f.Add(";")
|
||||
f.Add("0")
|
||||
f.Add("-")
|
||||
f.Add("-0")
|
||||
f.Add("--")
|
||||
f.Add("--0")
|
||||
f.Add("--\n")
|
||||
f.Add("--0\n")
|
||||
f.Add("/0")
|
||||
f.Add("/*")
|
||||
f.Add("/*/")
|
||||
f.Add("/**")
|
||||
f.Add("/*0")
|
||||
f.Add("/**/")
|
||||
f.Add("/***/")
|
||||
f.Add("/**0/")
|
||||
f.Add("\v")
|
||||
f.Add(" \v")
|
||||
f.Add("\xf0")
|
||||
f.Add("\000")
|
||||
|
||||
db, err := Open(":memory:")
|
||||
if err != nil {
|
||||
f.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
f.Fuzz(func(t *testing.T, str string) {
|
||||
if len(str) > 128 {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
c, err := db.Conn(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
c.Raw(func(driverConn any) error {
|
||||
conn := driverConn.(*conn).Conn
|
||||
stmt, tail, err := conn.Prepare(str)
|
||||
stmt.Close()
|
||||
|
||||
// It's hard to be bug for bug compatible with SQLite.
|
||||
// We settle for somewhat less:
|
||||
// - if SQLite reports whitespace, we must too
|
||||
// - if we report whitespace, SQLite must not parse a statement
|
||||
if notWhitespace(str) {
|
||||
if stmt == nil && tail == "" && err == nil {
|
||||
t.Errorf("was whitespace: %q", str)
|
||||
}
|
||||
} else {
|
||||
if stmt != nil {
|
||||
t.Errorf("was not whitespace: %q (%v)", str, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
Binary file not shown.
@@ -32,6 +32,11 @@ func Test_bcw2(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`DELETE FROM test LIMIT 1`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -13,15 +13,14 @@ mkdir -p build/ext/
|
||||
cp "$ROOT"/sqlite3/*.[ch] build/
|
||||
cp "$ROOT"/sqlite3/*.patch build/
|
||||
|
||||
# https://sqlite.org/src/info/08cfa7e8b3090151
|
||||
curl -# https://sqlite.org/src/tarball/sqlite.tar.gz?r=08cfa7e8 | tar xz
|
||||
# https://sqlite.org/src/info/ec5d7025cba9f4ac
|
||||
curl -# https://sqlite.org/src/tarball/sqlite.tar.gz?r=ec5d7025 | tar xz
|
||||
|
||||
cd sqlite
|
||||
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then
|
||||
MSYS_NO_PATHCONV=1 nmake /f makefile.msc sqlite3.c
|
||||
MSYS_NO_PATHCONV=1 nmake /f makefile.msc sqlite3.c OPTS=-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT
|
||||
else
|
||||
sh configure
|
||||
make sqlite3.c
|
||||
sh configure --enable-update-limit && make sqlite3.c
|
||||
fi
|
||||
cd ~-
|
||||
|
||||
@@ -54,6 +53,7 @@ cd ~-
|
||||
-Wl,--import-undefined \
|
||||
-Wl,--initial-memory=327680 \
|
||||
-D_HAVE_SQLITE_CONFIG_H \
|
||||
-DSQLITE_ENABLE_UPDATE_DELETE_LIMIT \
|
||||
-DSQLITE_CUSTOM_INCLUDE=sqlite_opt.h \
|
||||
$(awk '{print "-Wl,--export="$0}' ../exports.txt)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ go 1.21
|
||||
|
||||
toolchain go1.23.0
|
||||
|
||||
require github.com/ncruces/go-sqlite3 v0.20.4-0.20241205130447-8252198dd21c
|
||||
require github.com/ncruces/go-sqlite3 v0.21.0
|
||||
|
||||
require (
|
||||
github.com/ncruces/julianday v1.0.0 // indirect
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
github.com/ncruces/go-sqlite3 v0.20.4-0.20241205130447-8252198dd21c h1:VDpb7CDIfkEUtPsHgYQwygxST9VocLk8y0fDMQYGPzI=
|
||||
github.com/ncruces/go-sqlite3 v0.20.4-0.20241205130447-8252198dd21c/go.mod h1:2+txOGfyB5f8fUNOjmos2TuHYA8Xf/oAzI++lHwbHno=
|
||||
github.com/ncruces/go-sqlite3 v0.21.0 h1:EwKFoy1hHEopN4sFZarmi+McXdbCcbTuLixhEayXVbQ=
|
||||
github.com/ncruces/go-sqlite3 v0.21.0/go.mod h1:zxMOaSG5kFYVFK4xQa0pdwIszqxqJ0W0BxBgwdrNjuA=
|
||||
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
||||
github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4=
|
||||
|
||||
Binary file not shown.
@@ -1,3 +1,4 @@
|
||||
github.com/ncruces/go-sqlite3 v0.21.0/go.mod h1:zxMOaSG5kFYVFK4xQa0pdwIszqxqJ0W0BxBgwdrNjuA=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
|
||||
@@ -5,7 +5,7 @@ go 1.21
|
||||
toolchain go1.23.0
|
||||
|
||||
require (
|
||||
github.com/ncruces/go-sqlite3 v0.20.3
|
||||
github.com/ncruces/go-sqlite3 v0.21.0
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/ncruces/go-sqlite3 v0.20.3 h1:+4G4uEqOeusF0yRuQVUl9fuoEebUolwQSnBUjYBLYIw=
|
||||
github.com/ncruces/go-sqlite3 v0.20.3/go.mod h1:ojLIAB243gtz68Eo283Ps+k9PyR3dvzS+9/RgId4+AA=
|
||||
github.com/ncruces/go-sqlite3 v0.21.0 h1:EwKFoy1hHEopN4sFZarmi+McXdbCcbTuLixhEayXVbQ=
|
||||
github.com/ncruces/go-sqlite3 v0.21.0/go.mod h1:zxMOaSG5kFYVFK4xQa0pdwIszqxqJ0W0BxBgwdrNjuA=
|
||||
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
||||
github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4=
|
||||
|
||||
29
internal/dotlk/dotlk.go
Normal file
29
internal/dotlk/dotlk.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package dotlk
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
)
|
||||
|
||||
// LockShm creates a directory on disk to prevent SQLite
|
||||
// from using this path for a shared memory file.
|
||||
func LockShm(name string) error {
|
||||
err := os.Mkdir(name, 0777)
|
||||
if errors.Is(err, fs.ErrExist) {
|
||||
s, err := os.Lstat(name)
|
||||
if err == nil && s.IsDir() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Unlock removes the lock or shared memory file.
|
||||
func Unlock(name string) error {
|
||||
err := os.Remove(name)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
13
internal/dotlk/dotlk_other.go
Normal file
13
internal/dotlk/dotlk_other.go
Normal file
@@ -0,0 +1,13 @@
|
||||
//go:build !unix
|
||||
|
||||
package dotlk
|
||||
|
||||
import "os"
|
||||
|
||||
// TryLock returns nil if it acquired the lock,
|
||||
// fs.ErrExist if another process has the lock.
|
||||
func TryLock(name string) error {
|
||||
f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
50
internal/dotlk/dotlk_unix.go
Normal file
50
internal/dotlk/dotlk_unix.go
Normal file
@@ -0,0 +1,50 @@
|
||||
//go:build unix
|
||||
|
||||
package dotlk
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// TryLock returns nil if it acquired the lock,
|
||||
// fs.ErrExist if another process has the lock.
|
||||
func TryLock(name string) error {
|
||||
for retry := true; retry; retry = false {
|
||||
f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
|
||||
if err == nil {
|
||||
f.WriteString(strconv.Itoa(os.Getpid()))
|
||||
f.Close()
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, fs.ErrExist) {
|
||||
return err
|
||||
}
|
||||
if !removeStale(name) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return fs.ErrExist
|
||||
}
|
||||
|
||||
func removeStale(name string) bool {
|
||||
buf, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
return errors.Is(err, fs.ErrNotExist)
|
||||
}
|
||||
|
||||
pid, err := strconv.Atoi(string(buf))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if unix.Kill(pid, 0) == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
err = os.Remove(name)
|
||||
return err == nil || errors.Is(err, fs.ErrNotExist)
|
||||
}
|
||||
Binary file not shown.
@@ -48,11 +48,6 @@ On Unix, this package may use `mmap` to implement
|
||||
[shared-memory for the WAL-index](https://sqlite.org/wal.html#implementation_of_shared_memory_for_the_wal_index),
|
||||
like SQLite.
|
||||
|
||||
With [BSD locks](https://man.freebsd.org/cgi/man.cgi?query=flock&sektion=2)
|
||||
a WAL database can only be accessed by a single proccess.
|
||||
Other processes that attempt to access a database locked with BSD locks,
|
||||
will fail with the [`SQLITE_PROTOCOL`](https://sqlite.org/rescode.html#protocol) error code.
|
||||
|
||||
On Windows, this package may use `MapViewOfFile`, like SQLite.
|
||||
|
||||
You can also opt into a cross-platform, in-process, memory sharing implementation
|
||||
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
)
|
||||
|
||||
func osGetSharedLock(file *os.File) _ErrorCode {
|
||||
return osLock(file, unix.LOCK_SH|unix.LOCK_NB, _IOERR_RDLOCK)
|
||||
return osFlock(file, unix.LOCK_SH|unix.LOCK_NB, _IOERR_RDLOCK)
|
||||
}
|
||||
|
||||
func osGetReservedLock(file *os.File) _ErrorCode {
|
||||
rc := osLock(file, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK)
|
||||
rc := osFlock(file, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK)
|
||||
if rc == _BUSY {
|
||||
// The documentation states that a lock is upgraded by
|
||||
// releasing the previous lock, then acquiring the new lock.
|
||||
@@ -37,7 +37,7 @@ func osGetExclusiveLock(file *os.File, state *LockLevel) _ErrorCode {
|
||||
}
|
||||
|
||||
func osDowngradeLock(file *os.File, _ LockLevel) _ErrorCode {
|
||||
rc := osLock(file, unix.LOCK_SH|unix.LOCK_NB, _IOERR_RDLOCK)
|
||||
rc := osFlock(file, unix.LOCK_SH|unix.LOCK_NB, _IOERR_RDLOCK)
|
||||
if rc == _BUSY {
|
||||
// The documentation states that a lock is downgraded by
|
||||
// releasing the previous lock then acquiring the new lock.
|
||||
@@ -66,7 +66,36 @@ func osCheckReservedLock(file *os.File) (bool, _ErrorCode) {
|
||||
return lock == unix.F_WRLCK, rc
|
||||
}
|
||||
|
||||
func osLock(file *os.File, how int, def _ErrorCode) _ErrorCode {
|
||||
func osFlock(file *os.File, how int, def _ErrorCode) _ErrorCode {
|
||||
err := unix.Flock(int(file.Fd()), how)
|
||||
return osLockErrorCode(err, def)
|
||||
}
|
||||
|
||||
func osReadLock(file *os.File, start, len int64) _ErrorCode {
|
||||
return osLock(file, unix.F_RDLCK, start, len, _IOERR_RDLOCK)
|
||||
}
|
||||
|
||||
func osWriteLock(file *os.File, start, len int64) _ErrorCode {
|
||||
return osLock(file, unix.F_WRLCK, start, len, _IOERR_LOCK)
|
||||
}
|
||||
|
||||
func osLock(file *os.File, typ int16, start, len int64, def _ErrorCode) _ErrorCode {
|
||||
err := unix.FcntlFlock(file.Fd(), unix.F_SETLK, &unix.Flock_t{
|
||||
Type: typ,
|
||||
Start: start,
|
||||
Len: len,
|
||||
})
|
||||
return osLockErrorCode(err, def)
|
||||
}
|
||||
|
||||
func osUnlock(file *os.File, start, len int64) _ErrorCode {
|
||||
err := unix.FcntlFlock(file.Fd(), unix.F_SETLK, &unix.Flock_t{
|
||||
Type: unix.F_UNLCK,
|
||||
Start: start,
|
||||
Len: len,
|
||||
})
|
||||
if err != nil {
|
||||
return _IOERR_UNLOCK
|
||||
}
|
||||
return _OK
|
||||
}
|
||||
|
||||
@@ -56,16 +56,12 @@ func osAllocate(file *os.File, size int64) error {
|
||||
return file.Truncate(size)
|
||||
}
|
||||
|
||||
func osUnlock(file *os.File, start, len int64) _ErrorCode {
|
||||
err := unix.FcntlFlock(file.Fd(), _F_OFD_SETLK, &unix.Flock_t{
|
||||
Type: unix.F_UNLCK,
|
||||
Start: start,
|
||||
Len: len,
|
||||
})
|
||||
if err != nil {
|
||||
return _IOERR_UNLOCK
|
||||
}
|
||||
return _OK
|
||||
func osReadLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode {
|
||||
return osLock(file, unix.F_RDLCK, start, len, timeout, _IOERR_RDLOCK)
|
||||
}
|
||||
|
||||
func osWriteLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode {
|
||||
return osLock(file, unix.F_WRLCK, start, len, timeout, _IOERR_LOCK)
|
||||
}
|
||||
|
||||
func osLock(file *os.File, typ int16, start, len int64, timeout time.Duration, def _ErrorCode) _ErrorCode {
|
||||
@@ -88,10 +84,14 @@ func osLock(file *os.File, typ int16, start, len int64, timeout time.Duration, d
|
||||
return osLockErrorCode(err, def)
|
||||
}
|
||||
|
||||
func osReadLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode {
|
||||
return osLock(file, unix.F_RDLCK, start, len, timeout, _IOERR_RDLOCK)
|
||||
}
|
||||
|
||||
func osWriteLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode {
|
||||
return osLock(file, unix.F_WRLCK, start, len, timeout, _IOERR_LOCK)
|
||||
func osUnlock(file *os.File, start, len int64) _ErrorCode {
|
||||
err := unix.FcntlFlock(file.Fd(), _F_OFD_SETLK, &unix.Flock_t{
|
||||
Type: unix.F_UNLCK,
|
||||
Start: start,
|
||||
Len: len,
|
||||
})
|
||||
if err != nil {
|
||||
return _IOERR_UNLOCK
|
||||
}
|
||||
return _OK
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/dotlk"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -28,12 +30,10 @@ func osGetSharedLock(file *os.File) _ErrorCode {
|
||||
name := file.Name()
|
||||
locker := vfsDotLocks[name]
|
||||
if locker == nil {
|
||||
f, err := os.OpenFile(name+".lock", os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
|
||||
f.Close()
|
||||
if errors.Is(err, fs.ErrExist) {
|
||||
return _BUSY // Another process has the lock.
|
||||
}
|
||||
if err != nil {
|
||||
if err := dotlk.TryLock(name + ".lock"); err != nil {
|
||||
if errors.Is(err, fs.ErrExist) {
|
||||
return _BUSY // Another process has the lock.
|
||||
}
|
||||
return _IOERR_LOCK
|
||||
}
|
||||
locker = &vfsDotLocker{}
|
||||
@@ -114,8 +114,7 @@ func osReleaseLock(file *os.File, state LockLevel) _ErrorCode {
|
||||
}
|
||||
|
||||
if locker.shared == 1 {
|
||||
err := os.Remove(name + ".lock")
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
if err := dotlk.Unlock(name + ".lock"); err != nil {
|
||||
return _IOERR_UNLOCK
|
||||
}
|
||||
delete(vfsDotLocks, name)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
package vfs
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
@@ -22,6 +21,30 @@ func osAllocate(file *os.File, size int64) error {
|
||||
return unix.Fallocate(int(file.Fd()), 0, 0, size)
|
||||
}
|
||||
|
||||
func osReadLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode {
|
||||
return osLock(file, unix.F_RDLCK, start, len, timeout, _IOERR_RDLOCK)
|
||||
}
|
||||
|
||||
func osWriteLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode {
|
||||
return osLock(file, unix.F_WRLCK, start, len, timeout, _IOERR_LOCK)
|
||||
}
|
||||
|
||||
func osLock(file *os.File, typ int16, start, len int64, timeout time.Duration, def _ErrorCode) _ErrorCode {
|
||||
lock := unix.Flock_t{
|
||||
Type: typ,
|
||||
Start: start,
|
||||
Len: len,
|
||||
}
|
||||
var err error
|
||||
switch {
|
||||
case timeout < 0:
|
||||
err = unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLKW, &lock)
|
||||
default:
|
||||
err = unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLK, &lock)
|
||||
}
|
||||
return osLockErrorCode(err, def)
|
||||
}
|
||||
|
||||
func osUnlock(file *os.File, start, len int64) _ErrorCode {
|
||||
err := unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLK, &unix.Flock_t{
|
||||
Type: unix.F_UNLCK,
|
||||
@@ -33,40 +56,3 @@ func osUnlock(file *os.File, start, len int64) _ErrorCode {
|
||||
}
|
||||
return _OK
|
||||
}
|
||||
|
||||
func osLock(file *os.File, typ int16, start, len int64, timeout time.Duration, def _ErrorCode) _ErrorCode {
|
||||
lock := unix.Flock_t{
|
||||
Type: typ,
|
||||
Start: start,
|
||||
Len: len,
|
||||
}
|
||||
var err error
|
||||
switch {
|
||||
case timeout == 0:
|
||||
err = unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLK, &lock)
|
||||
case timeout < 0:
|
||||
err = unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLKW, &lock)
|
||||
default:
|
||||
before := time.Now()
|
||||
for {
|
||||
err = unix.FcntlFlock(file.Fd(), unix.F_OFD_SETLK, &lock)
|
||||
if errno, _ := err.(unix.Errno); errno != unix.EAGAIN {
|
||||
break
|
||||
}
|
||||
if time.Since(before) > timeout {
|
||||
break
|
||||
}
|
||||
const sleepIncrement = 1024*1024 - 1 // power of two, ~1ms
|
||||
time.Sleep(time.Duration(rand.Int63() & sleepIncrement))
|
||||
}
|
||||
}
|
||||
return osLockErrorCode(err, def)
|
||||
}
|
||||
|
||||
func osReadLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode {
|
||||
return osLock(file, unix.F_RDLCK, start, len, timeout, _IOERR_RDLOCK)
|
||||
}
|
||||
|
||||
func osWriteLock(file *os.File, start, len int64, timeout time.Duration) _ErrorCode {
|
||||
return osLock(file, unix.F_WRLCK, start, len, timeout, _IOERR_LOCK)
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ func osGetExclusiveLock(file *os.File, state *LockLevel) _ErrorCode {
|
||||
osUnlock(file, _SHARED_FIRST, _SHARED_SIZE)
|
||||
|
||||
// Acquire the EXCLUSIVE lock.
|
||||
// Can't wait here, because the file is not OVERLAPPED.
|
||||
rc := osWriteLock(file, _SHARED_FIRST, _SHARED_SIZE, 0)
|
||||
|
||||
if rc != _OK {
|
||||
@@ -106,6 +107,27 @@ func osCheckReservedLock(file *os.File) (bool, _ErrorCode) {
|
||||
return false, rc
|
||||
}
|
||||
|
||||
func osReadLock(file *os.File, start, len uint32, timeout time.Duration) _ErrorCode {
|
||||
return osLock(file, 0, start, len, timeout, _IOERR_RDLOCK)
|
||||
}
|
||||
|
||||
func osWriteLock(file *os.File, start, len uint32, timeout time.Duration) _ErrorCode {
|
||||
return osLock(file, windows.LOCKFILE_EXCLUSIVE_LOCK, start, len, timeout, _IOERR_LOCK)
|
||||
}
|
||||
|
||||
func osLock(file *os.File, flags, start, len uint32, timeout time.Duration, def _ErrorCode) _ErrorCode {
|
||||
var err error
|
||||
switch {
|
||||
case timeout == 0:
|
||||
err = osLockEx(file, flags|windows.LOCKFILE_FAIL_IMMEDIATELY, start, len)
|
||||
case timeout < 0:
|
||||
err = osLockEx(file, flags, start, len)
|
||||
default:
|
||||
err = osLockExTimeout(file, flags, start, len, timeout)
|
||||
}
|
||||
return osLockErrorCode(err, def)
|
||||
}
|
||||
|
||||
func osUnlock(file *os.File, start, len uint32) _ErrorCode {
|
||||
err := windows.UnlockFileEx(windows.Handle(file.Fd()),
|
||||
0, len, 0, &windows.Overlapped{Offset: start})
|
||||
@@ -118,52 +140,40 @@ func osUnlock(file *os.File, start, len uint32) _ErrorCode {
|
||||
return _OK
|
||||
}
|
||||
|
||||
func osLock(file *os.File, flags, start, len uint32, timeout time.Duration, def _ErrorCode) _ErrorCode {
|
||||
var err error
|
||||
switch {
|
||||
case timeout == 0:
|
||||
err = osLockEx(file, flags|windows.LOCKFILE_FAIL_IMMEDIATELY, start, len, 0)
|
||||
case timeout < 0:
|
||||
err = osLockEx(file, flags, start, len, 0)
|
||||
default:
|
||||
var event windows.Handle
|
||||
event, err = windows.CreateEvent(nil, 1, 0, nil)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
defer windows.CloseHandle(event)
|
||||
|
||||
err = osLockEx(file, flags, start, len, event)
|
||||
if err == windows.ERROR_IO_PENDING {
|
||||
rc, serr := windows.WaitForSingleObject(event, uint32(timeout/time.Millisecond))
|
||||
if rc == windows.WAIT_OBJECT_0 {
|
||||
return _OK
|
||||
}
|
||||
if serr != nil {
|
||||
err = serr
|
||||
} else {
|
||||
err = windows.Errno(rc)
|
||||
}
|
||||
windows.CancelIo(windows.Handle(file.Fd()))
|
||||
}
|
||||
}
|
||||
return osLockErrorCode(err, def)
|
||||
}
|
||||
|
||||
func osLockEx(file *os.File, flags, start, len uint32, event windows.Handle) error {
|
||||
func osLockEx(file *os.File, flags, start, len uint32) error {
|
||||
return windows.LockFileEx(windows.Handle(file.Fd()), flags,
|
||||
0, len, 0, &windows.Overlapped{
|
||||
Offset: start,
|
||||
HEvent: event,
|
||||
})
|
||||
0, len, 0, &windows.Overlapped{Offset: start})
|
||||
}
|
||||
|
||||
func osReadLock(file *os.File, start, len uint32, timeout time.Duration) _ErrorCode {
|
||||
return osLock(file, 0, start, len, timeout, _IOERR_RDLOCK)
|
||||
}
|
||||
func osLockExTimeout(file *os.File, flags, start, len uint32, timeout time.Duration) error {
|
||||
event, err := windows.CreateEvent(nil, 1, 0, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer windows.CloseHandle(event)
|
||||
|
||||
func osWriteLock(file *os.File, start, len uint32, timeout time.Duration) _ErrorCode {
|
||||
return osLock(file, windows.LOCKFILE_EXCLUSIVE_LOCK, start, len, timeout, _IOERR_LOCK)
|
||||
fd := windows.Handle(file.Fd())
|
||||
overlapped := &windows.Overlapped{
|
||||
Offset: start,
|
||||
HEvent: event,
|
||||
}
|
||||
|
||||
err = windows.LockFileEx(fd, flags, 0, len, 0, overlapped)
|
||||
if err != windows.ERROR_IO_PENDING {
|
||||
return err
|
||||
}
|
||||
|
||||
ms := (timeout + time.Millisecond - 1) / time.Millisecond
|
||||
rc, err := windows.WaitForSingleObject(event, uint32(ms))
|
||||
if rc == windows.WAIT_OBJECT_0 {
|
||||
return nil
|
||||
}
|
||||
defer windows.CancelIoEx(fd, overlapped)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return windows.Errno(rc)
|
||||
}
|
||||
|
||||
func osLockErrorCode(err error, def _ErrorCode) _ErrorCode {
|
||||
@@ -175,8 +185,8 @@ func osLockErrorCode(err error, def _ErrorCode) _ErrorCode {
|
||||
switch errno {
|
||||
case
|
||||
windows.ERROR_LOCK_VIOLATION,
|
||||
windows.ERROR_IO_PENDING,
|
||||
windows.ERROR_OPERATION_ABORTED,
|
||||
windows.ERROR_IO_PENDING,
|
||||
windows.WAIT_TIMEOUT:
|
||||
return _BUSY
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ package vfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
@@ -71,23 +73,21 @@ func (s *vfsShm) shmOpen() _ErrorCode {
|
||||
return _OK
|
||||
}
|
||||
|
||||
// Always open file read-write, as it will be shared.
|
||||
f, err := os.OpenFile(s.path,
|
||||
os.O_RDWR|os.O_CREATE|_O_NOFOLLOW, 0666)
|
||||
if err != nil {
|
||||
return _CANTOPEN
|
||||
}
|
||||
// Closes file if it's not nil.
|
||||
var f *os.File
|
||||
// Close file on error.
|
||||
// Keep this here to avoid confusing checklocks.
|
||||
defer func() { f.Close() }()
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return _IOERR_FSTAT
|
||||
}
|
||||
|
||||
vfsShmListMtx.Lock()
|
||||
defer vfsShmListMtx.Unlock()
|
||||
|
||||
// Stat file without opening it.
|
||||
// Closing it would release all POSIX locks on it.
|
||||
fi, err := os.Stat(s.path)
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return _IOERR_FSTAT
|
||||
}
|
||||
|
||||
// Find a shared file, increase the reference count.
|
||||
for _, g := range vfsShmList {
|
||||
if g != nil && os.SameFile(fi, g.info) {
|
||||
@@ -97,13 +97,33 @@ func (s *vfsShm) shmOpen() _ErrorCode {
|
||||
}
|
||||
}
|
||||
|
||||
// Lock and truncate the file.
|
||||
// The lock is only released by closing the file.
|
||||
if rc := osLock(f, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK); rc != _OK {
|
||||
// Always open file read-write, as it will be shared.
|
||||
f, err = os.OpenFile(s.path,
|
||||
os.O_RDWR|os.O_CREATE|_O_NOFOLLOW, 0666)
|
||||
if err != nil {
|
||||
return _CANTOPEN
|
||||
}
|
||||
|
||||
// Dead man's switch.
|
||||
if lock, rc := osTestLock(f, _SHM_DMS, 1); rc != _OK {
|
||||
return _IOERR_LOCK
|
||||
} else if lock == unix.F_WRLCK {
|
||||
return _BUSY
|
||||
} else if lock == unix.F_UNLCK {
|
||||
if rc := osWriteLock(f, _SHM_DMS, 1); rc != _OK {
|
||||
return rc
|
||||
}
|
||||
if err := f.Truncate(0); err != nil {
|
||||
return _IOERR_SHMOPEN
|
||||
}
|
||||
}
|
||||
if rc := osReadLock(f, _SHM_DMS, 1); rc != _OK {
|
||||
return rc
|
||||
}
|
||||
if err := f.Truncate(0); err != nil {
|
||||
return _IOERR_SHMOPEN
|
||||
|
||||
fi, err = f.Stat()
|
||||
if err != nil {
|
||||
return _IOERR_FSTAT
|
||||
}
|
||||
|
||||
// Add the new shared file.
|
||||
@@ -157,7 +177,30 @@ func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, ext
|
||||
func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) _ErrorCode {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
return s.shmMemLock(offset, n, flags)
|
||||
|
||||
// Check if we could obtain/release the lock locally.
|
||||
rc := s.shmMemLock(offset, n, flags)
|
||||
if rc != _OK {
|
||||
return rc
|
||||
}
|
||||
|
||||
// Obtain/release the appropriate file lock.
|
||||
switch {
|
||||
case flags&_SHM_UNLOCK != 0:
|
||||
return osUnlock(s.File, _SHM_BASE+int64(offset), int64(n))
|
||||
case flags&_SHM_SHARED != 0:
|
||||
rc = osReadLock(s.File, _SHM_BASE+int64(offset), int64(n))
|
||||
case flags&_SHM_EXCLUSIVE != 0:
|
||||
rc = osWriteLock(s.File, _SHM_BASE+int64(offset), int64(n))
|
||||
default:
|
||||
panic(util.AssertErr())
|
||||
}
|
||||
|
||||
// Release the local lock.
|
||||
if rc != _OK {
|
||||
s.shmMemLock(offset, n, flags^(_SHM_UNLOCK|_SHM_LOCK))
|
||||
}
|
||||
return rc
|
||||
}
|
||||
|
||||
func (s *vfsShm) shmUnmap(delete bool) {
|
||||
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
|
||||
"github.com/ncruces/go-sqlite3/internal/dotlk"
|
||||
"github.com/ncruces/go-sqlite3/internal/util"
|
||||
)
|
||||
|
||||
@@ -58,8 +58,7 @@ func (s *vfsShm) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := os.Remove(s.path)
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
if err := dotlk.Unlock(s.path); err != nil {
|
||||
return _IOERR_UNLOCK
|
||||
}
|
||||
delete(vfsShmList, s.path)
|
||||
@@ -82,9 +81,8 @@ func (s *vfsShm) shmOpen() _ErrorCode {
|
||||
return _OK
|
||||
}
|
||||
|
||||
// Create a directory on disk to ensure only this process
|
||||
// uses this path to register a shared memory.
|
||||
err := os.Mkdir(s.path, 0777)
|
||||
// Dead man's switch.
|
||||
err := dotlk.LockShm(s.path)
|
||||
if errors.Is(err, fs.ErrExist) {
|
||||
return _BUSY
|
||||
}
|
||||
|
||||
4
vfs/tests/mptest/testdata/mptest.wasm.bz2
vendored
4
vfs/tests/mptest/testdata/mptest.wasm.bz2
vendored
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:da52d62d7ccd9d203eaa6d281a8a61786f99b772b6de3c31c633e6a480e80df9
|
||||
size 480698
|
||||
oid sha256:e20f37d94223a88d8f94b3a20177c0fbf53392df2f9c59a28cc7f1f2b5d3de81
|
||||
size 477370
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1b6dfeb087f4463d8b2e5904721541908436bdcc1f1acf2bf5674bd697e2960f
|
||||
size 493536
|
||||
oid sha256:eebe395695c739a24e9cded13553b97d232eb268a5bc36f10f27cc13945e78cd
|
||||
size 491003
|
||||
|
||||
Reference in New Issue
Block a user