package handlers import ( "time" "unicode/utf8" ) // maxRenderedBodyBytes caps how many bytes of a stored event // body reach the event log page. Bodies come from the // unauthenticated receiver under the 1 MB ingest cap and // renderTemplate buffers a whole page before writing it, so // an uncapped page of paginationPerPage events is tens of // megabytes of resident memory per concurrent viewer. const maxRenderedBodyBytes = 8192 // eventLogColumns is the event log's projection. The casts to // blob are load-bearing: they make substr and length count // bytes rather than characters, so the cap bounds the page in // bytes whatever the payload's encoding. Cutting in SQLite // rather than in Go is the point of the projection — an // oversized body never becomes a Go string at all. const eventLogColumns = "id, created_at, method, content_type, " + "substr(cast(body as blob), 1, ?) AS body, " + "length(cast(body as blob)) AS body_bytes" // EventLogView is the display-safe projection of an event for // the event log page, alongside DeliveryView and TargetView. // It carries a capped body plus the true stored size, so the // page can mark a body as truncated without ever holding the // whole thing. type EventLogView struct { ID string CreatedAt time.Time Method string ContentType string // Body holds at most maxRenderedBodyBytes bytes of the // stored body. Body string // BodyBytes is the true size of the stored body. BodyBytes int64 // BodyTruncated reports that the stored body was larger // than the cap, so the page owes the reader a marker. BodyTruncated bool Deliveries []DeliveryView } // BodyShownBytes is how many body bytes the page is actually // rendering, which the truncation marker reports beside the // true size. func (v EventLogView) BodyShownBytes() int { return len(v.Body) } // eventLogRow is one row of the event log projection. Its // body column arrives already cut to the cap by SQLite, with // the true size beside it. type eventLogRow struct { ID string CreatedAt time.Time Method string ContentType string Body []byte BodyBytes int64 } // view projects a loaded row for rendering. func (r *eventLogRow) view() EventLogView { body := r.Body truncated := r.BodyBytes > int64(len(body)) // Only a cut body can have been left mid-sequence by // this query. A whole body is passed through exactly as // stored, however malformed. if truncated { body = trimPartialRune(body) } return EventLogView{ ID: r.ID, CreatedAt: r.CreatedAt, Method: r.Method, ContentType: r.ContentType, Body: string(body), BodyBytes: r.BodyBytes, BodyTruncated: truncated, } } // trimPartialRune drops a trailing UTF-8 sequence that the // byte-wise cut left incomplete, so a multi-byte rune severed // at the cap does not surface as a mojibake tail. // // Bytes that are merely invalid UTF-8 are left exactly as // stored: this service receives binary payloads, and rewriting // them would misreport what was delivered. The distinction is // utf8.FullRune's — it reports a complete sequence for an // invalid encoding too, since that decodes to a width-1 error // rune, so only a valid prefix still waiting for its // continuation bytes is removed. A tail with no rune start in // its last utf8.UTFMax bytes cannot be an incomplete sequence // either, and is likewise left alone. func trimPartialRune(b []byte) []byte { for i := len(b) - 1; i >= 0 && len(b)-i <= utf8.UTFMax; i-- { if !utf8.RuneStart(b[i]) { continue } if utf8.FullRune(b[i:]) { return b } return b[:i] } return b }