initial commit
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func TestOpenDatabaseReadsLiveWAL(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dbPath := filepath.Join(dir, "Cookies")
|
||||
|
||||
live, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
live.SetMaxOpenConns(1)
|
||||
defer live.Close()
|
||||
if _, err := live.Exec("PRAGMA journal_mode=WAL"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := live.Exec("CREATE TABLE cookies (host_key TEXT, value TEXT)"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := live.Exec("INSERT INTO cookies VALUES ('example.com', 'secret')"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
d, err := OpenDatabase(dbPath, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("OpenDatabase: %v", err)
|
||||
}
|
||||
defer d.Close()
|
||||
|
||||
var n int
|
||||
if err := d.QueryRow("SELECT COUNT(*) FROM cookies").Scan(&n); err != nil {
|
||||
t.Fatalf("query: %v", err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Fatalf("expected 1 cookie row, got %d", n)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"recovery/recovery/platform"
|
||||
|
||||
sqlite3 "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func OpenDatabase(dbPath string, pids []uint32) (*sql.DB, error) {
|
||||
cleanPath := dbPath
|
||||
if i := strings.IndexByte(dbPath, '?'); i >= 0 {
|
||||
cleanPath = dbPath[:i]
|
||||
}
|
||||
|
||||
hasWAL := false
|
||||
if wal, err := os.Stat(cleanPath + "-wal"); err == nil && wal.Size() > 0 {
|
||||
hasWAL = true
|
||||
}
|
||||
|
||||
if !hasWAL {
|
||||
uri := fmt.Sprintf("file:%s?mode=ro&nolock=1&immutable=1", dbPath)
|
||||
if db, err := sql.Open("sqlite3", uri); err == nil {
|
||||
if err := db.Ping(); err == nil {
|
||||
logf("opened %s via immutable snapshot", dbPath)
|
||||
return db, nil
|
||||
}
|
||||
db.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if snapshot, cloneErr := cloneSnapshot(cleanPath, pids); cloneErr == nil {
|
||||
logf("opened %s via cloned snapshot (%d bytes)", dbPath, len(snapshot))
|
||||
return OpenDatabaseFromBytes(snapshot)
|
||||
} else {
|
||||
logf("clone failed for %s: %v; falling back to direct read", dbPath, cloneErr)
|
||||
}
|
||||
|
||||
data, err := platform.ReadLockedFile(cleanPath, pids)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open %s: %w", dbPath, err)
|
||||
}
|
||||
logf("opened %s via injected ReadLockedFile (%d bytes)", dbPath, len(data))
|
||||
return OpenDatabaseFromBytes(data)
|
||||
}
|
||||
|
||||
func cloneSnapshot(dbPath string, pids []uint32) ([]byte, error) {
|
||||
tmp, err := os.MkdirTemp("", "kematian_db_*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
clonePath := filepath.Join(tmp, filepath.Base(dbPath))
|
||||
|
||||
mainData, err := platform.ReadLockedFile(dbPath, pids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.WriteFile(clonePath, mainData, 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, suffix := range []string{"-wal", "-journal"} {
|
||||
src := dbPath + suffix
|
||||
if info, err := os.Stat(src); err == nil && info.Size() > 0 {
|
||||
if data, err := platform.ReadLockedFile(src, pids); err == nil {
|
||||
if err := os.WriteFile(clonePath+suffix, data, 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
d, err := sql.Open("sqlite3", clonePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.SetMaxOpenConns(1)
|
||||
if _, err := d.Exec("PRAGMA journal_mode=DELETE"); err != nil {
|
||||
d.Close()
|
||||
return nil, err
|
||||
}
|
||||
d.Close()
|
||||
|
||||
return os.ReadFile(clonePath)
|
||||
}
|
||||
|
||||
func OpenDatabaseFromBytes(data []byte) (*sql.DB, error) {
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db.SetMaxOpenConns(1)
|
||||
|
||||
conn, err := db.Conn(context.Background())
|
||||
if err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = conn.Raw(func(driverConn interface{}) error {
|
||||
sqliteConn, ok := driverConn.(*sqlite3.SQLiteConn)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a sqlite3 connection")
|
||||
}
|
||||
return sqliteConn.Deserialize(data, "main")
|
||||
})
|
||||
conn.Close()
|
||||
|
||||
if err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("deserialize: %w", err)
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package db
|
||||
|
||||
import "log"
|
||||
|
||||
func logf(format string, args ...interface{}) {
|
||||
log.Printf("[db] "+format, args...)
|
||||
}
|
||||
Reference in New Issue
Block a user