diff --git a/cmd/bbolt/surgery_commands.go b/cmd/bbolt/surgery_commands.go index 79f41d2df..5553c8fa7 100644 --- a/cmd/bbolt/surgery_commands.go +++ b/cmd/bbolt/surgery_commands.go @@ -45,6 +45,8 @@ func (cmd *surgeryCommand) Run(args ...string) error { return newRevertMetaPageCommand(cmd).Run(args[1:]...) case "copy-page": return newCopyPageCommand(cmd).Run(args[1:]...) + case "clear-page": + return newClearPageCommand(cmd).Run(args[1:]...) default: return ErrUnknownCommand } @@ -225,7 +227,7 @@ func (cmd *copyPageCommand) Run(args ...string) error { return fmt.Errorf("copyPageCommand failed: %w", err) } - fmt.Fprintf(cmd.Stdout, "The page %d was copied to page %d", srcPageId, dstPageId) + fmt.Fprintf(cmd.Stdout, "The page %d was copied to page %d\n", srcPageId, dstPageId) return nil } @@ -241,3 +243,58 @@ at dstPageId in DST. The original database is left untouched. `, "\n") } + +// clearPageCommand represents the "surgery clear-page" command execution. +type clearPageCommand struct { + *surgeryCommand +} + +// newClearPageCommand returns a clearPageCommand. +func newClearPageCommand(m *surgeryCommand) *clearPageCommand { + c := &clearPageCommand{} + c.surgeryCommand = m + return c +} + +// Run executes the command. +func (cmd *clearPageCommand) Run(args ...string) error { + // Parse flags. + fs := flag.NewFlagSet("", flag.ContinueOnError) + help := fs.Bool("h", false, "") + if err := fs.Parse(args); err != nil { + return err + } else if *help { + fmt.Fprintln(cmd.Stderr, cmd.Usage()) + return ErrUsage + } + + if err := cmd.parsePathsAndCopyFile(fs); err != nil { + return fmt.Errorf("clearPageCommand failed to parse paths and copy file: %w", err) + } + + // Read page id. + pageId, err := strconv.ParseUint(fs.Arg(2), 10, 64) + if err != nil { + return err + } + + if err := surgeon.ClearPage(cmd.dstPath, guts_cli.Pgid(pageId)); err != nil { + return fmt.Errorf("clearPageCommand failed: %w", err) + } + + fmt.Fprintf(cmd.Stdout, "Page (%d) was cleared\n", pageId) + return nil +} + +// Usage returns the help message. +func (cmd *clearPageCommand) Usage() string { + return strings.TrimLeft(` +usage: bolt surgery clear-page SRC DST pageId + +ClearPage copies the database file at SRC to a newly created database +file at DST. Afterwards, it clears all elements in the page at pageId +in DST. + +The original database is left untouched. +`, "\n") +} diff --git a/cmd/bbolt/surgery_commands_test.go b/cmd/bbolt/surgery_commands_test.go index b911ce10c..997836800 100644 --- a/cmd/bbolt/surgery_commands_test.go +++ b/cmd/bbolt/surgery_commands_test.go @@ -70,7 +70,7 @@ func TestSurgery_CopyPage(t *testing.T) { defer requireDBNoChange(t, dbData(t, srcPath), srcPath) - // revert the meta page + // copy page 3 to page 2 t.Log("copy page 3 to page 2") dstPath := filepath.Join(t.TempDir(), "dstdb") m := NewMain() @@ -87,6 +87,39 @@ func TestSurgery_CopyPage(t *testing.T) { assert.Equal(t, pageDataWithoutPageId(srcPageId3Data), pageDataWithoutPageId(dstPageId2Data)) } +// TODO(ahrtr): add test case below for `surgery clear-page` command: +// 1. The page is a branch page. All its children should become free pages. +func TestSurgery_ClearPage(t *testing.T) { + pageSize := 4096 + db := btesting.MustCreateDBWithOption(t, &bolt.Options{PageSize: pageSize}) + srcPath := db.Path() + + // Insert some sample data + t.Log("Insert some sample data") + err := db.Fill([]byte("data"), 1, 20, + func(tx int, k int) []byte { return []byte(fmt.Sprintf("%04d", k)) }, + func(tx int, k int) []byte { return make([]byte, 10) }, + ) + require.NoError(t, err) + + defer requireDBNoChange(t, dbData(t, srcPath), srcPath) + + // clear page 3 + t.Log("clear page 3") + dstPath := filepath.Join(t.TempDir(), "dstdb") + m := NewMain() + err = m.Run("surgery", "clear-page", srcPath, dstPath, "3") + require.NoError(t, err) + + // The page 2 should have exactly the same data as page 3. + t.Log("Verify result") + dstPageId3Data := readPage(t, dstPath, 3, pageSize) + + p := guts_cli.LoadPage(dstPageId3Data) + assert.Equal(t, uint16(0), p.Count()) + assert.Equal(t, uint32(0), p.Overflow()) +} + func readPage(t *testing.T, path string, pageId int, pageSize int) []byte { dbFile, err := os.Open(path) require.NoError(t, err) diff --git a/internal/guts_cli/guts_cli.go b/internal/guts_cli/guts_cli.go index 719ed453b..30e55664d 100644 --- a/internal/guts_cli/guts_cli.go +++ b/internal/guts_cli/guts_cli.go @@ -139,6 +139,10 @@ func (p *Page) Overflow() uint32 { return p.overflow } +func (p *Page) String() string { + return fmt.Sprintf("ID: %d, Type: %s, count: %d, overflow: %d", p.id, p.Type(), p.count, p.overflow) +} + // DO NOT EDIT. Copied from the "bolt" package. // TODO(ptabor): Make the page-types an enum. @@ -178,6 +182,14 @@ func (p *Page) SetId(target Pgid) { p.id = target } +func (p *Page) SetCount(target uint16) { + p.count = target +} + +func (p *Page) SetOverflow(target uint32) { + p.overflow = target +} + // DO NOT EDIT. Copied from the "bolt" package. type BranchPageElement struct { pos uint32 @@ -276,6 +288,25 @@ func ReadPage(path string, pageID uint64) (*Page, []byte, error) { return p, buf, nil } +func WritePage(path string, pageBuf []byte) error { + page := LoadPage(pageBuf) + pageSize, _, err := ReadPageAndHWMSize(path) + if err != nil { + return err + } + expectedLen := pageSize * (uint64(page.Overflow()) + 1) + if expectedLen != uint64(len(pageBuf)) { + return fmt.Errorf("WritePage: len(buf):%d != pageSize*(overflow+1):%d", len(pageBuf), expectedLen) + } + f, err := os.OpenFile(path, os.O_WRONLY, 0) + if err != nil { + return err + } + defer f.Close() + _, err = f.WriteAt(pageBuf, int64(page.Id())*int64(pageSize)) + return err +} + // ReadPageAndHWMSize reads Page size and HWM (id of the last+1 Page). // This is not transactionally safe. func ReadPageAndHWMSize(path string) (uint64, Pgid, error) { diff --git a/internal/surgeon/surgeon.go b/internal/surgeon/surgeon.go index 7350cb65a..763583705 100644 --- a/internal/surgeon/surgeon.go +++ b/internal/surgeon/surgeon.go @@ -2,8 +2,6 @@ package surgeon import ( "fmt" - "os" - "go.etcd.io/bbolt/internal/guts_cli" ) @@ -13,25 +11,24 @@ func CopyPage(path string, srcPage guts_cli.Pgid, target guts_cli.Pgid) error { return err1 } p1.SetId(target) - return WritePage(path, d1) + return guts_cli.WritePage(path, d1) } -func WritePage(path string, pageBuf []byte) error { - page := guts_cli.LoadPage(pageBuf) - pageSize, _, err := guts_cli.ReadPageAndHWMSize(path) +func ClearPage(path string, pgId guts_cli.Pgid) error { + // Read the page + p, buf, err := guts_cli.ReadPage(path, uint64(pgId)) if err != nil { - return err + return fmt.Errorf("ReadPage failed: %w", err) } - if pageSize != uint64(len(pageBuf)) { - return fmt.Errorf("WritePage: len(buf)=%d != pageSize=%d", len(pageBuf), pageSize) - } - f, err := os.OpenFile(path, os.O_WRONLY, 0) - if err != nil { - return err + + // Update and rewrite the page + p.SetCount(0) + p.SetOverflow(0) + if err := guts_cli.WritePage(path, buf); err != nil { + return fmt.Errorf("WritePage failed: %w", err) } - defer f.Close() - _, err = f.WriteAt(pageBuf, int64(page.Id())*int64(pageSize)) - return err + + return nil } // RevertMetaPage replaces the newer metadata page with the older. diff --git a/internal/tests/tx_check_test.go b/internal/tests/tx_check_test.go index 342198fa5..8d67db307 100644 --- a/internal/tests/tx_check_test.go +++ b/internal/tests/tx_check_test.go @@ -69,7 +69,7 @@ func TestTx_RecursivelyCheckPages_CorruptedLeaf(t *testing.T) { require.NoError(t, err) require.Positive(t, p.Count(), "page must be not empty") p.LeafPageElement(p.Count() / 2).Key()[0] = 'z' - require.NoError(t, surgeon.WritePage(db.Path(), pbuf)) + require.NoError(t, guts_cli.WritePage(db.Path(), pbuf)) db.MustReopen() require.NoError(t, db.Update(func(tx *bolt.Tx) error {