internal/tui/view/request/body.go

package request

import (
	"bytes"
	"encoding/json"
	"strings"
	"unicode/utf8"

	"mercemay.top/httptap/internal/parser"
	"mercemay.top/httptap/internal/parser/content"
	"mercemay.top/httptap/internal/tui/theme"
)

// RenderBody produces a display string for the message body. JSON is
// pretty-printed, text/* is shown verbatim, binary is rendered as a
// compact hex dump.
func RenderBody(pal theme.Palette, msg parser.Message) string {
	body := msg.Body
	if enc := parser.Header(msg, "Content-Encoding"); enc != "" {
		if decoded, err := content.Decode(body, enc); err == nil {
			body = decoded
		}
	}
	if len(body) == 0 {
		return pal.Detail.Render("(empty body)")
	}
	ct := parser.Header(msg, "Content-Type")
	switch {
	case strings.Contains(ct, "application/json"):
		return prettyJSON(body)
	case strings.HasPrefix(ct, "text/"),
		strings.Contains(ct, "application/javascript"),
		strings.Contains(ct, "application/xml"):
		return string(body)
	case utf8.Valid(body) && !isMostlyControl(body):
		return string(body)
	default:
		return hexDump(body)
	}
}

// prettyJSON calls json.Indent. On failure it returns the raw string so
// the UI never looks blank.
func prettyJSON(body []byte) string {
	var buf bytes.Buffer
	if err := json.Indent(&buf, body, "", "  "); err != nil {
		return string(body)
	}
	return buf.String()
}

// hexDump is a compact xxd-style dump with 16 bytes per line.
func hexDump(body []byte) string {
	const width = 16
	var b strings.Builder
	for i := 0; i < len(body); i += width {
		end := i + width
		if end > len(body) {
			end = len(body)
		}
		line := body[i:end]
		b.WriteString(offsetHex(i))
		b.WriteString("  ")
		for j := 0; j < width; j++ {
			if j < len(line) {
				b.WriteString(byteHex(line[j]))
			} else {
				b.WriteString("   ")
			}
			if j == 7 {
				b.WriteByte(' ')
			}
		}
		b.WriteString(" |")
		for _, c := range line {
			if c >= 0x20 && c < 0x7f {
				b.WriteByte(c)
			} else {
				b.WriteByte('.')
			}
		}
		b.WriteString("|\n")
	}
	return strings.TrimRight(b.String(), "\n")
}

func offsetHex(n int) string {
	const digits = "0123456789abcdef"
	var out [8]byte
	for i := 7; i >= 0; i-- {
		out[i] = digits[n&0xf]
		n >>= 4
	}
	return string(out[:])
}

func byteHex(b byte) string {
	const digits = "0123456789abcdef"
	return string([]byte{digits[b>>4], digits[b&0xf], ' '})
}

func isMostlyControl(b []byte) bool {
	n := 0
	for _, c := range b {
		if c < 0x09 || (c > 0x0d && c < 0x20) {
			n++
		}
	}
	return len(b) > 0 && n*4 > len(b)
}