From af9795aa1a07e62a811ab73b4c425995047da823 Mon Sep 17 00:00:00 2001 From: Ben Johnson Date: Mon, 3 Jan 2022 13:13:02 -0700 Subject: [PATCH] Avoid panics in RBF debug tooling --- ctl/rbf_check.go | 12 +- ctl/rbf_check_test.go | 33 ++++ ctl/rbf_pages.go | 11 +- ctl/rbf_pages_test.go | 52 ++++++ .../rbf-check/err-invalid-page-type/data | Bin 0 -> 40960 bytes .../rbf-check/err-invalid-page-type/wal | 0 ctl/testdata/rbf-check/ok/data | Bin 0 -> 32768 bytes ctl/testdata/rbf-check/ok/wal | 0 .../rbf-pages/err-invalid-page-type/data | Bin 0 -> 40960 bytes .../rbf-pages/err-invalid-page-type/wal | 0 ctl/testdata/rbf-pages/ok/data | Bin 0 -> 32768 bytes ctl/testdata/rbf-pages/ok/wal | 0 rbf/cursor.go | 4 + rbf/rbf.go | 43 +++++ rbf/rbf/testdata/check/bad-freelist/data | Bin 0 -> 24576 bytes rbf/rbf/testdata/check/bad-freelist/wal | 0 rbf/rbf_test.go | 19 +- rbf/testdata/check/bad-bitmap/data | Bin 0 -> 32768 bytes rbf/testdata/check/bad-bitmap/wal | 0 rbf/testdata/check/bad-freelist/data | Bin 0 -> 32768 bytes rbf/testdata/check/bad-freelist/wal | 0 rbf/tx.go | 168 +++++++++++------- rbf/tx_test.go | 31 ++++ 23 files changed, 300 insertions(+), 73 deletions(-) create mode 100644 ctl/rbf_check_test.go create mode 100644 ctl/rbf_pages_test.go create mode 100644 ctl/testdata/rbf-check/err-invalid-page-type/data create mode 100644 ctl/testdata/rbf-check/err-invalid-page-type/wal create mode 100644 ctl/testdata/rbf-check/ok/data create mode 100644 ctl/testdata/rbf-check/ok/wal create mode 100644 ctl/testdata/rbf-pages/err-invalid-page-type/data create mode 100644 ctl/testdata/rbf-pages/err-invalid-page-type/wal create mode 100644 ctl/testdata/rbf-pages/ok/data create mode 100644 ctl/testdata/rbf-pages/ok/wal create mode 100644 rbf/rbf/testdata/check/bad-freelist/data create mode 100644 rbf/rbf/testdata/check/bad-freelist/wal create mode 100644 rbf/testdata/check/bad-bitmap/data create mode 100644 rbf/testdata/check/bad-bitmap/wal create mode 100644 rbf/testdata/check/bad-freelist/data create mode 100644 rbf/testdata/check/bad-freelist/wal diff --git a/ctl/rbf_check.go b/ctl/rbf_check.go index 00594b861..f3d40912d 100644 --- a/ctl/rbf_check.go +++ b/ctl/rbf_check.go @@ -26,7 +26,7 @@ func NewRBFCheckCommand(stdin io.Reader, stdout, stderr io.Writer) *RBFCheckComm } } -// Run executes the export. +// Run executes a consistency check of an RBF database. func (cmd *RBFCheckCommand) Run(ctx context.Context) error { // Open database. db := rbf.NewDB(cmd.Path, nil) @@ -37,7 +37,15 @@ func (cmd *RBFCheckCommand) Run(ctx context.Context) error { // Run check on the database. if err := db.Check(); err != nil { - return err + switch err := err.(type) { + case rbf.ErrorList: + for i := range err { + fmt.Fprintln(cmd.Stdout, err[i]) + } + default: + fmt.Fprintln(cmd.Stdout, err) + } + return fmt.Errorf("check failed") } // If successful, print a success message. diff --git a/ctl/rbf_check_test.go b/ctl/rbf_check_test.go new file mode 100644 index 000000000..c11cd30dc --- /dev/null +++ b/ctl/rbf_check_test.go @@ -0,0 +1,33 @@ +// Copyright 2021 Molecula Corp. All rights reserved. +package ctl + +import ( + "bytes" + "context" + "path/filepath" + "testing" +) + +func TestRBFCheckCommand_Run(t *testing.T) { + t.Run("OK", func(t *testing.T) { + var stdout, stderr bytes.Buffer + cmd := NewRBFCheckCommand(bytes.NewReader(nil), &stdout, &stderr) + cmd.Path = filepath.Join("testdata", "rbf-check", "ok") + if err := cmd.Run(context.Background()); err != nil { + t.Fatal(err) + } else if got, want := stdout.String(), `ok`+"\n"; got != want { + t.Fatalf("got:\n%s\n\nwant:\n%s", got, want) + } + }) + + t.Run("ErrInvalidPageType", func(t *testing.T) { + var stdout, stderr bytes.Buffer + cmd := NewRBFCheckCommand(bytes.NewReader(nil), &stdout, &stderr) + cmd.Path = filepath.Join("testdata", "rbf-check", "err-invalid-page-type") + if err := cmd.Run(context.Background()); err == nil || err.Error() != `check failed` { + t.Fatal(err) + } else if got, want := stdout.String(), `page not in-use & not free: pgno=4`+"\n"; got != want { + t.Fatalf("got:\n%s\n\nwant:\n%s", got, want) + } + }) +} diff --git a/ctl/rbf_pages.go b/ctl/rbf_pages.go index ca3484f31..b774175ae 100644 --- a/ctl/rbf_pages.go +++ b/ctl/rbf_pages.go @@ -49,7 +49,16 @@ func (cmd *RBFPagesCommand) Run(ctx context.Context) error { // Iterate over each page and grab info. infos, err := tx.PageInfos() if err != nil { - return err + fmt.Fprintln(cmd.Stdout, "ERRORS:") + switch err := err.(type) { + case rbf.ErrorList: + for i := range err { + fmt.Fprintln(cmd.Stdout, err[i]) + } + default: + fmt.Fprintln(cmd.Stdout, err) + } + fmt.Fprintln(cmd.Stdout, "") } // Write header. diff --git a/ctl/rbf_pages_test.go b/ctl/rbf_pages_test.go new file mode 100644 index 000000000..73c395e1b --- /dev/null +++ b/ctl/rbf_pages_test.go @@ -0,0 +1,52 @@ +// Copyright 2021 Molecula Corp. All rights reserved. +package ctl + +import ( + "bytes" + "context" + "path/filepath" + "testing" +) + +func TestRBFPagesCommand_Run(t *testing.T) { + t.Run("OK", func(t *testing.T) { + want := ` +ID TYPE EXTRA +======== ========== ==================== +0 meta pageN=4,walid=4,rootrec=1,freelist=2 +1 rootrec next=0 +2 leaf flags=x2,celln=0 +3 leaf flags=x2,celln=1 +`[1:] + + var stdout, stderr bytes.Buffer + cmd := NewRBFPagesCommand(bytes.NewReader(nil), &stdout, &stderr) + cmd.Path = filepath.Join("testdata", "rbf-pages", "ok") + if err := cmd.Run(context.Background()); err != nil { + t.Fatal(err) + } else if got := stdout.String(); got != want { + t.Fatalf("got:\n%s\n\nwant:\n%s", got, want) + } + }) + + t.Run("ErrInvalidPageType", func(t *testing.T) { + want := ` +ID TYPE EXTRA +======== ========== ==================== +0 meta pageN=5,walid=4,rootrec=1,freelist=2 +1 rootrec next=0 +2 leaf flags=x2,celln=0 +3 leaf flags=x2,celln=1 +4 unknown [] +`[1:] + + var stdout, stderr bytes.Buffer + cmd := NewRBFPagesCommand(bytes.NewReader(nil), &stdout, &stderr) + cmd.Path = filepath.Join("testdata", "rbf-pages", "err-invalid-page-type") + if err := cmd.Run(context.Background()); err != nil { + t.Fatal(err) + } else if got := stdout.String(); got != want { + t.Fatalf("got:\n%s\n\nwant:\n%s", got, want) + } + }) +} diff --git a/ctl/testdata/rbf-check/err-invalid-page-type/data b/ctl/testdata/rbf-check/err-invalid-page-type/data new file mode 100644 index 0000000000000000000000000000000000000000..f088c25c91420895455f7d925a815acc89c710a9 GIT binary patch literal 40960 zcmeI)y9t0W5CG5^wRa{92XF?5aC^NY7J{{)@D47QPZM6skR0p0losPvhp1g-y~YXw z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly@O^>2>(9*SOSiY4mH+_)1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oU&_K)&<$*8fi@X0HSY5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAVAa3PN)f&19@)MFX?-0Jd$ucE1&Dl((`o_)2oNAZfB*pk1PBlyK!5-N W0t5&UAV7cs0RjXF5FkKcLV*WZO9JEo literal 0 HcmV?d00001 diff --git a/ctl/testdata/rbf-check/ok/wal b/ctl/testdata/rbf-check/ok/wal new file mode 100644 index 000000000..e69de29bb diff --git a/ctl/testdata/rbf-pages/err-invalid-page-type/data b/ctl/testdata/rbf-pages/err-invalid-page-type/data new file mode 100644 index 0000000000000000000000000000000000000000..f088c25c91420895455f7d925a815acc89c710a9 GIT binary patch literal 40960 zcmeI)y9t0W5CG5^wRa{92XF?5aC^NY7J{{)@D47QPZM6skR0p0losPvhp1g-y~YXw z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly@O^>2>(9*SOSiY4mH+_)1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oU&_K)&<$*8fi@X0HSY5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAVAa3PN)f&19@)MFX?-0Jd$ucE1&Dl((`o_)2oNAZfB*pk1PBlyK!5-N W0t5&UAV7cs0RjXF5FkKcLV*WZO9JEo literal 0 HcmV?d00001 diff --git a/ctl/testdata/rbf-pages/ok/wal b/ctl/testdata/rbf-pages/ok/wal new file mode 100644 index 000000000..e69de29bb diff --git a/rbf/cursor.go b/rbf/cursor.go index eda6bec1d..2419bc5f8 100644 --- a/rbf/cursor.go +++ b/rbf/cursor.go @@ -932,6 +932,10 @@ func (c *Cursor) First() error { case PageTypeBranch: elem.index = 0 + if n := readCellN(buf); elem.index >= n { // branch cell index must less than cell count + return fmt.Errorf("branch cell index out of range: pgno=%d i=%d n=%d", elem.pgno, elem.index, n) + } + // Read cell pgno into the next stack level. cell := readBranchCell(buf, elem.index) diff --git a/rbf/rbf.go b/rbf/rbf.go index 74a5135eb..4a66ba309 100644 --- a/rbf/rbf.go +++ b/rbf/rbf.go @@ -799,3 +799,46 @@ func (m *Metric) Inc(d time.Duration) { fmt.Printf("metric:%10s avg=%dns\n", m.name, int(m.d)/m.n) } } + +// ErrorList represents a list of errors. +type ErrorList []error + +// Err returns the list if it contains errors. Otherwise returns nil. +func (a ErrorList) Err() error { + if len(a) > 0 { + return a + } + return nil +} + +func (a ErrorList) Error() string { + switch len(a) { + case 0: + return "no errors" + case 1: + return a[0].Error() + } + return fmt.Sprintf("%s (and %d more errors)", a[0], len(a)-1) +} + +func (a ErrorList) FullError() string { + if len(a) == 0 { + return "" + } + + var buf bytes.Buffer + for _, err := range a { + fmt.Fprintln(&buf, err) + } + return buf.String() +} + +// Append appends an error to the list. If err is an ErrorList then all errors are appended. +func (a *ErrorList) Append(err error) { + switch err := err.(type) { + case ErrorList: + *a = append(*a, err...) + default: + *a = append(*a, err) + } +} diff --git a/rbf/rbf/testdata/check/bad-freelist/data b/rbf/rbf/testdata/check/bad-freelist/data new file mode 100644 index 0000000000000000000000000000000000000000..8b03c7b022bf24d9f12c2a3688fb59e67a7f9799 GIT binary patch literal 24576 zcmeI(!3n@13 0 { cfg0 = cfg[0] } - db := rbf.NewDB(path, cfg0) - return db + return rbf.NewDB(path, cfg0) } // MustOpenDB returns a db opened on a temporary file. On error, fail test. func MustOpenDB(tb testing.TB, cfg ...*rbfcfg.Config) *rbf.DB { + tb.Helper() + path, err := testhook.TempDir(tb, "rbfdb") + if err != nil { + panic(err) + } + return MustOpenDBAt(tb, path, cfg...) +} + +// MustOpenDBAt returns a db opened on an existing file. On error, fail test. +func MustOpenDBAt(tb testing.TB, path string, cfg ...*rbfcfg.Config) *rbf.DB { tb.Helper() if len(cfg) == 0 || cfg[0] == nil { newconf := rbfcfg.NewDefaultConfig() @@ -73,7 +86,7 @@ func MustOpenDB(tb testing.TB, cfg ...*rbfcfg.Config) *rbf.DB { } else if cfg[0].Logger == nil { cfg[0].Logger = logger.NewLogfLogger(tb) } - db := NewDB(tb, cfg...) + db := NewDBAt(tb, path, cfg...) if err := db.Open(); err != nil { tb.Fatal(err) } diff --git a/rbf/testdata/check/bad-bitmap/data b/rbf/testdata/check/bad-bitmap/data new file mode 100644 index 0000000000000000000000000000000000000000..6cc32c0a1b7fa877dbeb1eac9bf9acc19daae59f GIT binary patch literal 32768 zcmeI)u?>JA5CA|C25=?|2XF?5aC_y01&Iqla-Z-#(2%1f@SOXxv?yJlYQ27A9RUIa z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FjwVK-~35i^xm5TO|`9K!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pkGYP~ye{cO~Ix%}CK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pke}P=5O8o&~>zCT60FkeLT1|ie0RjXF5FkK+009C72oNAZ WfB*pk1PBlyK!5-N0t5&QDDVQ9Jp$$c literal 0 HcmV?d00001 diff --git a/rbf/testdata/check/bad-bitmap/wal b/rbf/testdata/check/bad-bitmap/wal new file mode 100644 index 000000000..e69de29bb diff --git a/rbf/testdata/check/bad-freelist/data b/rbf/testdata/check/bad-freelist/data new file mode 100644 index 0000000000000000000000000000000000000000..a762ee9db3ccd44a4cfb13df4ffea92b2c55c154 GIT binary patch literal 32768 zcmeI)!3}^Q3;;kt2XH1I4&V$9;r0rm!4V+2CP3+{xi6vDzHXt${5xaL(|dOiAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0{sh=xBjw9Od*~%vlAddfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009Cc36y{S-{1d{`qnD}0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBly&;+KOraS>)X{9<4AU