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") {