diff options
Diffstat (limited to 'internal/persistence')
| -rw-r--r-- | internal/persistence/txn.go | 14 | ||||
| -rw-r--r-- | internal/persistence/txn_test.go | 20 |
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) + } +} |
