Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 66 additions & 2 deletions workflows/steps/services/web_feature_consumer/pkg/data/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -193,6 +252,8 @@ func postProcessFeatureValueV3(data json.RawMessage) (*webdxfeaturetypes.Feature
return nil, err
}
featureKinds.Split[id] = *split
default:
return nil, ErrUnexpectedFormat
}
}

Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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) {
Expand All @@ -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{
Expand Down