internal/unify: fix round-tripping strings with regexp metacharacters

Change-Id: I92956b13c7532b9a96386947ee19aa61142337c8
Reviewed-on: https://go-review.googlesource.com/c/arch/+/689478
Auto-Submit: Austin Clements <austin@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: David Chase <drchase@google.com>
diff --git a/internal/unify/domain.go b/internal/unify/domain.go
index c59bd62..00c8090 100644
--- a/internal/unify/domain.go
+++ b/internal/unify/domain.go
@@ -270,8 +270,8 @@
 			continue
 		}
 
-		if _, complete := re.LiteralPrefix(); complete {
-			v = String{kind: stringExact, exact: expr}
+		if exact, complete := re.LiteralPrefix(); complete {
+			v = String{kind: stringExact, exact: exact}
 		} else {
 			v.kind = stringRegex
 			v.re = append(v.re, re)
diff --git a/internal/unify/yaml.go b/internal/unify/yaml.go
index 4731140..6782b31 100644
--- a/internal/unify/yaml.go
+++ b/internal/unify/yaml.go
@@ -430,7 +430,14 @@
 				n.Tag = "tag:yaml.org,2002:int"
 				return &n
 			}
-			n.SetString(regexp.QuoteMeta(d.exact))
+			// If this doesn't require escaping, leave it as a str node to avoid
+			// the annoying YAML tags. Otherwise, mark it as an exact string.
+			// Alternatively, we could always emit a str node with regexp
+			// quoting.
+			n.SetString(d.exact)
+			if d.exact != regexp.QuoteMeta(d.exact) {
+				n.Tag = "!string"
+			}
 			return &n
 		case stringRegex:
 			o := make([]string, 0, 1)
diff --git a/internal/unify/yaml_test.go b/internal/unify/yaml_test.go
index af73001..05a26be 100644
--- a/internal/unify/yaml_test.go
+++ b/internal/unify/yaml_test.go
@@ -8,6 +8,9 @@
 	"bytes"
 	"fmt"
 	"iter"
+	"log"
+	"strings"
+	"testing"
 
 	"gopkg.in/yaml.v3"
 )
@@ -20,6 +23,19 @@
 	return c
 }
 
+func oneValue(t *testing.T, c Closure) *Value {
+	t.Helper()
+	var v *Value
+	var i int
+	for v = range c.All() {
+		i++
+	}
+	if i != 1 {
+		t.Fatalf("expected 1 value, got %d", i)
+	}
+	return v
+}
+
 func printYaml(val any) {
 	b, err := yaml.Marshal(val)
 	if err != nil {
@@ -89,3 +105,31 @@
 		}
 	}
 }
+
+func TestRoundTripString(t *testing.T) {
+	// Check that we can round-trip a string with regexp meta-characters in it.
+	const y = `!string test*`
+	t.Logf("input:\n%s", y)
+
+	v1 := oneValue(t, mustParse(y))
+	var buf1 strings.Builder
+	enc := yaml.NewEncoder(&buf1)
+	if err := enc.Encode(v1); err != nil {
+		log.Fatal(err)
+	}
+	enc.Close()
+	t.Logf("after parse 1:\n%s", buf1.String())
+
+	v2 := oneValue(t, mustParse(buf1.String()))
+	var buf2 strings.Builder
+	enc = yaml.NewEncoder(&buf2)
+	if err := enc.Encode(v2); err != nil {
+		log.Fatal(err)
+	}
+	enc.Close()
+	t.Logf("after parse 2:\n%s", buf2.String())
+
+	if buf1.String() != buf2.String() {
+		t.Fatal("parse 1 and parse 2 differ")
+	}
+}