Repository: cockroachdb/pebble
Pull request #4402 (2025-03-19 15:24:23)

internal/strparse: new package
Pull out the manifest package's debugParser used to facilitate parsing of debug representation's of data structures into a new internal package. This is in preparation for an additional use in parsing debug representations of blob handles.

cockroach-teamcity: 

<!-- Reviewable:start -->
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/cockroachdb/pebble/4402)
<!-- Reviewable:end -->


Patch:
diff --git a/internal/manifest/blob_metadata.go b/internal/manifest/blob_metadata.go
index 82dceca4..36d0ff91 100644
--- a/internal/manifest/blob_metadata.go
+++ b/internal/manifest/blob_metadata.go
@@ -11,6 +11,7 @@ import (
 	"github.com/cockroachdb/pebble/internal/base"
 	"github.com/cockroachdb/pebble/internal/humanize"
 	"github.com/cockroachdb/pebble/internal/invariants"
+	"github.com/cockroachdb/pebble/internal/strparse"
 	"github.com/cockroachdb/redact"
 )
 
@@ -148,7 +149,7 @@ func ParseBlobFileMetadataDebug(s string) (_ *BlobFileMetadata, err error) {
 	// Input format:
 	//  000000: size:[206536 (201KiB)], vals:[393256 (384KiB)]
 	m := &BlobFileMetadata{}
-	p := makeDebugParser(s)
+	p := strparse.MakeParser(debugParserSeparators, s)
 	m.FileNum = base.DiskFileNum(p.FileNum())
 
 	maybeSkipParens := func() {
diff --git a/internal/manifest/version.go b/internal/manifest/version.go
index 5d9e4c25..7282bc12 100644
--- a/internal/manifest/version.go
+++ b/internal/manifest/version.go
@@ -17,6 +17,7 @@ import (
 	"github.com/cockroachdb/errors"
 	"github.com/cockroachdb/pebble/internal/base"
 	"github.com/cockroachdb/pebble/internal/invariants"
+	"github.com/cockroachdb/pebble/internal/strparse"
 	"github.com/cockroachdb/pebble/sstable"
 	"github.com/cockroachdb/pebble/sstable/block"
 )
@@ -882,6 +883,16 @@ func (m *TableMetadata) DebugString(format base.FormatKey, verbose bool) string
 	return b.String()
 }
 
+const debugParserSeparators = ":-[]();"
+
+// errFromPanic can be used in a recover block to convert panics into errors.
+func errFromPanic(r any) error {
+	if err, ok := r.(error); ok {
+		return err
+	}
+	return errors.Errorf("%v", r)
+}
+
 // ParseTableMetadataDebug parses a TableMetadata from its DebugString
 // representation.
 func ParseTableMetadataDebug(s string) (_ *TableMetadata, err error) {
@@ -894,7 +905,7 @@ func ParseTableMetadataDebug(s string) (_ *TableMetadata, err error) {
 	// Input format:
 	//	000000:[a#0,SET-z#0,SET] seqnums:[5-5] points:[...] ranges:[...] size:5
 	m := &TableMetadata{}
-	p := makeDebugParser(s)
+	p := strparse.MakeParser(debugParserSeparators, s)
 	m.FileNum = p.FileNum()
 	var backingNum base.DiskFileNum
 	if p.Peek() == "(" {
@@ -1357,7 +1368,7 @@ func ParseVersionDebug(comparer *base.Comparer, flushSplitBytes int64, s string)
 		if l == "" {
 			continue
 		}
-		p := makeDebugParser(l)
+		p := strparse.MakeParser(debugParserSeparators, l)
 		if l, ok := p.TryLevel(); ok {
 			level = l
 			continue
diff --git a/internal/manifest/version_edit.go b/internal/manifest/version_edit.go
index 67ec6ab1..3b7bf066 100644
--- a/internal/manifest/version_edit.go
+++ b/internal/manifest/version_edit.go
@@ -18,6 +18,7 @@ import (
 	"github.com/cockroachdb/errors"
 	"github.com/cockroachdb/pebble/internal/base"
 	"github.com/cockroachdb/pebble/internal/invariants"
+	"github.com/cockroachdb/pebble/internal/strparse"
 	"github.com/cockroachdb/pebble/sstable"
 )
 
@@ -640,7 +641,7 @@ func ParseVersionEditDebug(s string) (_ *VersionEdit, err error) {
 			return nil, errors.Errorf("malformed line %q", l)
 		}
 		field = strings.TrimSpace(field)
-		p := makeDebugParser(value)
+		p := strparse.MakeParser(debugParserSeparators, value)
 		switch field {
 		case "add-table":
 			level := p.Level()


Test patch:
diff --git a/internal/manifest/testutils.go b/internal/strparse/strparse.go
similarity index 64%
rename from internal/manifest/testutils.go
rename to internal/strparse/strparse.go
index 44f99a70..65e8f96a 100644
--- a/internal/manifest/testutils.go
+++ b/internal/strparse/strparse.go
@@ -2,7 +2,9 @@
 // of this source code is governed by a BSD-style license that can be found in
 // the LICENSE file.
 
-package manifest
+// Package strparse provides facilities for parsing strings, intended for use in
+// tests and debug input.
+package strparse
 
 import (
 	"fmt"
@@ -14,31 +16,32 @@ import (
 	"github.com/cockroachdb/pebble/internal/base"
 )
 
-// debugParser is a helper used to implement parsing of debug strings, like
-// ParseFileMetadataDebug.
+// Parser is a helper used to implement parsing of strings, like
+// manifest.ParseFileMetadataDebug.
 //
 // It takes a string and splits it into tokens. Tokens are separated by
-// whitespace; in addition separators "_-[]()" are always separate tokens. For
-// example, the string `000001:[a - b]` results in tokens `000001`,
-// `:`, `[`, `a`, `-`, `b`, `]`, .
+// whitespace; in addition user-specified separators are also always separate
+// tokens. For example, when passed the separators `:-[]();` the string
+// `000001:[a - b]` results in tokens `000001`, `:`, `[`, `a`, `-`, `b`, `]`, .
 //
-// All debugParser methods throw panics instead of returning errors. The code
-// that uses a debugParser can recover them and convert them to errors.
-type debugParser struct {
+// All Parser methods throw panics instead of returning errors. The code
+// that uses a Parser can recover them and convert them to errors.
+type Parser struct {
 	original  string
 	tokens    []string
 	lastToken string
 }
 
-const debugParserSeparators = ":-[]();"
-
-func makeDebugParser(s string) debugParser {
-	p := debugParser{
-		original: s,
+// MakeParser constructs a new Parser that converts any instance of the runes
+// contained in [separators] into separate tokens, and consumes the provided
+// input string.
+func MakeParser(separators string, input string) Parser {
+	p := Parser{
+		original: input,
 	}
-	for _, f := range strings.Fields(s) {
+	for _, f := range strings.Fields(input) {
 		for f != "" {
-			pos := strings.IndexAny(f, debugParserSeparators)
+			pos := strings.IndexAny(f, separators)
 			if pos == -1 {
 				p.tokens = append(p.tokens, f)
 				break
@@ -54,13 +57,13 @@ func makeDebugParser(s string) debugParser {
 }
 
 // Done returns true if there are no more tokens.
-func (p *debugParser) Done() bool {
+func (p *Parser) Done() bool {
 	return len(p.tokens) == 0
 }
 
 // Peek returns the next token, without consuming the token. Returns "" if there
 // are no more tokens.
-func (p *debugParser) Peek() string {
+func (p *Parser) Peek() string {
 	if p.Done() {
 		p.lastToken = ""
 		return ""
@@ -70,7 +73,7 @@ func (p *debugParser) Peek() string {
 }
 
 // Next returns the next token, or "" if there are no more tokens.
-func (p *debugParser) Next() string {
+func (p *Parser) Next() string {
 	res := p.Peek()
 	if res != "" {
 		p.tokens = p.tokens[1:]
@@ -79,7 +82,7 @@ func (p *debugParser) Next() string {
 }
 
 // Remaining returns all the remaining tokens, separated by spaces.
-func (p *debugParser) Remaining() string {
+func (p *Parser) Remaining() string {
 	res := strings.Join(p.tokens, " ")
 	p.tokens = nil
 	return res
@@ -87,7 +90,7 @@ func (p *debugParser) Remaining() string {
 
 // Expect consumes the next tokens, verifying that they exactly match the
 // arguments.
-func (p *debugParser) Expect(tokens ...string) {
+func (p *Parser) Expect(tokens ...string) {
 	for _, tok := range tokens {
 		if res := p.Next(); res != tok {
 			p.Errf("expected %q, got %q", tok, res)
@@ -97,7 +100,7 @@ func (p *debugParser) Expect(tokens ...string) {
 
 // TryLevel tries to parse a token as a level (e.g. L1, L0.2). If successful,
 // the token is consumed.
-func (p *debugParser) TryLevel() (level int, ok bool) {
+func (p *Parser) TryLevel() (level int, ok bool) {
 	t := p.Peek()
 	if regexp.MustCompile(`^L[0-9](|\.[0-9]+)$`).MatchString(t) {
 		p.Next()
@@ -107,7 +110,7 @@ func (p *debugParser) TryLevel() (level int, ok bool) {
 }
 
 // Level parses the next token as a level.
-func (p *debugParser) Level() int {
+func (p *Parser) Level() int {
 	level, ok := p.TryLevel()
 	if !ok {
 		p.Errf("cannot parse level")
@@ -116,7 +119,7 @@ func (p *debugParser) Level() int {
 }
 
 // Int parses the next token as an integer.
-func (p *debugParser) Int() int {
+func (p *Parser) Int() int {
 	x, err := strconv.Atoi(p.Next())
 	if err != nil {
 		p.Errf("cannot parse number: %v", err)
@@ -125,7 +128,7 @@ func (p *debugParser) Int() int {
 }
 
 // Uint64 parses the next token as an uint64.
-func (p *debugParser) Uint64() uint64 {
+func (p *Parser) Uint64() uint64 {
 	x, err := strconv.ParseUint(p.Next(), 10, 64)
 	if err != nil {
 		p.Errf("cannot parse number: %v", err)
@@ -134,36 +137,28 @@ func (p *debugParser) Uint64() uint64 {
 }
 
 // Uint64 parses the next token as a sequence number.
-func (p *debugParser) SeqNum() base.SeqNum {
+func (p *Parser) SeqNum() base.SeqNum {
 	return base.ParseSeqNum(p.Next())
 }
 
 // FileNum parses the next token as a FileNum.
-func (p *debugParser) FileNum() base.FileNum {
+func (p *Parser) FileNum() base.FileNum {
 	return base.FileNum(p.Int())
 }
 
 // DiskFileNum parses the next token as a DiskFileNum.
-func (p *debugParser) DiskFileNum() base.DiskFileNum {
+func (p *Parser) DiskFileNum() base.DiskFileNum {
 	return base.DiskFileNum(p.Int())
 }
 
 // InternalKey parses the next token as an internal key.
-func (p *debugParser) InternalKey() base.InternalKey {
+func (p *Parser) InternalKey() base.InternalKey {
 	return base.ParseInternalKey(p.Next())
 }
 
 // Errf panics with an error which includes the original string and the last
 // token.
-func (p *debugParser) Errf(format string, args ...any) {
+func (p *Parser) Errf(format string, args ...any) {
 	msg := fmt.Sprintf(format, args...)
 	panic(errors.Errorf("error parsing %q at token %q: %s", p.original, p.lastToken, msg))
 }
-
-// errFromPanic can be used in a recover block to convert panics into errors.
-func errFromPanic(r any) error {
-	if err, ok := r.(error); ok {
-		return err
-	}
-	return errors.Errorf("%v", r)
-}