diff --git a/functional-tests/src/lib.rs b/functional-tests/src/lib.rs index 08eccea9c..6530d16bd 100644 --- a/functional-tests/src/lib.rs +++ b/functional-tests/src/lib.rs @@ -265,6 +265,57 @@ bar: baz } } + #[test] + fn test_ini_values_as_strings() { + let file_path = prepare_temp_file( + "test_ini_values_as_strings.yaml", + b"the_section: + int: 123 + float: 1.23 + bool: true + date: 2025-01-02 + timestamp: 2025-01-02 03:04:05 + utc_timestamp: 2025-01-02T03:04:05Z + string: this is a string", + ); + assert!( + Command::new(SOPS_BINARY_PATH) + .arg("encrypt") + .arg("-i") + .arg(file_path.clone()) + .output() + .expect("Error running sops") + .status + .success(), + "sops didn't exit successfully" + ); + let output = Command::new(SOPS_BINARY_PATH) + .arg("decrypt") + .arg("--output-type") + .arg("ini") + .arg(file_path.clone()) + .output() + .expect("Error running sops"); + println!( + "stdout: {}, stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.status.success(), "sops didn't exit successfully"); + let data = &String::from_utf8_lossy(&output.stdout); + assert!( + data == "[the_section] +int = 123 +float = 1.23 +bool = true +date = 2025-01-02T00:00:00Z +timestamp = 2025-01-02T03:04:05Z +utc_timestamp = 2025-01-02T03:04:05Z +string = this is a string +" + ); + } + #[test] fn encrypt_yaml_file() { let file_path = prepare_temp_file( diff --git a/stores/ini/store.go b/stores/ini/store.go index 83605000e..d8e72990d 100644 --- a/stores/ini/store.go +++ b/stores/ini/store.go @@ -4,8 +4,6 @@ import ( "bytes" "encoding/json" "fmt" - - "strconv" "strings" "github.com/getsops/sops/v3" @@ -56,7 +54,7 @@ func (store Store) encodeTree(branches sops.TreeBranches) ([]byte, error) { lastItem.Comment = comment.Value } } else { - lastItem, err = section.NewKey(keyVal.Key.(string), store.valToString(keyVal.Value)) + lastItem, err = section.NewKey(keyVal.Key.(string), stores.ValToString(keyVal.Value)) if err != nil { return nil, fmt.Errorf("Error encoding key: %s", err) } @@ -78,19 +76,6 @@ func (store Store) stripCommentChar(comment string) string { return comment } -func (store Store) valToString(v interface{}) string { - switch v := v.(type) { - case fmt.Stringer: - return v.String() - case float64: - return strconv.FormatFloat(v, 'f', 6, 64) - case bool: - return strconv.FormatBool(v) - default: - return fmt.Sprintf("%s", v) - } -} - func (store Store) iniFromTreeBranches(branches sops.TreeBranches) ([]byte, error) { return store.encodeTree(branches) } diff --git a/stores/stores.go b/stores/stores.go index bdc026a00..4cd74b2f3 100644 --- a/stores/stores.go +++ b/stores/stores.go @@ -10,9 +10,10 @@ of the purpose of this package is to make it easy to change the SOPS file format package stores import ( - "time" - "fmt" + "strconv" + "strings" + "time" "github.com/getsops/sops/v3" "github.com/getsops/sops/v3/age" @@ -533,3 +534,25 @@ func HasSopsTopLevelKey(branch sops.TreeBranch) bool { } return false } + +// ValToString converts a simple value to a string. +// It does not handle complex values (arrays and mappings). +func ValToString(v interface{}) string { + switch v := v.(type) { + case float64: + result := strconv.FormatFloat(v, 'G', -1, 64) + // If the result can be confused with an integer, make sure we have at least one decimal digit + if !strings.ContainsRune(result, '.') && !strings.ContainsRune(result, 'E') { + result = strconv.FormatFloat(v, 'f', 1, 64) + } + return result + case bool: + return strconv.FormatBool(v) + case time.Time: + return v.Format(time.RFC3339) + case fmt.Stringer: + return v.String() + default: + return fmt.Sprintf("%v", v) + } +} diff --git a/stores/stores_test.go b/stores/stores_test.go new file mode 100644 index 000000000..31ced210a --- /dev/null +++ b/stores/stores_test.go @@ -0,0 +1,27 @@ +package stores + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + + +func TestValToString(t *testing.T) { + assert.Equal(t, "1", ValToString(1)) + assert.Equal(t, "1.0", ValToString(1.0)) + assert.Equal(t, "1.1", ValToString(1.10)) + assert.Equal(t, "1.23", ValToString(1.23)) + assert.Equal(t, "1.2345678901234567", ValToString(1.234567890123456789)) + assert.Equal(t, "200000.0", ValToString(2E5)) + assert.Equal(t, "-2E+10", ValToString(-2E10)) + assert.Equal(t, "2E-10", ValToString(2E-10)) + assert.Equal(t, "1.2345E+100", ValToString(1.2345E100)) + assert.Equal(t, "1.2345E-100", ValToString(1.2345E-100)) + assert.Equal(t, "true", ValToString(true)) + assert.Equal(t, "false", ValToString(false)) + ts, _ := time.Parse(time.RFC3339, "2025-01-02T03:04:05Z") + assert.Equal(t, "2025-01-02T03:04:05Z", ValToString(ts)) + assert.Equal(t, "a string", ValToString("a string")) +}