Repository: d5/tengo (go)

Issue:
Runtime error: slice bounds out of range in textREFind when using alternation patterns
## Description
When using the text library with a regex pattern containing multiple capture groups and alternation operator `|`, the library throws a runtime error. The error occurs specifically with patterns like `([a-zA-Z])|([0-9])`.

## Example Code
```
fmt := import("fmt")
text := import("text")
pattern := "([a-zA-Z])|([0-9])"
text_content := "a"
result := text.re_find(pattern, text_content, -1)
fmt.println(result)
```

## Error Message
runtime error: slice bounds out of range [:-1]

## Root Cause Analysis
The issue stems from the implementation of the `textREFind` function, which internally uses Go's standard library `regexp.FindAllStringSubmatchIndex`. When certain capture groups don't match, `FindAllStringSubmatchIndex` returns indices containing `-1`. These negative indices are then used in a slice operation `s2[-1:-1]`, causing the runtime error.

## Expected Behavior
The regex pattern should correctly match either letters or numbers as defined in the alternation groups.

## Actual Behavior
The program crashes with a slice bounds runtime error instead of performing the pattern matching.

## Environment
- Library version: [v2.17.0]
- Go version: [1.22.12]
- OS: [macOS]

## Additional Notes
This is a critical bug that affects pattern matching functionality when using alternation with capture groups. The issue is reproducible and affects core functionality of the text library.

Pull request:
fix regex alternation
fixes #459 

Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index cb363d9..1fbf378 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -8,17 +8,17 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: check out
-        uses: actions/checkout@v2
-      - name: Unshallow
-        run: git fetch --prune --unshallow
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
       - name: set up Go
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v4
         with:
           go-version: 1.18
       - name: run goreleaser
-        uses: goreleaser/goreleaser-action@v3
+        uses: goreleaser/goreleaser-action@v5
         with:
           version: latest
-          args: release --rm-dist
+          args: release --clean
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/stdlib/text.go b/stdlib/text.go
index d7d5d1d..c40b68a 100644
--- a/stdlib/text.go
+++ b/stdlib/text.go
@@ -284,12 +284,14 @@ func textREFind(args ...tengo.Object) (ret tengo.Object, err error) {
 
 		arr := &tengo.Array{}
 		for i := 0; i < len(m); i += 2 {
-			arr.Value = append(arr.Value,
-				&tengo.ImmutableMap{Value: map[string]tengo.Object{
-					"text":  &tengo.String{Value: s2[m[i]:m[i+1]]},
-					"begin": &tengo.Int{Value: int64(m[i])},
-					"end":   &tengo.Int{Value: int64(m[i+1])},
-				}})
+			if m[i] >= 0 && m[i+1] >= 0 {
+				arr.Value = append(arr.Value,
+					&tengo.ImmutableMap{Value: map[string]tengo.Object{
+						"text":  &tengo.String{Value: s2[m[i]:m[i+1]]},
+						"begin": &tengo.Int{Value: int64(m[i])},
+						"end":   &tengo.Int{Value: int64(m[i+1])},
+					}})
+			}
 		}
 
 		ret = &tengo.Array{Value: []tengo.Object{arr}}
@@ -316,12 +318,14 @@ func textREFind(args ...tengo.Object) (ret tengo.Object, err error) {
 	for _, m := range m {
 		subMatch := &tengo.Array{}
 		for i := 0; i < len(m); i += 2 {
-			subMatch.Value = append(subMatch.Value,
-				&tengo.ImmutableMap{Value: map[string]tengo.Object{
-					"text":  &tengo.String{Value: s2[m[i]:m[i+1]]},
-					"begin": &tengo.Int{Value: int64(m[i])},
-					"end":   &tengo.Int{Value: int64(m[i+1])},
-				}})
+			if m[i] >= 0 && m[i+1] >= 0 {
+				subMatch.Value = append(subMatch.Value,
+					&tengo.ImmutableMap{Value: map[string]tengo.Object{
+						"text":  &tengo.String{Value: s2[m[i]:m[i+1]]},
+						"begin": &tengo.Int{Value: int64(m[i])},
+						"end":   &tengo.Int{Value: int64(m[i+1])},
+					}})
+			}
 		}
 
 		arr.Value = append(arr.Value, subMatch)


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 5edb1a3..64f2ff6 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -9,25 +9,19 @@ jobs:
     name: build
     runs-on: ubuntu-latest
     steps:
+    - name: check out code
+      uses: actions/checkout@v4
     - name: set up Go
-      uses: actions/setup-go@v1
+      uses: actions/setup-go@v4
       with:
         go-version: 1.18
-      id: go
     - name: set up Go module cache
-      uses: actions/cache@v1
+      uses: actions/cache@v4
       with:
         path: ~/go/pkg/mod
         key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
         restore-keys: |
           ${{ runner.os }}-go-
-    - name: setup env
-      run: |
-        echo "name=GOPATH::$(go env GOPATH)" >> $GITHUB_ENV
-        echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
-      shell: bash
-    - name: check out code
-      uses: actions/checkout@v2
     - name: install golint
       run: go install golang.org/x/lint/golint@latest
     - name: run tests
diff --git a/stdlib/text_regexp_test.go b/stdlib/text_regexp_test.go
new file mode 100644
index 0000000..6d25386
--- /dev/null
+++ b/stdlib/text_regexp_test.go
@@ -0,0 +1,52 @@
+package stdlib_test
+
+import (
+	"testing"
+
+	"github.com/d5/tengo/v2"
+)
+
+func TestTextREAlternation(t *testing.T) {
+	module(t, "text").call("re_find", "([a-zA-Z])|([0-9])", "a").expect(ARR{
+		ARR{
+			IMAP{"text": "a", "begin": 0, "end": 1},
+			IMAP{"text": "a", "begin": 0, "end": 1},
+		},
+	}, "alternation with letter")
+
+	module(t, "text").call("re_find", "([a-zA-Z])|([0-9])", "5").expect(ARR{
+		ARR{
+			IMAP{"text": "5", "begin": 0, "end": 1},
+			IMAP{"text": "5", "begin": 0, "end": 1},
+		},
+	}, "alternation with number")
+
+	module(t, "text").call("re_find", "([a-zA-Z])|([0-9])", "").expect(tengo.UndefinedValue, "empty input")
+
+	module(t, "text").call("re_find", "([a-zA-Z])|([0-9])", "!").expect(tengo.UndefinedValue, "non-matching input")
+
+	module(t, "text").call("re_find", "(?:([a-zA-Z])|([0-9]))+", "a5b").expect(ARR{
+		ARR{
+			IMAP{"text": "a5b", "begin": 0, "end": 3},
+			IMAP{"text": "b", "begin": 2, "end": 3},
+			IMAP{"text": "5", "begin": 1, "end": 2},
+		},
+	}, "multiple alternations")
+
+	module(t, "text").call("re_find", "(foo)|(bar)|(baz)", "foo").expect(ARR{
+		ARR{
+			IMAP{"text": "foo", "begin": 0, "end": 3},
+			IMAP{"text": "foo", "begin": 0, "end": 3},
+		},
+	}, "multiple groups with non-matches")
+
+	module(t, "text").call("re_find", "((cat)|(dog))((run)|(walk))", "catrun").expect(ARR{
+		ARR{
+			IMAP{"text": "catrun", "begin": 0, "end": 6},
+			IMAP{"text": "cat", "begin": 0, "end": 3},
+			IMAP{"text": "cat", "begin": 0, "end": 3},
+			IMAP{"text": "run", "begin": 3, "end": 6},
+			IMAP{"text": "run", "begin": 3, "end": 6},
+		},
+	}, "nested groups with alternation")
+}