diff --git a/pkg/fuzz/dataformat/multipart.go b/pkg/fuzz/dataformat/multipart.go index 483814f28..a82ffcce0 100644 --- a/pkg/fuzz/dataformat/multipart.go +++ b/pkg/fuzz/dataformat/multipart.go @@ -27,7 +27,29 @@ var ( // NewMultiPartForm returns a new MultiPartForm encoder func NewMultiPartForm() *MultiPartForm { - return &MultiPartForm{} + return &MultiPartForm{ + filesMetadata: make(map[string]FileMetadata), + } +} + +// SetFileMetadata sets the file metadata for a given field name +func (m *MultiPartForm) SetFileMetadata(fieldName string, metadata FileMetadata) { + if m.filesMetadata == nil { + m.filesMetadata = make(map[string]FileMetadata) + } + + m.filesMetadata[fieldName] = metadata +} + +// GetFileMetadata gets the file metadata for a given field name +func (m *MultiPartForm) GetFileMetadata(fieldName string) (FileMetadata, bool) { + if m.filesMetadata == nil { + return FileMetadata{}, false + } + + metadata, exists := m.filesMetadata[fieldName] + + return metadata, exists } // IsType returns true if the data is MultiPartForm encoded @@ -125,16 +147,24 @@ func (m *MultiPartForm) ParseBoundary(contentType string) error { if m.boundary == "" { return fmt.Errorf("no boundary found in the content type") } + + // NOTE(dwisiswant0): boundary cannot exceed 70 characters according to + // RFC-2046. + if len(m.boundary) > 70 { + return fmt.Errorf("boundary exceeds maximum length of 70 characters") + } + return nil } // Decode decodes the data from MultiPartForm format func (m *MultiPartForm) Decode(data string) (KV, error) { + if m.boundary == "" { + return KV{}, fmt.Errorf("boundary not set, call ParseBoundary first") + } + // Create a buffer from the string data b := bytes.NewBufferString(data) - // The boundary parameter should be extracted from the Content-Type header of the HTTP request - // which is not available in this context, so this is a placeholder for demonstration. - // You will need to pass the actual boundary value to this function. r := multipart.NewReader(b, m.boundary) form, err := r.ReadForm(32 << 20) // 32MB is the max memory used to parse the form @@ -153,30 +183,44 @@ func (m *MultiPartForm) Decode(data string) (KV, error) { result.Set(key, values[0]) } } - m.filesMetadata = make(map[string]FileMetadata) + + if m.filesMetadata == nil { + m.filesMetadata = make(map[string]FileMetadata) + } + for key, files := range form.File { fileContents := []interface{}{} + var fileMetadataList []FileMetadata + for _, fileHeader := range files { file, err := fileHeader.Open() if err != nil { return KV{}, err } - defer func() { - _ = file.Close() - }() buffer := new(bytes.Buffer) if _, err := buffer.ReadFrom(file); err != nil { + _ = file.Close() + return KV{}, err } + _ = file.Close() + fileContents = append(fileContents, buffer.String()) - m.filesMetadata[key] = FileMetadata{ + fileMetadataList = append(fileMetadataList, FileMetadata{ ContentType: fileHeader.Header.Get("Content-Type"), Filename: fileHeader.Filename, - } + }) } + result.Set(key, fileContents) + + // NOTE(dwisiswant0): store the first file's metadata instead of the + // last one + if len(fileMetadataList) > 0 { + m.filesMetadata[key] = fileMetadataList[0] + } } return KVOrderedMap(&result), nil } diff --git a/pkg/fuzz/dataformat/multipart_test.go b/pkg/fuzz/dataformat/multipart_test.go index a649ee13a..1493e20d3 100644 --- a/pkg/fuzz/dataformat/multipart_test.go +++ b/pkg/fuzz/dataformat/multipart_test.go @@ -240,3 +240,131 @@ Python assert.Contains(t, documentsArray, "fake_pdf_content_1") assert.Contains(t, documentsArray, "fake_pdf_content_2") } + +func TestMultiPartForm_SetGetFileMetadata(t *testing.T) { + form := NewMultiPartForm() + metadata := FileMetadata{ + ContentType: "image/jpeg", + Filename: "test.jpg", + } + form.SetFileMetadata("avatar", metadata) + + // Test GetFileMetadata for existing field + retrievedMetadata, exists := form.GetFileMetadata("avatar") + assert.True(t, exists) + assert.Equal(t, metadata.ContentType, retrievedMetadata.ContentType) + assert.Equal(t, metadata.Filename, retrievedMetadata.Filename) + + // Test GetFileMetadata for non-existing field + _, exists = form.GetFileMetadata("nonexistent") + assert.False(t, exists) +} + +func TestMultiPartForm_FilesMetadataInitialization(t *testing.T) { + form := NewMultiPartForm() + assert.NotNil(t, form.filesMetadata) + + metadata := FileMetadata{ + ContentType: "text/plain", + Filename: "test.txt", + } + form.SetFileMetadata("file", metadata) + + retrievedMetadata, exists := form.GetFileMetadata("file") + assert.True(t, exists) + assert.Equal(t, metadata, retrievedMetadata) +} + +func TestMultiPartForm_BoundaryValidation(t *testing.T) { + form := NewMultiPartForm() + + // Test valid boundary + err := form.ParseBoundary("multipart/form-data; boundary=testboundary") + assert.NoError(t, err) + assert.Equal(t, "testboundary", form.boundary) + + // Test missing boundary + err = form.ParseBoundary("multipart/form-data") + assert.Error(t, err) + assert.Contains(t, err.Error(), "no boundary found") + + // Test boundary too long (over 70 characters) + longBoundary := "multipart/form-data; boundary=" + string(make([]byte, 71)) + for i := range longBoundary[len("multipart/form-data; boundary="):] { + longBoundary = longBoundary[:len("multipart/form-data; boundary=")+i] + "a" + longBoundary[len("multipart/form-data; boundary=")+i+1:] + } + + err = form.ParseBoundary(longBoundary) + assert.Error(t, err) + assert.Contains(t, err.Error(), "boundary exceeds maximum length") +} + +func TestMultiPartForm_DecodeRequiresBoundary(t *testing.T) { + form := NewMultiPartForm() + + // Decode should fail if boundary is not set + _, err := form.Decode("some data") + assert.Error(t, err) + assert.Contains(t, err.Error(), "boundary not set") +} + +func TestMultiPartForm_MultipleFilesMetadata(t *testing.T) { + form := NewMultiPartForm() + form.boundary = "----WebKitFormBoundaryMultiFileTest" + + // Test with multiple files having the same field name + multipartData := `------WebKitFormBoundaryMultiFileTest +Content-Disposition: form-data; name="documents"; filename="file1.txt" +Content-Type: text/plain + +content1 +------WebKitFormBoundaryMultiFileTest +Content-Disposition: form-data; name="documents"; filename="file2.txt" +Content-Type: text/plain + +content2 +------WebKitFormBoundaryMultiFileTest-- +` + + decoded, err := form.Decode(multipartData) + require.NoError(t, err) + + // Verify files are decoded correctly + documents := decoded.Get("documents") + require.NotNil(t, documents) + documentsArray, ok := documents.([]interface{}) + require.True(t, ok) + require.Len(t, documentsArray, 2) + assert.Contains(t, documentsArray, "content1") + assert.Contains(t, documentsArray, "content2") + + // Verify metadata for the field exists (should be from the first file) + metadata, exists := form.GetFileMetadata("documents") + assert.True(t, exists) + assert.Equal(t, "text/plain", metadata.ContentType) + assert.Equal(t, "file1.txt", metadata.Filename) // Should be from first file, not last +} + +func TestMultiPartForm_SetFileMetadataWithNilMap(t *testing.T) { + form := &MultiPartForm{} + + // SetFileMetadata should handle nil filesMetadata + metadata := FileMetadata{ + ContentType: "application/pdf", + Filename: "document.pdf", + } + form.SetFileMetadata("doc", metadata) + + // Should be able to retrieve the metadata + retrievedMetadata, exists := form.GetFileMetadata("doc") + assert.True(t, exists) + assert.Equal(t, metadata, retrievedMetadata) +} + +func TestMultiPartForm_GetFileMetadataWithNilMap(t *testing.T) { + form := &MultiPartForm{} + + // GetFileMetadata should handle nil filesMetadata gracefully + _, exists := form.GetFileMetadata("anything") + assert.False(t, exists) +}