ident: decode twitch vod name
There are a few unknown fields, represented as is for now. Signed-off-by: Yonah <contrib@gensokyo.uk>
This commit is contained in:
142
ident.go
Normal file
142
ident.go
Normal file
@@ -0,0 +1,142 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user