From 0b3ccb2039c8fd787f78efe66abaa9a4e8cc56ad Mon Sep 17 00:00:00 2001 From: Ewuji John Date: Fri, 11 Sep 2026 19:16:04 +0100 Subject: [PATCH 1/2] detect incomplete package streams Signed-off-by: Ewuji John --- pkg/xpkg/parser/parser.go | 2 +- pkg/xpkg/parser/parser_test.go | 49 ++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/pkg/xpkg/parser/parser.go b/pkg/xpkg/parser/parser.go index 561c2f9f8..e77cb159f 100644 --- a/pkg/xpkg/parser/parser.go +++ b/pkg/xpkg/parser/parser.go @@ -115,7 +115,7 @@ func (p *PackageParser) Parse(_ context.Context, reader io.ReadCloser) (*Package for { content, err := yr.Read() if err != nil && !errors.Is(err, io.EOF) { - return pkg, err + return pkg, annotateErr(err, reader) } if errors.Is(err, io.EOF) { diff --git a/pkg/xpkg/parser/parser_test.go b/pkg/xpkg/parser/parser_test.go index 94d84d472..596555a24 100644 --- a/pkg/xpkg/parser/parser_test.go +++ b/pkg/xpkg/parser/parser_test.go @@ -19,6 +19,8 @@ package parser import ( "bytes" "context" + "errors" + "io" "testing" "github.com/google/go-cmp/cmp" @@ -220,6 +222,53 @@ func TestParser(t *testing.T) { } } +func TestParserRejectsIncompleteDocument(t *testing.T) { + objScheme := runtime.NewScheme() + metaScheme := runtime.NewScheme() + _ = apiextensions.AddToScheme(objScheme) + _ = appsv1.AddToScheme(metaScheme) + + cases := map[string]struct { + reader io.ReadCloser + }{ + "MalformedYAML": { + reader: io.NopCloser(bytes.NewReader([]byte(`apiVersion: apps/v1 +kind: Deployment +metadata: + name: "test`))), + }, + "UnexpectedEOF": { + reader: &errorReadCloser{reader: bytes.NewReader([]byte(`apiVersion: apps/v1 +kind: Deployment +metadata: + name: test`))}, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + _, err := New(metaScheme, objScheme).Parse(context.Background(), tc.reader) + if err == nil { + t.Fatal("Parse(...): expected error") + } + }) + } +} + +type errorReadCloser struct { + reader *bytes.Reader +} + +func (r *errorReadCloser) Read(p []byte) (int, error) { + n, err := r.reader.Read(p) + if err == io.EOF { + return n, errors.New("unexpected EOF") + } + return n, err +} + +func (r *errorReadCloser) Close() error { return nil } + func TestCleanYAML(t *testing.T) { type args struct { in []byte From 9dc2cea0b2fa05dc77b195714a53f22a8b381706 Mon Sep 17 00:00:00 2001 From: Ewuji John Date: Fri, 11 Sep 2026 19:48:16 +0100 Subject: [PATCH 2/2] test annotated parser read errors Signed-off-by: Ewuji John --- pkg/xpkg/parser/parser_test.go | 43 +++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/pkg/xpkg/parser/parser_test.go b/pkg/xpkg/parser/parser_test.go index 596555a24..a474a74ec 100644 --- a/pkg/xpkg/parser/parser_test.go +++ b/pkg/xpkg/parser/parser_test.go @@ -20,6 +20,7 @@ import ( "bytes" "context" "errors" + "fmt" "io" "testing" @@ -227,42 +228,72 @@ func TestParserRejectsIncompleteDocument(t *testing.T) { metaScheme := runtime.NewScheme() _ = apiextensions.AddToScheme(objScheme) _ = appsv1.AddToScheme(metaScheme) + errUnexpectedEOF := errors.New("unexpected EOF") cases := map[string]struct { + reason string reader io.ReadCloser + want error }{ "MalformedYAML": { + reason: "should reject malformed YAML", reader: io.NopCloser(bytes.NewReader([]byte(`apiVersion: apps/v1 kind: Deployment metadata: name: "test`))), }, - "UnexpectedEOF": { - reader: &errorReadCloser{reader: bytes.NewReader([]byte(`apiVersion: apps/v1 + "AnnotatedUnexpectedEOF": { + reason: "should preserve the reader annotation on stream errors", + reader: &annotatedReadCloser{ + ReadCloser: &errorReadCloser{ + reader: bytes.NewReader([]byte(`apiVersion: apps/v1 kind: Deployment metadata: - name: test`))}, + name: test`)), + err: errUnexpectedEOF, + }, + annotation: "package.yaml", + }, + want: fmt.Errorf("package.yaml: %w", errUnexpectedEOF), }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { _, err := New(metaScheme, objScheme).Parse(context.Background(), tc.reader) - if err == nil { - t.Fatal("Parse(...): expected error") + if tc.want == nil { + if err == nil { + t.Errorf("%s: Parse(...): expected error, got nil", tc.reason) + } + return + } + + if diff := cmp.Diff(errUnexpectedEOF, errors.Unwrap(err), cmpopts.EquateErrors()); diff != "" { + t.Errorf("%s: Parse(...): -want cause, +got cause:\n%s", tc.reason, diff) + } + if diff := cmp.Diff(tc.want.Error(), err.Error()); diff != "" { + t.Errorf("%s: Parse(...): -want err, +got err:\n%s", tc.reason, diff) } }) } } +type annotatedReadCloser struct { + io.ReadCloser + annotation any +} + +func (r *annotatedReadCloser) Annotate() any { return r.annotation } + type errorReadCloser struct { reader *bytes.Reader + err error } func (r *errorReadCloser) Read(p []byte) (int, error) { n, err := r.reader.Read(p) if err == io.EOF { - return n, errors.New("unexpected EOF") + return n, r.err } return n, err }