diff --git a/bytestrings/bytestrings.go b/bytestrings/bytestrings.go index b4e1b3c..0d3fcaf 100644 --- a/bytestrings/bytestrings.go +++ b/bytestrings/bytestrings.go @@ -1,11 +1,13 @@ package bytestrings import ( + "iter" "strings" "unsafe" ) // NextNonEmptyLine returns the next non-empty line and the remaining text. +// The line has its line feed ('\n') and carriage return ('\r') characters removed. func NextNonEmptyLine[T ~[]byte | ~string](text T) (T, T) { for { lfIndex := strings.IndexByte(*(*string)(unsafe.Pointer(&text)), '\n') @@ -26,3 +28,31 @@ func NextNonEmptyLine[T ~[]byte | ~string](text T) (T, T) { return line, text } } + +// NonEmptyLines returns an iterator over the non-empty lines in the text, +// with line feed ('\n') and carriage return ('\r') characters removed. +func NonEmptyLines[T ~[]byte | ~string](text T) iter.Seq[T] { + return func(yield func(T) bool) { + for lfIndex := 0; len(text) > 0; text = text[lfIndex+1:] { + lfIndex = strings.IndexByte(*(*string)(unsafe.Pointer(&text)), '\n') + switch lfIndex { + case -1: + _ = yield(text) + return + case 0: + continue + } + + line := text[:lfIndex] + if line[len(line)-1] == '\r' { + line = line[:len(line)-1] + } + if len(line) == 0 { + continue + } + if !yield(line) { + return + } + } + } +} diff --git a/bytestrings/bytestrings_test.go b/bytestrings/bytestrings_test.go index 6f89da2..d4d0d47 100644 --- a/bytestrings/bytestrings_test.go +++ b/bytestrings/bytestrings_test.go @@ -1,6 +1,9 @@ package bytestrings -import "testing" +import ( + "slices" + "testing" +) const multiline = "\n1\r\n2\n\n3\r\n\r\n4" @@ -37,3 +40,11 @@ func TestNextNonEmptyLine(t *testing.T) { t.Fatalf("Expected text '%s', got '%s'", multiline[13:], text) } } + +func TestNonEmptyLines(t *testing.T) { + expectedLines := []string{"1", "2", "3", "4"} + lines := slices.AppendSeq(make([]string, 0, len(expectedLines)), NonEmptyLines(multiline)) + if !slices.Equal(lines, expectedLines) { + t.Errorf("Expected lines %v, got %v", expectedLines, lines) + } +} diff --git a/cmd/shadowsocks-go-domain-set-converter/main.go b/cmd/shadowsocks-go-domain-set-converter/main.go index d0e2b8f..3009b3c 100644 --- a/cmd/shadowsocks-go-domain-set-converter/main.go +++ b/cmd/shadowsocks-go-domain-set-converter/main.go @@ -128,14 +128,7 @@ func DomainSetBuilderFromDlc(text string) (domainset.Builder, error) { domainset.NewRegexpMatcherBuilder(0), } - var line string - - for { - line, text = bytestrings.NextNonEmptyLine(text) - if len(line) == 0 { - break - } - + for line := range bytestrings.NonEmptyLines(text) { if line[0] == '#' { continue } diff --git a/domainset/domainset.go b/domainset/domainset.go index 14e058f..f1a509f 100644 --- a/domainset/domainset.go +++ b/domainset/domainset.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "iter" "strconv" "strings" @@ -206,8 +207,11 @@ func BuilderFromTextFunc( newKeywordMatcherBuilderFunc, newRegexpMatcherBuilderFunc func(int) MatcherBuilder, ) (Builder, error) { - line, text := bytestrings.NextNonEmptyLine(text) - if len(line) == 0 { + next, stop := iter.Pull(bytestrings.NonEmptyLines(text)) + defer stop() + + line, ok := next() + if !ok { return Builder{}, errEmptySet } @@ -216,8 +220,8 @@ func BuilderFromTextFunc( return Builder{}, err } if found { - line, text = bytestrings.NextNonEmptyLine(text) - if len(line) == 0 { + line, ok = next() + if !ok { return Builder{}, errEmptySet } } @@ -255,8 +259,8 @@ func BuilderFromTextFunc( } next: - line, text = bytestrings.NextNonEmptyLine(text) - if len(line) == 0 { + line, ok = next() + if !ok { break } } diff --git a/prefixset/prefixset.go b/prefixset/prefixset.go index 9535b9e..57eb983 100644 --- a/prefixset/prefixset.go +++ b/prefixset/prefixset.go @@ -28,17 +28,9 @@ func (psc Config) IPSet() (*netipx.IPSet, error) { // IPSetFromText parses prefixes from the text and builds a prefix set. func IPSetFromText(text string) (*netipx.IPSet, error) { - var ( - line string - sb netipx.IPSetBuilder - ) - - for { - line, text = bytestrings.NextNonEmptyLine(text) - if len(line) == 0 { - break - } + var sb netipx.IPSetBuilder + for line := range bytestrings.NonEmptyLines(text) { if line[0] == '#' { continue }