Files
olcrtc/internal/transport/common/common.go
zarazaex69 80cc3bafe4 chore(lint): satisfy golangci-lint after big refactor
Address 25 issues reported by golangci-lint following the structural
refactor:

- cyclop: split common.Reassembler.Push into upsert/storeChunk/deliver
  helpers (12→5). Move seichannel option-default fill into Options.
  withDefaults so New stays under the limit.
- exhaustive: enumerate ResultPartial / ResultIgnore explicitly in
  seichannel and videochannel switches over common.Result.
- gosec G115: annotate the test-fixture int→uint16/uint32 conversions
  in common_test.go with //nolint:gosec.
- lll: break up the 130+ character one-liners in transport
  unit/integration tests and the videochannel track-ID construction.
- nolintlint: drop the stale //nolint:cyclop in mobile_test.go where
  the underlying complexity already cleared the limit.
- wrapcheck: wrap errors returned from internal/framing and
  internal/runtime in their public callers (handshake, control,
  server.setupCipher, client.setupCipher) so they carry the layer name.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 14:38:03 +03:00

224 lines
6.1 KiB
Go

// Package common provides building blocks shared by the video-track based
// transports (videochannel, seichannel) — fragment/reassembly, ack waiters,
// and per-peer random IDs. vp8channel does its own KCP-based framing and
// only consumes RandomID.
package common
import (
"crypto/rand"
"encoding/hex"
"fmt"
"hash/crc32"
"sync"
"time"
)
// RandomID returns 8 random hex characters for use as a per-peer suffix on
// track and stream IDs. Required for Jitsi: msid collisions between
// participants cause Jicofo to reject session-accept.
func RandomID() string {
var b [4]byte
if _, err := rand.Read(b[:]); err != nil {
return fmt.Sprintf("%08x", time.Now().UnixNano())
}
return hex.EncodeToString(b[:])
}
// FragmentPayload splits data into chunks of at most maxSize bytes. An empty
// payload produces a single empty fragment so the caller can still ack a
// zero-byte message round-trip.
func FragmentPayload(data []byte, maxSize int) [][]byte {
if len(data) == 0 {
return [][]byte{{}}
}
out := make([][]byte, 0, (len(data)+maxSize-1)/maxSize)
for start := 0; start < len(data); start += maxSize {
end := start + maxSize
if end > len(data) {
end = len(data)
}
chunk := make([]byte, end-start)
copy(chunk, data[start:end])
out = append(out, chunk)
}
return out
}
// Fragment describes one piece of a fragmented message on the wire.
type Fragment struct {
Seq uint32
CRC uint32
TotalLen uint32
FragIdx uint16
FragTotal uint16
Payload []byte
}
// InboundMessage tracks reassembly state for one inbound message.
type InboundMessage struct {
TotalLen uint32
CRC uint32
frags [][]byte
remain int
}
// Reassembler holds inbound message state and a sliding window of recently
// delivered (seq, crc) pairs so duplicate fragments resolve to a fresh ack
// rather than a re-delivery.
type Reassembler struct {
mu sync.Mutex
inbound map[uint32]*InboundMessage
delivered map[uint32]uint32
maxRecent int
}
// NewReassembler creates a reassembler with the given recent-delivery cap.
// When the delivered map exceeds maxRecent entries it is reset; a value of
// 256 is a reasonable default for the video transports.
func NewReassembler(maxRecent int) *Reassembler {
if maxRecent <= 0 {
maxRecent = 256
}
return &Reassembler{
inbound: make(map[uint32]*InboundMessage),
delivered: make(map[uint32]uint32),
maxRecent: maxRecent,
}
}
// Result classifies what Push computed for a fragment.
type Result int
const (
// ResultIgnore means the fragment was malformed or out of range.
ResultIgnore Result = iota
// ResultPartial means the fragment was stored but the message is not
// fully reassembled yet.
ResultPartial
// ResultDuplicate means the message identified by (Seq, CRC) was
// already delivered. Caller should re-ack without invoking OnData.
ResultDuplicate
// ResultDelivered means the message is complete; Data carries the
// reassembled payload.
ResultDelivered
)
// Push integrates fragment into reassembly state and returns one of the
// Result values. When ResultDelivered, the second return holds the
// reassembled payload bytes; otherwise it is nil.
func (r *Reassembler) Push(fragment Fragment) (Result, []byte) {
r.mu.Lock()
defer r.mu.Unlock()
if crc, ok := r.delivered[fragment.Seq]; ok && crc == fragment.CRC {
return ResultDuplicate, nil
}
msg := r.upsert(fragment)
if int(fragment.FragIdx) >= len(msg.frags) {
return ResultIgnore, nil
}
r.storeChunk(msg, fragment)
if msg.remain > 0 {
return ResultPartial, nil
}
return r.deliver(fragment.Seq, msg)
}
// upsert returns the inbound message tracking entry for fragment.Seq,
// creating a fresh entry if no compatible one is present.
func (r *Reassembler) upsert(fragment Fragment) *InboundMessage {
msg, ok := r.inbound[fragment.Seq]
if ok && msg.CRC == fragment.CRC && msg.TotalLen == fragment.TotalLen &&
len(msg.frags) == int(fragment.FragTotal) {
return msg
}
msg = &InboundMessage{
TotalLen: fragment.TotalLen,
CRC: fragment.CRC,
frags: make([][]byte, fragment.FragTotal),
remain: int(fragment.FragTotal),
}
r.inbound[fragment.Seq] = msg
return msg
}
func (r *Reassembler) storeChunk(msg *InboundMessage, fragment Fragment) {
if msg.frags[fragment.FragIdx] != nil {
return
}
chunk := make([]byte, len(fragment.Payload))
copy(chunk, fragment.Payload)
msg.frags[fragment.FragIdx] = chunk
msg.remain--
}
func (r *Reassembler) deliver(seq uint32, msg *InboundMessage) (Result, []byte) {
delete(r.inbound, seq)
data := assemble(msg)
if crc32.ChecksumIEEE(data) != msg.CRC {
return ResultIgnore, nil
}
if len(r.delivered) > r.maxRecent {
r.delivered = make(map[uint32]uint32)
}
r.delivered[seq] = msg.CRC
return ResultDelivered, data
}
func assemble(msg *InboundMessage) []byte {
out := make([]byte, 0, msg.TotalLen)
for _, frag := range msg.frags {
out = append(out, frag...)
}
if uint32(len(out)) > msg.TotalLen { //nolint:gosec // G115: bounded by allocation size
out = out[:msg.TotalLen]
}
return out
}
// AckRegistry tracks in-flight Send calls waiting for their peer ack. Each
// Send registers a waiter keyed by sequence number and reads from it; the
// receive loop calls Resolve when an ack arrives.
type AckRegistry struct {
mu sync.Mutex
waiters map[uint32]chan uint32
}
// NewAckRegistry creates an empty ack registry.
func NewAckRegistry() *AckRegistry {
return &AckRegistry{waiters: make(map[uint32]chan uint32)}
}
// Register installs a waiter for seq and returns its channel. The caller
// must drop the waiter via Unregister when it is done.
func (a *AckRegistry) Register(seq uint32) chan uint32 {
ch := make(chan uint32, 1)
a.mu.Lock()
a.waiters[seq] = ch
a.mu.Unlock()
return ch
}
// Unregister drops the waiter for seq.
func (a *AckRegistry) Unregister(seq uint32) {
a.mu.Lock()
delete(a.waiters, seq)
a.mu.Unlock()
}
// Resolve delivers crc to the waiter for seq, if present. A missing waiter
// is silently ignored — the sender has already moved on.
func (a *AckRegistry) Resolve(seq, crc uint32) {
a.mu.Lock()
waiter := a.waiters[seq]
a.mu.Unlock()
if waiter == nil {
return
}
select {
case waiter <- crc:
default:
}
}