diff --git a/src/pkg/util/export.go b/src/pkg/util/export.go
index db81fcb1..63fe5d73 100644
--- a/src/pkg/util/export.go
+++ b/src/pkg/util/export.go
@@ -19,7 +19,10 @@ type KeyValue struct {
func ExportDotenv(secrets []KeyValue) {
for _, kv := range secrets {
- fmt.Printf("%s=\"%s\"\n", kv.Key, kv.Value)
+ escaped := strings.ReplaceAll(kv.Value, "\\", "\\\\")
+ escaped = strings.ReplaceAll(escaped, "\"", "\\\"")
+ escaped = strings.ReplaceAll(escaped, "\n", "\\n")
+ fmt.Printf("%s=\"%s\"\n", kv.Key, escaped)
}
}
@@ -79,22 +82,28 @@ func ExportYAML(secrets []KeyValue) {
func ExportXML(secrets []KeyValue) {
fmt.Println("")
for _, kv := range secrets {
- var escaped strings.Builder
- xml.EscapeText(&escaped, []byte(kv.Value))
- fmt.Printf(" %s\n", kv.Key, escaped.String())
+ var escapedKey strings.Builder
+ xml.EscapeText(&escapedKey, []byte(kv.Key))
+ nameAttr := strings.ReplaceAll(escapedKey.String(), "\"", """)
+ var escapedVal strings.Builder
+ xml.EscapeText(&escapedVal, []byte(kv.Value))
+ fmt.Printf(" %s\n", nameAttr, escapedVal.String())
}
fmt.Println("")
}
func ExportTOML(secrets []KeyValue) {
for _, kv := range secrets {
- fmt.Printf("%s = \"%s\"\n", kv.Key, kv.Value)
+ escaped := strings.ReplaceAll(kv.Value, "\\", "\\\\")
+ escaped = strings.ReplaceAll(escaped, "\"", "\\\"")
+ fmt.Printf("%s = \"%s\"\n", kv.Key, escaped)
}
}
func ExportHCL(secrets []KeyValue) {
for _, kv := range secrets {
- escaped := strings.ReplaceAll(kv.Value, "\"", "\\\"")
+ escaped := strings.ReplaceAll(kv.Value, "\\", "\\\\")
+ escaped = strings.ReplaceAll(escaped, "\"", "\\\"")
fmt.Printf("variable \"%s\" {\n", kv.Key)
fmt.Printf(" default = \"%s\"\n", escaped)
fmt.Println("}")
diff --git a/src/pkg/util/export_test.go b/src/pkg/util/export_test.go
index 0ef40716..df0c8098 100644
--- a/src/pkg/util/export_test.go
+++ b/src/pkg/util/export_test.go
@@ -174,6 +174,98 @@ func TestExportDotenvAndKVLikeFormats(t *testing.T) {
}
}
+func TestExportDotenv_Escaping(t *testing.T) {
+ secrets := []KeyValue{
+ {Key: "SIMPLE", Value: "hello"},
+ {Key: "WITH_QUOTES", Value: `he said "hello"`},
+ {Key: "WITH_BACKSLASH", Value: `path\to\file`},
+ {Key: "WITH_NEWLINE", Value: "line1\nline2"},
+ {Key: "WITH_ALL", Value: "say \"hi\"\nand \\go"},
+ }
+ out := captureStdout(t, func() { ExportDotenv(secrets) })
+
+ if !strings.Contains(out, `SIMPLE="hello"`) {
+ t.Fatalf("simple value wrong: %s", out)
+ }
+ if !strings.Contains(out, `WITH_QUOTES="he said \"hello\""`) {
+ t.Fatalf("quoted value not escaped: %s", out)
+ }
+ if !strings.Contains(out, `WITH_BACKSLASH="path\\to\\file"`) {
+ t.Fatalf("backslash not escaped: %s", out)
+ }
+ if !strings.Contains(out, `WITH_NEWLINE="line1\nline2"`) {
+ t.Fatalf("newline not escaped: %s", out)
+ }
+ if !strings.Contains(out, `WITH_ALL="say \"hi\"\nand \\go"`) {
+ t.Fatalf("combined escaping wrong: %s", out)
+ }
+}
+
+func TestExportTOML_Escaping(t *testing.T) {
+ secrets := []KeyValue{
+ {Key: "SIMPLE", Value: "hello"},
+ {Key: "WITH_QUOTES", Value: `he said "hello"`},
+ {Key: "WITH_BACKSLASH", Value: `path\to\file`},
+ {Key: "WITH_BOTH", Value: `say "hi" and \ go`},
+ }
+ out := captureStdout(t, func() { ExportTOML(secrets) })
+
+ if !strings.Contains(out, `SIMPLE = "hello"`) {
+ t.Fatalf("simple value wrong: %s", out)
+ }
+ if !strings.Contains(out, `WITH_QUOTES = "he said \"hello\""`) {
+ t.Fatalf("quoted value not escaped: %s", out)
+ }
+ if !strings.Contains(out, `WITH_BACKSLASH = "path\\to\\file"`) {
+ t.Fatalf("backslash not escaped: %s", out)
+ }
+ if !strings.Contains(out, `WITH_BOTH = "say \"hi\" and \\ go"`) {
+ t.Fatalf("combined escaping wrong: %s", out)
+ }
+}
+
+func TestExportXML_KeyEscaping(t *testing.T) {
+ secrets := []KeyValue{
+ {Key: `key"with"quotes`, Value: "val1"},
+ {Key: "key&", Value: "val2"},
+ {Key: "key", Value: "val3"},
+ }
+ out := captureStdout(t, func() { ExportXML(secrets) })
+
+ var parsed xmlSecrets
+ if err := xml.Unmarshal([]byte(out), &parsed); err != nil {
+ t.Fatalf("xml with special key chars should parse, got: %v\noutput: %s", err, out)
+ }
+ got := map[string]string{}
+ for _, e := range parsed.Entries {
+ got[e.Name] = e.Value
+ }
+ for _, kv := range secrets {
+ if got[kv.Key] != kv.Value {
+ t.Fatalf("xml roundtrip for key %q: got %q want %q", kv.Key, got[kv.Key], kv.Value)
+ }
+ }
+}
+
+func TestExportHCL_Escaping(t *testing.T) {
+ secrets := []KeyValue{
+ {Key: "SIMPLE", Value: "hello"},
+ {Key: "WITH_QUOTES", Value: `he said "hello"`},
+ {Key: "WITH_BACKSLASH", Value: `path\to\file`},
+ }
+ out := captureStdout(t, func() { ExportHCL(secrets) })
+
+ if !strings.Contains(out, `default = "hello"`) {
+ t.Fatalf("simple value wrong: %s", out)
+ }
+ if !strings.Contains(out, `default = "he said \"hello\""`) {
+ t.Fatalf("quoted value not escaped: %s", out)
+ }
+ if !strings.Contains(out, `default = "path\\to\\file"`) {
+ t.Fatalf("backslash not escaped: %s", out)
+ }
+}
+
func TestExportINI_EscapesPercent(t *testing.T) {
out := captureStdout(t, func() { ExportINI(sampleSecrets) })
if !strings.HasPrefix(out, "[DEFAULT]\n") {