package protocol import ( "encoding/hex" "fmt" "hash/fnv" "strconv" "strings" ) // Relay indices: each MediaMeta.Relays[N] flags whether the file is // reachable via that relay. Order is fixed so the wire format is positional. // Future relays append to this list; older clients ignore unknown trailing // flags. const ( RelayDNS = 0 // slow path — bytes assembled from DNS blocks RelayGitHub = 1 // fast path — bytes pulled from a GitHub repo ) // MediaMeta describes a downloadable media blob attached to a feed message. // // Wire format (immediately after the media tag, before any caption): // // [IMAGE]:,,...:::[:] // // where each is "1" or "0" indicating availability via relay N. // : are only meaningful when f0 (RelayDNS) is set. type MediaMeta struct { Tag string // e.g. MediaImage, MediaVideo, MediaFile Size int64 Relays []bool // index = relay constant, value = availability Channel uint16 // DNS channel (when Relays[RelayDNS]) Blocks uint16 // DNS block count (when Relays[RelayDNS]) CRC32 uint32 Filename string } // HasRelay reports whether the relay at idx is available. Out-of-range and // nil-relay-list both return false. func (m MediaMeta) HasRelay(idx int) bool { if idx < 0 || idx >= len(m.Relays) { return false } return m.Relays[idx] } // HasAnyRelay reports whether at least one relay can serve this file. func (m MediaMeta) HasAnyRelay() bool { for _, on := range m.Relays { if on { return true } } return false } // String renders the metadata in the wire format documented above. func (m MediaMeta) String() string { flags := encodeRelayFlags(m.Relays) if fn := SanitiseMediaFilename(m.Filename); fn != "" { return fmt.Sprintf("%s%d:%s:%d:%d:%08x:%s\n", m.Tag, m.Size, flags, m.Channel, m.Blocks, m.CRC32, fn) } return fmt.Sprintf("%s%d:%s:%d:%d:%08x\n", m.Tag, m.Size, flags, m.Channel, m.Blocks, m.CRC32) } // encodeRelayFlags serialises a relay list as "1,0,1". An empty list is // "0,0" (DNS off, GitHub off) so older clients always see at least the two // known relay slots. func encodeRelayFlags(relays []bool) string { n := len(relays) if n < 2 { n = 2 } parts := make([]string, n) for i := 0; i < n; i++ { on := i < len(relays) && relays[i] if on { parts[i] = "1" } else { parts[i] = "0" } } return strings.Join(parts, ",") } // parseRelayFlags decodes "1,0,1" into a relay slice sized to the input. // Caller-side accessors guard against out-of-range reads, so future relays // can be added without breaking older clients. func parseRelayFlags(s string) ([]bool, bool) { if s == "" { return nil, false } parts := strings.Split(s, ",") out := make([]bool, len(parts)) for i, p := range parts { p = strings.TrimSpace(p) if p != "0" && p != "1" { return nil, false } out[i] = p == "1" } return out, true } // SanitiseMediaFilename returns a filename safe to embed in the wire // metadata line. The output uses a restricted alphabet ([A-Za-z0-9._-]) so // no path separator, colon, newline, or control char can ever survive. // When the input is too long the base name is replaced with a short // hash-derived id but the extension is preserved so other OSes still // recognise the file type. func SanitiseMediaFilename(s string) string { s = strings.TrimSpace(s) if s == "" { return "" } if i := strings.LastIndexAny(s, `/\`); i >= 0 { s = s[i+1:] } cleaned := filterFilenameRunes(s) if cleaned == "" || cleaned == "." || cleaned == ".." { return "" } const maxBase = 64 const maxExt = 8 base, ext := splitFilenameExt(cleaned) if len(ext) > maxExt { ext = ext[:maxExt] } if len(base) > maxBase { h := fnv.New64a() _, _ = h.Write([]byte(cleaned)) base = "media-" + hex.EncodeToString(h.Sum(nil))[:8] } if base == "" || base == "." { base = "media" } if ext != "" { return base + "." + ext } return base } func filterFilenameRunes(s string) string { var b strings.Builder for _, r := range s { switch { case r >= '0' && r <= '9', r >= 'A' && r <= 'Z', r >= 'a' && r <= 'z', r == '.', r == '_', r == '-': b.WriteRune(r) } } return b.String() } func splitFilenameExt(s string) (base, ext string) { if i := strings.LastIndexByte(s, '.'); i >= 0 && i < len(s)-1 { return s[:i], s[i+1:] } return s, "" } // EncodeMediaText prepends the metadata line to an optional caption and // returns the combined message text. A nil/empty caption yields just the tag // + metadata + trailing newline-less string (the caption split is by the // metadata line's trailing \n, so an empty caption simply has no extra body). func EncodeMediaText(meta MediaMeta, caption string) string { header := meta.String() if caption == "" { // Drop the trailing newline so the message text doesn't end with a // blank line for caption-less media. return strings.TrimSuffix(header, "\n") } return header + caption } // ParseMediaText parses a message body that begins with a known media tag. // Returns metadata + remaining caption. Legacy "[TAG]\ncaption" bodies parse // with empty Relays (HasAnyRelay()==false). Unknown tags return ok=false. func ParseMediaText(body string) (meta MediaMeta, caption string, ok bool) { tag, rest, found := splitKnownMediaTag(body) if !found { return MediaMeta{}, body, false } meta.Tag = tag // The bit between the tag and the first newline is the metadata payload. nl := strings.IndexByte(rest, '\n') var metaLine string if nl < 0 { metaLine = rest caption = "" } else { metaLine = rest[:nl] caption = rest[nl+1:] } metaLine = strings.TrimSpace(metaLine) if metaLine == "" { // Legacy [TAG]\ncaption — no per-file metadata. Treat as not-downloadable. return MediaMeta{Tag: tag}, caption, true } parts := strings.Split(metaLine, ":") if len(parts) < 5 { return MediaMeta{Tag: tag}, rest, true } size, err := strconv.ParseInt(parts[0], 10, 64) if err != nil || size < 0 { return MediaMeta{Tag: tag}, rest, true } relays, ok := parseRelayFlags(parts[1]) if !ok { return MediaMeta{Tag: tag}, rest, true } ch, err := strconv.ParseUint(parts[2], 10, 16) if err != nil { return MediaMeta{Tag: tag}, rest, true } blk, err := strconv.ParseUint(parts[3], 10, 16) if err != nil { return MediaMeta{Tag: tag}, rest, true } crc, err := strconv.ParseUint(parts[4], 16, 32) if err != nil { return MediaMeta{Tag: tag}, rest, true } // Reject DNS availability if the channel/block range is malformed — // other relays stay as-claimed. channel := uint16(ch) if len(relays) > RelayDNS && relays[RelayDNS] && (!IsMediaChannel(channel) || blk == 0) { relays[RelayDNS] = false } meta.Size = size meta.Relays = relays meta.Channel = channel meta.Blocks = uint16(blk) meta.CRC32 = uint32(crc) if len(parts) >= 6 { meta.Filename = SanitiseMediaFilename(parts[5]) } return meta, caption, true } // knownMediaTags are the message text prefixes that mark a downloadable media // attachment. Order matters only for prefix matching; longer/more-specific // tags are not currently aliased so the order is alphabetical for clarity. var knownMediaTags = []string{ MediaAudio, MediaFile, MediaGIF, MediaImage, MediaSticker, MediaVideo, } // splitKnownMediaTag returns the matched tag and the remainder of the body // when body starts with one of knownMediaTags. func splitKnownMediaTag(body string) (tag, rest string, ok bool) { for _, t := range knownMediaTags { if strings.HasPrefix(body, t) { return t, body[len(t):], true } } return "", body, false }