// Copyright (c) 2016 Daniel Oaks // released under the MIT license package isupport import ( "fmt" "slices" "strings" ) const ( maxPayloadLength = 380 /* Modern: "As the maximum number of message parameters to any reply is 15, the maximum number of RPL_ISUPPORT tokens that can be advertised is 13." [up to 13 parameters] */ maxParameters = 13 ) // List holds a list of ISUPPORT tokens type List struct { Tokens map[string]string CachedReply [][]string } // NewList returns a new List func NewList() *List { var il List il.Initialize() return &il } func (il *List) Initialize() { il.Tokens = make(map[string]string) il.CachedReply = make([][]string, 0) } // Add adds an RPL_ISUPPORT token to our internal list func (il *List) Add(name string, value string) { il.Tokens[name] = value } // AddNoValue adds an RPL_ISUPPORT token that does not have a value func (il *List) AddNoValue(name string) { il.Tokens[name] = "" } // Contains returns whether the list already contains a token func (il *List) Contains(name string) bool { _, ok := il.Tokens[name] return ok } // getTokenString gets the appropriate string for a token+value. func getTokenString(name string, value string) string { if len(value) == 0 { return name } return fmt.Sprintf("%s=%s", name, value) } // GetDifference returns the difference between two token lists. func (il *List) GetDifference(newil *List) [][]string { var outTokens []string // append removed tokens for name := range il.Tokens { _, exists := newil.Tokens[name] if exists { continue } token := fmt.Sprintf("-%s", name) outTokens = append(outTokens, token) } // append added tokens for name, value := range newil.Tokens { newval, exists := il.Tokens[name] if exists && value == newval { continue } token := getTokenString(name, value) outTokens = append(outTokens, token) } slices.Sort(outTokens) // create output list replies := make([][]string, 0) var length int // Length of the current cache var cache []string // Token list cache for _, token := range outTokens { if len(token)+length <= maxPayloadLength { // account for the space separating tokens if len(cache) > 0 { length++ } cache = append(cache, token) length += len(token) } if len(cache) == maxParameters || len(token)+length >= maxPayloadLength { replies = append(replies, cache) cache = make([]string, 0) length = 0 } } if len(cache) > 0 { replies = append(replies, cache) } return replies } func validateToken(token string) error { if len(token) == 0 || token[0] == ':' || strings.Contains(token, " ") { return fmt.Errorf("bad isupport token (cannot be sent as IRC parameter): `%s`", token) } if strings.ContainsAny(token, "\n\r\x00") { return fmt.Errorf("bad isupport token (contains forbidden octets)") } // technically a token can be maxPayloadLength if it occurs alone, // but fail it just to be safe if len(token) >= maxPayloadLength { return fmt.Errorf("bad isupport token (too long): `%s`", token) } return nil } // RegenerateCachedReply regenerates the cached RPL_ISUPPORT reply func (il *List) RegenerateCachedReply() (err error) { var tokens []string for name, value := range il.Tokens { token := getTokenString(name, value) if tokenErr := validateToken(token); tokenErr == nil { tokens = append(tokens, token) } else { err = tokenErr } } // make sure we get a sorted list of tokens, needed for tests and looks nice slices.Sort(tokens) var cache []string // Tokens in current line var length int // Length of the current line for _, token := range tokens { // account for the space separating tokens if len(cache) == maxParameters || (len(token)+1)+length > maxPayloadLength { il.CachedReply = append(il.CachedReply, cache) cache = nil length = 0 } if len(cache) > 0 { length++ } length += len(token) cache = append(cache, token) } if len(cache) > 0 { il.CachedReply = append(il.CachedReply, cache) } return }