350 lines
11 KiB
Go
350 lines
11 KiB
Go
package layerwire
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/gotd/td/bin"
|
|
)
|
|
|
|
// ruleRaw is the generated, compact form of a single changed-constructor
|
|
// downgrade (see tables_gen.go). Mechanical rules carry the target CRC plus the
|
|
// canonical field names retained at the target layer; structural rules name a
|
|
// hand-written transform registered in fallback.go.
|
|
type ruleRaw struct {
|
|
target uint32
|
|
keep []string
|
|
structural string
|
|
}
|
|
|
|
// layerRaw is the generated downgrade table for one target layer.
|
|
type layerRaw struct {
|
|
rules map[uint32]ruleRaw
|
|
newTypes []uint32 // canonical CRCs that do not exist at this layer
|
|
}
|
|
|
|
// downgradeRule is the runtime form of ruleRaw with keep as a set.
|
|
type downgradeRule struct {
|
|
target uint32
|
|
keep map[string]bool
|
|
structural string
|
|
}
|
|
|
|
// layerTables is the runtime downgrade model for one target layer.
|
|
type layerTables struct {
|
|
rules map[uint32]*downgradeRule
|
|
newTypes map[uint32]bool
|
|
dirty map[uint32]bool // ctor CRC needs a deep walk
|
|
dirtyT map[string]bool // abstract/bare type name reaches a dirty ctor
|
|
}
|
|
|
|
// tables holds the runtime model per supported layer, built lazily.
|
|
var tables = func() map[int]*layerTables {
|
|
out := make(map[int]*layerTables, len(generatedTables))
|
|
for layer, raw := range generatedTables {
|
|
out[layer] = buildLayerTables(raw)
|
|
}
|
|
return out
|
|
}()
|
|
|
|
func buildLayerTables(raw layerRaw) *layerTables {
|
|
lt := &layerTables{
|
|
rules: make(map[uint32]*downgradeRule, len(raw.rules)),
|
|
newTypes: make(map[uint32]bool, len(raw.newTypes)),
|
|
}
|
|
for crc, r := range raw.rules {
|
|
dr := &downgradeRule{target: r.target, structural: r.structural}
|
|
if r.structural == "" {
|
|
dr.keep = make(map[string]bool, len(r.keep))
|
|
for _, n := range r.keep {
|
|
dr.keep[n] = true
|
|
}
|
|
}
|
|
lt.rules[crc] = dr
|
|
}
|
|
for _, crc := range raw.newTypes {
|
|
lt.newTypes[crc] = true
|
|
}
|
|
lt.computeDirty()
|
|
return lt
|
|
}
|
|
|
|
// computeDirty marks every constructor (and abstract/bare type) that can
|
|
// transitively contain a changed, structural, or layer-absent constructor, so
|
|
// the transcoder can byte-copy the ~86% of the type graph that is unaffected.
|
|
func (lt *layerTables) computeDirty() {
|
|
lt.dirty = make(map[uint32]bool)
|
|
lt.dirtyT = make(map[string]bool)
|
|
// Seed: rules + new types are themselves dirty.
|
|
for crc := range lt.rules {
|
|
lt.dirty[crc] = true
|
|
}
|
|
for crc := range lt.newTypes {
|
|
lt.dirty[crc] = true
|
|
}
|
|
markType := func(name string) {
|
|
if name != "" && !lt.dirtyT[name] {
|
|
lt.dirtyT[name] = true
|
|
}
|
|
}
|
|
// Seed dirty types from seeded dirty ctors.
|
|
for crc := range lt.dirty {
|
|
if cl := canonical.byCRC[crc]; cl != nil {
|
|
markType(cl.result)
|
|
markType(cl.name) // bare reference
|
|
}
|
|
}
|
|
// Fixpoint: a ctor is dirty if any field's type is dirty; a type is dirty
|
|
// if any of its constructors is dirty.
|
|
for changed := true; changed; {
|
|
changed = false
|
|
for crc, cl := range canonical.byCRC {
|
|
if lt.dirty[crc] {
|
|
continue
|
|
}
|
|
if lt.ctorHasDirtyField(cl) {
|
|
lt.dirty[crc] = true
|
|
if !lt.dirtyT[cl.result] {
|
|
lt.dirtyT[cl.result] = true
|
|
}
|
|
if !lt.dirtyT[cl.name] {
|
|
lt.dirtyT[cl.name] = true
|
|
}
|
|
changed = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (lt *layerTables) ctorHasDirtyField(cl *ctorLayout) bool {
|
|
for i := range cl.fields {
|
|
if lt.fieldDirty(&cl.fields[i]) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (lt *layerTables) fieldDirty(f *fieldLayout) bool {
|
|
switch f.kind {
|
|
case kindObject, kindBareObject:
|
|
return lt.dirtyT[f.typeName]
|
|
case kindVector, kindVectorBare:
|
|
return lt.fieldDirty(f.elem)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// structuralFunc transforms a changed constructor whose downgrade is not a pure
|
|
// field drop. The leading CRC has already been consumed from in; the transform
|
|
// reads the canonical body from in and writes the target-layer object (whose
|
|
// constructor id is target) to out.
|
|
type structuralFunc func(cl *ctorLayout, target uint32, in, out *bin.Buffer, layer int) error
|
|
|
|
// fallbackFunc replaces a layer-absent (227-only) constructor with an
|
|
// equivalent the target layer understands. The leading CRC is NOT yet consumed.
|
|
type fallbackFunc func(cl *ctorLayout, in, out *bin.Buffer, layer int) error
|
|
|
|
// structuralTransforms and the newType fallback registries are populated in
|
|
// fallback.go. newTypeFallbacks is keyed by canonical CRC (specific override);
|
|
// newTypeFallbacksByType is keyed by the canonical abstract result type and
|
|
// covers every 227-only constructor of that class (e.g. any new MessageAction).
|
|
var (
|
|
structuralTransforms = map[string]structuralFunc{}
|
|
newTypeFallbacks = map[uint32]fallbackFunc{}
|
|
newTypeFallbacksByType = map[string]fallbackFunc{}
|
|
)
|
|
|
|
// Transcode downgrades a single canonical (Layer 227) boxed object to the wire
|
|
// shape of layer. layer >= CanonicalLayer (or unsupported) returns in verbatim.
|
|
// On any transform gap it returns an error so the edge can fall back to sending
|
|
// the canonical bytes rather than corrupting the stream.
|
|
func Transcode(canonicalBytes []byte, layer int) ([]byte, error) {
|
|
if layer >= CanonicalLayer {
|
|
return canonicalBytes, nil
|
|
}
|
|
lt := tables[layer]
|
|
if lt == nil {
|
|
return canonicalBytes, nil // unsupported floor: best-effort passthrough
|
|
}
|
|
// Top-level constructors that are not in the canonical tg schema are MTProto
|
|
// control/error objects (mt.*, e.g. rpc_error) — layer-invariant, so pass
|
|
// them through. A nested unknown id is still a hard error (real gap).
|
|
if id, err := (&bin.Buffer{Buf: canonicalBytes}).PeekID(); err != nil || canonical.byCRC[id] == nil {
|
|
return canonicalBytes, nil
|
|
}
|
|
in := &bin.Buffer{Buf: canonicalBytes}
|
|
out := &bin.Buffer{}
|
|
if err := lt.transcodeObject(in, out, layer); err != nil {
|
|
return nil, err
|
|
}
|
|
if in.Len() != 0 {
|
|
return nil, fmt.Errorf("layerwire: %d trailing bytes after transcode to layer %d", in.Len(), layer)
|
|
}
|
|
return out.Buf, nil
|
|
}
|
|
|
|
// UpgradeMethodCRC maps an old client's method constructor id to the canonical
|
|
// (227) id when the request body is byte-compatible — i.e. swapping the leading
|
|
// 4-byte id yields a valid 227 request. It unifies two sources: generated
|
|
// official layer drift (inboundMethodUpgrades) and hand-maintained client
|
|
// constructor drift (clientMethodAliases). Returns ok=false for unchanged
|
|
// methods and for changes that need a real decode (those stay as rpc handlers).
|
|
func UpgradeMethodCRC(oldID uint32) (uint32, bool) {
|
|
if newID, ok := inboundMethodUpgrades[oldID]; ok {
|
|
return newID, true
|
|
}
|
|
newID, ok := clientMethodAliases[oldID]
|
|
return newID, ok
|
|
}
|
|
|
|
func (lt *layerTables) transcodeObject(in, out *bin.Buffer, layer int) error {
|
|
id, err := in.PeekID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cl, ok := canonical.byCRC[id]
|
|
if !ok {
|
|
return fmt.Errorf("layerwire: unknown constructor %#08x", id)
|
|
}
|
|
if rule := lt.rules[id]; rule != nil {
|
|
if err := in.ConsumeID(id); err != nil {
|
|
return err
|
|
}
|
|
if rule.structural != "" {
|
|
fn := structuralTransforms[rule.structural]
|
|
if fn == nil {
|
|
return fmt.Errorf("layerwire: no structural transform %q for %s@%d", rule.structural, cl.name, layer)
|
|
}
|
|
return fn(cl, rule.target, in, out, layer)
|
|
}
|
|
out.PutID(rule.target)
|
|
return lt.transcodeBody(in, out, cl, rule.keep, layer)
|
|
}
|
|
if lt.newTypes[id] {
|
|
fn := newTypeFallbacks[id]
|
|
if fn == nil {
|
|
fn = newTypeFallbacksByType[cl.result]
|
|
}
|
|
if fn == nil {
|
|
return fmt.Errorf("layerwire: %s (%#08x) absent at layer %d and no fallback", cl.name, id, layer)
|
|
}
|
|
return fn(cl, in, out, layer)
|
|
}
|
|
if !lt.dirty[id] {
|
|
// Unaffected subtree: byte-for-byte copy.
|
|
pre := in.Buf
|
|
if err := canonical.skipObject(in); err != nil {
|
|
return err
|
|
}
|
|
out.Put(pre[:len(pre)-len(in.Buf)])
|
|
return nil
|
|
}
|
|
// Unchanged at this level but a descendant is dirty: keep CRC, recurse.
|
|
if err := in.ConsumeID(id); err != nil {
|
|
return err
|
|
}
|
|
out.PutID(id)
|
|
return lt.transcodeBody(in, out, cl, nil, layer)
|
|
}
|
|
|
|
// transcodeBody re-encodes a constructor body. keep==nil means retain every
|
|
// field (recursing into dirty descendants); otherwise only the named canonical
|
|
// fields are written, flag integers are remasked to the retained bits, and
|
|
// dropped fields are read-and-discarded.
|
|
func (lt *layerTables) transcodeBody(in, out *bin.Buffer, cl *ctorLayout, keep map[string]bool, layer int) error {
|
|
kept := func(name string) bool { return keep == nil || keep[name] }
|
|
var flags map[string]uint32
|
|
for i := range cl.fields {
|
|
f := &cl.fields[i]
|
|
if f.isFlags {
|
|
v, err := in.Uint32()
|
|
if err != nil {
|
|
return fmt.Errorf("%s.%s: %w", cl.name, f.name, err)
|
|
}
|
|
if flags == nil {
|
|
flags = make(map[string]uint32, 2)
|
|
}
|
|
flags[f.name] = v
|
|
if kept(f.name) {
|
|
out.PutUint32(v & lt.keptMask(cl, f.name, kept))
|
|
}
|
|
continue
|
|
}
|
|
present := !f.conditional() || flags[f.flagName]&(1<<uint(f.flagBit)) != 0
|
|
if !present {
|
|
continue
|
|
}
|
|
if kept(f.name) {
|
|
if err := lt.transcodeValue(in, out, f, layer); err != nil {
|
|
return fmt.Errorf("%s.%s: %w", cl.name, f.name, err)
|
|
}
|
|
} else if err := canonical.skipValue(in, f); err != nil {
|
|
return fmt.Errorf("%s.%s (drop): %w", cl.name, f.name, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// keptMask is the OR of bits for retained conditional fields gated by flagName,
|
|
// clearing bits whose fields are dropped at the target layer.
|
|
func (lt *layerTables) keptMask(cl *ctorLayout, flagName string, kept func(string) bool) uint32 {
|
|
var mask uint32
|
|
for i := range cl.fields {
|
|
g := &cl.fields[i]
|
|
if g.conditional() && g.flagName == flagName && kept(g.name) {
|
|
mask |= 1 << uint(g.flagBit)
|
|
}
|
|
}
|
|
return mask
|
|
}
|
|
|
|
// transcodeValue writes one present field value, recursing only into dirty
|
|
// subtrees and byte-copying everything else.
|
|
func (lt *layerTables) transcodeValue(in, out *bin.Buffer, f *fieldLayout, layer int) error {
|
|
if !lt.fieldDirty(f) {
|
|
pre := in.Buf
|
|
if err := canonical.skipValue(in, f); err != nil {
|
|
return err
|
|
}
|
|
out.Put(pre[:len(pre)-len(in.Buf)])
|
|
return nil
|
|
}
|
|
switch f.kind {
|
|
case kindVector, kindVectorBare:
|
|
if f.kind == kindVector {
|
|
id, err := in.Uint32()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if id != vectorTypeID {
|
|
return fmt.Errorf("expected vector id, got %#08x", id)
|
|
}
|
|
out.PutUint32(vectorTypeID)
|
|
}
|
|
n, err := in.Int()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out.PutInt(n)
|
|
for i := 0; i < n; i++ {
|
|
if err := lt.transcodeValue(in, out, f.elem, layer); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
case kindObject:
|
|
return lt.transcodeObject(in, out, layer)
|
|
case kindBareObject:
|
|
cl, ok := canonical.bareByT[f.typeName]
|
|
if !ok {
|
|
return fmt.Errorf("unknown bare type %q", f.typeName)
|
|
}
|
|
// Bare objects have no CRC and (within 220..227) no changed bare ctor;
|
|
// recurse all-kept to reach any dirty descendants.
|
|
return lt.transcodeBody(in, out, cl, nil, layer)
|
|
default:
|
|
// Primitive marked dirty should be impossible.
|
|
return fmt.Errorf("unexpected dirty primitive kind %d", f.kind)
|
|
}
|
|
}
|