mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-13 23:04:04 -04:00
@@ -47,7 +47,15 @@ func Store(data []byte, mimeType string) error {
|
||||
return StoreWithConfig(data, mimeType, DefaultStoreConfig())
|
||||
}
|
||||
|
||||
func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
|
||||
func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) (err error) {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("clipboard db panic: %v", r)
|
||||
}()
|
||||
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -113,7 +113,61 @@ func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error)
|
||||
}
|
||||
|
||||
func openDB(path string) (*bolt.DB, error) {
|
||||
db, err := bolt.Open(path, 0o644, &bolt.Options{
|
||||
db, err := tryOpenDB(path)
|
||||
if err == nil {
|
||||
return db, nil
|
||||
}
|
||||
|
||||
log.Errorf("Clipboard db corrupted, salvaging readable entries: %v", err)
|
||||
|
||||
salvagePath := path + ".salvage"
|
||||
os.Remove(salvagePath)
|
||||
// chunked commits keep entries copied before the first unreadable page
|
||||
if salvageErr := compactInto(path, salvagePath, 64<<10); salvageErr != nil {
|
||||
log.Warnf("Clipboard db salvage stopped early: %v", salvageErr)
|
||||
}
|
||||
|
||||
corruptPath := path + ".corrupt"
|
||||
os.Remove(corruptPath)
|
||||
if renameErr := os.Rename(path, corruptPath); renameErr != nil {
|
||||
os.Remove(salvagePath)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
os.Rename(salvagePath, path)
|
||||
|
||||
db, err = tryOpenDB(path)
|
||||
if err == nil {
|
||||
return db, nil
|
||||
}
|
||||
|
||||
os.Remove(path)
|
||||
return tryOpenDB(path)
|
||||
}
|
||||
|
||||
// bbolt reports corrupted pages by panicking (internal/common/verify.go), not
|
||||
// by returning errors, so every db touchpoint recovers and converts to error.
|
||||
func recoverDBPanic(err *error) {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
*err = fmt.Errorf("clipboard db panic: %v", r)
|
||||
}
|
||||
|
||||
func tryOpenDB(path string) (db *bolt.DB, err error) {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
if db != nil {
|
||||
db.Close()
|
||||
}
|
||||
db, err = nil, fmt.Errorf("clipboard db panic: %v", r)
|
||||
}()
|
||||
|
||||
db, err = bolt.Open(path, 0o644, &bolt.Options{
|
||||
Timeout: 1 * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -129,9 +183,31 @@ func openDB(path string) (*bolt.DB, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.ForEach(func(k, v []byte) error { return nil })
|
||||
})
|
||||
if err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func (m *Manager) dbUpdate(fn func(tx *bolt.Tx) error) (err error) {
|
||||
defer recoverDBPanic(&err)
|
||||
return m.db.Update(fn)
|
||||
}
|
||||
|
||||
func (m *Manager) dbView(fn func(tx *bolt.Tx) error) (err error) {
|
||||
defer recoverDBPanic(&err)
|
||||
return m.db.View(fn)
|
||||
}
|
||||
|
||||
func (m *Manager) post(fn func()) {
|
||||
m.wlCtx.Post(fn)
|
||||
}
|
||||
@@ -444,8 +520,11 @@ func (m *Manager) storeEntry(entry Entry) error {
|
||||
|
||||
entry.Hash = computeHash(entry.Data)
|
||||
|
||||
return m.db.Update(func(tx *bolt.Tx) error {
|
||||
return m.dbUpdate(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return fmt.Errorf("clipboard bucket missing")
|
||||
}
|
||||
|
||||
if err := m.deduplicateInTx(b, entry.Hash); err != nil {
|
||||
return err
|
||||
@@ -908,8 +987,11 @@ func (m *Manager) GetHistory() []Entry {
|
||||
var history []Entry
|
||||
var stale []uint64
|
||||
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
if err := m.dbView(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
c := b.Cursor()
|
||||
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
@@ -940,8 +1022,11 @@ func (m *Manager) deleteStaleEntries(ids []uint64) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.db.Update(func(tx *bolt.Tx) error {
|
||||
if err := m.dbUpdate(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
for _, id := range ids {
|
||||
if err := b.Delete(itob(id)); err != nil {
|
||||
log.Errorf("Failed to delete stale entry %d: %v", id, err)
|
||||
@@ -961,8 +1046,11 @@ func (m *Manager) GetEntry(id uint64) (*Entry, error) {
|
||||
var entry Entry
|
||||
var found bool
|
||||
|
||||
err := m.db.View(func(tx *bolt.Tx) error {
|
||||
err := m.dbView(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
v := b.Get(itob(id))
|
||||
if v == nil {
|
||||
return nil
|
||||
@@ -992,8 +1080,11 @@ func (m *Manager) DeleteEntry(id uint64) error {
|
||||
return fmt.Errorf("database not available")
|
||||
}
|
||||
|
||||
err := m.db.Update(func(tx *bolt.Tx) error {
|
||||
err := m.dbUpdate(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
return b.Delete(itob(id))
|
||||
})
|
||||
|
||||
@@ -1061,7 +1152,7 @@ func (m *Manager) ClearHistory() {
|
||||
}
|
||||
|
||||
// Delete only non-pinned entries
|
||||
if err := m.db.Update(func(tx *bolt.Tx) error {
|
||||
if err := m.dbUpdate(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
@@ -1088,7 +1179,7 @@ func (m *Manager) ClearHistory() {
|
||||
}
|
||||
|
||||
pinnedCount := 0
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
if err := m.dbView(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b != nil {
|
||||
c := b.Cursor()
|
||||
@@ -1120,42 +1211,55 @@ func (m *Manager) compactDB() error {
|
||||
tmpPath := m.dbPath + ".compact"
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
srcDB, err := bolt.Open(m.dbPath, 0o644, &bolt.Options{ReadOnly: true, Timeout: time.Second})
|
||||
if err != nil {
|
||||
m.db, _ = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second})
|
||||
return fmt.Errorf("open source: %w", err)
|
||||
if err := compactInto(m.dbPath, tmpPath, 0); err != nil {
|
||||
m.reopenDB()
|
||||
return err
|
||||
}
|
||||
|
||||
dstDB, err := bolt.Open(tmpPath, 0o644, &bolt.Options{Timeout: time.Second})
|
||||
if err != nil {
|
||||
srcDB.Close()
|
||||
m.db, _ = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second})
|
||||
return fmt.Errorf("open destination: %w", err)
|
||||
}
|
||||
|
||||
if err := bolt.Compact(dstDB, srcDB, 0); err != nil {
|
||||
srcDB.Close()
|
||||
dstDB.Close()
|
||||
m.db, _ = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second})
|
||||
return fmt.Errorf("compact: %w", err)
|
||||
}
|
||||
|
||||
srcDB.Close()
|
||||
dstDB.Close()
|
||||
|
||||
if err := os.Rename(tmpPath, m.dbPath); err != nil {
|
||||
m.db, _ = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second})
|
||||
m.reopenDB()
|
||||
return fmt.Errorf("rename: %w", err)
|
||||
}
|
||||
|
||||
m.db, err = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second})
|
||||
if err != nil {
|
||||
return fmt.Errorf("reopen: %w", err)
|
||||
m.reopenDB()
|
||||
if m.db == nil {
|
||||
return fmt.Errorf("reopen failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func compactInto(srcPath, dstPath string, txMaxSize int64) (err error) {
|
||||
defer recoverDBPanic(&err)
|
||||
|
||||
srcDB, err := bolt.Open(srcPath, 0o644, &bolt.Options{ReadOnly: true, Timeout: time.Second})
|
||||
if err != nil {
|
||||
return fmt.Errorf("open source: %w", err)
|
||||
}
|
||||
defer srcDB.Close()
|
||||
|
||||
dstDB, err := bolt.Open(dstPath, 0o644, &bolt.Options{Timeout: time.Second})
|
||||
if err != nil {
|
||||
return fmt.Errorf("open destination: %w", err)
|
||||
}
|
||||
defer dstDB.Close()
|
||||
|
||||
if err := bolt.Compact(dstDB, srcDB, txMaxSize); err != nil {
|
||||
return fmt.Errorf("compact: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) reopenDB() {
|
||||
db, err := openDB(m.dbPath)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to reopen clipboard db: %v", err)
|
||||
m.db = nil
|
||||
return
|
||||
}
|
||||
m.db = db
|
||||
}
|
||||
|
||||
func (m *Manager) SetClipboard(data []byte, mimeType string) error {
|
||||
if int64(len(data)) > m.config.MaxEntrySize {
|
||||
return fmt.Errorf("data too large")
|
||||
@@ -1339,7 +1443,7 @@ func (m *Manager) Close() {
|
||||
}
|
||||
|
||||
func (m *Manager) clearHistoryInternal() error {
|
||||
return m.db.Update(func(tx *bolt.Tx) error {
|
||||
return m.dbUpdate(func(tx *bolt.Tx) error {
|
||||
if err := tx.DeleteBucket([]byte("clipboard")); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1351,7 +1455,7 @@ func (m *Manager) clearHistoryInternal() error {
|
||||
func (m *Manager) clearOldEntries(days int) error {
|
||||
cutoff := time.Now().AddDate(0, 0, -days)
|
||||
|
||||
return m.db.Update(func(tx *bolt.Tx) error {
|
||||
return m.dbUpdate(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
@@ -1387,7 +1491,7 @@ func (m *Manager) migrateHashes() error {
|
||||
}
|
||||
|
||||
var needsMigration bool
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
if err := m.dbView(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
@@ -1410,7 +1514,7 @@ func (m *Manager) migrateHashes() error {
|
||||
|
||||
log.Info("Migrating clipboard entries to add hashes...")
|
||||
|
||||
return m.db.Update(func(tx *bolt.Tx) error {
|
||||
return m.dbUpdate(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
@@ -1470,7 +1574,7 @@ func (m *Manager) Search(params SearchParams) SearchResult {
|
||||
mimeFilter := strings.ToLower(params.MimeType)
|
||||
|
||||
var all []Entry
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
if err := m.dbView(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
@@ -1664,7 +1768,7 @@ func (m *Manager) PinEntry(id uint64) error {
|
||||
}
|
||||
|
||||
var hashExists bool
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
if err := m.dbView(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
@@ -1691,7 +1795,7 @@ func (m *Manager) PinEntry(id uint64) error {
|
||||
|
||||
cfg := m.getConfig()
|
||||
pinnedCount := 0
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
if err := m.dbView(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
@@ -1712,8 +1816,11 @@ func (m *Manager) PinEntry(id uint64) error {
|
||||
return fmt.Errorf("maximum pinned entries reached (%d)", cfg.MaxPinned)
|
||||
}
|
||||
|
||||
err = m.db.Update(func(tx *bolt.Tx) error {
|
||||
err = m.dbUpdate(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return fmt.Errorf("entry not found")
|
||||
}
|
||||
v := b.Get(itob(id))
|
||||
if v == nil {
|
||||
return fmt.Errorf("entry not found")
|
||||
@@ -1746,8 +1853,11 @@ func (m *Manager) UnpinEntry(id uint64) error {
|
||||
return fmt.Errorf("database not available")
|
||||
}
|
||||
|
||||
err := m.db.Update(func(tx *bolt.Tx) error {
|
||||
err := m.dbUpdate(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return fmt.Errorf("entry not found")
|
||||
}
|
||||
v := b.Get(itob(id))
|
||||
if v == nil {
|
||||
return fmt.Errorf("entry not found")
|
||||
@@ -1812,7 +1922,7 @@ func (m *Manager) GetPinnedEntries() []Entry {
|
||||
}
|
||||
|
||||
var pinned []Entry
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
if err := m.dbView(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
@@ -1842,7 +1952,7 @@ func (m *Manager) GetPinnedCount() int {
|
||||
}
|
||||
|
||||
count := 0
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
if err := m.dbView(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -792,3 +793,63 @@ func TestManager_ConcurrentPostWithMock(t *testing.T) {
|
||||
wg.Wait()
|
||||
assert.Equal(t, int32(100), postCount.Load())
|
||||
}
|
||||
|
||||
func corruptInlineBucketRoot(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
|
||||
f, err := os.OpenFile(path, os.O_RDWR, 0o644)
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
|
||||
// flips the inline bucket root pgid to an out-of-range page, per issue #3232
|
||||
_, err = f.WriteAt([]byte{16}, int64(4*os.Getpagesize()+32+9))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestOpenDB_HealsCorruptedBucketPage(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "db")
|
||||
|
||||
db, err := openDB(path)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.Close())
|
||||
|
||||
corruptInlineBucketRoot(t, path)
|
||||
|
||||
db, err = openDB(path)
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
m := &Manager{config: DefaultConfig(), db: db, dbPath: path}
|
||||
require.NoError(t, m.storeEntry(Entry{Data: []byte("x"), MimeType: "text/plain"}))
|
||||
assert.Len(t, m.GetHistory(), 1)
|
||||
}
|
||||
|
||||
func TestOpenDB_HealsGarbageFile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "db")
|
||||
require.NoError(t, os.WriteFile(path, bytes.Repeat([]byte{0xAB}, 64<<10), 0o644))
|
||||
|
||||
db, err := openDB(path)
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
_, err = os.Stat(path + ".corrupt")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestStoreEntry_CorruptDBReturnsErrorNotPanic(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "db")
|
||||
|
||||
db, err := openDB(path)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.Close())
|
||||
|
||||
corruptInlineBucketRoot(t, path)
|
||||
|
||||
db, err = bolt.Open(path, 0o644, &bolt.Options{Timeout: time.Second})
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
m := &Manager{config: DefaultConfig(), db: db, dbPath: path}
|
||||
_ = m.storeEntry(Entry{Data: []byte("x"), MimeType: "text/plain"})
|
||||
_ = m.GetHistory()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user