diff --git a/command/v7/curl_command.go b/command/v7/curl_command.go index 764c5075254..13094126f45 100644 --- a/command/v7/curl_command.go +++ b/command/v7/curl_command.go @@ -1,8 +1,11 @@ package v7 import ( + "bytes" + "net/http" "net/http/httputil" "os" + "strings" "code.cloudfoundry.org/cli/command/flag" "code.cloudfoundry.org/cli/command/translatableerror" @@ -39,9 +42,9 @@ func (cmd CurlCommand) Execute(args []string) error { } var bytesToWrite []byte - - if cmd.IncludeResponseHeaders { - headerBytes, _ := httputil.DumpResponse(httpResponse, false) + var headerBytes []byte + if cmd.IncludeResponseHeaders && httpResponse != nil { + headerBytes, _ = httputil.DumpResponse(httpResponse, false) bytesToWrite = append(bytesToWrite, headerBytes...) } @@ -54,9 +57,38 @@ func (cmd CurlCommand) Execute(args []string) error { } cmd.UI.DisplayOK() + return nil + } + + // Check if the response contains binary data + if isBinary(httpResponse, responseBodyBytes) { + // For binary data, write response headers with string conversion + // and the response body without string conversion + if cmd.IncludeResponseHeaders { + cmd.UI.DisplayTextLiteral(string(headerBytes)) + } + cmd.UI.GetOut().Write(responseBodyBytes) } else { cmd.UI.DisplayText(string(bytesToWrite)) } return nil } + +// isBinary determines if the provided `data` is likely binary content. +// It first checks if the given `contentType` (e.g., from an HTTP header) is a known binary MIME type. +// If not, it then scans the `data` byte slice for the presence of null bytes (0x00), +// which are a strong heuristic for binary data. +// Returns `true` if identified as binary, `false` otherwise. +func isBinary(response *http.Response, data []byte) bool { + responseContextType := "" + if response != nil && response.Header != nil { + responseContextType = response.Header.Get("Content-Type") + } + if strings.Contains(responseContextType, "image/") || + strings.Contains(responseContextType, "application/octet-stream") || + strings.Contains(responseContextType, "application/pdf") { + return true + } + return bytes.ContainsRune(data, 0x00) // Check for null byte +} diff --git a/command/v7/curl_command_test.go b/command/v7/curl_command_test.go index e7fc9fe8a89..561259997aa 100644 --- a/command/v7/curl_command_test.go +++ b/command/v7/curl_command_test.go @@ -149,4 +149,88 @@ var _ = Describe("curl Command", func() { }) }) + When("the response contains binary data", func() { + var binaryData []byte + + BeforeEach(func() { + // Create binary data with null bytes (like a droplet file) + binaryData = []byte{0x50, 0x4B, 0x03, 0x04, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00} + fakeActor.MakeCurlRequestReturns(binaryData, &http.Response{ + Header: http.Header{ + "Content-Type": []string{"application/octet-stream"}, + }, + }, nil) + }) + + It("writes binary data directly to stdout without string conversion", func() { + Expect(executeErr).NotTo(HaveOccurred()) + Expect(testUI.Out).To(Say(string(binaryData))) + }) + + When("Content-Type is not a known binary MIME type", func() { + BeforeEach(func() { + // Create binary data with null bytes (like a droplet file) + binaryData = []byte{0x50, 0x4B, 0x03, 0x04, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00} + fakeActor.MakeCurlRequestReturns(binaryData, &http.Response{ + Header: http.Header{ + "Content-Type": []string{"text/plain"}, + }, + }, nil) + }) + It("inspects the response data and writes binary data directly to stdout without string conversion", func() { + Expect(executeErr).NotTo(HaveOccurred()) + Expect(testUI.Out).To(Say(string(binaryData))) + }) + }) + + When("include-response-headers flag is set", func() { + BeforeEach(func() { + cmd.IncludeResponseHeaders = true + }) + + It("writes headers as text and binary data separately", func() { + Expect(executeErr).NotTo(HaveOccurred()) + + // Check that headers are written as text (using DisplayTextLiteral) + Expect(testUI.Out).To(Say("Content-Type: application/octet-stream")) + + // Check that binary data is preserved + Expect(testUI.Out).To(Say(string(binaryData))) + }) + }) + + When("output file is specified", func() { + BeforeEach(func() { + outputFile, err := os.CreateTemp("", "binary-output") + Expect(err).NotTo(HaveOccurred()) + cmd.OutputFile = flag.Path(outputFile.Name()) + }) + + AfterEach(func() { + os.RemoveAll(string(cmd.OutputFile)) + }) + + It("writes binary data to the file correctly", func() { + Expect(executeErr).NotTo(HaveOccurred()) + + fileContents, err := os.ReadFile(string(cmd.OutputFile)) + Expect(err).ToNot(HaveOccurred()) + Expect(fileContents).To(Equal(binaryData)) + }) + }) + }) + + When("the response is empty", func() { + BeforeEach(func() { + fakeActor.MakeCurlRequestReturns([]byte{}, &http.Response{ + Header: http.Header{}, + }, nil) + }) + + It("handles empty response correctly", func() { + Expect(executeErr).NotTo(HaveOccurred()) + Expect(testUI.Out).To(Say("")) + }) + }) + })