package streamdata import ( "bytes" "encoding/hex" "errors" "strconv" "unsafe" ) const ( // identFields is the expected number of fields in the [Ident] wire format. identFields = 7 // identSeparator is the byte separating [Ident] fields. identSeparator = '-' // identFF0Len is the expected size of the first free-form [Ident] field. identFF0Len = 4 // identFF123Len is the expected size of the second, third, fourth free-form // [Ident] fields. identFF123Len = 2 // identFF4Len is the expected size of the fifth free-form [Ident] field. identFF4Len = 6 // IdentFFLen is the total size of all free-form fields. IdentFFLen = identFF0Len + identFF123Len*3 + identFF4Len ) // identFF are segment lengths of [Ident.Data]. var identFF = []int{ identFF0Len, identFF123Len, identFF123Len, identFF123Len, identFF4Len, } // Ident represents the unique identifier of a VOD, returned by Twitch. type Ident struct { // An incrementing internal identifier, decimal encoding. Serial uint64 // Internal identifier specific to the channel, decimal encoding. Channel uint64 // Constant-size remaining data, unknown semantics, hexadecimal encoding. Data [IdentFFLen]byte } // Encode encodes a canonical representation of ident. func (ident *Ident) Encode() []byte { data := make( []byte, 0, 20 /* decimal numbers with one extra byte of headroom */ + identFields-1+ IdentFFLen*2, ) data = strconv.AppendUint(data, ident.Serial, 10) data = append(data, identSeparator) data = strconv.AppendUint(data, ident.Channel, 10) ff := ident.Data[:] for _, el := range identFF { data = append(data, identSeparator) data = hex.AppendEncode(data, ff[:el]) ff = ff[el:] } return data } // MarshalText encodes a canonical representation of ident. func (ident *Ident) MarshalText() (data []byte, _ error) { return ident.Encode(), nil } // String returns the result of MarshalText as a string. func (ident *Ident) String() string { data := ident.Encode() return unsafe.String(unsafe.SliceData(data), len(data)) } // atoi is like [strconv.Atoi], but with the resulting error unwrapped for // cleaner error messages. The caller must ensure data is never modified. func atoi(data []byte) (v uint64, err error) { v, err = strconv.ParseUint( unsafe.String(unsafe.SliceData(data), len(data)), 10, 64, ) if err != nil { var numError *strconv.NumError if errors.As(err, &numError) && numError != nil { err = numError.Unwrap() } } return } // IdentFieldError describes an [Ident] representation with not enough fields. type IdentFieldError int func (e IdentFieldError) Error() string { return "got " + strconv.Itoa(int(e)) + " field(s) instead of " + strconv.Itoa(identFields) } // IdentFFError describes an [Ident] representation with an irregular size // free-form field. type IdentFFError struct { Got, Want int } func (e *IdentFFError) Error() string { return "got " + strconv.Itoa(e.Got) + " bytes for a " + strconv.Itoa(e.Want) + "-byte long free-form field" } // UnmarshalText strictly decodes an unsuffixed VOD name returned by Twitch. func (ident *Ident) UnmarshalText(data []byte) (err error) { fields := bytes.SplitN(data, []byte{identSeparator}, identFields) if l := IdentFieldError(len(fields)); l != identFields { return l } if ident.Serial, err = atoi(fields[0]); err != nil { return } if ident.Channel, err = atoi(fields[1]); err != nil { return } var n int buf := ident.Data[:] for i, el := range identFF { field := fields[2+i] if n, err = hex.Decode(buf, field); err != nil { return err } else if n != el { return &IdentFFError{n, el} } buf = buf[el:] } return nil }