Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions replica.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,12 @@ func (r *Replica) Restore(ctx context.Context, opt RestoreOptions) (err error) {
}()

for _, info := range infos {
// Validate file size - must be at least header size to be readable
if info.Size < ltx.HeaderSize {
return fmt.Errorf("invalid ltx file: level=%d min=%s max=%s has size %d bytes (minimum %d)",
info.Level, info.MinTXID, info.MaxTXID, info.Size, ltx.HeaderSize)
}

r.Logger().Debug("opening ltx file for restore", "level", info.Level, "min", info.MinTXID, "max", info.MaxTXID)

// Add file to be compacted.
Expand Down
63 changes: 63 additions & 0 deletions replica_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,69 @@ func TestReplica_CalcRestorePlan(t *testing.T) {
})
}

func TestReplica_Restore_InvalidFileSize(t *testing.T) {
db, sqldb := testingutil.MustOpenDBs(t)
defer testingutil.MustCloseDBs(t, db, sqldb)

t.Run("EmptyFile", func(t *testing.T) {
var c mock.ReplicaClient
c.LTXFilesFunc = func(ctx context.Context, level int, seek ltx.TXID, useMetadata bool) (ltx.FileIterator, error) {
if level == litestream.SnapshotLevel {
return ltx.NewFileInfoSliceIterator([]*ltx.FileInfo{{
Level: litestream.SnapshotLevel,
MinTXID: 1,
MaxTXID: 10,
Size: 0, // Empty file - this should cause an error
CreatedAt: time.Now(),
}}), nil
}
return ltx.NewFileInfoSliceIterator(nil), nil
}

r := litestream.NewReplicaWithClient(db, &c)
outputPath := t.TempDir() + "/restored.db"

err := r.Restore(context.Background(), litestream.RestoreOptions{
OutputPath: outputPath,
})
if err == nil {
t.Fatal("expected error for empty file, got nil")
}
if !strings.Contains(err.Error(), "invalid ltx file") {
t.Fatalf("expected 'invalid ltx file' error, got: %v", err)
}
})

t.Run("TruncatedFile", func(t *testing.T) {
var c mock.ReplicaClient
c.LTXFilesFunc = func(ctx context.Context, level int, seek ltx.TXID, useMetadata bool) (ltx.FileIterator, error) {
if level == litestream.SnapshotLevel {
return ltx.NewFileInfoSliceIterator([]*ltx.FileInfo{{
Level: litestream.SnapshotLevel,
MinTXID: 1,
MaxTXID: 10,
Size: 50, // Less than ltx.HeaderSize (100) - should cause an error
CreatedAt: time.Now(),
}}), nil
}
return ltx.NewFileInfoSliceIterator(nil), nil
}

r := litestream.NewReplicaWithClient(db, &c)
outputPath := t.TempDir() + "/restored.db"

err := r.Restore(context.Background(), litestream.RestoreOptions{
OutputPath: outputPath,
})
if err == nil {
t.Fatal("expected error for truncated file, got nil")
}
if !strings.Contains(err.Error(), "invalid ltx file") {
t.Fatalf("expected 'invalid ltx file' error, got: %v", err)
}
})
}

func TestReplica_ContextCancellationNoLogs(t *testing.T) {
// This test verifies that context cancellation errors are not logged during shutdown.
// The fix for issue #235 ensures that context.Canceled and context.DeadlineExceeded
Expand Down