diff --git a/internal/chart/v3/loader/load.go b/internal/chart/v3/loader/load.go index 48f346ccf..0ff040eae 100644 --- a/internal/chart/v3/loader/load.go +++ b/internal/chart/v3/loader/load.go @@ -183,8 +183,20 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { // The reader is expected to contain one or more YAML documents, the values of which are merged. // And the values can be either a chart's default values or user-supplied values. func LoadValues(data io.Reader) (map[string]any, error) { + // Read fully first. YAMLReader/LineReader can drop a final unterminated + // line when its length is an exact multiple of bufio.Reader's default + // buffer (4096). Appending a trailing newline avoids that case. + // See https://github.com/helm/helm/issues/32506 + b, err := io.ReadAll(data) + if err != nil { + return nil, err + } + if len(b) > 0 && b[len(b)-1] != '\n' { + b = append(b, '\n') + } + values := map[string]any{} - reader := utilyaml.NewYAMLReader(bufio.NewReader(data)) + reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(b))) for { currentMap := map[string]any{} raw, err := reader.Read() diff --git a/internal/chart/v3/loader/load_test.go b/internal/chart/v3/loader/load_test.go index dd5fdc8db..e34364f39 100644 --- a/internal/chart/v3/loader/load_test.go +++ b/internal/chart/v3/loader/load_test.go @@ -21,6 +21,7 @@ import ( "bytes" "compress/gzip" "errors" + "fmt" "io" "log" "os" @@ -419,6 +420,33 @@ foo: } } +func TestLoadValuesEOFBoundary(t *testing.T) { + // Reproduces #32506: a single logical line whose length is a multiple of + // bufio's default buffer (4096) and has no trailing newline used to be + // dropped entirely by YAMLReader, yielding empty values. + // Also cover 8192 (2x buffer) so we do not only hit the single-buffer case. + for _, size := range []int{4096, 8192} { + t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) { + prefix := []byte(`{"foo":"`) + suffix := []byte(`"}`) + pad := size - len(prefix) - len(suffix) + data := make([]byte, 0, size) + data = append(data, prefix...) + data = append(data, bytes.Repeat([]byte("x"), pad)...) + data = append(data, suffix...) + if len(data) != size { + t.Fatalf("test setup: want data length %d, got %d", size, len(data)) + } + + values, err := LoadValues(bytes.NewReader(data)) + require.NoError(t, err) + assert.Equal(t, map[string]any{ + "foo": string(bytes.Repeat([]byte("x"), pad)), + }, values) + }) + } +} + func TestMergeValuesV3(t *testing.T) { nestedMap := map[string]any{ "foo": "bar", diff --git a/pkg/chart/v2/loader/load.go b/pkg/chart/v2/loader/load.go index 28115d062..d7b125b9b 100644 --- a/pkg/chart/v2/loader/load.go +++ b/pkg/chart/v2/loader/load.go @@ -210,8 +210,20 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { // The reader is expected to contain one or more YAML documents, the values of which are merged. // And the values can be either a chart's default values or user-supplied values. func LoadValues(data io.Reader) (map[string]any, error) { + // Read fully first. YAMLReader/LineReader can drop a final unterminated + // line when its length is an exact multiple of bufio.Reader's default + // buffer (4096). Appending a trailing newline avoids that case. + // See https://github.com/helm/helm/issues/32506 + b, err := io.ReadAll(data) + if err != nil { + return nil, err + } + if len(b) > 0 && b[len(b)-1] != '\n' { + b = append(b, '\n') + } + values := map[string]any{} - reader := utilyaml.NewYAMLReader(bufio.NewReader(data)) + reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(b))) for { currentMap := map[string]any{} raw, err := reader.Read() diff --git a/pkg/chart/v2/loader/load_test.go b/pkg/chart/v2/loader/load_test.go index 8bf254321..ae1da1130 100644 --- a/pkg/chart/v2/loader/load_test.go +++ b/pkg/chart/v2/loader/load_test.go @@ -21,6 +21,7 @@ import ( "bytes" "compress/gzip" "errors" + "fmt" "io" "log" "os" @@ -463,6 +464,33 @@ foo: } } +func TestLoadValuesEOFBoundary(t *testing.T) { + // Reproduces #32506: a single logical line whose length is a multiple of + // bufio's default buffer (4096) and has no trailing newline used to be + // dropped entirely by YAMLReader, yielding empty values. + // Also cover 8192 (2x buffer) so we do not only hit the single-buffer case. + for _, size := range []int{4096, 8192} { + t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) { + prefix := []byte(`{"foo":"`) + suffix := []byte(`"}`) + pad := size - len(prefix) - len(suffix) + data := make([]byte, 0, size) + data = append(data, prefix...) + data = append(data, bytes.Repeat([]byte("x"), pad)...) + data = append(data, suffix...) + if len(data) != size { + t.Fatalf("test setup: want data length %d, got %d", size, len(data)) + } + + values, err := LoadValues(bytes.NewReader(data)) + require.NoError(t, err) + assert.Equal(t, map[string]any{ + "foo": string(bytes.Repeat([]byte("x"), pad)), + }, values) + }) + } +} + func TestMergeValuesV2(t *testing.T) { nestedMap := map[string]any{ "foo": "bar",