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 inputChannelEmptyID = 0xee8c1e86 // inputChannelEmpty inputChannelID = 0xf35aec28 // inputChannel channel_id:long access_hash:long inputChannelFromMessageID = 0x5b934f9d // inputChannelFromMessage peer:InputPeer msg_id:int channel_id:long inputPeerEmptyID = 0x7f3b18ea // inputPeerEmpty inputPeerChannelID = 0x27bcbbfc // inputPeerChannel channel_id:long access_hash:long inputPeerChannelFromMessageID = 0xbd2a0840 // inputPeerChannelFromMessage peer:InputPeer msg_id:int channel_id:long 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 "\x00" -> 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", "messages.editChatCreator\x00peer": "channel", } // fieldConverter rewrites one field whose wire type changed between the old and // canonical layout. Keyed by "->"; 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 -> id:Vector (wrap each int in inputMessageID). "Vector->Vector": 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 }, // channel:InputChannel -> peer:InputPeer for the old channels.editCreator // Android constructor. Concrete layouts are otherwise byte-compatible. "InputChannel->InputPeer": func(raw []byte, out *bin.Buffer) error { in := &bin.Buffer{Buf: raw} id, err := in.ID() if err != nil { return err } switch id { case inputChannelEmptyID: out.PutID(inputPeerEmptyID) case inputChannelID: out.PutID(inputPeerChannelID) out.Put(in.Buf) case inputChannelFromMessageID: out.PutID(inputPeerChannelFromMessageID) out.Put(in.Buf) default: return bin.NewUnexpectedID(id) } 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<"+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) } }