aboutsummaryrefslogtreecommitdiff
path: root/internal/persistence
diff options
context:
space:
mode:
Diffstat (limited to 'internal/persistence')
-rw-r--r--internal/persistence/txn.go14
-rw-r--r--internal/persistence/txn_test.go20
2 files changed, 32 insertions, 2 deletions
diff --git a/internal/persistence/txn.go b/internal/persistence/txn.go
index a12e227..988af23 100644
--- a/internal/persistence/txn.go
+++ b/internal/persistence/txn.go
@@ -34,6 +34,10 @@ import (
// as a committed transaction that the next startup will roll back.
const journalRemoveAttempts = 3
+// journalVersion is the current journal schema version. Recover refuses a
+// version it does not understand rather than misreading a newer journal.
+const journalVersion = 1
+
type fileOp struct {
// Replace / Delete / Move data
Path string `json:"path"` // file this record is keyed by (its before-image lives here)
@@ -57,7 +61,10 @@ const (
)
type journalEntry struct {
- Ops []fileOp `json:"ops"`
+ // Version guards against a downgrade silently misreading a newer journal.
+ // 0 is the legacy unversioned format; unknown future versions are refused.
+ Version int `json:"version"`
+ Ops []fileOp `json:"ops"`
}
type Transaction struct {
@@ -244,7 +251,7 @@ func (t *Transaction) MarshalJournal() ([]byte, error) {
for _, p := range t.order {
ops = append(ops, *t.ops[p])
}
- return json.Marshal(journalEntry{Ops: ops})
+ return json.Marshal(journalEntry{Version: journalVersion, Ops: ops})
}
// Commit writes the journal (durable), applies all operations, and removes the
@@ -432,6 +439,9 @@ func Recover(journalDir string) error {
}
continue
}
+ if entry.Version != 0 && entry.Version != journalVersion {
+ return fmt.Errorf("journal %s: unsupported version %d", path, entry.Version)
+ }
t := &Transaction{ops: make(map[string]*fileOp), journalDir: journalDir}
for i := range entry.Ops {
op := entry.Ops[i]
diff --git a/internal/persistence/txn_test.go b/internal/persistence/txn_test.go
index be19c7c..e1cd05d 100644
--- a/internal/persistence/txn_test.go
+++ b/internal/persistence/txn_test.go
@@ -280,3 +280,23 @@ func TestRecoverKeepsJournalWithUnknownOpKind(t *testing.T) {
t.Fatalf("journal with unknown kind was removed: %v", err)
}
}
+
+// Regression: a journal written by a newer version must be refused, not
+// silently misread or deleted.
+func TestRecoverRefusesUnknownJournalVersion(t *testing.T) {
+ dir := t.TempDir()
+ journalDir := filepath.Join(dir, ".journal")
+ if err := os.MkdirAll(journalDir, 0700); err != nil {
+ t.Fatal(err)
+ }
+ path := filepath.Join(journalDir, "txn-future.json")
+ if err := os.WriteFile(path, []byte(`{"version":99,"ops":[]}`), 0600); err != nil {
+ t.Fatal(err)
+ }
+ if err := Recover(journalDir); err == nil {
+ t.Fatal("Recover accepted an unknown journal version")
+ }
+ if _, err := os.Stat(path); err != nil {
+ t.Fatalf("journal with an unknown version was removed: %v", err)
+ }
+}