Repository: gokcehan/lf (go)

Issue:
panic: runtime error: index out of range [3] with length 3
```panic: runtime error: index out of range [3] with length 3

goroutine 1 [running]:
main.applyAnsiCodes({0xc0000c4e62?, 0x0?}, {0x100000001, 0x0, 0x0, 0x0, 0x4, {0x0, 0x0}, {0x0, ...}})
	/home/yilisharcs/go/pkg/mod/github.com/gokcehan/lf@v0.0.0-20250305225025-9dc4e6b43195/colors.go:180 +0x22c5
main.(*win).print(0xc000022320, {0x6cf810, 0xc00020a0c0}, 0x0, 0x0, {0x0, 0x0, 0x0, 0x0, 0x0, ...}, ...)
	/home/yilisharcs/go/pkg/mod/github.com/gokcehan/lf@v0.0.0-20250305225025-9dc4e6b43195/ui.go:208 +0x29d
main.(*ui).drawRuler(0xc00012c4e0, 0xc000007dc0)
	/home/yilisharcs/go/pkg/mod/github.com/gokcehan/lf@v0.0.0-20250305225025-9dc4e6b43195/ui.go:899 +0x105
main.(*ui).draw(0xc00012c4e0, 0xc000007dc0)
	/home/yilisharcs/go/pkg/mod/github.com/gokcehan/lf@v0.0.0-20250305225025-9dc4e6b43195/ui.go:1062 +0x34b
main.(*app).loop(0xc00007cf70)
	/home/yilisharcs/go/pkg/mod/github.com/gokcehan/lf@v0.0.0-20250305225025-9dc4e6b43195/app.go:494 +0x11b3
main.run()
	/home/yilisharcs/go/pkg/mod/github.com/gokcehan/lf@v0.0.0-20250305225025-9dc4e6b43195/client.go:65 +0x30f
main.main()
	/home/yilisharcs/go/pkg/mod/github.com/gokcehan/lf@v0.0.0-20250305225025-9dc4e6b43195/main.go:357 +0x819
```

Windows 10, Ubuntu 24.04.2 LTS with WSL.

What causes this is `set errorfmt "\033[7;31;48m"` (vs default `47m`) when I hit unmapped keys, call a command that doesn't exist, etc. It's the same setting I had on an actual Linux machine. Odd that it doesn't work here.

Pull request description:
Add function for parsing SGR colors
- Fixes #1931

Can be reproduced using the following config file:

```
set promptfmt "\033[38m%d%f" 
```

The crash occurs because the arguments to `38`/`48` are stored in an array, and are accessed without checking if the array is empty or not.

Also refactored out the code for parsing colors into a separate function.

Patch:
diff --git a/colors.go b/colors.go
index 54e5b21..9e7d1e4 100644
--- a/colors.go
+++ b/colors.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"fmt"
 	"log"
 	"os"
 	"path/filepath"
@@ -95,6 +96,42 @@ func parseEscapeSequence(s string) tcell.Style {
 	return applyAnsiCodes(s, tcell.StyleDefault)
 }
 
+func parseColor(toks []string) (tcell.Color, int, error) {
+	if len(toks) == 0 {
+		return tcell.ColorDefault, 0, fmt.Errorf("invalid args: %v", toks)
+	}
+
+	if toks[0] == "5" && len(toks) >= 2 {
+		n, err := strconv.Atoi(toks[1])
+		if err != nil {
+			return tcell.ColorDefault, 0, fmt.Errorf("invalid args: %v", toks)
+		}
+
+		return tcell.PaletteColor(n), 2, nil
+	}
+
+	if toks[0] == "2" && len(toks) >= 4 {
+		r, err := strconv.Atoi(toks[1])
+		if err != nil {
+			return tcell.ColorDefault, 0, fmt.Errorf("invalid args: %v", toks)
+		}
+
+		g, err := strconv.Atoi(toks[2])
+		if err != nil {
+			return tcell.ColorDefault, 0, fmt.Errorf("invalid args: %v", toks)
+		}
+
+		b, err := strconv.Atoi(toks[3])
+		if err != nil {
+			return tcell.ColorDefault, 0, fmt.Errorf("invalid args: %v", toks)
+		}
+
+		return tcell.NewRGBColor(int32(r), int32(g), int32(b)), 4, nil
+	}
+
+	return tcell.ColorDefault, 0, fmt.Errorf("invalid args: %v", toks)
+}
+
 func applyAnsiCodes(s string, st tcell.Style) tcell.Style {
 	toks := strings.Split(s, ";")
 
@@ -102,6 +139,8 @@ func applyAnsiCodes(s string, st tcell.Style) tcell.Style {
 	// TODO: should we support turning off attributes?
 	//    Probably because this is used for previewers too
 	tokslen := len(toks)
+
+loop:
 	for i := 0; i < tokslen; i++ {
 		switch strings.TrimLeft(toks[i], "0") {
 		case "":
@@ -141,35 +180,13 @@ func applyAnsiCodes(s string, st tcell.Style) tcell.Style {
 			n, _ := strconv.Atoi(toks[i])
 			st = st.Foreground(tcell.PaletteColor(n - 82))
 		case "38":
-			if toks[i+1] == "5" && i+2 < tokslen {
-				n, err := strconv.Atoi(toks[i+2])
-				if err != nil {
-					log.Printf("unknown ansi code: %s", toks[i+2])
-					continue
-				}
-				st = st.Foreground(tcell.PaletteColor(n))
-				i += 2
-			} else if toks[i+1] == "2" && i+4 < tokslen {
-				r, err := strconv.Atoi(toks[i+2])
-				if err != nil {
-					log.Printf("unknown ansi code: %s", toks[i+2])
-					continue
-				}
-				g, err := strconv.Atoi(toks[i+3])
-				if err != nil {
-					log.Printf("unknown ansi code: %s", toks[i+3])
-					continue
-				}
-				b, err := strconv.Atoi(toks[i+4])
-				if err != nil {
-					log.Printf("unknown ansi code: %s", toks[i+4])
-					continue
-				}
-				st = st.Foreground(tcell.NewRGBColor(int32(r), int32(g), int32(b)))
-				i += 4
-			} else {
-				log.Printf("unknown ansi code or incorrect form: %s", toks[i])
+			color, offset, err := parseColor(toks[i+1:])
+			if err != nil {
+				log.Printf("error processing ansi code 38: %s", err)
+				break loop
 			}
+			st = st.Foreground(color)
+			i += offset
 		case "40", "41", "42", "43", "44", "45", "46", "47":
 			n, _ := strconv.Atoi(toks[i])
 			st = st.Background(tcell.PaletteColor(n - 40))
@@ -177,35 +194,13 @@ func applyAnsiCodes(s string, st tcell.Style) tcell.Style {
 			n, _ := strconv.Atoi(toks[i])
 			st = st.Background(tcell.PaletteColor(n - 92))
 		case "48":
-			if toks[i+1] == "5" && i+2 < tokslen {
-				n, err := strconv.Atoi(toks[i+2])
-				if err != nil {
-					log.Printf("unknown ansi code: %s", toks[i+2])
-					continue
-				}
-				st = st.Background(tcell.PaletteColor(n))
-				i += 2
-			} else if toks[i+1] == "2" && i+4 < tokslen {
-				r, err := strconv.Atoi(toks[i+2])
-				if err != nil {
-					log.Printf("unknown ansi code: %s", toks[i+2])
-					continue
-				}
-				g, err := strconv.Atoi(toks[i+3])
-				if err != nil {
-					log.Printf("unknown ansi code: %s", toks[i+3])
-					continue
-				}
-				b, err := strconv.Atoi(toks[i+4])
-				if err != nil {
-					log.Printf("unknown ansi code: %s", toks[i+4])
-					continue
-				}
-				st = st.Background(tcell.NewRGBColor(int32(r), int32(g), int32(b)))
-				i += 4
-			} else {
-				log.Printf("unknown ansi code or incorrect form: %s", toks[i])
+			color, offset, err := parseColor(toks[i+1:])
+			if err != nil {
+				log.Printf("error processing ansi code 48: %s", err)
+				break loop
 			}
+			st = st.Background(color)
+			i += offset
 		default:
 			log.Printf("unknown ansi code: %s", toks[i])
 		}


Test patch:
diff --git a/colors_test.go b/colors_test.go
index 8085ce1..77e5afc 100644
--- a/colors_test.go
+++ b/colors_test.go
@@ -6,6 +6,35 @@ import (
 	"github.com/gdamore/tcell/v2"
 )
 
+func TestParseColor(t *testing.T) {
+	tests := []struct {
+		toks    []string
+		color   tcell.Color
+		offset  int
+		success bool
+	}{
+		{[]string{}, tcell.ColorDefault, 0, false},
+		{[]string{"foo"}, tcell.ColorDefault, 0, false},
+		{[]string{"5"}, tcell.ColorDefault, 0, false},
+		{[]string{"5", "foo"}, tcell.ColorDefault, 0, false},
+		{[]string{"5", "42"}, tcell.PaletteColor(42), 2, true},
+		{[]string{"2"}, tcell.ColorDefault, 0, false},
+		{[]string{"2", "foo"}, tcell.ColorDefault, 0, false},
+		{[]string{"2", "42", "foo"}, tcell.ColorDefault, 0, false},
+		{[]string{"2", "42", "43", "foo"}, tcell.ColorDefault, 0, false},
+		{[]string{"2", "42", "43", "44"}, tcell.NewRGBColor(42, 43, 44), 4, true},
+	}
+
+	for _, test := range tests {
+		color, offset, err := parseColor(test.toks)
+		success := err == nil
+		if color != test.color || offset != test.offset || success != test.success {
+			t.Errorf("at input %v expected (%v, %v, %v) but got (%v, %v, %v)",
+				test.toks, test.color, test.offset, test.success, color, offset, success)
+		}
+	}
+}
+
 func TestApplyAnsiCodes(t *testing.T) {
 	none := tcell.StyleDefault