mirror of
https://github.com/sartoopjj/thefeed.git
synced 2026-05-18 06:44:34 +03:00
219 lines
6.2 KiB
Go
219 lines
6.2 KiB
Go
package protocol
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"hash/crc32"
|
|
)
|
|
|
|
// Profile pictures use a hybrid layout: every avatar is concatenated
|
|
// into one bundle uploaded to the GitHub relay (one file → no
|
|
// per-file rate limit), and each avatar also gets its own DNS media
|
|
// channel so partial fetches over DNS still display.
|
|
//
|
|
// Wire layout of ProfilePicsChannel (after the block-count prefix the
|
|
// Feed layer adds):
|
|
//
|
|
// bundleSize uint32
|
|
// bundleCRC uint32
|
|
// relayCount uint8 — N
|
|
// relays [N]u8 — bool per relay (RelayDNS=0, RelayGitHub=1, …)
|
|
// count uint16
|
|
// entries:
|
|
// usernameLen uint8
|
|
// username [usernameLen]byte
|
|
// offset uint32 — within the GitHub bundle
|
|
// size uint32
|
|
// crc uint32 — CRC32 of bundle[offset:offset+size]
|
|
// mime uint8 — 0=jpeg, 1=png, 2=webp
|
|
// dnsChannel uint16 — 0 if not on DNS
|
|
// dnsBlocks uint16
|
|
type ProfilePicsBundleHeader struct {
|
|
BundleSize uint32
|
|
BundleCRC uint32
|
|
// One bool per relay constant. RelayGitHub here means the bundle
|
|
// is on GitHub; RelayDNS for the bundle is rare (per-entry DNS
|
|
// channels handle the DNS path).
|
|
Relays []bool
|
|
}
|
|
|
|
// HasRelay reports whether the relay at idx is set. Out of range returns false.
|
|
func (h ProfilePicsBundleHeader) HasRelay(idx int) bool {
|
|
if idx < 0 || idx >= len(h.Relays) {
|
|
return false
|
|
}
|
|
return h.Relays[idx]
|
|
}
|
|
|
|
// ProfilePicEntry points at one avatar via either the GitHub bundle
|
|
// (Offset/Size into the concatenated blob) or its own DNS channel
|
|
// (DNSChannel/DNSBlocks). Both paths verify the same Size + CRC.
|
|
type ProfilePicEntry struct {
|
|
Username string
|
|
Offset uint32
|
|
Size uint32
|
|
CRC uint32
|
|
MIME uint8
|
|
DNSChannel uint16
|
|
DNSBlocks uint16
|
|
}
|
|
|
|
// MIME tag values.
|
|
const (
|
|
ProfilePicMimeJPEG uint8 = 0
|
|
ProfilePicMimePNG uint8 = 1
|
|
ProfilePicMimeWebP uint8 = 2
|
|
)
|
|
|
|
// On-the-wire byte counts.
|
|
const (
|
|
profilePicEntryFixed = 4 + 4 + 4 + 1 + 2 + 2 // offset+size+crc+mime+dnsCh+dnsBlk
|
|
profilePicsHeaderFixed = 4 + 4 + 1 // bundleSize+bundleCRC+relayCount
|
|
)
|
|
|
|
// ProfilePicsBundle is the directory (header + entries). The bundle
|
|
// bytes themselves live in the referenced media channel / relay.
|
|
type ProfilePicsBundle struct {
|
|
Header ProfilePicsBundleHeader
|
|
Entries []ProfilePicEntry
|
|
}
|
|
|
|
// EncodeProfilePicsBundle serialises the directory.
|
|
func EncodeProfilePicsBundle(b ProfilePicsBundle) []byte {
|
|
relayCount := len(b.Header.Relays)
|
|
if relayCount > 255 {
|
|
relayCount = 255
|
|
}
|
|
size := profilePicsHeaderFixed + relayCount + 2 /*entry count*/
|
|
for _, e := range b.Entries {
|
|
n := len(e.Username)
|
|
if n > 255 {
|
|
n = 255
|
|
}
|
|
size += 1 + n + profilePicEntryFixed
|
|
}
|
|
buf := make([]byte, size)
|
|
off := 0
|
|
binary.BigEndian.PutUint32(buf[off:], b.Header.BundleSize)
|
|
off += 4
|
|
binary.BigEndian.PutUint32(buf[off:], b.Header.BundleCRC)
|
|
off += 4
|
|
buf[off] = byte(relayCount)
|
|
off++
|
|
for i := 0; i < relayCount; i++ {
|
|
if b.Header.Relays[i] {
|
|
buf[off] = 1
|
|
}
|
|
off++
|
|
}
|
|
binary.BigEndian.PutUint16(buf[off:], uint16(len(b.Entries)))
|
|
off += 2
|
|
for _, e := range b.Entries {
|
|
nb := []byte(e.Username)
|
|
if len(nb) > 255 {
|
|
nb = nb[:255]
|
|
}
|
|
buf[off] = byte(len(nb))
|
|
off++
|
|
copy(buf[off:], nb)
|
|
off += len(nb)
|
|
binary.BigEndian.PutUint32(buf[off:], e.Offset)
|
|
off += 4
|
|
binary.BigEndian.PutUint32(buf[off:], e.Size)
|
|
off += 4
|
|
binary.BigEndian.PutUint32(buf[off:], e.CRC)
|
|
off += 4
|
|
buf[off] = e.MIME
|
|
off++
|
|
binary.BigEndian.PutUint16(buf[off:], e.DNSChannel)
|
|
off += 2
|
|
binary.BigEndian.PutUint16(buf[off:], e.DNSBlocks)
|
|
off += 2
|
|
}
|
|
return buf
|
|
}
|
|
|
|
// DecodeProfilePicsBundle parses bytes produced by EncodeProfilePicsBundle.
|
|
func DecodeProfilePicsBundle(data []byte) (ProfilePicsBundle, error) {
|
|
var out ProfilePicsBundle
|
|
if len(data) < profilePicsHeaderFixed+2 {
|
|
return out, fmt.Errorf("profile-pics bundle too short: %d bytes", len(data))
|
|
}
|
|
off := 0
|
|
out.Header.BundleSize = binary.BigEndian.Uint32(data[off:])
|
|
off += 4
|
|
out.Header.BundleCRC = binary.BigEndian.Uint32(data[off:])
|
|
off += 4
|
|
relayCount := int(data[off])
|
|
off++
|
|
if off+relayCount+2 > len(data) {
|
|
return out, fmt.Errorf("profile-pics bundle: truncated relay list")
|
|
}
|
|
if relayCount > 0 {
|
|
out.Header.Relays = make([]bool, relayCount)
|
|
for i := 0; i < relayCount; i++ {
|
|
out.Header.Relays[i] = data[off] != 0
|
|
off++
|
|
}
|
|
}
|
|
count := int(binary.BigEndian.Uint16(data[off:]))
|
|
off += 2
|
|
out.Entries = make([]ProfilePicEntry, 0, count)
|
|
for i := 0; i < count; i++ {
|
|
if off >= len(data) {
|
|
return out, fmt.Errorf("profile-pics: truncated at entry %d", i)
|
|
}
|
|
nameLen := int(data[off])
|
|
off++
|
|
if off+nameLen+profilePicEntryFixed > len(data) {
|
|
return out, fmt.Errorf("profile-pics: truncated entry %d body", i)
|
|
}
|
|
name := string(data[off : off+nameLen])
|
|
off += nameLen
|
|
offset := binary.BigEndian.Uint32(data[off:])
|
|
off += 4
|
|
sz := binary.BigEndian.Uint32(data[off:])
|
|
off += 4
|
|
cr := binary.BigEndian.Uint32(data[off:])
|
|
off += 4
|
|
mime := data[off]
|
|
off++
|
|
dnsCh := binary.BigEndian.Uint16(data[off:])
|
|
off += 2
|
|
dnsBlk := binary.BigEndian.Uint16(data[off:])
|
|
off += 2
|
|
out.Entries = append(out.Entries, ProfilePicEntry{
|
|
Username: name,
|
|
Offset: offset,
|
|
Size: sz,
|
|
CRC: cr,
|
|
MIME: mime,
|
|
DNSChannel: dnsCh,
|
|
DNSBlocks: dnsBlk,
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// VerifyEntry returns bundle[entry.Offset:entry.Offset+entry.Size] if
|
|
// the slice is in-range and its CRC32-IEEE matches entry.CRC. The
|
|
// hash check is what stops a misaligned bundle from serving the wrong
|
|
// avatar under a username.
|
|
func VerifyEntry(bundle []byte, entry ProfilePicEntry) ([]byte, error) {
|
|
end := uint64(entry.Offset) + uint64(entry.Size)
|
|
if end > uint64(len(bundle)) {
|
|
return nil, fmt.Errorf("entry %q out of range: offset=%d size=%d bundle=%d",
|
|
entry.Username, entry.Offset, entry.Size, len(bundle))
|
|
}
|
|
slice := bundle[entry.Offset:end]
|
|
if uint32(len(slice)) != entry.Size {
|
|
return nil, fmt.Errorf("entry %q size mismatch: have %d want %d",
|
|
entry.Username, len(slice), entry.Size)
|
|
}
|
|
if got := crc32.ChecksumIEEE(slice); got != entry.CRC {
|
|
return nil, fmt.Errorf("entry %q crc mismatch: have %08x want %08x",
|
|
entry.Username, got, entry.CRC)
|
|
}
|
|
return slice, nil
|
|
}
|