From 5091e19239c3524ca6a3af657c65d10354b6daec Mon Sep 17 00:00:00 2001 From: Nanook Date: Fri, 5 Jun 2026 13:43:10 +0000 Subject: [PATCH] fix: validate web feature entries --- .../web_feature_consumer/pkg/data/parser.go | 68 ++++++++++++++++++- .../pkg/data/parser_test.go | 61 ++++++++++++++++- 2 files changed, 126 insertions(+), 3 deletions(-) diff --git a/workflows/steps/services/web_feature_consumer/pkg/data/parser.go b/workflows/steps/services/web_feature_consumer/pkg/data/parser.go index 2c926a569..275473b7e 100644 --- a/workflows/steps/services/web_feature_consumer/pkg/data/parser.go +++ b/workflows/steps/services/web_feature_consumer/pkg/data/parser.go @@ -59,6 +59,11 @@ func (p V3Parser) Parse(in io.ReadCloser) (*webdxfeaturetypes.ProcessedWebFeatur return nil, errors.Join(ErrUnexpectedFormat, err) } + err = validateRawWebFeaturesJSONDataV3(&source) + if err != nil { + return nil, errors.Join(ErrUnexpectedFormat, err) + } + processedData, err := postProcessV3(&source) if err != nil { return nil, errors.Join(ErrUnableToProcess, err) @@ -67,6 +72,61 @@ func (p V3Parser) Parse(in io.ReadCloser) (*webdxfeaturetypes.ProcessedWebFeatur return processedData, nil } +func validateRawWebFeaturesJSONDataV3(data *rawWebFeaturesJSONDataV3) error { + return validateFeaturesV3(data.Features) +} + +func validateFeaturesV3(data json.RawMessage) error { + if len(data) == 0 { + return errors.New("missing web features") + } + + featureRawMessageMap := make(map[string]json.RawMessage) + if err := json.Unmarshal(data, &featureRawMessageMap); err != nil { + return err + } + if len(featureRawMessageMap) == 0 { + return errors.New("missing web features") + } + + for id, rawFeature := range featureRawMessageMap { + if strings.TrimSpace(id) == "" { + return errors.New("empty web feature ID") + } + + var peek featureKindPeek + if err := json.Unmarshal(rawFeature, &peek); err != nil { + return err + } + + switch peek.Kind { + case string(web_platform_dx__web_features_v3.Feature): + if err := validateFeatureNameV3(rawFeature); err != nil { + return err + } + case string(web_platform_dx__web_features_v3.Moved), string(web_platform_dx__web_features_v3.Split): + default: + return errors.New("unknown web feature kind") + } + } + + return nil +} + +func validateFeatureNameV3(rawFeature json.RawMessage) error { + var value struct { + Name *string `json:"name"` + } + if err := json.Unmarshal(rawFeature, &value); err != nil { + return err + } + if value.Name == nil || strings.TrimSpace(*value.Name) == "" { + return errors.New("empty web feature name") + } + + return nil +} + func postProcessV3(data *rawWebFeaturesJSONDataV3) (*webdxfeaturetypes.ProcessedWebFeaturesData, error) { featureKinds, err := postProcessFeatureValueV3(data.Features) if err != nil { @@ -158,8 +218,7 @@ func postProcessFeatureValueV3(data json.RawMessage) (*webdxfeaturetypes.Feature // Peek inside the raw JSON to find the "kind" var peek featureKindPeek if err := json.Unmarshal(rawFeature, &peek); err != nil { - // Skip or log features that don't have a 'kind' field - continue + return nil, err } // Switch on the explicit "kind" to unmarshal into the correct type @@ -193,6 +252,8 @@ func postProcessFeatureValueV3(data json.RawMessage) (*webdxfeaturetypes.Feature return nil, err } featureKinds.Split[id] = *split + default: + return nil, ErrUnexpectedFormat } } @@ -209,6 +270,9 @@ func processFeatureKind(rawFeature json.RawMessage) (*webdxfeaturetypes.FeatureV if value.Description == nil || value.DescriptionHTML == nil || value.Name == nil || value.Status == nil { return nil, ErrUnexpectedFormat } + if strings.TrimSpace(*value.Name) == "" { + return nil, ErrUnexpectedFormat + } feature := &webdxfeaturetypes.FeatureValue{ Caniuse: value.Caniuse, CompatFeatures: value.CompatFeatures, diff --git a/workflows/steps/services/web_feature_consumer/pkg/data/parser_test.go b/workflows/steps/services/web_feature_consumer/pkg/data/parser_test.go index 0e19db993..2f5c01485 100644 --- a/workflows/steps/services/web_feature_consumer/pkg/data/parser_test.go +++ b/workflows/steps/services/web_feature_consumer/pkg/data/parser_test.go @@ -46,7 +46,7 @@ func TestParseV3(t *testing.T) { } result, err := V3Parser{}.Parse(file) if err != nil { - t.Errorf("unable to parse file err %s", err.Error()) + t.Fatalf("unable to parse file err %s", err.Error()) } if len(result.Features.Data) == 0 { t.Error("unexpected empty map for features") @@ -85,6 +85,30 @@ func TestParseError(t *testing.T) { expectedError: ErrUnexpectedFormat, testParser: V3Parser{}, }, + { + name: "empty feature name", + input: io.NopCloser(strings.NewReader(dataJSONV3(emptyFeatureNameMapV3()))), + expectedError: ErrUnexpectedFormat, + testParser: V3Parser{}, + }, + { + name: "unexpected feature-like data", + input: io.NopCloser(strings.NewReader(dataJSONV3(unexpectedFeatureLikeMapV3()))), + expectedError: ErrUnexpectedFormat, + testParser: V3Parser{}, + }, + { + name: "empty feature ID", + input: io.NopCloser(strings.NewReader(dataJSONV3(emptyFeatureIDMapV3()))), + expectedError: ErrUnexpectedFormat, + testParser: V3Parser{}, + }, + { + name: "malformed feature JSON", + input: io.NopCloser(strings.NewReader(dataJSONV3(malformedFeatureMapV3()))), + expectedError: ErrUnexpectedFormat, + testParser: V3Parser{}, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { @@ -99,6 +123,41 @@ func TestParseError(t *testing.T) { } } +func dataJSONV3(features string) string { + return `{"browsers":{ + "chrome":{"name":"Chrome","releases":[]}, + "chrome_android":{"name":"Chrome Android","releases":[]}, + "edge":{"name":"Edge","releases":[]}, + "firefox":{"name":"Firefox","releases":[]}, + "firefox_android":{"name":"Firefox Android","releases":[]}, + "safari":{"name":"Safari","releases":[]}, + "safari_ios":{"name":"Safari on iOS","releases":[]} +},"features":` + features + `,"groups":{},"snapshots":{}}` +} + +func emptyFeatureNameMapV3() string { + return `{"feature1":{ + "kind":"feature", + "name":"", + "description":"Description", + "description_html":"Description", + "spec":["https://example.com"], + "status":{"baseline":false,"support":{}} +}}` +} + +func unexpectedFeatureLikeMapV3() string { + return `{"feature1":{"feature_id":"features","name":""}}` +} + +func emptyFeatureIDMapV3() string { + return `{"":{"kind":"feature","name":"Feature 1"}}` +} + +func malformedFeatureMapV3() string { + return `{"feature1":false}` +} + func testBrowsersV3() web_platform_dx__web_features_v3.Browsers { return web_platform_dx__web_features_v3.Browsers{ Chrome: web_platform_dx__web_features_v3.BrowserData{