owpengram-server/internal/compat/layerwire/inbound.go

260 lines
7.5 KiB
Go

package layerwire
import (
_ "embed"
"encoding/binary"
"fmt"
"github.com/gotd/td/bin"
)
// Canonical ids used to synthesize converted/defaulted values.
const (
inputUserID = 0xf21158c6 // inputUser user_id:long access_hash:long
inputMessageID = 0xa676a322 // inputMessageID id:int
boolFalseID = 0xbc799737 // boolFalse
)
//go:embed schema/client-drift.tl
var clientDriftSchema string
// driftModel holds the declared old-layout of each client-drift constructor.
var driftModel = mustLoadDrift()
func mustLoadDrift() *schemaModel {
m, err := parseSchemaModel(clientDriftSchema)
if err != nil {
panic("layerwire: parse client-drift schema: " + err.Error())
}
return m
}
// driftFieldRenames maps a canonical field that was renamed from the client's
// old constructor: key "<qualified method>\x00<canonical field>" -> old field.
// Pure schema diff cannot recover a rename, so it is declared here (data, not a
// transform). It is the only thing a structural rename needs.
var driftFieldRenames = map[string]string{
"bots.exportBotToken\x00bot": "bot_id",
}
// fieldConverter rewrites one field whose wire type changed between the old and
// canonical layout. Keyed by "<oldTypeSig>-><newTypeSig>"; raw is the old field's
// encoded bytes. Reusable across any method with the same type change.
type fieldConverter func(raw []byte, out *bin.Buffer) error
var fieldConverters = map[string]fieldConverter{
// id:Vector<int> -> id:Vector<InputMessage> (wrap each int in inputMessageID).
"Vector<int>->Vector<InputMessage>": func(raw []byte, out *bin.Buffer) error {
in := &bin.Buffer{Buf: raw}
n, err := in.VectorHeader()
if err != nil {
return err
}
out.PutVectorHeader(n)
for i := 0; i < n; i++ {
v, err := in.Int()
if err != nil {
return err
}
out.PutID(inputMessageID)
out.PutInt(v)
}
return nil
},
// bot_id:long -> bot:InputUser{user_id, access_hash=0}.
"long->InputUser": func(raw []byte, out *bin.Buffer) error {
in := &bin.Buffer{Buf: raw}
id, err := in.Long()
if err != nil {
return err
}
out.PutID(inputUserID)
out.PutLong(id)
out.PutLong(0)
return nil
},
}
// UpgradeInbound converts an old client's inbound request to canonical (227)
// form so the normal gotd dispatcher can handle it. It unifies three data-driven
// sources, all of which require no per-method handler code:
// - inboundMethodUpgrades (generated from api.tl diff): official layer drift.
// - clientMethodAliases (client_aliases.go): body-identical client drift.
// - driftModel (client-drift.tl): body-different client drift, upgraded by the
// generic engine below.
//
// ok=false means no upgrade applies. On ok=true the returned buffer (canonical
// id + body) is what to dispatch.
func UpgradeInbound(id uint32, in *bin.Buffer) (*bin.Buffer, bool, error) {
if newID, ok := UpgradeMethodCRC(id); ok {
if len(in.Buf) < 4 {
return nil, false, fmt.Errorf("layerwire: short inbound buffer for %#08x", id)
}
// Copy rather than rewrite in place: never mutate the caller's buffer
// (matches the body-transform path, which also returns a fresh buffer).
out := &bin.Buffer{Buf: append([]byte(nil), in.Buf...)}
binary.LittleEndian.PutUint32(out.Buf[:4], newID)
return out, true, nil
}
if old := driftModel.byCRC[id]; old != nil {
out, err := upgradeFromDrift(old, in)
if err != nil {
return nil, false, fmt.Errorf("layerwire: upgrade %s (%#08x): %w", old.name, id, err)
}
return out, true, nil
}
return nil, false, nil
}
// IsClientDrift reports whether id is a client-private constructor (DrKLO
// constructor drift), as opposed to official layer drift from api.tl.
func IsClientDrift(id uint32) bool {
if _, ok := clientMethodAliases[id]; ok {
return true
}
return driftModel.byCRC[id] != nil
}
// upgradeFromDrift rebuilds a canonical (227) request from an old client-drift
// body, comparing the declared old layout to the canonical layout field by field.
func upgradeFromDrift(old *ctorLayout, in *bin.Buffer) (*bin.Buffer, error) {
target := canonical.byName[old.name]
if target == nil {
return nil, fmt.Errorf("no canonical method %q", old.name)
}
if err := in.ConsumeID(old.crc); err != nil {
return nil, err
}
// Decode the old body: capture each present field's raw bytes + flag ints.
vals := make(map[string][]byte, len(old.fields))
present := make(map[string]bool, len(old.fields))
oldFlags := make(map[string]uint32, 2)
oldByName := make(map[string]*fieldLayout, len(old.fields))
for i := range old.fields {
f := &old.fields[i]
oldByName[f.name] = f
if f.isFlags {
v, err := in.Uint32()
if err != nil {
return nil, err
}
oldFlags[f.name] = v
continue
}
if f.conditional() && oldFlags[f.flagName]&(1<<uint(f.flagBit)) == 0 {
continue
}
present[f.name] = true
if f.kind == kindTrue {
continue
}
pre := in.Buf
if err := canonical.skipValue(in, f); err != nil {
return nil, fmt.Errorf("decode old field %q: %w", f.name, err)
}
vals[f.name] = pre[:len(pre)-len(in.Buf)]
}
if in.Len() != 0 {
return nil, fmt.Errorf("%d trailing bytes after old body", in.Len())
}
// Emit the canonical body.
out := &bin.Buffer{}
out.PutID(target.crc)
for i := range target.fields {
nf := &target.fields[i]
if nf.isFlags {
out.PutUint32(oldFlags[nf.name]) // 0 when absent in old (new flags int)
continue
}
oldName := nf.name
if mapped, ok := driftFieldRenames[old.name+"\x00"+nf.name]; ok {
oldName = mapped
}
if present[oldName] {
of := oldByName[oldName]
if of != nil && typeSig(of) != typeSig(nf) {
conv := fieldConverters[typeSig(of)+"->"+typeSig(nf)]
if conv == nil {
return nil, fmt.Errorf("field %q: no converter %s->%s", nf.name, typeSig(of), typeSig(nf))
}
if err := conv(vals[oldName], out); err != nil {
return nil, fmt.Errorf("field %q convert: %w", nf.name, err)
}
} else {
out.Put(vals[oldName]) // shared field, identical wire (kindTrue => no bytes)
}
continue
}
// Canonical-only field absent in old.
if nf.conditional() || nf.kind == kindTrue {
continue // optional: leave absent (its flag bit is clear)
}
if err := writeDefault(nf, out); err != nil {
return nil, fmt.Errorf("field %q default: %w", nf.name, err)
}
}
return out, nil
}
// writeDefault writes the zero value of a required canonical-only field.
func writeDefault(f *fieldLayout, out *bin.Buffer) error {
switch f.kind {
case kindInt:
out.PutInt(0)
case kindLong:
out.PutLong(0)
case kindDouble:
out.PutDouble(0)
case kindInt128:
out.PutInt128(bin.Int128{})
case kindInt256:
out.PutInt256(bin.Int256{})
case kindBytes:
out.PutBytes(nil)
case kindString:
out.PutString("")
case kindBool:
out.PutID(boolFalseID)
case kindVector:
out.PutVectorHeader(0)
case kindVectorBare:
out.PutInt(0)
default:
return fmt.Errorf("cannot default kind %d (boxed object needs a transform)", f.kind)
}
return nil
}
// typeSig is a stable wire-type signature for matching/converter lookup.
func typeSig(f *fieldLayout) string {
switch f.kind {
case kindInt:
return "int"
case kindLong:
return "long"
case kindDouble:
return "double"
case kindInt128:
return "int128"
case kindInt256:
return "int256"
case kindBytes:
return "bytes"
case kindString:
return "string"
case kindBool:
return "Bool"
case kindTrue:
return "true"
case kindVector:
return "Vector<" + typeSig(f.elem) + ">"
case kindVectorBare:
return "vector<" + typeSig(f.elem) + ">"
case kindObject, kindBareObject:
return f.typeName
default:
return fmt.Sprintf("kind%d", f.kind)
}
}