feat: sync multilayer td integration

This commit is contained in:
A 2026-07-15 13:32:06 +08:00
parent 20a310f6ca
commit 766c5db992
491 changed files with 26235 additions and 35340 deletions

View file

@ -0,0 +1,38 @@
package android
import (
"errors"
"github.com/iamxvbaba/td/bin"
"github.com/iamxvbaba/td/tg"
)
var ErrPrivateLayerRPCInvalid = errors.New("android private layer RPC is invalid")
// AdaptPrivateLayerRPC invokes the provenance-locked static gotdgen overlay
// from the generated unknown-method view. Nested values decode with the exact
// connection profile and the canonical request is re-profiled by gotd core.
func AdaptPrivateLayerRPC(view tg.LayerRPCUnknownMethodView) (tg.LayerOutboundCall, bool, error) {
outbound, handled, err := view.AdaptClientRPCOverlay(tg.LayerClientRPCOverlayDrkloAndroid)
if err == nil && !handled {
outbound, handled, err = view.AdaptClientRPCOverlay(tg.LayerClientRPCOverlayDrkloAndroidTheme)
}
if err != nil {
return tg.LayerOutboundCall{}, handled, errors.Join(ErrPrivateLayerRPCInvalid, err)
}
return outbound, handled, nil
}
// UpgradePrivateLayerRPC is retained only for Router.Dispatch's legacy test
// seam. Production admission uses AdaptPrivateLayerRPC above so its decode
// shares the outer generated request budget.
func UpgradePrivateLayerRPC(profile tg.LayerProfile, in *bin.Buffer, limits tg.LayerDecodeLimits) (*bin.Buffer, bool, error) {
upgraded, handled, err := tg.AdaptClientRPCOverlayWithLimits(profile, tg.LayerClientRPCOverlayDrkloAndroid, in, limits)
if err == nil && !handled {
upgraded, handled, err = tg.AdaptClientRPCOverlayWithLimits(profile, tg.LayerClientRPCOverlayDrkloAndroidTheme, in, limits)
}
if err != nil {
return nil, handled, errors.Join(ErrPrivateLayerRPCInvalid, err)
}
return upgraded, handled, nil
}

View file

@ -0,0 +1,54 @@
package android
import (
"errors"
"testing"
"github.com/iamxvbaba/td/bin"
"github.com/iamxvbaba/td/tg"
)
func TestUpgradePrivateLayerRPCOnlyAcceptsAuditedAndroidConstructors(t *testing.T) {
// DrKLO messages.forwardMessages private CRC has a body identical to the
// canonical request for the flags=0 empty-vector case.
private := bin.Buffer{}
private.PutID(0x41d41ade)
private.PutInt(0)
private.PutID(0x7f3b18ea) // inputPeerEmpty
private.PutVectorHeader(0)
private.PutVectorHeader(0)
private.PutID(0x7f3b18ea) // inputPeerEmpty
in := &bin.Buffer{Buf: private.Copy()}
upgraded, ok, err := UpgradePrivateLayerRPC(tg.LayerProfileCanonical, in, tg.LayerDecodeLimits{})
if err != nil || !ok {
t.Fatalf("upgrade private method = ok:%v err:%v", ok, err)
}
if in.Len() != 0 {
t.Fatalf("successful private method left %d bytes", in.Len())
}
if id, peekErr := upgraded.PeekID(); peekErr != nil || id != 0x13704a7c {
t.Fatalf("canonical id = %#x err=%v", id, peekErr)
}
official := bin.Buffer{}
official.PutID(0xb921bd04) // arbitrary non-private/official constructor
if value, handled, err := UpgradePrivateLayerRPC(tg.LayerProfileCanonical, &official, tg.LayerDecodeLimits{}); value != nil || handled || err != nil {
t.Fatalf("non-private method = value:%v handled:%v err:%v", value, handled, err)
}
}
func TestGeneratedPrivateLayerRPCOverlayHasAllAuditedMethods(t *testing.T) {
if got, want := tg.LayerClientRPCOverlayMethodCount(tg.LayerClientRPCOverlayDrkloAndroid), 15; got != want {
t.Fatalf("generated DrKLO method count = %d, want %d", got, want)
}
}
func TestUpgradePrivateLayerRPCRejectsMalformedBody(t *testing.T) {
malformed := bin.Buffer{}
malformed.PutID(0x41d41ade)
_, ok, err := UpgradePrivateLayerRPC(tg.LayerProfileCanonical, &malformed, tg.LayerDecodeLimits{})
if !ok || !errors.Is(err, ErrPrivateLayerRPCInvalid) {
t.Fatalf("malformed private method = ok:%v err:%v", ok, err)
}
}

View file

@ -1,6 +1,6 @@
package ios
import "github.com/gotd/td/tg"
import "github.com/iamxvbaba/td/tg"
// NoAppUpdate is the bounded answer used until telesrv has an application
// release catalog. It makes iOS keep its installed build and retry on its

View file

@ -1,142 +0,0 @@
# layerwire 操作手册(多 Layer 向后兼容)
> 本文是 **怎么操作**runbook。**为什么这么设计**见 [`docs/layer-compat-220-227-design.md`](../../../docs/layer-compat-220-227-design.md)。
> 改这个包前请先读完本文 + 设计文档。所有命令都从 **telesrv 模块根目录**执行。
## 这个包是干什么的
让 telesrv 同时正确服务 **Layer 220227** 的客户端,而业务 handler / gotd 永远只跑 canonical(227)、一行不改。
- **出站**:把 227 对象降级成老客户端能解的 wire 形态(`Transcode`)。
- **入站**:把老客户端发来的旧构造器升级成 227 请求,再交正常 gotd dispatcher`UpgradeInbound`)。
两个正交维度(**务必分清**
- **官方层漂移**:构造器在 layer N 的字段/CRC 与 227 不同。真值=官方 TDesktop `api.tl` 各层。**自动从 schema 生成**。
- **客户端构造器漂移**:某客户端(如 DrKLO Android手维护的 TL 实际发了个旧 layer 的官方构造器,但它声明的整体 layer 却是新的。真值=该客户端源码TLRPC.java。**声明在 `client-drift.tl` / `client_aliases.go`**。
## 文件地图
| 文件 | 角色 | 谁改 |
|---|---|---|
| `schema/canonical-227.tl` | **embed**,运行期 walker 的 227 字段布局(= gotd `td/_schema/tdesktop.tl` 的副本) | gotd 升级时 re-sync |
| `_schema/layer-2NN.tl` | 历史层官方 schema从 TDesktop git 抽,**仅生成期用**,下划线=不编译/不 embed | 升级/下探 floor 时抽取 |
| `schema/client-drift.tl` | **声明式**客户端发的旧构造器老布局body 与 227 不同的) | 发现客户端漂移时 +1 行 |
| `schema/routable-compat.tl` | **仅结构预检**:已有 RPC fallback adapter 的非 canonical wire 布局(当前只含 4 个 DrKLO theme 构造器);与 canonical 图合并后完整 walk但不自动升级 | 收敛既有手写 adapter 时维护,禁止借此新增业务 fallback |
| `client_aliases.go` | 客户端漂移里 **body 与 227 字节一致**的,纯 `老CRC→227CRC` | 发现纯换 CRC 漂移时 +1 条 |
| `tables_gen.go` | **生成产物**(勿手改):官方层降级表 + 入站升级表 + 新类型集 | 跑 `gen` 重生成 |
| `gen/main.go` | 生成器:对拍 schema、证明机械性、产 `tables_gen.go` | 升级逻辑变更时 |
| `layout.go` `walk.go` `tables.go` | 通用解释器(读/丈量/递归转码)| 核心,少动 |
| `fallback.go` | 出站手写兜底(结构性 / 227-only 类型)| CoverageGate 报缺时 |
| `inbound.go` | 入站通用升级引擎 + `fieldConverters` + `driftFieldRenames` | DriftCoverage 报缺时 |
## 核心命令
```bash
# 复核 schema 差异数字(不改文件)
go run ./internal/compat/layerwire/gen -report
# 重新生成 tables_gen.go官方层漂移表
go run ./internal/compat/layerwire/gen -emit internal/compat/layerwire/tables_gen.go
# 全部护栏(漂移门禁 + 对各历史层真实 schema 对拍 + 性能基准)
go test ./internal/compat/layerwire/
go test ./internal/compat/layerwire/ -run '^$' -bench . -benchmem # 性能
# 改完务必:
gofmt -w internal/compat/layerwire/ && go build ./... && go vet ./internal/...
```
---
## 操作 1gotd 升级canonical layer 上移,例 227 → 230
> gotd bump 是显式任务(见 AGENTS.md 铁律 #6。canonical schema 随之变化,按下列步骤同步。
1. **同步 canonical schema**gotd 的就是实际编出的字节):
```bash
cp ../td/_schema/tdesktop.tl internal/compat/layerwire/schema/canonical-230.tl
rm internal/compat/layerwire/schema/canonical-227.tl
```
`layout.go``//go:embed schema/canonical-230.tl``const CanonicalLayer = 230`
2. **把原 canonical 层并入历史 TO 层**:现在 227/228/229 成了"老层",从 TDesktop git 抽进 `_schema/`(见文末「抽取 api.tl@N」)。
3. **改生成期常量**`gen/main.go``canonicalLayer = 230`。(`supportedFloor` 不变。)
4. **重生成 + 复核**
```bash
go run ./internal/compat/layerwire/gen -report # 看 changed/new 数字是否合理
go run ./internal/compat/layerwire/gen -emit internal/compat/layerwire/tables_gen.go
```
5. **跑护栏、按报告 triage**
```bash
go test ./internal/compat/layerwire/
```
- `TestCoverageGate` 失败 = 出现了 telesrv 可达但没处理的 227(新 canonical)-only / 结构性类型 → 去 `fallback.go` 加 by-type 兜底或结构性转换,或确认 telesrv 不发就加进 `unemittedAllowlist``gate_test.go`,附理由)。
- 生成器 `-report` 里 "structural" 列出的需手写转换(参照 `fallback.go transcodePollAnswerVoters`)。
6. `gofmt`/`build`/`vet`/全量 `go test`。真机 220/老层/新层各一台回归。
## 操作 2下探 floor支持更老客户端例 220 → 215
1. 从 TDesktop git 抽 `layer-215.tl … layer-219.tl``_schema/`(见文末)。
2. 改 `supportedFloor``layout.go``SupportedFloor = 215` **和** `gen/main.go``supportedFloor = 215`(两处都要)。
3. `go run ... -emit ...` 重生成 → `go test`
4. 越老的层结构性差异越多,按 `TestCoverageGate` / 生成器 report triage同操作 1 第 5 步)。
## 操作 3新增「客户端构造器漂移」最常见
触发:某客户端发的旧构造器导致 `NOT_IMPLEMENTED`(入站)或对端渲染异常;或主动审计客户端源码发现它发旧 CRC。
1. **拿到老构造器的精确 TL 定义**
- 优先看该客户端源码的序列化DrKLO Android`TMessagesProj/.../TLRPC.java``serializeToStream`,按 `writeInt32/writeString/...` 顺序还原字段)。
- 或它是某旧 layer 官方构造器:`git -C ../tdesktop/tdesktop log -S"#<crc>" -- <api.tl>` 找到所在层,再取该层定义。
2. **判断 body 是否与 227 字节一致**
- **一致**(只是 CRC 不同;典型=227 只追加了 flag-gated 可选字段而客户端不设)→ 往 `client_aliases.go clientMethodAliases``0x<老CRC>: 0x<227CRC>`
- **不一致**(缺 flags 整数 / 字段类型变了 / 缺必填字段)→ 往 `schema/client-drift.tl` 加**一行老布局 TL**(用 method 的限定名,结果类型随便填合法值,引擎只按名字匹配 227
3. **跑测试**
```bash
go test ./internal/compat/layerwire/ -run TestInbound
```
- 绿 = 通用引擎已能自动升级(复制共享字段 + 插 flags=0 + 按 kind 补默认)。**完事**。
- `TestInboundDriftCoverage``needs converter A->B` = 有字段类型变更 → 往 `inbound.go fieldConverters` 加一条 `"A->B"`(可复用,参照 `Vector<int>->Vector<InputMessage>`)。
- 报 `field X not defaultable` 或字段**改名** → 往 `inbound.go driftFieldRenames``"<method>\x00<227字段>": "<老字段>"`(参照 `bots.exportBotToken\x00bot`)。
4. **绝不**为此写一个新的 `handleLegacyXxx` 解码 handler——统一走数据 + 通用引擎。`routable-compat.tl` 只给既存 DrKLO theme fallback 补 dispatcher 前结构门禁,不是新增 adapter 的入口。
## 操作 4出站 `TestCoverageGate` 失败
说明 telesrv 现在会发某个"经保留字段可达"的 227-only / 结构性类型,但没处理。
- 该类**有同抽象类的老成员**可降级 → `fallback.go``newTypeFallbacksByType["<抽象类>"]`(如 `PageBlock→pageBlockUnsupported`)。
- 是**结构性变更类型**且 telesrv 真发 → `fallback.go structuralTransforms` 加手写转换。
- **确认 telesrv 不发** → 加进 `gate_test.go unemittedAllowlist`**必须附理由**,引用出站构造器审计)。
---
## 护栏:每个测试拦什么
| 测试 | 拦截 |
|---|---|
| `TestWalkConsumesCanonicalObjects` | 解释器读不全某个 227 类型(字段布局漏) |
| `TestTranscodeDowngradeValid` | 降级输出对 220..226 **真实 schema** 解析失败/有残留字节 |
| `TestTranscodeChangedTypeNestedInUnchangedContainer` | 「外层 CRC 不变但内含变更类型」被误整段拷贝 |
| `TestCoverageGate` | 出站 227-only/结构性类型无 handler 又不在 allowlistgotd bump/客户端升级引入新形态时报) |
| `TestInboundDriftCoverage` | `client-drift.tl` 某条目无法自动升级(缺 converter/rename |
| `TestInboundBodyTransforms` / `TestInboundCRCSwaps` | 入站升级产出不是合法 227 请求 |
| `TestNegotiatedLayerStickyContract` | layer 协商的 `(layer, ok)` 契约(避免缓存驱逐把老客户端误降回 227 |
**运行期 fail-safe**:出站遇未处理类型 → `Transcode` 返错 → 边界记日志并发 canonical 字节(连接存活,单对象可能渲染异常)。入站遇未覆盖旧 CRC → 落 gotd dispatcher → `NOT_IMPLEMENTED`(须按 AGENTS.md #5 进 compatibility trace + 矩阵)。**护栏的意义就是把这些从"线上撞见"提前到"提交期/测试期发现"。**
## 抽取 api.tl@N(操作 1/2 用)
```bash
TD=../tdesktop/tdesktop
APITL=Telegram/SourceFiles/mtproto/scheme/api.tl
# 找 layer N 的提交(取最后一个写入 "// LAYER N" 的;可能有初版+修订,选最全的)
git -C "$TD" log --oneline -S"// LAYER N" -- "$APITL"
# 抽取(务必校验文件末尾确是 "// LAYER N"
git -C "$TD" show <commit>:"$APITL" > internal/compat/layerwire/_schema/layer-N.tl
tail -1 internal/compat/layerwire/_schema/layer-N.tl # 应为: // LAYER N
```
各层→commit 对照见设计文档 §3 表220..227 的 canonical 抽取点)。`gotd/tl` 解析器能直接吃 TDesktop api.tl无需改格式。
## 稳态心法
**喂新 schemagotd 或更老层)→ 跑 `gen` + `go test` → 护栏吐出短清单 → 人只处理新出现的 fallback / 结构性 / converter / rename。** 不再有"运行时撞 NOT_IMPLEMENTED 再手写 handler"。

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,83 +0,0 @@
package layerwire
import (
"testing"
"github.com/gotd/td/bin"
"github.com/gotd/td/tg"
)
func benchEncode(o bin.Encoder) []byte {
var b bin.Buffer
if err := o.Encode(&b); err != nil {
panic(err)
}
return b.Copy()
}
// BenchmarkTranscodeOutbound measures the outbound seam: the 227 passthrough
// (the overwhelmingly common case) vs a real message downgrade to 220.
func BenchmarkTranscodeOutbound(b *testing.B) {
richMessage := canonicalCorpus()[1]
raw := benchEncode(richMessage)
b.Run("identity_227", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if _, err := Transcode(raw, 227); err != nil {
b.Fatal(err)
}
}
})
b.Run("downgrade_220_message", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if _, err := Transcode(raw, 220); err != nil {
b.Fatal(err)
}
}
})
dialogs := benchEncode(canonicalCorpus()[11]) // messages.dialogs
b.Run("downgrade_220_dialogs", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if _, err := Transcode(dialogs, 220); err != nil {
b.Fatal(err)
}
}
})
}
// BenchmarkUpgradeInbound measures the inbound seam: a 227 client (miss, the
// common case), a CRC-swap drift, and a body-transform drift.
func BenchmarkUpgradeInbound(b *testing.B) {
b.Run("miss_227", func(b *testing.B) {
// A canonical method id that needs no upgrade.
body := benchEncode(&tg.HelpGetConfigRequest{})
b.ReportAllocs()
for i := 0; i < b.N; i++ {
in := &bin.Buffer{Buf: body}
id, _ := in.PeekID()
if _, ok, _ := UpgradeInbound(id, in); ok {
b.Fatal("unexpected upgrade")
}
}
})
// uploadMedia body transform (peer+media -> flags+peer+media).
var um bin.Buffer
um.PutID(0x519bc2b1)
_ = (&tg.InputPeerSelf{}).Encode(&um)
_ = (&tg.InputMediaUploadedPhoto{File: &tg.InputFile{ID: 10, Parts: 1, Name: "a.jpg"}}).Encode(&um)
umRaw := um.Copy()
b.Run("drift_uploadMedia", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
in := &bin.Buffer{Buf: append([]byte(nil), umRaw...)}
if _, ok, err := UpgradeInbound(0x519bc2b1, in); !ok || err != nil {
b.Fatal(ok, err)
}
}
})
}

View file

@ -1,36 +0,0 @@
package layerwire
// clientMethodAliases maps method constructor ids emitted by a specific client's
// hand-maintained TL (constructor *drift*, NOT official api.tl layer drift) to
// the canonical (227) id — for the subset whose request body is byte-identical
// to canonical so a 4-byte id swap suffices.
//
// This is the second, hand-maintained half of the inbound compat table; the
// generated inboundMethodUpgrades (tables_gen.go) covers official layer drift
// derived from TDesktop api.tl, which by construction never contains these
// client-private ids. Entries here are sourced from client source (e.g. DrKLO
// TLRPC.java), each verified body-compatible against the canonical layout.
//
// Client-drift constructors whose body differs structurally (a missing flags
// integer, a different field type, or that need business logic such as
// access_hash resolution or a legacy-shaped response) are NOT here — they remain
// dedicated decode handlers in internal/rpc (dispatchCompat), because the body
// cannot be reused as-is and the transform needs more than an id swap.
var clientMethodAliases = map[uint32]uint32{
// DrKLO Android (post-Layer225) messages.forwardMessages. Wire layout is
// identical to canonical #13704a7c for every flag bit the client can set
// (the only schema delta is flags it never sets), so the body decodes as-is.
0x41d41ade: 0x13704a7c,
// DrKLO Android channels.inviteToChannel. Body is still
// channel:InputChannel users:Vector<InputUser> = canonical #c9e33d54.
0x199f3a6c: 0xc9e33d54,
// DrKLO Android updates.getDifference. Old layout only uses flags.0
// (pts_total_limit); canonical #19c2f763 adds pts_limit(flags.1)/
// qts_limit(flags.2) which the client leaves clear ⇒ zero wire bytes, so the
// old body decodes byte-for-byte as canonical.
0x25939651: 0x19c2f763,
// DrKLO Android messages.createChat. Body is byte-identical to canonical
// #92ceddd4; the legacy-shaped response is produced by ClientType==Android
// (createChatNeedsLegacyChat), so no dedicated handler is needed.
0x0034a818: 0x92ceddd4,
}

View file

@ -1,126 +0,0 @@
package layerwire
import (
"github.com/gotd/td/bin"
"github.com/gotd/td/tg"
)
// canonicalCorpus is a diverse set of canonical (gotd, Layer 227) objects shared
// by the walker and transcoder tests. It deliberately exercises changed types
// (message, messageMediaPhoto, keyboardButton*, dialog, channelFull, userFull,
// pollResults/pollAnswerVoters), nested containers, vectors, and multi-flags.
func canonicalCorpus() []bin.Encoder {
photo := &tg.Photo{
ID: 10,
AccessHash: 11,
FileReference: []byte{1, 2, 3},
Date: 100,
Sizes: []tg.PhotoSizeClass{
&tg.PhotoSize{Type: "x", W: 100, H: 100, Size: 2048},
&tg.PhotoStrippedSize{Type: "i", Bytes: []byte{9, 8, 7}},
},
DCID: 2,
}
return []bin.Encoder{
&tg.Message{ID: 1, PeerID: &tg.PeerUser{UserID: 2}, Date: 100, Message: "hi"},
&tg.Message{
Out: true,
ID: 2,
FromID: &tg.PeerUser{UserID: 3},
PeerID: &tg.PeerChannel{ChannelID: 4},
Date: 101,
Message: "rich",
Media: &tg.MessageMediaPhoto{Photo: photo, TTLSeconds: 5},
Entities: []tg.MessageEntityClass{
&tg.MessageEntityBold{Offset: 0, Length: 2},
&tg.MessageEntityTextURL{Offset: 0, Length: 2, URL: "https://x"},
},
ReplyMarkup: &tg.ReplyInlineMarkup{Rows: []tg.KeyboardButtonRow{
{Buttons: []tg.KeyboardButtonClass{
&tg.KeyboardButtonCallback{Text: "ok", Data: []byte("d")},
&tg.KeyboardButtonURL{Text: "go", URL: "https://y"},
}},
}},
ReplyTo: &tg.MessageReplyHeader{ReplyToMsgID: 1},
FwdFrom: tg.MessageFwdHeader{FromName: "n", Date: 99},
Views: 7,
Forwards: 2,
Reactions: tg.MessageReactions{Results: []tg.ReactionCount{{Reaction: &tg.ReactionEmoji{Emoticon: "👍"}, Count: 3}}},
GroupedID: 555,
},
&tg.MessageService{
ID: 3,
PeerID: &tg.PeerUser{UserID: 2},
Date: 102,
Action: &tg.MessageActionChatEditTitle{Title: "t"},
},
&tg.Updates{
Updates: []tg.UpdateClass{
&tg.UpdateNewMessage{Message: &tg.Message{ID: 9, PeerID: &tg.PeerUser{UserID: 2}, Date: 1, Message: "u"}, Pts: 1, PtsCount: 1},
&tg.UpdateMessageID{ID: 9, RandomID: 123},
},
Users: []tg.UserClass{&tg.User{ID: 2, AccessHash: 5, FirstName: "A"}},
Chats: []tg.ChatClass{&tg.Channel{ID: 4, AccessHash: 6, Title: "C", Photo: &tg.ChatPhotoEmpty{}}},
Date: 100,
Seq: 1,
},
&tg.User{
ID: 2,
AccessHash: 5,
FirstName: "A",
Username: "a",
Photo: &tg.UserProfilePhoto{PhotoID: 7, DCID: 2},
Status: &tg.UserStatusOnline{Expires: 999},
},
&tg.UserFull{
ID: 2,
About: "hi",
Settings: tg.PeerSettings{},
NotifySettings: tg.PeerNotifySettings{},
CommonChatsCount: 0,
},
&tg.Channel{ID: 4, AccessHash: 6, Title: "C", Megagroup: true, Photo: &tg.ChatPhotoEmpty{}},
&tg.ChannelFull{
ID: 4,
About: "about",
ReadInboxMaxID: 1,
ReadOutboxMaxID: 1,
UnreadCount: 0,
ChatPhoto: &tg.PhotoEmpty{ID: 0},
NotifySettings: tg.PeerNotifySettings{},
Pts: 1,
},
&tg.Dialog{
Peer: &tg.PeerUser{UserID: 2},
TopMessage: 2,
ReadInboxMaxID: 1,
NotifySettings: tg.PeerNotifySettings{},
},
&tg.Poll{
ID: 1,
Question: tg.TextWithEntities{Text: "q?"},
Answers: []tg.PollAnswerClass{
&tg.PollAnswer{Text: tg.TextWithEntities{Text: "a"}, Option: []byte{0}},
&tg.PollAnswer{Text: tg.TextWithEntities{Text: "b"}, Option: []byte{1}},
},
},
&tg.PollResults{
Results: []tg.PollAnswerVoters{
{Option: []byte{0}, Voters: 3, Chosen: true},
{Option: []byte{1}, Voters: 1},
},
TotalVoters: 4,
},
&tg.MessagesDialogs{
Dialogs: []tg.DialogClass{&tg.Dialog{Peer: &tg.PeerUser{UserID: 2}, TopMessage: 2, NotifySettings: tg.PeerNotifySettings{}}},
Messages: []tg.MessageClass{&tg.Message{ID: 2, PeerID: &tg.PeerUser{UserID: 2}, Date: 1, Message: "x"}},
Chats: []tg.ChatClass{},
Users: []tg.UserClass{&tg.User{ID: 2, AccessHash: 5, FirstName: "A"}},
},
&tg.MessagesMessages{
Messages: []tg.MessageClass{&tg.Message{ID: 2, PeerID: &tg.PeerUser{UserID: 2}, Date: 1, Message: "x"}},
Chats: []tg.ChatClass{},
Users: []tg.UserClass{&tg.User{ID: 2, AccessHash: 5, FirstName: "A"}},
},
}
}

View file

@ -1,144 +0,0 @@
package layerwire
import "github.com/gotd/td/bin"
// Hand-written transforms for the message subtree: structural changes that are
// not pure field drops, and 227-only constructors that older clients cannot
// decode. By-abstract-type fallbacks auto-cover future variants of the same
// class (e.g. a new MessageAction added in a later layer still degrades to
// messageActionEmpty). See docs/layer-compat-220-227-design.md §5.2.
const (
messageActionEmptyID = 0xb6aef7b0 // messageActionEmpty = MessageAction
messageEntityUnknownID = 0xbb92ba95 // messageEntityUnknown offset:int length:int = MessageEntity
pageBlockUnsupportedID = 0x13567e8a // pageBlockUnsupported = PageBlock
textEmptyID = 0xdc3d824f // textEmpty = RichText
)
func init() {
structuralTransforms["pollAnswerVoters"] = transcodePollAnswerVoters
// 227-only constructors degrade to a class member every supported layer has.
// Each target carries no body, so the replacement is a bare id (a new
// variant inside a Vector keeps its slot — no element drop needed).
newTypeFallbacksByType["MessageAction"] = replaceWithBare(messageActionEmptyID)
newTypeFallbacksByType["PageBlock"] = replaceWithBare(pageBlockUnsupportedID)
newTypeFallbacksByType["RichText"] = replaceWithBare(textEmptyID)
newTypeFallbacksByType["MessageEntity"] = fallbackMessageEntity
}
// replaceWithBare consumes the canonical (227-only) object and emits a
// bodyless constructor id the target layer understands.
func replaceWithBare(id uint32) fallbackFunc {
return func(cl *ctorLayout, in, out *bin.Buffer, layer, depth int, walk *walkState) error {
if err := in.ConsumeID(cl.crc); err != nil {
return err
}
if err := walk.skipCtorBody(canonical, in, cl, depth); err != nil {
return err
}
out.PutID(id)
return nil
}
}
// peerVectorField is a synthetic Vector<Peer> layout used to consume the
// canonical recent_voters field.
var peerVectorField = fieldLayout{
kind: kindVector,
flagBit: -1,
elem: &fieldLayout{kind: kindObject, typeName: "Peer", flagBit: -1},
}
var pollOptionBytesField = fieldLayout{kind: kindBytes, flagBit: -1}
// transcodePollAnswerVoters downgrades pollAnswerVoters: canonical (227) made
// voters conditional (flags.2?int) and added recent_voters (flags.2?Vector<Peer>);
// older layers carry voters as a plain int. The leading CRC is already consumed.
//
// 227: flags:# chosen:flags.0?true correct:flags.1?true option:bytes
// voters:flags.2?int recent_voters:flags.2?Vector<Peer>
// <=226: flags:# chosen:flags.0?true correct:flags.1?true option:bytes voters:int
func transcodePollAnswerVoters(cl *ctorLayout, target uint32, in, out *bin.Buffer, layer, depth int, walk *walkState) error {
flags, err := in.Uint32()
if err != nil {
return err
}
optionStart := in.Buf
if err := walk.skipValue(canonical, in, &pollOptionBytesField, cl, depth); err != nil {
return err
}
optionRaw := optionStart[:len(optionStart)-len(in.Buf)]
var voters int
if flags&(1<<2) != 0 {
if voters, err = in.Int(); err != nil {
return err
}
if err := walk.skipValue(canonical, in, &peerVectorField, cl, depth); err != nil {
return err
}
}
out.PutID(target)
out.PutUint32(flags & 0b11) // retain chosen/correct, clear the moved bit 2
out.Put(optionRaw)
out.PutInt(voters)
return nil
}
// fallbackMessageEntity replaces any 227-only MessageEntity with
// messageEntityUnknown, preserving offset/length so text positions stay valid.
func fallbackMessageEntity(cl *ctorLayout, in, out *bin.Buffer, layer, depth int, walk *walkState) error {
id, err := in.PeekID()
if err != nil {
return err
}
if err := in.ConsumeID(id); err != nil {
return err
}
offset, length, err := canonical.decodeOffsetLength(in, cl, depth, walk)
if err != nil {
return err
}
out.PutID(messageEntityUnknownID)
out.PutInt(offset)
out.PutInt(length)
return nil
}
// decodeOffsetLength walks a constructor body (no leading CRC) per the canonical
// layout, returning its offset/length int fields and discarding the rest.
func (m *schemaModel) decodeOffsetLength(in *bin.Buffer, cl *ctorLayout, depth int, walk *walkState) (offset, length int, err error) {
var flags map[string]uint32
for i := range cl.fields {
f := &cl.fields[i]
if f.isFlags {
v, e := in.Uint32()
if e != nil {
return 0, 0, e
}
if flags == nil {
flags = make(map[string]uint32, 2)
}
flags[f.name] = v
continue
}
if f.conditional() && flags[f.flagName]&(1<<uint(f.flagBit)) == 0 {
continue
}
switch {
case f.kind == kindInt && f.name == "offset":
if offset, err = in.Int(); err != nil {
return
}
case f.kind == kindInt && f.name == "length":
if length, err = in.Int(); err != nil {
return
}
default:
if err = walk.skipValue(m, in, f, cl, depth); err != nil {
return
}
}
}
return
}

View file

@ -1,163 +0,0 @@
package layerwire
import (
"sort"
"strings"
"testing"
)
// isInbound reports whether a constructor is a client->server (Input*) type the
// server never emits, so it cannot appear in downgraded output.
func isInbound(cl *ctorLayout) bool {
return strings.HasPrefix(cl.result, "Input") || strings.HasPrefix(cl.name, "input")
}
// collectReachableTypes returns abstract/bare type names that can appear in
// downgraded output at a layer: those referenced by a *retained* field of a
// constructor that is itself emittable (not a function, not a 227-only type that
// is replaced wholesale, not an inbound Input* type).
func collectReachableTypes(lt *layerTables) map[string]bool {
refs := map[string]bool{}
var addField func(f *fieldLayout)
addField = func(f *fieldLayout) {
switch f.kind {
case kindObject, kindBareObject:
refs[f.typeName] = true
case kindVector, kindVectorBare:
addField(f.elem)
}
}
for crc, cl := range canonical.byCRC {
if cl.isFunc || isInbound(cl) || lt.newTypes[crc] {
continue
}
var keep map[string]bool
if r := lt.rules[crc]; r != nil && r.structural == "" {
keep = r.keep
}
for i := range cl.fields {
f := &cl.fields[i]
if f.isFlags {
continue
}
if keep != nil && !keep[f.name] {
continue // dropped by a mechanical rule
}
addField(f)
}
}
return refs
}
// unemittedAllowlist is the curated set of reachable-but-unhandled 227-only
// constructors that telesrv does not actually emit (confirmed against the
// outbound-constructor scoping audit, 2026-06-25). They live behind features the
// server lacks (instant-view rich pages, AI compose, managed bots, web-browser
// settings, guest chat, star-gift rarity/craft, join-chat bot results). The gate
// fails if a NEW reachable type appears that is neither handled nor listed here,
// forcing a human to triage on every gotd bump / client upgrade.
var unemittedAllowlist = map[string]bool{
"aiComposeTone": true,
"aiComposeToneDefault": true,
"aiComposeToneExample": true,
"botInlineMessageRichMessage": true,
"channelAdminLogEventActionParticipantEditRank": true,
"joinChatBotResultApproved": true,
"joinChatBotResultDeclined": true,
"joinChatBotResultQueued": true,
"joinChatBotResultWebView": true,
"messages.chatInviteJoinResultWebView": true,
"messages.emojiGameDiceInfo": true,
"messages.emojiGameUnavailable": true,
"requestPeerTypeCreateBot": true,
"richMessage": true,
"sendMessageRichMessageDraftAction": true,
"starGiftAttributeRarity": true,
"starGiftAttributeRarityEpic": true,
"starGiftAttributeRarityLegendary": true,
"starGiftAttributeRarityRare": true,
"starGiftAttributeRarityUncommon": true,
"topPeerCategoryBotsGuestChat": true,
"updateAiComposeTones": true,
"updateBotGuestChatQuery": true,
"updateChatParticipantRank": true,
"updateEmojiGameInfo": true,
"updateJoinChatWebViewDecision": true,
"updateManagedBot": true,
"updateNewBotConnection": true,
"updateStarGiftCraftFail": true,
"updateWebBrowserException": true,
"updateWebBrowserSettings": true,
"webDomainException": true,
"webPageAttributeAiComposeTone": true,
// Structural changed-types telesrv does not emit (see design Appendix C and
// the scoping audit); their hand transforms are deferred to CI-todo.
"pageListOrderedItemText": true,
"pageListOrderedItemBlocks": true,
"starGiftAttributeModel": true,
"starGiftAttributeBackdrop": true,
"starGiftAttributePattern": true,
"urlAuthResultAccepted": true,
"inputMediaPoll": true, // inbound only
}
func newTypeHandled(crc uint32, result string) bool {
if newTypeFallbacks[crc] != nil {
return true
}
return newTypeFallbacksByType[result] != nil
}
// TestCoverageGate is the drift gate. For every supported layer, each 227-only
// or structural constructor that can appear in downgraded output must be either
// handled (fallback / structural transform) or explicitly allowlisted as not
// emitted. A bare failure means new wire shape slipped in unhandled.
func TestCoverageGate(t *testing.T) {
for layer := SupportedFloor; layer < CanonicalLayer; layer++ {
lt := tables[layer]
if lt == nil {
t.Fatalf("no tables for layer %d", layer)
}
reach := collectReachableTypes(lt)
// Structural rules that are reachable need a registered transform.
for crc, r := range lt.rules {
if r.structural == "" {
continue
}
cl := canonical.byCRC[crc]
reachable := cl != nil && reach[cl.result] && !isInbound(cl)
handled := structuralTransforms[r.structural] != nil
if reachable && !handled && !unemittedAllowlist[nameOf(crc)] {
t.Errorf("layer %d: reachable structural %s (%#08x) has no transform", layer, nameOf(crc), crc)
}
}
// New constructors reachable through a retained field need a fallback.
var gaps []string
for crc := range lt.newTypes {
cl := canonical.byCRC[crc]
if cl == nil || cl.isFunc || isInbound(cl) {
continue
}
if !reach[cl.result] {
continue
}
if newTypeHandled(crc, cl.result) || unemittedAllowlist[cl.name] {
continue
}
gaps = append(gaps, cl.name)
}
if len(gaps) > 0 {
sort.Strings(gaps)
t.Errorf("layer %d: %d reachable 227-only types lack a fallback or allowlist entry:\n %v", layer, len(gaps), gaps)
}
}
}
func nameOf(crc uint32) string {
if cl := canonical.byCRC[crc]; cl != nil {
return cl.name
}
return "?"
}

View file

@ -1,450 +0,0 @@
// Command layerwire-gen diffs the canonical gotd schema (Layer 227, the bytes
// telesrv actually emits) against historical TDesktop api.tl layers (220..226)
// and classifies every per-constructor change as either MECHANICAL (a pure
// append-only delta that can be downgraded by dropping trailing/optional fields
// and masking flag bits) or STRUCTURAL (field reorder / reinterpretation that
// needs a hand-written transform).
//
// It is the generate-time half of the layer-compat design
// (docs/layer-compat-220-227-design.md). Run from the telesrv module root:
//
// go run ./internal/compat/layerwire/gen -report
//
// This first iteration only prints a report so the numbers can be validated
// against the design doc before any table is emitted.
package main
import (
"flag"
"fmt"
"go/format"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/gotd/tl"
)
// canonicalLayer is the layer telesrv's gotd is pinned to.
const canonicalLayer = 227
// supportedFloor is the oldest client layer telesrv aims to serve.
const supportedFloor = 220
// spec is a single TL constructor or method with field-level metadata.
type spec struct {
qname string // qualified name, e.g. "messages.dialogs" or "message"
crc uint32
params []tl.Parameter
isFunc bool
}
// schema indexes one parsed .tl file by qualified name and by CRC.
type schema struct {
layer int
byName map[string]*spec
byCRC map[uint32]*spec
ordered []*spec
}
func qualify(d tl.Definition) string {
if len(d.Namespace) == 0 {
return d.Name
}
return strings.Join(d.Namespace, ".") + "." + d.Name
}
func load(path string) (*schema, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
parsed, err := tl.Parse(f)
if err != nil {
return nil, fmt.Errorf("parse %s: %w", path, err)
}
s := &schema{
layer: parsed.Layer,
byName: make(map[string]*spec),
byCRC: make(map[uint32]*spec),
}
for i := range parsed.Definitions {
sd := parsed.Definitions[i]
d := sd.Definition
sp := &spec{
qname: qualify(d),
crc: d.ID,
params: d.Params,
isFunc: sd.Category == tl.CategoryFunction,
}
// Skip the implicit vector pseudo-definition if present.
if sp.qname == "vector" {
continue
}
s.byName[sp.qname] = sp
s.byCRC[sp.crc] = sp
s.ordered = append(s.ordered, sp)
}
return s, nil
}
// classifyResult describes how a changed constructor downgrades from canonical
// (227) to a target layer.
type classifyResult struct {
mechanical bool
drops []string // canonical fields absent at the target layer
reason string // populated when !mechanical
}
// classifyDowngrade aligns the target params as a subsequence (by name) of the
// canonical params. Success ⇒ mechanical drop of the unmatched canonical fields.
// Any name mismatch, type change, or flag-condition change ⇒ structural.
func classifyDowngrade(from, to *spec) classifyResult {
var drops []string
i, j := 0, 0
fp, tp := from.params, to.params
for j < len(tp) {
// Advance over canonical fields until we reach the target field name.
for i < len(fp) && fp[i].Name != tp[j].Name {
drops = append(drops, fp[i].Name)
i++
}
if i == len(fp) {
return classifyResult{reason: fmt.Sprintf("target field %q not found in canonical (reorder/insert)", tp[j].Name)}
}
if reason := compatible(fp[i], tp[j]); reason != "" {
return classifyResult{reason: fmt.Sprintf("field %q: %s", tp[j].Name, reason)}
}
i++
j++
}
for ; i < len(fp); i++ {
drops = append(drops, fp[i].Name)
}
return classifyResult{mechanical: true, drops: drops}
}
// compatible reports "" if a kept field is wire-compatible between canonical and
// target, or a reason string otherwise.
func compatible(f, t tl.Parameter) string {
if f.Flags != t.Flags {
return "flags-int vs field mismatch"
}
if f.Flags {
// Both are `#` flag integers; the name must match because conditional
// fields reference it by name.
if f.Name != t.Name {
return fmt.Sprintf("flags int renamed %q->%q", t.Name, f.Name)
}
return ""
}
// Conditional-ness must match exactly (no flag-bit remap supported yet).
fc, tc := f.Flag != nil, t.Flag != nil
if fc != tc {
return "conditional-ness changed"
}
if fc {
if f.Flag.Name != t.Flag.Name || f.Flag.Index != t.Flag.Index {
return fmt.Sprintf("flag moved %s.%d->%s.%d", t.Flag.Name, t.Flag.Index, f.Flag.Name, f.Flag.Index)
}
}
if f.Type.String() != t.Type.String() {
return fmt.Sprintf("type changed %s->%s", t.Type.String(), f.Type.String())
}
return ""
}
type changed struct {
qname string
fromCRC, toCRC uint32
res classifyResult
}
// diff compares canonical (from) against a single target layer (to).
type diffResult struct {
layer int
changedTypes []changed
changedMethods []changed
newTypes []string // exist in canonical, absent at target
newMethods []string
removedTypes []string // exist at target, absent in canonical
}
func diff(from, to *schema) diffResult {
r := diffResult{layer: to.layer}
for _, sp := range from.ordered {
other, ok := to.byName[sp.qname]
if !ok {
if sp.isFunc {
r.newMethods = append(r.newMethods, sp.qname)
} else {
r.newTypes = append(r.newTypes, sp.qname)
}
continue
}
if other.crc == sp.crc {
continue
}
c := changed{qname: sp.qname, fromCRC: sp.crc, toCRC: other.crc, res: classifyDowngrade(sp, other)}
if sp.isFunc {
r.changedMethods = append(r.changedMethods, c)
} else {
r.changedTypes = append(r.changedTypes, c)
}
}
for _, sp := range to.ordered {
if _, ok := from.byName[sp.qname]; !ok {
r.removedTypes = append(r.removedTypes, sp.qname)
}
}
return r
}
func main() {
var (
schemaDir = flag.String("schema", "internal/compat/layerwire/_schema", "dir with layer-NNN.tl")
canonical = flag.String("canonical", "internal/compat/layerwire/schema/canonical-227.tl", "gotd canonical 227 schema")
emit = flag.String("emit", "", "write generated tables_gen.go to this path")
_ = flag.Bool("report", true, "print report")
)
flag.Parse()
canon, err := load(*canonical)
if err != nil {
fmt.Fprintln(os.Stderr, "load canonical:", err)
os.Exit(1)
}
if *emit != "" {
if err := emitTables(canon, *schemaDir, *emit); err != nil {
fmt.Fprintln(os.Stderr, "emit:", err)
os.Exit(1)
}
fmt.Printf("wrote %s\n", *emit)
return
}
fmt.Printf("canonical (gotd) layer=%d defs=%d\n", canon.layer, len(canon.ordered))
// Per-layer diff + union across the supported range.
unionChangedTypes := map[string]bool{}
unionChangedMethods := map[string]bool{}
unionNewTypes := map[string]bool{}
unionNewMethods := map[string]bool{}
structuralTypes := map[string]string{} // qname -> reason (worst case seen)
for L := supportedFloor; L < canonicalLayer; L++ {
path := filepath.Join(*schemaDir, fmt.Sprintf("layer-%d.tl", L))
tgt, err := load(path)
if err != nil {
fmt.Fprintln(os.Stderr, "load", path, ":", err)
os.Exit(1)
}
r := diff(canon, tgt)
mech, struc := 0, 0
for _, c := range r.changedTypes {
unionChangedTypes[c.qname] = true
if c.res.mechanical {
mech++
} else {
struc++
structuralTypes[c.qname] = c.res.reason
}
}
for _, c := range r.changedMethods {
unionChangedMethods[c.qname] = true
}
for _, n := range r.newTypes {
unionNewTypes[n] = true
}
for _, n := range r.newMethods {
unionNewMethods[n] = true
}
fmt.Printf("layer %d: defs=%d changedTypes=%d (mech=%d struc=%d) changedMethods=%d newTypes=%d newMethods=%d removed=%d\n",
L, len(tgt.ordered), len(r.changedTypes), mech, struc, len(r.changedMethods), len(r.newTypes), len(r.newMethods), len(r.removedTypes))
}
fmt.Printf("\n=== UNION %d..%d vs %d ===\n", supportedFloor, canonicalLayer-1, canonicalLayer)
fmt.Printf("changed types: %d\n", len(unionChangedTypes))
fmt.Printf("changed methods: %d\n", len(unionChangedMethods))
fmt.Printf("new types: %d\n", len(unionNewTypes))
fmt.Printf("new methods: %d\n", len(unionNewMethods))
fmt.Printf("structural types (need hand transform): %d\n", len(structuralTypes))
for _, q := range sortedKeys(structuralTypes) {
fmt.Printf(" - %s : %s\n", q, structuralTypes[q])
}
// Detailed 220-vs-227 drop table (matches design doc Appendix A).
fmt.Printf("\n=== 220 vs 227 changed-type drop table ===\n")
tgt220, _ := load(filepath.Join(*schemaDir, "layer-220.tl"))
r := diff(canon, tgt220)
sort.Slice(r.changedTypes, func(a, b int) bool { return r.changedTypes[a].qname < r.changedTypes[b].qname })
for _, c := range r.changedTypes {
tag := "MECH"
detail := "drop: " + strings.Join(c.res.drops, ", ")
if !c.res.mechanical {
tag = "STRUCT"
detail = c.res.reason
}
fmt.Printf(" [%-6s] %-34s %#08x->%#08x %s\n", tag, c.qname, c.toCRC, c.fromCRC, detail)
}
}
// emitTables writes the runtime downgrade tables (tables_gen.go) for every
// supported layer: per changed constructor a mechanical keep-list or a
// structural marker, plus the set of canonical CRCs absent at that layer.
func emitTables(canon *schema, schemaDir, outPath string) error {
var b strings.Builder
b.WriteString("// Code generated by ./internal/compat/layerwire/gen; DO NOT EDIT.\n")
b.WriteString("// Source: gotd canonical schema (Layer 227) diffed against TDesktop api.tl@N.\n\n")
b.WriteString("package layerwire\n\n")
b.WriteString("// generatedTables maps a supported client layer to its canonical(227)->layer\n")
b.WriteString("// downgrade table. See docs/layer-compat-220-227-design.md.\n")
b.WriteString("var generatedTables = map[int]layerRaw{\n")
// inbound 方法升级(扁平:老方法 CRC -> 227 CRC。老 CRC 本身编码了格式,故无需 layer 维度。
// 仅收"升级安全"的方法227 新增字段全为 flag-gated 条件字段(老客户端清零位=零字节,
// 其 body 本就是合法 227 body换 4 字节 CRC 即可交给 227 handler
inboundUpgrades := map[uint32]uint32{} // oldCRC -> 227CRC
inboundUnsafe := map[string]string{} // qname -> reason
for L := supportedFloor; L < canonicalLayer; L++ {
tgt, err := load(filepath.Join(schemaDir, fmt.Sprintf("layer-%d.tl", L)))
if err != nil {
return err
}
r := diff(canon, tgt)
for _, c := range r.changedMethods {
canonSpec := canon.byName[c.qname]
if reason := methodUpgradeSafe(canonSpec, c.res); reason == "" {
inboundUpgrades[c.toCRC] = c.fromCRC // client(old) -> canonical(227)
} else if _, done := inboundUpgrades[c.toCRC]; !done {
inboundUnsafe[c.qname] = reason
}
}
fmt.Fprintf(&b, "\t%d: {\n", L)
sort.Slice(r.changedTypes, func(i, j int) bool { return r.changedTypes[i].fromCRC < r.changedTypes[j].fromCRC })
b.WriteString("\t\trules: map[uint32]ruleRaw{\n")
for _, c := range r.changedTypes {
canonSpec := canon.byName[c.qname]
if c.res.mechanical {
dropSet := map[string]bool{}
for _, d := range c.res.drops {
dropSet[d] = true
}
var keep []string
for _, p := range canonSpec.params {
if !dropSet[p.Name] {
keep = append(keep, p.Name)
}
}
fmt.Fprintf(&b, "\t\t\t0x%08x: {target: 0x%08x, keep: %s}, // %s\n", c.fromCRC, c.toCRC, goStrSlice(keep), c.qname)
} else {
fmt.Fprintf(&b, "\t\t\t0x%08x: {target: 0x%08x, structural: %q}, // %s\n", c.fromCRC, c.toCRC, c.qname, c.res.reason)
}
}
b.WriteString("\t\t},\n")
var newCRC []uint32
for _, q := range r.newTypes {
if sp := canon.byName[q]; sp != nil {
newCRC = append(newCRC, sp.crc)
}
}
sort.Slice(newCRC, func(i, j int) bool { return newCRC[i] < newCRC[j] })
b.WriteString("\t\tnewTypes: []uint32{")
for i, c := range newCRC {
if i%6 == 0 {
b.WriteString("\n\t\t\t")
}
fmt.Fprintf(&b, "0x%08x, ", c)
}
if len(newCRC) > 0 {
b.WriteString("\n\t\t")
}
b.WriteString("},\n")
b.WriteString("\t},\n")
}
b.WriteString("}\n\n")
// Flat inbound method CRC upgrade table.
b.WriteString("// inboundMethodUpgrades maps an old client method constructor id to the\n")
b.WriteString("// canonical (227) id. Only upgrade-safe changes (all 227 additions flag-gated)\n")
b.WriteString("// are listed: rewriting the 4-byte id yields a valid 227 request body.\n")
if len(inboundUnsafe) > 0 {
b.WriteString("// NOT upgrade-safe as a pure id swap (declare a body transform in client-drift.tl when needed):\n")
for _, q := range sortedKeys(inboundUnsafe) {
fmt.Fprintf(&b, "// %s: %s\n", q, inboundUnsafe[q])
}
}
b.WriteString("var inboundMethodUpgrades = map[uint32]uint32{\n")
oldCRCs := make([]uint32, 0, len(inboundUpgrades))
for old := range inboundUpgrades {
oldCRCs = append(oldCRCs, old)
}
sort.Slice(oldCRCs, func(i, j int) bool { return oldCRCs[i] < oldCRCs[j] })
for _, old := range oldCRCs {
fmt.Fprintf(&b, "\t0x%08x: 0x%08x, // %s\n", old, inboundUpgrades[old], canon.byCRC[inboundUpgrades[old]].qname)
}
b.WriteString("}\n")
formatted, err := format.Source([]byte(b.String()))
if err != nil {
_ = os.WriteFile(outPath, []byte(b.String()), 0o644)
return fmt.Errorf("gofmt: %w", err)
}
return os.WriteFile(outPath, formatted, 0o644)
}
// methodUpgradeSafe reports "" if a layer-N request body for a changed method
// is also a valid 227 body after only swapping the constructor id — i.e. the
// downgrade is mechanical and every 227-only field is flag-gated (a conditional
// field the old client leaves clear ⇒ zero wire bytes). A 227-only non-conditional
// field or an inserted flags integer breaks the byte alignment ⇒ unsafe.
func methodUpgradeSafe(canonSpec *spec, res classifyResult) string {
if !res.mechanical {
return res.reason
}
byName := map[string]tl.Parameter{}
for _, p := range canonSpec.params {
byName[p.Name] = p
}
for _, d := range res.drops {
p, ok := byName[d]
if !ok {
return fmt.Sprintf("dropped field %q not in canonical", d)
}
if p.Flags {
return fmt.Sprintf("227 inserts flags integer %q", d)
}
if p.Flag == nil {
return fmt.Sprintf("227-only field %q is non-conditional", d)
}
}
return ""
}
func goStrSlice(ss []string) string {
var b strings.Builder
b.WriteString("[]string{")
for i, s := range ss {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(strconv.Quote(s))
}
b.WriteString("}")
return b.String()
}
func sortedKeys[V any](m map[string]V) []string {
ks := make([]string, 0, len(m))
for k := range m {
ks = append(ks, k)
}
sort.Strings(ks)
return ks
}

View file

@ -1,326 +0,0 @@
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 "<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",
"messages.editChatCreator\x00peer": "channel",
}
// 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, walk *walkState, owner *ctorLayout, field *fieldLayout) 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, walk *walkState, owner *ctorLayout, field *fieldLayout) error {
in := &bin.Buffer{Buf: raw}
n, err := in.VectorHeader()
if err != nil {
return err
}
if max := walk.vectorLimit(owner, field); n > max {
return limitf("vector %s.%s length %d exceeds limit %d", ownerName(owner), fieldName(field), n, max)
}
out.PutVectorHeader(n)
for i := 0; i < n; i++ {
v, err := in.Int()
if err != nil {
return err
}
out.PutID(inputMessageID)
out.PutInt(v)
}
if in.Len() != 0 {
return malformedf("%d trailing bytes in Vector<int> converter", in.Len())
}
return nil
},
// bot_id:long -> bot:InputUser{user_id, access_hash=0}.
"long->InputUser": func(raw []byte, out *bin.Buffer, walk *walkState, owner *ctorLayout, field *fieldLayout) error {
in := &bin.Buffer{Buf: raw}
id, err := in.Long()
if err != nil {
return err
}
out.PutID(inputUserID)
out.PutLong(id)
out.PutLong(0)
if in.Len() != 0 {
return malformedf("%d trailing bytes in long converter", in.Len())
}
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, walk *walkState, owner *ctorLayout, field *fieldLayout) 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 {
target := canonical.byCRC[newID]
if target == nil || !target.isFunc {
return nil, true, malformedf("alias %#08x targets unknown canonical method %#08x", id, newID)
}
if err := validateAliasedMethod(id, target, in.Buf); err != nil {
return nil, true, err
}
// 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, newWalkState())
if err != nil {
return nil, true, classifyWalkError(fmt.Errorf("layerwire: upgrade %s (%#08x): %w", old.name, id, err))
}
return out, true, nil
}
return nil, false, nil
}
// validateAliasedMethod validates the old-id/canonical-body shape before
// allocating the replacement buffer. The body is walked against the canonical
// target layout while the original constructor id remains untouched.
func validateAliasedMethod(oldID uint32, target *ctorLayout, raw []byte) error {
walk := newWalkState()
if err := walk.enter(1, "constructor"); err != nil {
return err
}
b := &bin.Buffer{Buf: raw}
if err := b.ConsumeID(oldID); err != nil {
return classifyWalkError(err)
}
if err := walk.skipCtorBody(canonical, b, target, 1); err != nil {
return classifyWalkError(err)
}
if b.Len() != 0 {
return malformedf("%d trailing bytes after aliased method %s", b.Len(), target.name)
}
return 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, walk *walkState) (*bin.Buffer, error) {
target := canonical.byName[old.name]
if target == nil {
return nil, fmt.Errorf("no canonical method %q", old.name)
}
if err := walk.enter(1, "constructor"); err != nil {
return nil, err
}
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 := walk.skipValue(canonical, in, f, old, 1); 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, walk, old, of); 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)
}
}

View file

@ -1,115 +0,0 @@
package layerwire
import "testing"
// TestInboundUpgradeTableWellFormed checks every inbound upgrade maps an old id
// to a real canonical method id, and that the old id is genuinely historical
// (not already a canonical constructor).
func TestInboundUpgradeTableWellFormed(t *testing.T) {
if len(inboundMethodUpgrades) == 0 {
t.Fatal("inboundMethodUpgrades is empty")
}
for oldID, newID := range inboundMethodUpgrades {
cl := canonical.byCRC[newID]
if cl == nil {
t.Errorf("upgrade target %#08x is not a canonical constructor", newID)
continue
}
if !cl.isFunc {
t.Errorf("upgrade target %s (%#08x) is not a method", cl.name, newID)
}
if oldID == newID {
t.Errorf("%s: old id equals canonical id %#08x", cl.name, oldID)
}
if prev := canonical.byCRC[oldID]; prev != nil {
t.Errorf("old id %#08x collides with canonical %s", oldID, prev.name)
}
}
}
// TestClientMethodAliasesWellFormed checks every hand-maintained client-drift
// alias maps to a real canonical method, and is reachable via UpgradeMethodCRC.
func TestClientMethodAliasesWellFormed(t *testing.T) {
for oldID, newID := range clientMethodAliases {
cl := canonical.byCRC[newID]
if cl == nil || !cl.isFunc {
t.Errorf("alias target %#08x is not a canonical method", newID)
}
if _, ok := inboundMethodUpgrades[oldID]; ok {
t.Errorf("alias %#08x duplicates a generated upgrade entry", oldID)
}
if got, ok := UpgradeMethodCRC(oldID); !ok || got != newID {
t.Errorf("UpgradeMethodCRC(%#08x) = (%#08x,%v), want (%#08x,true)", oldID, got, ok, newID)
}
}
}
// TestInboundDriftCoverage is the inbound drift gate: it statically proves every
// client-drift constructor in client-drift.tl can be upgraded to its canonical
// method — shared fields match (or have a converter), canonical-only required
// fields are defaultable, and renamed fields are mapped. Adding a TL line that
// isn't auto-upgradable fails here, telling the author exactly what converter or
// rename to declare (instead of discovering it at runtime).
func TestInboundDriftCoverage(t *testing.T) {
defaultable := map[wireKind]bool{
kindInt: true, kindLong: true, kindDouble: true, kindInt128: true, kindInt256: true,
kindBytes: true, kindString: true, kindBool: true, kindVector: true, kindVectorBare: true,
}
for crc, old := range driftModel.byCRC {
target := canonical.byName[old.name]
if target == nil {
t.Errorf("drift %s (%#08x): no canonical method of that name", old.name, crc)
continue
}
oldHas := map[string]*fieldLayout{}
for i := range old.fields {
oldHas[old.fields[i].name] = &old.fields[i]
}
for i := range target.fields {
nf := &target.fields[i]
if nf.isFlags {
continue
}
oldName := nf.name
if m, ok := driftFieldRenames[old.name+"\x00"+nf.name]; ok {
oldName = m
}
if of, ok := oldHas[oldName]; ok {
if typeSig(of) != typeSig(nf) && fieldConverters[typeSig(of)+"->"+typeSig(nf)] == nil {
t.Errorf("drift %s: field %q needs converter %s->%s", old.name, nf.name, typeSig(of), typeSig(nf))
}
continue
}
if nf.conditional() || nf.kind == kindTrue {
continue // optional canonical-only field — left absent
}
if !defaultable[nf.kind] {
t.Errorf("drift %s: canonical-only required field %q (kind %d) is not defaultable; declare a transform", old.name, nf.name, nf.kind)
}
}
}
}
// TestInboundUpgradeSendMessage validates the full chain for the highest-value
// method: a layer-220 client's messages.sendMessage id upgrades to the 227 id.
func TestInboundUpgradeSendMessage(t *testing.T) {
m220 := loadLayerModel(t, 220)
old, ok := m220.byName["messages.sendMessage"]
if !ok {
t.Fatal("messages.sendMessage missing from layer-220 schema")
}
canon, ok := canonical.byName["messages.sendMessage"]
if !ok {
t.Fatal("messages.sendMessage missing from canonical schema")
}
if old.crc == canon.crc {
t.Skip("sendMessage unchanged 220->227; nothing to upgrade")
}
newID, ok := UpgradeMethodCRC(old.crc)
if !ok {
t.Fatalf("sendMessage@220 (%#08x) not in upgrade table", old.crc)
}
if newID != canon.crc {
t.Fatalf("sendMessage upgrade = %#08x, want canonical %#08x", newID, canon.crc)
}
}

View file

@ -1,236 +0,0 @@
package layerwire
import (
"testing"
"github.com/gotd/td/bin"
"github.com/gotd/td/tg"
)
// validateMethodRequest asserts that buf holds a single canonical (227) method
// request: its leading id equals wantCRC and the canonical walker consumes every
// byte (proving the rebuilt body matches the 227 layout).
func validateMethodRequest(t *testing.T, buf *bin.Buffer, wantCRC uint32, label string) {
t.Helper()
id, err := (&bin.Buffer{Buf: buf.Buf}).PeekID()
if err != nil {
t.Fatalf("%s: peek id: %v", label, err)
}
if id != wantCRC {
t.Fatalf("%s: id = %#08x, want %#08x", label, id, wantCRC)
}
probe := &bin.Buffer{Buf: append([]byte(nil), buf.Buf...)}
if err := canonical.skipObject(probe); err != nil {
t.Fatalf("%s: result not a valid 227 request: %v", label, err)
}
if probe.Len() != 0 {
t.Fatalf("%s: %d trailing bytes in rebuilt request", label, probe.Len())
}
}
func TestInboundBodyTransforms(t *testing.T) {
// uploadMedia: peer + media -> flags + peer + media.
t.Run("uploadMedia", func(t *testing.T) {
var in bin.Buffer
in.PutID(0x519bc2b1)
_ = (&tg.InputPeerSelf{}).Encode(&in)
_ = (&tg.InputMediaUploadedPhoto{File: &tg.InputFile{ID: 10, Parts: 1, Name: "a.jpg"}}).Encode(&in)
out, ok, err := UpgradeInbound(0x519bc2b1, &in)
if !ok || err != nil {
t.Fatalf("upgrade: ok=%v err=%v", ok, err)
}
validateMethodRequest(t, out, 0x14967978, "uploadMedia")
})
t.Run("authSignUp", func(t *testing.T) {
var in bin.Buffer
in.PutID(0x80eee427)
in.PutString("+15550000000")
in.PutString("hash")
in.PutString("First")
in.PutString("Last")
out, ok, err := UpgradeInbound(0x80eee427, &in)
if !ok || err != nil {
t.Fatalf("upgrade: ok=%v err=%v", ok, err)
}
validateMethodRequest(t, out, 0xaac7b717, "authSignUp")
})
t.Run("channelsGetMessages", func(t *testing.T) {
var in bin.Buffer
in.PutID(0x93d7b347)
_ = (&tg.InputChannel{ChannelID: 4, AccessHash: 5}).Encode(&in)
in.PutVectorHeader(2)
in.PutInt(11)
in.PutInt(12)
out, ok, err := UpgradeInbound(0x93d7b347, &in)
if !ok || err != nil {
t.Fatalf("upgrade: ok=%v err=%v", ok, err)
}
validateMethodRequest(t, out, 0xad8c9a23, "channelsGetMessages")
})
t.Run("messagesGetMessages", func(t *testing.T) {
var in bin.Buffer
in.PutID(0x4222fa74)
in.PutVectorHeader(2)
in.PutInt(21)
in.PutInt(22)
out, ok, err := UpgradeInbound(0x4222fa74, &in)
if !ok || err != nil {
t.Fatalf("upgrade: ok=%v err=%v", ok, err)
}
validateMethodRequest(t, out, tg.MessagesGetMessagesRequestTypeID, "messagesGetMessages")
var req tg.MessagesGetMessagesRequest
if err := req.Decode(&bin.Buffer{Buf: append([]byte(nil), out.Buf...)}); err != nil {
t.Fatalf("decode upgraded messages.getMessages: %v", err)
}
if len(req.ID) != 2 {
t.Fatalf("upgraded ids = %d, want 2", len(req.ID))
}
first, ok := req.ID[0].(*tg.InputMessageID)
if !ok || first.ID != 21 {
t.Fatalf("upgraded id[0] = %T %+v, want inputMessageID(21)", req.ID[0], req.ID[0])
}
})
t.Run("botsExportBotToken", func(t *testing.T) {
var in bin.Buffer
in.PutID(0x0063b089)
in.PutLong(777)
in.PutID(0x997275b5) // boolTrue (revoke)
out, ok, err := UpgradeInbound(0x0063b089, &in)
if !ok || err != nil {
t.Fatalf("upgrade: ok=%v err=%v", ok, err)
}
validateMethodRequest(t, out, 0xbd0d99eb, "botsExportBotToken")
})
t.Run("accountRegisterDevice", func(t *testing.T) {
var in bin.Buffer
in.PutID(0x637ea878)
in.PutInt(2)
in.PutString("token-blob")
out, ok, err := UpgradeInbound(0x637ea878, &in)
if !ok || err != nil {
t.Fatalf("upgrade: ok=%v err=%v", ok, err)
}
validateMethodRequest(t, out, 0xec86017a, "accountRegisterDevice")
})
t.Run("contactsSearch", func(t *testing.T) {
var in bin.Buffer
in.PutID(0x11f812d8)
in.PutString("ngame")
in.PutInt(20)
out, ok, err := UpgradeInbound(0x11f812d8, &in)
if !ok || err != nil {
t.Fatalf("upgrade: ok=%v err=%v", ok, err)
}
validateMethodRequest(t, out, tg.ContactsSearchRequestTypeID, "contactsSearch")
var req tg.ContactsSearchRequest
if err := req.Decode(&bin.Buffer{Buf: append([]byte(nil), out.Buf...)}); err != nil {
t.Fatalf("decode upgraded contacts.search: %v", err)
}
if req.Flags != 0 || req.Q != "ngame" || req.Limit != 20 {
t.Fatalf("upgraded contacts.search = flags:%#x q:%q limit:%d", req.Flags, req.Q, req.Limit)
}
})
t.Run("langpackGetLangPack", func(t *testing.T) {
var in bin.Buffer
in.PutID(0x9ab5c58e)
in.PutString("en")
out, ok, err := UpgradeInbound(0x9ab5c58e, &in)
if !ok || err != nil {
t.Fatalf("upgrade: ok=%v err=%v", ok, err)
}
validateMethodRequest(t, out, 0xf2f2330a, "langpackGetLangPack")
})
t.Run("langpackGetStrings", func(t *testing.T) {
var in bin.Buffer
in.PutID(0x2e1ee318)
in.PutString("en")
in.PutVectorHeader(2)
in.PutString("key1")
in.PutString("key2")
out, ok, err := UpgradeInbound(0x2e1ee318, &in)
if !ok || err != nil {
t.Fatalf("upgrade: ok=%v err=%v", ok, err)
}
validateMethodRequest(t, out, 0xefea3803, "langpackGetStrings")
})
t.Run("langpackGetLanguages", func(t *testing.T) {
var in bin.Buffer
in.PutID(0x800fd57d)
out, ok, err := UpgradeInbound(0x800fd57d, &in)
if !ok || err != nil {
t.Fatalf("upgrade: ok=%v err=%v", ok, err)
}
validateMethodRequest(t, out, 0x42c6978f, "langpackGetLanguages")
})
t.Run("channelsEditCreatorToMessagesEditChatCreator", func(t *testing.T) {
var in bin.Buffer
in.PutID(0x8f38cd1f)
_ = (&tg.InputChannel{ChannelID: 132, AccessHash: 8956724956393200600}).Encode(&in)
_ = (&tg.InputUser{UserID: 1780243211, AccessHash: 42}).Encode(&in)
_ = (&tg.InputCheckPasswordEmpty{}).Encode(&in)
out, ok, err := UpgradeInbound(0x8f38cd1f, &in)
if !ok || err != nil {
t.Fatalf("upgrade: ok=%v err=%v", ok, err)
}
validateMethodRequest(t, out, 0xf743b857, "messagesEditChatCreator")
var req tg.MessagesEditChatCreatorRequest
if err := req.Decode(&bin.Buffer{Buf: append([]byte(nil), out.Buf...)}); err != nil {
t.Fatalf("decode upgraded editChatCreator: %v", err)
}
peer, ok := req.Peer.(*tg.InputPeerChannel)
if !ok || peer.ChannelID != 132 || peer.AccessHash != 8956724956393200600 {
t.Fatalf("upgraded peer = %T %+v, want inputPeerChannel", req.Peer, req.Peer)
}
user, ok := req.UserID.(*tg.InputUser)
if !ok || user.UserID != 1780243211 || user.AccessHash != 42 {
t.Fatalf("upgraded user = %T %+v, want inputUser", req.UserID, req.UserID)
}
if _, ok := req.Password.(*tg.InputCheckPasswordEmpty); !ok {
t.Fatalf("upgraded password = %T, want inputCheckPasswordEmpty", req.Password)
}
})
}
// TestInboundCRCSwaps covers the body-compatible client-drift methods that only
// need a 4-byte id swap.
func TestInboundCRCSwaps(t *testing.T) {
t.Run("updatesGetDifference", func(t *testing.T) {
var in bin.Buffer
in.PutID(0x25939651)
in.PutUint32(0) // flags (no pts_total_limit)
in.PutInt(100) // pts
in.PutInt(200) // date
in.PutInt(0) // qts
out, ok, err := UpgradeInbound(0x25939651, &in)
if !ok || err != nil {
t.Fatalf("upgrade: ok=%v err=%v", ok, err)
}
validateMethodRequest(t, out, 0x19c2f763, "updatesGetDifference")
})
t.Run("createChat", func(t *testing.T) {
var in bin.Buffer
in.PutID(0x0034a818)
if err := (&tg.MessagesCreateChatRequest{
Users: []tg.InputUserClass{&tg.InputUser{UserID: 2, AccessHash: 3}},
Title: "Group",
}).EncodeBare(&in); err != nil {
t.Fatalf("encode createChat body: %v", err)
}
out, ok, err := UpgradeInbound(0x0034a818, &in)
if !ok || err != nil {
t.Fatalf("upgrade: ok=%v err=%v", ok, err)
}
validateMethodRequest(t, out, 0x92ceddd4, "createChat")
})
}

View file

@ -1,204 +0,0 @@
// Package layerwire downgrades canonical (Layer 227, the bytes gotd actually
// emits) TL objects to the wire shape expected by older clients (down to
// Layer 220), and is the runtime half of docs/layer-compat-220-227-design.md.
//
// The package is schema-driven: at init it parses the embedded canonical-227
// schema into a per-constructor field layout used by a generic walker; the
// generate-time tables (tables_gen.go, produced by ./gen) describe the
// per-layer downgrade rules. Business handlers and gotd are never touched —
// they always produce Layer 227, and transcoding happens only at the edge and
// only when the negotiated client layer is below 227.
package layerwire
import (
_ "embed"
"fmt"
"strings"
"github.com/gotd/tl"
)
// CanonicalLayer is the layer telesrv's pinned gotd emits.
const CanonicalLayer = 227
// SupportedFloor is the oldest client layer the transcoder targets.
const SupportedFloor = 220
// vectorTypeID is the boxed Vector constructor id.
const vectorTypeID = 0x1cb5c415
//go:embed schema/canonical-227.tl
var canonicalSchema string
// wireKind is the on-wire representation of a single TL value.
type wireKind uint8
const (
kindInt wireKind = iota // 4 bytes
kindLong // 8 bytes
kindDouble // 8 bytes
kindInt128 // 16 bytes
kindInt256 // 32 bytes
kindBytes // TL bytes (length-prefixed, padded)
kindString // TL string (same wire as bytes)
kindBool // boxed Bool (4-byte CRC)
kindTrue // flag-only pseudo value, 0 bytes
kindVector // boxed Vector<elem> (0x1cb5c415 + n + elems)
kindVectorBare // bare vector<elem> (n + elems, no id)
kindObject // boxed object (4-byte CRC + body)
kindBareObject // bare object (body only, resolved by typeName)
)
// fieldLayout is one parameter of a constructor with its decoded wire shape.
type fieldLayout struct {
name string
kind wireKind
isFlags bool // this is a `#` flags integer
flagName string // when conditional: which flags integer gates it
flagBit int // when conditional: bit index; -1 otherwise
elem *fieldLayout // vector element layout
typeName string // object/bareObject: (qualified) referenced type name
}
func (f fieldLayout) conditional() bool { return f.flagBit >= 0 }
// ctorLayout is the decoded field layout of a single constructor.
type ctorLayout struct {
crc uint32
name string // qualified TL name, e.g. "messages.dialogs" / "message"
result string // qualified result (abstract) type name
fields []fieldLayout
isFunc bool
}
// schemaModel is the parsed canonical schema indexed for the walker.
type schemaModel struct {
byCRC map[uint32]*ctorLayout
byName map[string]*ctorLayout // qualified ctor name -> layout
bareByT map[string]*ctorLayout // bare type name -> its single constructor
ctorsOfT map[string][]*ctorLayout // abstract result type -> constructors
}
// canonical is the parsed Layer 227 model, built once at init.
var canonical = mustLoadCanonical()
func mustLoadCanonical() *schemaModel {
m, err := parseSchemaModel(canonicalSchema)
if err != nil {
panic("layerwire: parse canonical schema: " + err.Error())
}
return m
}
func qualifyName(ns []string, name string) string {
if len(ns) == 0 {
return name
}
return strings.Join(ns, ".") + "." + name
}
func qualifyType(t tl.Type) string {
return qualifyName(t.Namespace, t.Name)
}
func parseSchemaModel(src string) (*schemaModel, error) {
parsed, err := tl.Parse(strings.NewReader(src))
if err != nil {
return nil, err
}
m := &schemaModel{
byCRC: make(map[uint32]*ctorLayout),
byName: make(map[string]*ctorLayout),
bareByT: make(map[string]*ctorLayout),
ctorsOfT: make(map[string][]*ctorLayout),
}
for i := range parsed.Definitions {
sd := parsed.Definitions[i]
d := sd.Definition
name := qualifyName(d.Namespace, d.Name)
if name == "vector" {
continue // implicit Vector pseudo-definition
}
cl := &ctorLayout{
crc: d.ID,
name: name,
result: qualifyType(d.Type),
isFunc: sd.Category == tl.CategoryFunction,
}
for _, p := range d.Params {
fl, err := toFieldLayout(p)
if err != nil {
return nil, fmt.Errorf("%s field %q: %w", name, p.Name, err)
}
cl.fields = append(cl.fields, fl)
}
if prev, ok := m.byCRC[cl.crc]; ok && prev.name != cl.name {
return nil, fmt.Errorf("crc collision %#08x: %s vs %s", cl.crc, prev.name, cl.name)
}
m.byCRC[cl.crc] = cl
m.byName[cl.name] = cl
if !cl.isFunc {
m.ctorsOfT[cl.result] = append(m.ctorsOfT[cl.result], cl)
// A bare type name is the lowercase constructor name itself.
m.bareByT[cl.name] = cl
}
}
return m, nil
}
func toFieldLayout(p tl.Parameter) (fieldLayout, error) {
if p.Flags {
return fieldLayout{name: p.Name, kind: kindInt, isFlags: true, flagBit: -1}, nil
}
fl := fieldLayout{name: p.Name, flagBit: -1}
if p.Flag != nil {
fl.flagName = p.Flag.Name
fl.flagBit = p.Flag.Index
}
kind, typeName, elem, err := resolveType(p.Type)
if err != nil {
return fieldLayout{}, err
}
fl.kind = kind
fl.typeName = typeName
fl.elem = elem
return fl, nil
}
func resolveType(t tl.Type) (kind wireKind, typeName string, elem *fieldLayout, err error) {
if t.GenericArg != nil {
ek, etn, eel, eerr := resolveType(*t.GenericArg)
if eerr != nil {
return 0, "", nil, eerr
}
el := &fieldLayout{kind: ek, typeName: etn, elem: eel, flagBit: -1}
if t.Name == "vector" { // bare vector
return kindVectorBare, "", el, nil
}
return kindVector, "", el, nil
}
switch t.Name {
case "int":
return kindInt, "", nil, nil
case "long":
return kindLong, "", nil, nil
case "double":
return kindDouble, "", nil, nil
case "int128":
return kindInt128, "", nil, nil
case "int256":
return kindInt256, "", nil, nil
case "bytes":
return kindBytes, "", nil, nil
case "string":
return kindString, "", nil, nil
case "Bool":
return kindBool, "", nil, nil
case "true":
return kindTrue, "", nil, nil
}
if t.Bare {
return kindBareObject, qualifyType(t), nil, nil
}
return kindObject, qualifyType(t), nil, nil
}

View file

@ -1,80 +0,0 @@
package layerwire
import (
_ "embed"
"fmt"
"github.com/gotd/td/bin"
)
const maxOpaqueRequestBytes = 16 << 20
//go:embed schema/routable-compat.tl
var routableCompatSchema string
// routable combines the canonical Layer 227 model with the small set of
// explicitly declared compatibility-only methods. Nested objects in those
// methods are canonical Input* constructors, so one combined graph is needed
// for the same depth/vector/bytes walker to validate the complete request.
var routable = mustLoadRoutable()
func mustLoadRoutable() *schemaModel {
compat, err := parseSchemaModel(routableCompatSchema)
if err != nil {
panic("layerwire: parse routable compat schema: " + err.Error())
}
m := &schemaModel{
byCRC: make(map[uint32]*ctorLayout, len(canonical.byCRC)+len(compat.byCRC)),
byName: make(map[string]*ctorLayout, len(canonical.byName)+len(compat.byName)),
bareByT: make(map[string]*ctorLayout, len(canonical.bareByT)),
ctorsOfT: make(map[string][]*ctorLayout, len(canonical.ctorsOfT)),
}
for id, cl := range canonical.byCRC {
m.byCRC[id] = cl
}
for name, cl := range canonical.byName {
m.byName[name] = cl
}
for name, cl := range canonical.bareByT {
m.bareByT[name] = cl
}
for name, ctors := range canonical.ctorsOfT {
m.ctorsOfT[name] = ctors
}
for id, cl := range compat.byCRC {
if existing := m.byCRC[id]; existing != nil {
panic(fmt.Sprintf("layerwire: routable compat crc %#08x collides with %s", id, existing.name))
}
m.byCRC[id] = cl
m.byName[cl.name] = cl
}
return m
}
// ValidateRoutableRequest validates every request shape the router knows how to
// decode, including compatibility-only fallback methods. known=false denotes
// a genuinely unknown top-level constructor. Such a request is never decoded:
// it is treated as opaque, word-aligned TL data, bounded by both this total-size
// cap and mtprotoedge's transport/RPC budgets, and must continue to the router's
// compatibility trace rather than being mislabeled as malformed input.
func ValidateRoutableRequest(body []byte) (known bool, err error) {
b := &bin.Buffer{Buf: body}
id, err := b.PeekID()
if err != nil {
return false, classifyWalkError(err)
}
cl := routable.byCRC[id]
if cl == nil {
if len(body) > maxOpaqueRequestBytes {
return false, limitf("opaque request length %d exceeds limit %d", len(body), maxOpaqueRequestBytes)
}
if len(body)%bin.Word != 0 {
return false, malformedf("opaque request length %d is not word aligned", len(body))
}
return false, nil
}
if !cl.isFunc {
return true, malformedf("constructor %s (%#08x) is not a method", cl.name, id)
}
return true, validateRequestLayout(routable, cl, body)
}

View file

@ -1,43 +0,0 @@
package layerwire
import (
"errors"
"testing"
"github.com/gotd/td/bin"
"github.com/gotd/td/tg"
)
func TestValidateRoutableRequestCompatibilityAndUnknown(t *testing.T) {
t.Run("legacy theme is fully walked", func(t *testing.T) {
var b bin.Buffer
b.PutID(0x8d9d742b)
b.PutString("android")
(&tg.InputThemeSlug{Slug: "night"}).Encode(&b)
b.PutLong(42)
known, err := ValidateRoutableRequest(b.Buf)
if err != nil || !known {
t.Fatalf("legacy theme known=%v err=%v, want true/nil", known, err)
}
b.Buf = b.Buf[:len(b.Buf)-4]
known, err = ValidateRoutableRequest(b.Buf)
if !known || !errors.Is(err, ErrMalformed) {
t.Fatalf("truncated legacy theme known=%v err=%v, want true/malformed", known, err)
}
})
t.Run("unknown stays opaque and bounded", func(t *testing.T) {
var b bin.Buffer
b.PutID(0x12345678)
b.PutUint32(0xffffffff)
known, err := ValidateRoutableRequest(b.Buf)
if err != nil || known {
t.Fatalf("opaque unknown known=%v err=%v, want false/nil", known, err)
}
known, err = ValidateRoutableRequest(append(b.Buf, 1))
if known || !errors.Is(err, ErrMalformed) {
t.Fatalf("unaligned unknown known=%v err=%v, want false/malformed", known, err)
}
})
}

File diff suppressed because it is too large Load diff

View file

@ -1,33 +0,0 @@
// Client constructor drift — method constructors emitted by a specific client's
// hand-maintained TL (DrKLO Android, TLRPC.java) that are absent from the
// canonical (227) schema. They are old-layer API versions the client never
// updated; from the server's view they request the SAME api method, only with an
// older wire shape.
//
// The generic inbound upgrader (inbound.go) matches each by qualified name to
// the canonical method and rebuilds a canonical request: copy shared fields,
// write 0 for inserted flags integers, synthesize defaults for new required
// fields, and convert changed field types via the converter registry. So adding
// support for a newly-observed drifted constructor is one line here + a `gen`
// run — never a runtime-discovered hand patch.
//
// Only bodies that differ structurally belong here. Body-identical drift (only
// the constructor id differs) is a plain id swap in clientMethodAliases
// (client_aliases.go).
---functions---
messages.uploadMedia#519bc2b1 peer:InputPeer media:InputMedia = MessageMedia;
auth.signUp#80eee427 phone_number:string phone_code_hash:string first_name:string last_name:string = auth.Authorization;
messages.getMessages#4222fa74 id:Vector<int> = messages.Messages;
channels.getMessages#93d7b347 channel:InputChannel id:Vector<int> = messages.Messages;
bots.exportBotToken#0063b089 bot_id:long revoke:Bool = bots.ExportedBotToken;
account.registerDevice#637ea878 token_type:int token:string = Bool;
contacts.search#11f812d8 q:string limit:int = contacts.Found;
langpack.getLangPack#9ab5c58e lang_code:string = LangPackDifference;
langpack.getStrings#2e1ee318 lang_code:string keys:Vector<string> = Vector<LangPackString>;
langpack.getLanguages#800fd57d = Vector<LangPackLanguage>;
// DrKLO still emits old channels.editCreator#8f38cd1f; canonical 227 replaced
// that flow with messages.editChatCreator(peer:InputPeer,...). Keep the old
// constructor id here but target the canonical method name for generic upgrade.
messages.editChatCreator#8f38cd1f channel:InputChannel user_id:InputUser password:InputCheckPasswordSRP = Updates;

View file

@ -1,11 +0,0 @@
// Hand-maintained request layouts that are intentionally handled by the RPC
// fallback instead of gotd's canonical ServerDispatcher. They still belong in
// the structural preflight model: fallback handlers must never become a way to
// bypass the canonical vector/depth/bytes budgets.
---functions---
compat.legacyCreateTheme#8432c21f flags:# slug:string title:string document:flags.2?InputDocument settings:flags.3?InputThemeSettings = Object;
compat.legacyUpdateTheme#5cb367d5 flags:# format:string theme:InputTheme slug:flags.0?string title:flags.1?string document:flags.2?InputDocument settings:flags.3?InputThemeSettings = Object;
compat.legacyInstallTheme#7ae43737 flags:# dark:flags.0?true format:flags.1?string theme:flags.1?InputTheme = Object;
compat.legacyGetTheme#8d9d742b format:string theme:InputTheme document_id:long = Object;

View file

@ -1,374 +0,0 @@
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, depth int, walk *walkState) 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, depth int, walk *walkState) 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{}
walk := newWalkState()
if err := lt.transcodeObject(in, out, layer, 1, walk); err != nil {
return nil, classifyWalkError(err)
}
if in.Len() != 0 {
return nil, malformedf("%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, depth int, walk *walkState) error {
if err := walk.enter(depth, "constructor"); err != nil {
return err
}
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, depth, walk)
}
out.PutID(rule.target)
return lt.transcodeBody(in, out, cl, rule.keep, layer, depth, walk)
}
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, depth, walk)
}
if !lt.dirty[id] {
// Unaffected subtree: byte-for-byte copy.
pre := in.Buf
if err := in.ConsumeID(id); err != nil {
return err
}
if err := walk.skipCtorBody(canonical, in, cl, depth); 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, depth, walk)
}
// 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, depth int, walk *walkState) 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, cl, layer, depth, walk); err != nil {
return fmt.Errorf("%s.%s: %w", cl.name, f.name, err)
}
} else if err := walk.skipValue(canonical, in, f, cl, depth); 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, owner *ctorLayout, layer, depth int, walk *walkState) error {
if !lt.fieldDirty(f) {
pre := in.Buf
if err := walk.skipValue(canonical, in, f, owner, depth); err != nil {
return err
}
out.Put(pre[:len(pre)-len(in.Buf)])
return nil
}
switch f.kind {
case kindVector, kindVectorBare:
vectorDepth := depth + 1
if vectorDepth <= 0 || vectorDepth > walk.limits.maxDepth {
return limitf("vector nesting depth %d exceeds limit %d", vectorDepth, walk.limits.maxDepth)
}
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
}
if n < 0 {
return malformedf("negative vector length %d", n)
}
if max := walk.vectorLimit(owner, f); n > max {
return limitf("vector %s.%s length %d exceeds limit %d", ownerName(owner), fieldName(f), n, max)
}
if err := walk.addUnits(n, "vector "+ownerName(owner)+"."+fieldName(f)); err != nil {
return err
}
out.PutInt(n)
for i := 0; i < n; i++ {
if err := lt.transcodeValue(in, out, f.elem, nil, layer, vectorDepth, walk); err != nil {
return err
}
}
return nil
case kindObject:
return lt.transcodeObject(in, out, layer, depth+1, walk)
case kindBareObject:
bareDepth := depth + 1
if err := walk.enter(bareDepth, "bare constructor"); err != nil {
return err
}
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, bareDepth, walk)
default:
// Primitive marked dirty should be impossible.
return fmt.Errorf("unexpected dirty primitive kind %d", f.kind)
}
}

View file

@ -1,388 +0,0 @@
// Code generated by ./internal/compat/layerwire/gen; DO NOT EDIT.
// Source: gotd canonical schema (Layer 227) diffed against TDesktop api.tl@N.
package layerwire
// generatedTables maps a supported client layer to its canonical(227)->layer
// downgrade table. See docs/layer-compat-220-227-design.md.
var generatedTables = map[int]layerRaw{
220: {
rules: map[uint32]ruleRaw{
0x02b78156: {target: 0xc9662d05, keep: []string{"flags", "name_requested", "username_requested", "photo_requested", "text", "button_id", "peer_type", "max_quantity"}}, // inputKeyboardButtonRequestPeer
0x033ed001: {target: 0xcd64636c, keep: []string{"flags", "bot_id", "recipients", "rights"}}, // connectedBot
0x0360d5d2: {target: 0xa0933f5b, keep: []string{"user_id", "inviter_id", "date"}}, // chatParticipantAdmin
0x06cbe645: {target: 0xa02bc13e, keep: []string{"flags", "blocked", "phone_calls_available", "phone_calls_private", "can_pin_message", "has_scheduled", "video_calls_available", "voice_messages_forbidden", "translations_disabled", "stories_pinned_available", "blocked_my_stories_from", "wallpaper_overridden", "contact_require_premium", "read_dates_private", "flags2", "sponsored_enabled", "can_view_revenue", "bot_can_manage_emoji_status", "display_gifts_button", "id", "about", "settings", "personal_photo", "profile_photo", "fallback_photo", "notify_settings", "bot_info", "pinned_msg_id", "common_chats_count", "folder_id", "ttl_period", "theme", "private_forward_name", "bot_group_admin_rights", "bot_broadcast_admin_rights", "wallpaper", "stories", "business_work_hours", "business_location", "business_greeting_message", "business_away_message", "business_intro", "birthday", "personal_channel_id", "personal_channel_message", "stargifts_count", "starref_program", "bot_verification", "send_paid_messages_stars", "disallowed_gifts", "stars_rating", "stars_my_pending_rating", "stars_my_pending_rating_date", "main_tab", "saved_music", "note"}}, // userFull
0x08cbec07: {target: 0x3f7ee58b, keep: []string{"value", "emoticon"}}, // messageMediaDice
0x15031189: {target: 0x5e068047, structural: "pageListOrderedItemText"}, // field "num": conditional-ness changed
0x16a4b93c: {target: 0xedf164f1, keep: []string{"flags", "pinned", "public", "close_friends", "min", "noforwards", "edited", "contacts", "selected_contacts", "out", "id", "date", "from_id", "fwd_from", "expire_date", "caption", "entities", "media", "media_areas", "privacy", "views", "sent_reaction", "albums"}}, // storyItem
0x1b97dd66: {target: 0x6917560b, keep: []string{"flags", "reply_to_scheduled", "forum_topic", "quote", "reply_to_msg_id", "reply_to_peer_id", "reply_from", "reply_media", "reply_to_top_id", "quote_text", "quote_entities", "quote_offset", "todo_item_id"}}, // messageReplyHeader
0x1bd54456: {target: 0xcb397619, keep: []string{"flags", "user_id", "date", "subscription_until_date"}}, // channelParticipant
0x1fd6f6c1: {target: 0x9a8ae1e1, keep: []string{"items"}}, // pageBlockOrderedList
0x2f58683c: {target: 0xb92fb6cd, keep: []string{"text"}}, // pageListItemText
0x3645230a: {target: 0x3b6ddad2, structural: "pollAnswerVoters"}, // field "voters": conditional-ness changed
0x38e79fde: {target: 0xc02d4007, keep: []string{"user_id", "inviter_id", "date"}}, // chatParticipant
0x3bd4b7c2: {target: 0x869fbe10, keep: []string{"flags", "reply_to_msg_id", "top_msg_id", "reply_to_peer_id", "quote_text", "quote_entities", "quote_offset", "monoforum_peer_id", "todo_item_id"}}, // inputReplyToMessage
0x3cd623ec: {target: 0x92d33a0e, keep: []string{"flags", "request_write_access", "bot", "domain"}}, // urlAuthResultRequest
0x3fa53905: {target: 0xafd93fbb, keep: []string{"text"}}, // keyboardButtonBuy
0x3fc18057: {target: 0x9bb2636d, keep: []string{"flags", "restore", "phone_number", "phone_code_hash", "currency", "amount"}}, // inputStorePaymentAuthCode
0x417efd8f: {target: 0xb16a6c29, keep: []string{"text"}}, // keyboardButtonRequestPhone
0x41df43fc: {target: 0xead6805e, keep: []string{"flags", "name_hidden", "unsaved", "refunded", "can_upgrade", "pinned_to_top", "upgrade_separate", "from_id", "date", "gift", "message", "msg_id", "saved_id", "convert_stars", "upgrade_stars", "can_export_at", "transfer_stars", "can_transfer_at", "can_resell_at", "collection_id", "prepaid_upgrade_hash", "drop_original_details_stars", "gift_num"}}, // savedStarGift
0x4b7d786a: {target: 0xff16e2ca, keep: []string{"text", "option"}}, // pollAnswer
0x4e7085ea: {target: 0x13acff19, structural: "starGiftAttributePattern"}, // target field "rarity_permille" not found in canonical (reorder/insert)
0x565251e2: {target: 0x39d99013, structural: "starGiftAttributeModel"}, // target field "rarity_permille" not found in canonical (reorder/insert)
0x5b0f15f5: {target: 0x53d7bfd8, keep: []string{"text", "button_id", "peer_type", "max_quantity"}}, // keyboardButtonRequestPeer
0x60fe3294: {target: 0x96eaa5eb, keep: []string{"flags", "no_webpage", "invert_media", "reply_to", "message", "entities", "media", "date", "effect", "suggested_post"}}, // draftMessage
0x623a8fa0: {target: 0x8f8c0e4e, structural: "urlAuthResultAccepted"}, // field "url": conditional-ness changed
0x63ca67aa: {target: 0x25e073fc, keep: []string{"blocks"}}, // pageListItemBlocks
0x68013e72: {target: 0xd02e7fd4, keep: []string{"flags", "request_write_access", "text", "fwd_text", "url", "bot"}}, // inputKeyboardButtonUrlAuth
0x71e4ea58: {target: 0x56e34970, keep: []string{"flags", "messages_notify_from", "stories_notify_from", "sound", "show_previews"}}, // reactionsNotifySettings
0x7600b9d3: {target: 0xb92f76cf, keep: []string{"flags", "out", "mentioned", "media_unread", "silent", "post", "from_scheduled", "legacy", "edit_hide", "pinned", "noforwards", "invert_media", "flags2", "offline", "video_processing_pending", "paid_suggested_post_stars", "paid_suggested_post_ton", "id", "from_id", "from_boosts_applied", "peer_id", "saved_peer_id", "fwd_from", "via_bot_id", "via_business_bot_id", "reply_to", "date", "message", "media", "reply_markup", "entities", "views", "forwards", "replies", "edit_date", "post_author", "grouped_id", "reactions", "restriction_reason", "ttl_period", "quick_reply_shortcut_id", "effect", "factcheck", "report_delivery_until_date", "paid_message_stars", "suggested_post", "schedule_repeat_period"}}, // message
0x7699f014: {target: 0x24f40e77, keep: []string{"poll_id", "peer", "options", "qts"}}, // updateMessagePollVote
0x773f4e66: {target: 0x4bd6e798, keep: []string{"poll", "results"}}, // messageMediaPoll
0x7a11d782: {target: 0xbbc7515d, keep: []string{"flags", "quiz", "text"}}, // keyboardButtonRequestPoll
0x7cb34d79: {target: 0x11dfa986, keep: []string{"peer", "date", "user_id", "about", "invite", "qts"}}, // updateBotChatInviteRequester
0x7d170cff: {target: 0xa2fa4880, keep: []string{"text"}}, // keyboardButton
0x7d5e07c7: {target: 0xe988037b, keep: []string{"text", "user_id"}}, // inputKeyboardButtonUserProfile
0x7d8375da: {target: 0x1e287d04, keep: []string{"flags", "spoiler", "file", "stickers", "ttl_seconds"}}, // inputMediaUploadedPhoto
0x85f0a9cd: {target: 0x569d64c9, keep: []string{"flags", "require_premium", "resale_ton_only", "theme_available", "id", "gift_id", "title", "slug", "num", "owner_id", "owner_name", "owner_address", "attributes", "availability_issued", "availability_total", "gift_address", "resell_amount", "released_by", "value_amount", "value_currency", "value_usd_amount", "theme_peer", "peer_color", "host_id", "offer_min_stars"}}, // starGiftUnique
0x883a4108: {target: 0x0f94e5f1, structural: "inputMediaPoll"}, // field "correct_answers": type changed Vector<bytes>->Vector<int>
0x89c590f9: {target: 0x50f41ccf, keep: []string{"text"}}, // keyboardButtonGame
0x8ff2d5f0: {target: 0x98dd8936, structural: "pageListOrderedItemBlocks"}, // field "num": conditional-ness changed
0x966e2dbf: {target: 0x58747131, keep: []string{"id", "flags", "closed", "public_voters", "multiple_choice", "quiz", "question", "answers", "close_period", "close_date"}}, // poll
0x991399fc: {target: 0x93b9fbb5, keep: []string{"flags", "same_peer", "text", "query", "peer_types"}}, // keyboardButtonSwitchInline
0x9f2504e4: {target: 0xd93d859c, structural: "starGiftAttributeBackdrop"}, // target field "rarity_permille" not found in canonical (reorder/insert)
0xa04e8d3a: {target: 0xe4e0b29d, keep: []string{"flags", "can_view_participants", "can_set_username", "can_set_stickers", "hidden_prehistory", "can_set_location", "has_scheduled", "can_view_stats", "blocked", "flags2", "can_delete_channel", "antispam", "participants_hidden", "translations_disabled", "stories_pinned_available", "view_forum_as_messages", "restricted_sponsored", "can_view_revenue", "paid_media_allowed", "can_view_stars_revenue", "paid_reactions_available", "stargifts_available", "paid_messages_available", "id", "about", "participants_count", "admins_count", "kicked_count", "banned_count", "online_count", "read_inbox_max_id", "read_outbox_max_id", "unread_count", "chat_photo", "notify_settings", "exported_invite", "bot_info", "migrated_from_chat_id", "migrated_from_max_id", "pinned_msg_id", "stickerset", "available_min_id", "folder_id", "linked_chat_id", "location", "slowmode_seconds", "slowmode_next_send_date", "stats_dc", "pts", "call", "ttl_period", "pending_suggestions", "groupcall_default_join_as", "theme_emoticon", "requests_pending", "recent_requesters", "default_send_as", "available_reactions", "reactions_limit", "stories", "wallpaper", "boosts_applied", "boosts_unrestrict", "emojiset", "bot_verification", "stargifts_count", "send_paid_messages_stars", "main_tab"}}, // channelFull
0xa9478a1a: {target: 0x4f607bef, keep: []string{"flags", "via_request", "user_id", "inviter_id", "date", "subscription_until_date"}}, // channelParticipantSelf
0xaa40f94d: {target: 0xfc796b3f, keep: []string{"text"}}, // keyboardButtonRequestGeoLocation
0xba7bb15e: {target: 0x7adf2420, keep: []string{"flags", "min", "results", "total_voters", "recent_voters", "solution", "solution_entities"}}, // pollResults
0xbcc4af10: {target: 0x75d2698e, keep: []string{"text", "copy_text"}}, // keyboardButtonCopy
0xc0fd5d09: {target: 0x308660c1, keep: []string{"text", "user_id"}}, // keyboardButtonUserProfile
0xd5f0ad91: {target: 0x6df8014e, keep: []string{"flags", "left", "peer", "kicked_by", "date", "banned_rights"}}, // channelParticipantBanned
0xd64c522b: {target: 0xaca1657b, keep: []string{"flags", "poll_id", "poll", "results"}}, // updateMessagePoll
0xd80c25ec: {target: 0x258aff05, keep: []string{"text", "url"}}, // keyboardButtonUrl
0xe15c4370: {target: 0xa0c0505c, keep: []string{"text", "url"}}, // keyboardButtonSimpleWebView
0xe1f867b8: {target: 0xe46bcee4, keep: []string{"user_id"}}, // chatParticipantCreator
0xe216eb63: {target: 0x695150d7, keep: []string{"flags", "spoiler", "photo", "ttl_seconds"}}, // messageMediaPhoto
0xe3af4434: {target: 0xb3ba0635, keep: []string{"flags", "spoiler", "id", "ttl_seconds"}}, // inputMediaPhoto
0xe62bc960: {target: 0x35bbdb6b, keep: []string{"flags", "requires_password", "text", "data"}}, // keyboardButtonCallback
0xe6c31522: {target: 0x95728543, keep: []string{"flags", "upgrade", "transferred", "saved", "refunded", "prepaid_upgrade", "assigned", "from_offer", "gift", "can_export_at", "transfer_stars", "from_id", "peer", "saved_id", "resale_amount", "can_transfer_at", "can_resell_at", "drop_original_details_stars"}}, // messageActionStarGiftUnique
0xe846b1a0: {target: 0x13767230, keep: []string{"text", "url"}}, // keyboardButtonWebView
0xf51006f9: {target: 0x10b78d29, keep: []string{"flags", "text", "fwd_text", "url", "button_id"}}, // keyboardButtonUrlAuth
0xf8827ebf: {target: 0xe0955a3c, keep: []string{"store_product", "phone_code_hash", "support_email_address", "support_email_subject", "currency", "amount"}}, // auth.sentCodePaymentRequired
0xfc89f7f3: {target: 0xd58a08c6, keep: []string{"flags", "pinned", "unread_mark", "view_forum_as_messages", "peer", "top_message", "read_inbox_max_id", "read_outbox_max_id", "unread_count", "unread_mentions_count", "unread_reactions_count", "notify_settings", "pts", "draft", "folder_id", "ttl_period"}}, // dialog
0xfcdad815: {target: 0xcdff0eca, keep: []string{"flags", "my", "closed", "pinned", "short", "hidden", "title_missing", "id", "date", "peer", "title", "icon_color", "icon_emoji_id", "top_message", "read_inbox_max_id", "read_outbox_max_id", "unread_count", "unread_mentions_count", "unread_reactions_count", "from_id", "notify_settings", "draft"}}, // forumTopic
},
newTypes: []uint32{
0x004b572c, 0x01a9fbfc, 0x02ff29d3, 0x0652c1c5, 0x0773c080, 0x096b2aec,
0x0a617e7b, 0x0e6e47c4, 0x0efa0194, 0x140502d1, 0x16605e3e, 0x199fed96,
0x1fa01357, 0x1fe9a9bf, 0x24c26789, 0x2999beed, 0x2f51c337, 0x36437737,
0x399674dc, 0x3c29a3e2, 0x3c60b621, 0x3e2793ba, 0x3e81e078, 0x402b4495,
0x445663a7, 0x44e56023, 0x4880ed9a, 0x4c2a5d62, 0x4fdd3430, 0x519524ea,
0x574b617f, 0x5806b4ec, 0x59080c20, 0x59e65335, 0x5b1ccb28, 0x67e731ad,
0x682a41a9, 0x6c24f3dd, 0x6c9d0efe, 0x71777116, 0x7781fe18, 0x78fbf3a8,
0x79eb8cb3, 0x7b9e1801, 0x83281dbd, 0x8c0f91fb, 0x904ac7c7, 0x90d7adfa,
0x933ca597, 0x98a3a840, 0x9b00622b, 0x9bad6414, 0x9d2eac97, 0x9da1cd6c,
0xa26156c0, 0xa2cb24f9, 0xa5b45e2b, 0xac072444, 0xac6a83aa, 0xae152a69,
0xb07ed085, 0xb22083a6, 0xb43df56c, 0xb532772b, 0xb956812d, 0xbaf39d8b,
0xbaff072f, 0xbd8367b9, 0xbdac7e70, 0xbf7d6572, 0xc1f46103, 0xc31c8f4e,
0xc39a2ade, 0xc556a45d, 0xc6c1e5a7, 0xcd24cf44, 0xcdd4093d, 0xcef7e7a8,
0xcff63ea9, 0xd6e3b813, 0xda2ad647, 0xdacb836a, 0xdbbe6c6a, 0xdbce6389,
0xdd1fbf93, 0xe188503b, 0xe2b23b51, 0xe4c449fc, 0xf08d516b, 0xf13bbcd7,
0xf1d628ec, 0xf3a9244a, 0xfa2bc90a, 0xfb9c547a,
},
},
221: {
rules: map[uint32]ruleRaw{
0x02b78156: {target: 0xc9662d05, keep: []string{"flags", "name_requested", "username_requested", "photo_requested", "text", "button_id", "peer_type", "max_quantity"}}, // inputKeyboardButtonRequestPeer
0x033ed001: {target: 0xcd64636c, keep: []string{"flags", "bot_id", "recipients", "rights"}}, // connectedBot
0x0360d5d2: {target: 0xa0933f5b, keep: []string{"user_id", "inviter_id", "date"}}, // chatParticipantAdmin
0x06cbe645: {target: 0xa02bc13e, keep: []string{"flags", "blocked", "phone_calls_available", "phone_calls_private", "can_pin_message", "has_scheduled", "video_calls_available", "voice_messages_forbidden", "translations_disabled", "stories_pinned_available", "blocked_my_stories_from", "wallpaper_overridden", "contact_require_premium", "read_dates_private", "flags2", "sponsored_enabled", "can_view_revenue", "bot_can_manage_emoji_status", "display_gifts_button", "id", "about", "settings", "personal_photo", "profile_photo", "fallback_photo", "notify_settings", "bot_info", "pinned_msg_id", "common_chats_count", "folder_id", "ttl_period", "theme", "private_forward_name", "bot_group_admin_rights", "bot_broadcast_admin_rights", "wallpaper", "stories", "business_work_hours", "business_location", "business_greeting_message", "business_away_message", "business_intro", "birthday", "personal_channel_id", "personal_channel_message", "stargifts_count", "starref_program", "bot_verification", "send_paid_messages_stars", "disallowed_gifts", "stars_rating", "stars_my_pending_rating", "stars_my_pending_rating_date", "main_tab", "saved_music", "note"}}, // userFull
0x15031189: {target: 0x5e068047, structural: "pageListOrderedItemText"}, // field "num": conditional-ness changed
0x16a4b93c: {target: 0xedf164f1, keep: []string{"flags", "pinned", "public", "close_friends", "min", "noforwards", "edited", "contacts", "selected_contacts", "out", "id", "date", "from_id", "fwd_from", "expire_date", "caption", "entities", "media", "media_areas", "privacy", "views", "sent_reaction", "albums"}}, // storyItem
0x1b97dd66: {target: 0x6917560b, keep: []string{"flags", "reply_to_scheduled", "forum_topic", "quote", "reply_to_msg_id", "reply_to_peer_id", "reply_from", "reply_media", "reply_to_top_id", "quote_text", "quote_entities", "quote_offset", "todo_item_id"}}, // messageReplyHeader
0x1bd54456: {target: 0xcb397619, keep: []string{"flags", "user_id", "date", "subscription_until_date"}}, // channelParticipant
0x1fd6f6c1: {target: 0x9a8ae1e1, keep: []string{"items"}}, // pageBlockOrderedList
0x2f58683c: {target: 0xb92fb6cd, keep: []string{"text"}}, // pageListItemText
0x3645230a: {target: 0x3b6ddad2, structural: "pollAnswerVoters"}, // field "voters": conditional-ness changed
0x38e79fde: {target: 0xc02d4007, keep: []string{"user_id", "inviter_id", "date"}}, // chatParticipant
0x3bd4b7c2: {target: 0x869fbe10, keep: []string{"flags", "reply_to_msg_id", "top_msg_id", "reply_to_peer_id", "quote_text", "quote_entities", "quote_offset", "monoforum_peer_id", "todo_item_id"}}, // inputReplyToMessage
0x3cd623ec: {target: 0x92d33a0e, keep: []string{"flags", "request_write_access", "bot", "domain"}}, // urlAuthResultRequest
0x3fa53905: {target: 0xafd93fbb, keep: []string{"text"}}, // keyboardButtonBuy
0x3fc18057: {target: 0x9bb2636d, keep: []string{"flags", "restore", "phone_number", "phone_code_hash", "currency", "amount"}}, // inputStorePaymentAuthCode
0x417efd8f: {target: 0xb16a6c29, keep: []string{"text"}}, // keyboardButtonRequestPhone
0x41df43fc: {target: 0xead6805e, keep: []string{"flags", "name_hidden", "unsaved", "refunded", "can_upgrade", "pinned_to_top", "upgrade_separate", "from_id", "date", "gift", "message", "msg_id", "saved_id", "convert_stars", "upgrade_stars", "can_export_at", "transfer_stars", "can_transfer_at", "can_resell_at", "collection_id", "prepaid_upgrade_hash", "drop_original_details_stars", "gift_num"}}, // savedStarGift
0x4b7d786a: {target: 0xff16e2ca, keep: []string{"text", "option"}}, // pollAnswer
0x4e7085ea: {target: 0x13acff19, structural: "starGiftAttributePattern"}, // target field "rarity_permille" not found in canonical (reorder/insert)
0x565251e2: {target: 0x39d99013, structural: "starGiftAttributeModel"}, // target field "rarity_permille" not found in canonical (reorder/insert)
0x5b0f15f5: {target: 0x53d7bfd8, keep: []string{"text", "button_id", "peer_type", "max_quantity"}}, // keyboardButtonRequestPeer
0x60fe3294: {target: 0x96eaa5eb, keep: []string{"flags", "no_webpage", "invert_media", "reply_to", "message", "entities", "media", "date", "effect", "suggested_post"}}, // draftMessage
0x623a8fa0: {target: 0x8f8c0e4e, structural: "urlAuthResultAccepted"}, // field "url": conditional-ness changed
0x63ca67aa: {target: 0x25e073fc, keep: []string{"blocks"}}, // pageListItemBlocks
0x68013e72: {target: 0xd02e7fd4, keep: []string{"flags", "request_write_access", "text", "fwd_text", "url", "bot"}}, // inputKeyboardButtonUrlAuth
0x71e4ea58: {target: 0x56e34970, keep: []string{"flags", "messages_notify_from", "stories_notify_from", "sound", "show_previews"}}, // reactionsNotifySettings
0x7600b9d3: {target: 0x9cb490e9, keep: []string{"flags", "out", "mentioned", "media_unread", "silent", "post", "from_scheduled", "legacy", "edit_hide", "pinned", "noforwards", "invert_media", "flags2", "offline", "video_processing_pending", "paid_suggested_post_stars", "paid_suggested_post_ton", "id", "from_id", "from_boosts_applied", "peer_id", "saved_peer_id", "fwd_from", "via_bot_id", "via_business_bot_id", "reply_to", "date", "message", "media", "reply_markup", "entities", "views", "forwards", "replies", "edit_date", "post_author", "grouped_id", "reactions", "restriction_reason", "ttl_period", "quick_reply_shortcut_id", "effect", "factcheck", "report_delivery_until_date", "paid_message_stars", "suggested_post", "schedule_repeat_period", "summary_from_language"}}, // message
0x7699f014: {target: 0x24f40e77, keep: []string{"poll_id", "peer", "options", "qts"}}, // updateMessagePollVote
0x773f4e66: {target: 0x4bd6e798, keep: []string{"poll", "results"}}, // messageMediaPoll
0x7a11d782: {target: 0xbbc7515d, keep: []string{"flags", "quiz", "text"}}, // keyboardButtonRequestPoll
0x7cb34d79: {target: 0x11dfa986, keep: []string{"peer", "date", "user_id", "about", "invite", "qts"}}, // updateBotChatInviteRequester
0x7d170cff: {target: 0xa2fa4880, keep: []string{"text"}}, // keyboardButton
0x7d5e07c7: {target: 0xe988037b, keep: []string{"text", "user_id"}}, // inputKeyboardButtonUserProfile
0x7d8375da: {target: 0x1e287d04, keep: []string{"flags", "spoiler", "file", "stickers", "ttl_seconds"}}, // inputMediaUploadedPhoto
0x85f0a9cd: {target: 0x569d64c9, keep: []string{"flags", "require_premium", "resale_ton_only", "theme_available", "id", "gift_id", "title", "slug", "num", "owner_id", "owner_name", "owner_address", "attributes", "availability_issued", "availability_total", "gift_address", "resell_amount", "released_by", "value_amount", "value_currency", "value_usd_amount", "theme_peer", "peer_color", "host_id", "offer_min_stars"}}, // starGiftUnique
0x883a4108: {target: 0x0f94e5f1, structural: "inputMediaPoll"}, // field "correct_answers": type changed Vector<bytes>->Vector<int>
0x89c590f9: {target: 0x50f41ccf, keep: []string{"text"}}, // keyboardButtonGame
0x8ff2d5f0: {target: 0x98dd8936, structural: "pageListOrderedItemBlocks"}, // field "num": conditional-ness changed
0x966e2dbf: {target: 0x58747131, keep: []string{"id", "flags", "closed", "public_voters", "multiple_choice", "quiz", "question", "answers", "close_period", "close_date"}}, // poll
0x991399fc: {target: 0x93b9fbb5, keep: []string{"flags", "same_peer", "text", "query", "peer_types"}}, // keyboardButtonSwitchInline
0x9f2504e4: {target: 0xd93d859c, structural: "starGiftAttributeBackdrop"}, // target field "rarity_permille" not found in canonical (reorder/insert)
0xa04e8d3a: {target: 0xe4e0b29d, keep: []string{"flags", "can_view_participants", "can_set_username", "can_set_stickers", "hidden_prehistory", "can_set_location", "has_scheduled", "can_view_stats", "blocked", "flags2", "can_delete_channel", "antispam", "participants_hidden", "translations_disabled", "stories_pinned_available", "view_forum_as_messages", "restricted_sponsored", "can_view_revenue", "paid_media_allowed", "can_view_stars_revenue", "paid_reactions_available", "stargifts_available", "paid_messages_available", "id", "about", "participants_count", "admins_count", "kicked_count", "banned_count", "online_count", "read_inbox_max_id", "read_outbox_max_id", "unread_count", "chat_photo", "notify_settings", "exported_invite", "bot_info", "migrated_from_chat_id", "migrated_from_max_id", "pinned_msg_id", "stickerset", "available_min_id", "folder_id", "linked_chat_id", "location", "slowmode_seconds", "slowmode_next_send_date", "stats_dc", "pts", "call", "ttl_period", "pending_suggestions", "groupcall_default_join_as", "theme_emoticon", "requests_pending", "recent_requesters", "default_send_as", "available_reactions", "reactions_limit", "stories", "wallpaper", "boosts_applied", "boosts_unrestrict", "emojiset", "bot_verification", "stargifts_count", "send_paid_messages_stars", "main_tab"}}, // channelFull
0xa9478a1a: {target: 0x4f607bef, keep: []string{"flags", "via_request", "user_id", "inviter_id", "date", "subscription_until_date"}}, // channelParticipantSelf
0xaa40f94d: {target: 0xfc796b3f, keep: []string{"text"}}, // keyboardButtonRequestGeoLocation
0xba7bb15e: {target: 0x7adf2420, keep: []string{"flags", "min", "results", "total_voters", "recent_voters", "solution", "solution_entities"}}, // pollResults
0xbcc4af10: {target: 0x75d2698e, keep: []string{"text", "copy_text"}}, // keyboardButtonCopy
0xc0fd5d09: {target: 0x308660c1, keep: []string{"text", "user_id"}}, // keyboardButtonUserProfile
0xd5f0ad91: {target: 0x6df8014e, keep: []string{"flags", "left", "peer", "kicked_by", "date", "banned_rights"}}, // channelParticipantBanned
0xd64c522b: {target: 0xaca1657b, keep: []string{"flags", "poll_id", "poll", "results"}}, // updateMessagePoll
0xd80c25ec: {target: 0x258aff05, keep: []string{"text", "url"}}, // keyboardButtonUrl
0xe15c4370: {target: 0xa0c0505c, keep: []string{"text", "url"}}, // keyboardButtonSimpleWebView
0xe1f867b8: {target: 0xe46bcee4, keep: []string{"user_id"}}, // chatParticipantCreator
0xe216eb63: {target: 0x695150d7, keep: []string{"flags", "spoiler", "photo", "ttl_seconds"}}, // messageMediaPhoto
0xe3af4434: {target: 0xb3ba0635, keep: []string{"flags", "spoiler", "id", "ttl_seconds"}}, // inputMediaPhoto
0xe62bc960: {target: 0x35bbdb6b, keep: []string{"flags", "requires_password", "text", "data"}}, // keyboardButtonCallback
0xe6c31522: {target: 0x95728543, keep: []string{"flags", "upgrade", "transferred", "saved", "refunded", "prepaid_upgrade", "assigned", "from_offer", "gift", "can_export_at", "transfer_stars", "from_id", "peer", "saved_id", "resale_amount", "can_transfer_at", "can_resell_at", "drop_original_details_stars"}}, // messageActionStarGiftUnique
0xe846b1a0: {target: 0x13767230, keep: []string{"text", "url"}}, // keyboardButtonWebView
0xf51006f9: {target: 0x10b78d29, keep: []string{"flags", "text", "fwd_text", "url", "button_id"}}, // keyboardButtonUrlAuth
0xf8827ebf: {target: 0xe0955a3c, keep: []string{"store_product", "phone_code_hash", "support_email_address", "support_email_subject", "currency", "amount"}}, // auth.sentCodePaymentRequired
0xfc89f7f3: {target: 0xd58a08c6, keep: []string{"flags", "pinned", "unread_mark", "view_forum_as_messages", "peer", "top_message", "read_inbox_max_id", "read_outbox_max_id", "unread_count", "unread_mentions_count", "unread_reactions_count", "notify_settings", "pts", "draft", "folder_id", "ttl_period"}}, // dialog
0xfcdad815: {target: 0xcdff0eca, keep: []string{"flags", "my", "closed", "pinned", "short", "hidden", "title_missing", "id", "date", "peer", "title", "icon_color", "icon_emoji_id", "top_message", "read_inbox_max_id", "read_outbox_max_id", "unread_count", "unread_mentions_count", "unread_reactions_count", "from_id", "notify_settings", "draft"}}, // forumTopic
},
newTypes: []uint32{
0x004b572c, 0x01a9fbfc, 0x02ff29d3, 0x0652c1c5, 0x0773c080, 0x096b2aec,
0x0a617e7b, 0x0e6e47c4, 0x0efa0194, 0x140502d1, 0x16605e3e, 0x199fed96,
0x1fa01357, 0x1fe9a9bf, 0x24c26789, 0x2999beed, 0x2f51c337, 0x36437737,
0x399674dc, 0x3c29a3e2, 0x3c60b621, 0x3e2793ba, 0x3e81e078, 0x402b4495,
0x445663a7, 0x4880ed9a, 0x4c2a5d62, 0x4fdd3430, 0x519524ea, 0x574b617f,
0x5806b4ec, 0x59080c20, 0x67e731ad, 0x682a41a9, 0x6c24f3dd, 0x6c9d0efe,
0x71777116, 0x7781fe18, 0x78fbf3a8, 0x79eb8cb3, 0x7b9e1801, 0x83281dbd,
0x8c0f91fb, 0x904ac7c7, 0x90d7adfa, 0x933ca597, 0x98a3a840, 0x9b00622b,
0x9bad6414, 0x9d2eac97, 0x9da1cd6c, 0xa26156c0, 0xa2cb24f9, 0xa5b45e2b,
0xac072444, 0xac6a83aa, 0xae152a69, 0xb07ed085, 0xb22083a6, 0xb43df56c,
0xb532772b, 0xb956812d, 0xbaf39d8b, 0xbaff072f, 0xbd8367b9, 0xbdac7e70,
0xbf7d6572, 0xc1f46103, 0xc31c8f4e, 0xc39a2ade, 0xc556a45d, 0xc6c1e5a7,
0xcd24cf44, 0xcdd4093d, 0xcef7e7a8, 0xcff63ea9, 0xd6e3b813, 0xdacb836a,
0xdbbe6c6a, 0xdbce6389, 0xdd1fbf93, 0xe188503b, 0xe2b23b51, 0xe4c449fc,
0xf08d516b, 0xf13bbcd7, 0xf1d628ec, 0xfa2bc90a,
},
},
222: {
rules: map[uint32]ruleRaw{
0x033ed001: {target: 0xcd64636c, keep: []string{"flags", "bot_id", "recipients", "rights"}}, // connectedBot
0x0360d5d2: {target: 0xa0933f5b, keep: []string{"user_id", "inviter_id", "date"}}, // chatParticipantAdmin
0x06cbe645: {target: 0xa02bc13e, keep: []string{"flags", "blocked", "phone_calls_available", "phone_calls_private", "can_pin_message", "has_scheduled", "video_calls_available", "voice_messages_forbidden", "translations_disabled", "stories_pinned_available", "blocked_my_stories_from", "wallpaper_overridden", "contact_require_premium", "read_dates_private", "flags2", "sponsored_enabled", "can_view_revenue", "bot_can_manage_emoji_status", "display_gifts_button", "id", "about", "settings", "personal_photo", "profile_photo", "fallback_photo", "notify_settings", "bot_info", "pinned_msg_id", "common_chats_count", "folder_id", "ttl_period", "theme", "private_forward_name", "bot_group_admin_rights", "bot_broadcast_admin_rights", "wallpaper", "stories", "business_work_hours", "business_location", "business_greeting_message", "business_away_message", "business_intro", "birthday", "personal_channel_id", "personal_channel_message", "stargifts_count", "starref_program", "bot_verification", "send_paid_messages_stars", "disallowed_gifts", "stars_rating", "stars_my_pending_rating", "stars_my_pending_rating_date", "main_tab", "saved_music", "note"}}, // userFull
0x15031189: {target: 0x5e068047, structural: "pageListOrderedItemText"}, // field "num": conditional-ness changed
0x16a4b93c: {target: 0xedf164f1, keep: []string{"flags", "pinned", "public", "close_friends", "min", "noforwards", "edited", "contacts", "selected_contacts", "out", "id", "date", "from_id", "fwd_from", "expire_date", "caption", "entities", "media", "media_areas", "privacy", "views", "sent_reaction", "albums"}}, // storyItem
0x1b97dd66: {target: 0x6917560b, keep: []string{"flags", "reply_to_scheduled", "forum_topic", "quote", "reply_to_msg_id", "reply_to_peer_id", "reply_from", "reply_media", "reply_to_top_id", "quote_text", "quote_entities", "quote_offset", "todo_item_id"}}, // messageReplyHeader
0x1bd54456: {target: 0xcb397619, keep: []string{"flags", "user_id", "date", "subscription_until_date"}}, // channelParticipant
0x1fd6f6c1: {target: 0x9a8ae1e1, keep: []string{"items"}}, // pageBlockOrderedList
0x2f58683c: {target: 0xb92fb6cd, keep: []string{"text"}}, // pageListItemText
0x3645230a: {target: 0x3b6ddad2, structural: "pollAnswerVoters"}, // field "voters": conditional-ness changed
0x38e79fde: {target: 0xc02d4007, keep: []string{"user_id", "inviter_id", "date"}}, // chatParticipant
0x3bd4b7c2: {target: 0x869fbe10, keep: []string{"flags", "reply_to_msg_id", "top_msg_id", "reply_to_peer_id", "quote_text", "quote_entities", "quote_offset", "monoforum_peer_id", "todo_item_id"}}, // inputReplyToMessage
0x3cd623ec: {target: 0x32fabf1a, keep: []string{"flags", "request_write_access", "request_phone_number", "bot", "domain", "browser", "platform", "ip", "region"}}, // urlAuthResultRequest
0x3fc18057: {target: 0x9bb2636d, keep: []string{"flags", "restore", "phone_number", "phone_code_hash", "currency", "amount"}}, // inputStorePaymentAuthCode
0x4b7d786a: {target: 0xff16e2ca, keep: []string{"text", "option"}}, // pollAnswer
0x60fe3294: {target: 0x96eaa5eb, keep: []string{"flags", "no_webpage", "invert_media", "reply_to", "message", "entities", "media", "date", "effect", "suggested_post"}}, // draftMessage
0x63ca67aa: {target: 0x25e073fc, keep: []string{"blocks"}}, // pageListItemBlocks
0x71e4ea58: {target: 0x56e34970, keep: []string{"flags", "messages_notify_from", "stories_notify_from", "sound", "show_previews"}}, // reactionsNotifySettings
0x7600b9d3: {target: 0x9cb490e9, keep: []string{"flags", "out", "mentioned", "media_unread", "silent", "post", "from_scheduled", "legacy", "edit_hide", "pinned", "noforwards", "invert_media", "flags2", "offline", "video_processing_pending", "paid_suggested_post_stars", "paid_suggested_post_ton", "id", "from_id", "from_boosts_applied", "peer_id", "saved_peer_id", "fwd_from", "via_bot_id", "via_business_bot_id", "reply_to", "date", "message", "media", "reply_markup", "entities", "views", "forwards", "replies", "edit_date", "post_author", "grouped_id", "reactions", "restriction_reason", "ttl_period", "quick_reply_shortcut_id", "effect", "factcheck", "report_delivery_until_date", "paid_message_stars", "suggested_post", "schedule_repeat_period", "summary_from_language"}}, // message
0x7699f014: {target: 0x24f40e77, keep: []string{"poll_id", "peer", "options", "qts"}}, // updateMessagePollVote
0x773f4e66: {target: 0x4bd6e798, keep: []string{"poll", "results"}}, // messageMediaPoll
0x7cb34d79: {target: 0x11dfa986, keep: []string{"peer", "date", "user_id", "about", "invite", "qts"}}, // updateBotChatInviteRequester
0x7d8375da: {target: 0x1e287d04, keep: []string{"flags", "spoiler", "file", "stickers", "ttl_seconds"}}, // inputMediaUploadedPhoto
0x883a4108: {target: 0x0f94e5f1, structural: "inputMediaPoll"}, // field "correct_answers": type changed Vector<bytes>->Vector<int>
0x8ff2d5f0: {target: 0x98dd8936, structural: "pageListOrderedItemBlocks"}, // field "num": conditional-ness changed
0x966e2dbf: {target: 0x58747131, keep: []string{"id", "flags", "closed", "public_voters", "multiple_choice", "quiz", "question", "answers", "close_period", "close_date"}}, // poll
0xa04e8d3a: {target: 0xe4e0b29d, keep: []string{"flags", "can_view_participants", "can_set_username", "can_set_stickers", "hidden_prehistory", "can_set_location", "has_scheduled", "can_view_stats", "blocked", "flags2", "can_delete_channel", "antispam", "participants_hidden", "translations_disabled", "stories_pinned_available", "view_forum_as_messages", "restricted_sponsored", "can_view_revenue", "paid_media_allowed", "can_view_stars_revenue", "paid_reactions_available", "stargifts_available", "paid_messages_available", "id", "about", "participants_count", "admins_count", "kicked_count", "banned_count", "online_count", "read_inbox_max_id", "read_outbox_max_id", "unread_count", "chat_photo", "notify_settings", "exported_invite", "bot_info", "migrated_from_chat_id", "migrated_from_max_id", "pinned_msg_id", "stickerset", "available_min_id", "folder_id", "linked_chat_id", "location", "slowmode_seconds", "slowmode_next_send_date", "stats_dc", "pts", "call", "ttl_period", "pending_suggestions", "groupcall_default_join_as", "theme_emoticon", "requests_pending", "recent_requesters", "default_send_as", "available_reactions", "reactions_limit", "stories", "wallpaper", "boosts_applied", "boosts_unrestrict", "emojiset", "bot_verification", "stargifts_count", "send_paid_messages_stars", "main_tab"}}, // channelFull
0xa9478a1a: {target: 0x4f607bef, keep: []string{"flags", "via_request", "user_id", "inviter_id", "date", "subscription_until_date"}}, // channelParticipantSelf
0xba7bb15e: {target: 0x7adf2420, keep: []string{"flags", "min", "results", "total_voters", "recent_voters", "solution", "solution_entities"}}, // pollResults
0xd5f0ad91: {target: 0x6df8014e, keep: []string{"flags", "left", "peer", "kicked_by", "date", "banned_rights"}}, // channelParticipantBanned
0xd64c522b: {target: 0xaca1657b, keep: []string{"flags", "poll_id", "poll", "results"}}, // updateMessagePoll
0xe1f867b8: {target: 0xe46bcee4, keep: []string{"user_id"}}, // chatParticipantCreator
0xe216eb63: {target: 0x695150d7, keep: []string{"flags", "spoiler", "photo", "ttl_seconds"}}, // messageMediaPhoto
0xe3af4434: {target: 0xb3ba0635, keep: []string{"flags", "spoiler", "id", "ttl_seconds"}}, // inputMediaPhoto
0xf8827ebf: {target: 0xe0955a3c, keep: []string{"store_product", "phone_code_hash", "support_email_address", "support_email_subject", "currency", "amount"}}, // auth.sentCodePaymentRequired
0xfc89f7f3: {target: 0xd58a08c6, keep: []string{"flags", "pinned", "unread_mark", "view_forum_as_messages", "peer", "top_message", "read_inbox_max_id", "read_outbox_max_id", "unread_count", "unread_mentions_count", "unread_reactions_count", "notify_settings", "pts", "draft", "folder_id", "ttl_period"}}, // dialog
0xfcdad815: {target: 0xcdff0eca, keep: []string{"flags", "my", "closed", "pinned", "short", "hidden", "title_missing", "id", "date", "peer", "title", "icon_color", "icon_emoji_id", "top_message", "read_inbox_max_id", "read_outbox_max_id", "unread_count", "unread_mentions_count", "unread_reactions_count", "from_id", "notify_settings", "draft"}}, // forumTopic
},
newTypes: []uint32{
0x004b572c, 0x01a9fbfc, 0x02ff29d3, 0x0652c1c5, 0x0773c080, 0x096b2aec,
0x0a617e7b, 0x0e6e47c4, 0x0efa0194, 0x140502d1, 0x16605e3e, 0x199fed96,
0x1fa01357, 0x1fe9a9bf, 0x24c26789, 0x2999beed, 0x2f51c337, 0x399674dc,
0x3c29a3e2, 0x3c60b621, 0x3e2793ba, 0x3e81e078, 0x402b4495, 0x445663a7,
0x4880ed9a, 0x4c2a5d62, 0x519524ea, 0x574b617f, 0x5806b4ec, 0x59080c20,
0x67e731ad, 0x682a41a9, 0x6c24f3dd, 0x6c9d0efe, 0x71777116, 0x7781fe18,
0x79eb8cb3, 0x7b9e1801, 0x83281dbd, 0x8c0f91fb, 0x904ac7c7, 0x90d7adfa,
0x933ca597, 0x98a3a840, 0x9b00622b, 0x9bad6414, 0x9d2eac97, 0x9da1cd6c,
0xa26156c0, 0xa2cb24f9, 0xa5b45e2b, 0xac6a83aa, 0xae152a69, 0xb22083a6,
0xb43df56c, 0xb532772b, 0xb956812d, 0xbaf39d8b, 0xbaff072f, 0xbd8367b9,
0xbdac7e70, 0xbf7d6572, 0xc1f46103, 0xc31c8f4e, 0xc39a2ade, 0xc556a45d,
0xc6c1e5a7, 0xcd24cf44, 0xcdd4093d, 0xcff63ea9, 0xd6e3b813, 0xdacb836a,
0xdbbe6c6a, 0xdd1fbf93, 0xe2b23b51, 0xe4c449fc, 0xf13bbcd7, 0xf1d628ec,
0xfa2bc90a,
},
},
223: {
rules: map[uint32]ruleRaw{
0x033ed001: {target: 0xcd64636c, keep: []string{"flags", "bot_id", "recipients", "rights"}}, // connectedBot
0x06cbe645: {target: 0xa02bc13e, keep: []string{"flags", "blocked", "phone_calls_available", "phone_calls_private", "can_pin_message", "has_scheduled", "video_calls_available", "voice_messages_forbidden", "translations_disabled", "stories_pinned_available", "blocked_my_stories_from", "wallpaper_overridden", "contact_require_premium", "read_dates_private", "flags2", "sponsored_enabled", "can_view_revenue", "bot_can_manage_emoji_status", "display_gifts_button", "noforwards_my_enabled", "noforwards_peer_enabled", "id", "about", "settings", "personal_photo", "profile_photo", "fallback_photo", "notify_settings", "bot_info", "pinned_msg_id", "common_chats_count", "folder_id", "ttl_period", "theme", "private_forward_name", "bot_group_admin_rights", "bot_broadcast_admin_rights", "wallpaper", "stories", "business_work_hours", "business_location", "business_greeting_message", "business_away_message", "business_intro", "birthday", "personal_channel_id", "personal_channel_message", "stargifts_count", "starref_program", "bot_verification", "send_paid_messages_stars", "disallowed_gifts", "stars_rating", "stars_my_pending_rating", "stars_my_pending_rating_date", "main_tab", "saved_music", "note"}}, // userFull
0x15031189: {target: 0x5e068047, structural: "pageListOrderedItemText"}, // field "num": conditional-ness changed
0x16a4b93c: {target: 0xedf164f1, keep: []string{"flags", "pinned", "public", "close_friends", "min", "noforwards", "edited", "contacts", "selected_contacts", "out", "id", "date", "from_id", "fwd_from", "expire_date", "caption", "entities", "media", "media_areas", "privacy", "views", "sent_reaction", "albums"}}, // storyItem
0x1b97dd66: {target: 0x6917560b, keep: []string{"flags", "reply_to_scheduled", "forum_topic", "quote", "reply_to_msg_id", "reply_to_peer_id", "reply_from", "reply_media", "reply_to_top_id", "quote_text", "quote_entities", "quote_offset", "todo_item_id"}}, // messageReplyHeader
0x1fd6f6c1: {target: 0x9a8ae1e1, keep: []string{"items"}}, // pageBlockOrderedList
0x2f58683c: {target: 0xb92fb6cd, keep: []string{"text"}}, // pageListItemText
0x3645230a: {target: 0x3b6ddad2, structural: "pollAnswerVoters"}, // field "voters": conditional-ness changed
0x3bd4b7c2: {target: 0x869fbe10, keep: []string{"flags", "reply_to_msg_id", "top_msg_id", "reply_to_peer_id", "quote_text", "quote_entities", "quote_offset", "monoforum_peer_id", "todo_item_id"}}, // inputReplyToMessage
0x3cd623ec: {target: 0xf8f8eb1e, keep: []string{"flags", "request_write_access", "request_phone_number", "match_codes_first", "bot", "domain", "browser", "platform", "ip", "region", "match_codes", "user_id_hint"}}, // urlAuthResultRequest
0x3fc18057: {target: 0x9bb2636d, keep: []string{"flags", "restore", "phone_number", "phone_code_hash", "currency", "amount"}}, // inputStorePaymentAuthCode
0x4b7d786a: {target: 0xff16e2ca, keep: []string{"text", "option"}}, // pollAnswer
0x60fe3294: {target: 0x96eaa5eb, keep: []string{"flags", "no_webpage", "invert_media", "reply_to", "message", "entities", "media", "date", "effect", "suggested_post"}}, // draftMessage
0x63ca67aa: {target: 0x25e073fc, keep: []string{"blocks"}}, // pageListItemBlocks
0x71e4ea58: {target: 0x56e34970, keep: []string{"flags", "messages_notify_from", "stories_notify_from", "sound", "show_previews"}}, // reactionsNotifySettings
0x7600b9d3: {target: 0x3ae56482, keep: []string{"flags", "out", "mentioned", "media_unread", "silent", "post", "from_scheduled", "legacy", "edit_hide", "pinned", "noforwards", "invert_media", "flags2", "offline", "video_processing_pending", "paid_suggested_post_stars", "paid_suggested_post_ton", "id", "from_id", "from_boosts_applied", "from_rank", "peer_id", "saved_peer_id", "fwd_from", "via_bot_id", "via_business_bot_id", "reply_to", "date", "message", "media", "reply_markup", "entities", "views", "forwards", "replies", "edit_date", "post_author", "grouped_id", "reactions", "restriction_reason", "ttl_period", "quick_reply_shortcut_id", "effect", "factcheck", "report_delivery_until_date", "paid_message_stars", "suggested_post", "schedule_repeat_period", "summary_from_language"}}, // message
0x7699f014: {target: 0x24f40e77, keep: []string{"poll_id", "peer", "options", "qts"}}, // updateMessagePollVote
0x773f4e66: {target: 0x4bd6e798, keep: []string{"poll", "results"}}, // messageMediaPoll
0x7cb34d79: {target: 0x11dfa986, keep: []string{"peer", "date", "user_id", "about", "invite", "qts"}}, // updateBotChatInviteRequester
0x7d8375da: {target: 0x1e287d04, keep: []string{"flags", "spoiler", "file", "stickers", "ttl_seconds"}}, // inputMediaUploadedPhoto
0x883a4108: {target: 0x0f94e5f1, structural: "inputMediaPoll"}, // field "correct_answers": type changed Vector<bytes>->Vector<int>
0x8ff2d5f0: {target: 0x98dd8936, structural: "pageListOrderedItemBlocks"}, // field "num": conditional-ness changed
0x966e2dbf: {target: 0x58747131, keep: []string{"id", "flags", "closed", "public_voters", "multiple_choice", "quiz", "question", "answers", "close_period", "close_date"}}, // poll
0xa04e8d3a: {target: 0xe4e0b29d, keep: []string{"flags", "can_view_participants", "can_set_username", "can_set_stickers", "hidden_prehistory", "can_set_location", "has_scheduled", "can_view_stats", "blocked", "flags2", "can_delete_channel", "antispam", "participants_hidden", "translations_disabled", "stories_pinned_available", "view_forum_as_messages", "restricted_sponsored", "can_view_revenue", "paid_media_allowed", "can_view_stars_revenue", "paid_reactions_available", "stargifts_available", "paid_messages_available", "id", "about", "participants_count", "admins_count", "kicked_count", "banned_count", "online_count", "read_inbox_max_id", "read_outbox_max_id", "unread_count", "chat_photo", "notify_settings", "exported_invite", "bot_info", "migrated_from_chat_id", "migrated_from_max_id", "pinned_msg_id", "stickerset", "available_min_id", "folder_id", "linked_chat_id", "location", "slowmode_seconds", "slowmode_next_send_date", "stats_dc", "pts", "call", "ttl_period", "pending_suggestions", "groupcall_default_join_as", "theme_emoticon", "requests_pending", "recent_requesters", "default_send_as", "available_reactions", "reactions_limit", "stories", "wallpaper", "boosts_applied", "boosts_unrestrict", "emojiset", "bot_verification", "stargifts_count", "send_paid_messages_stars", "main_tab"}}, // channelFull
0xba7bb15e: {target: 0x7adf2420, keep: []string{"flags", "min", "results", "total_voters", "recent_voters", "solution", "solution_entities"}}, // pollResults
0xd64c522b: {target: 0xaca1657b, keep: []string{"flags", "poll_id", "poll", "results"}}, // updateMessagePoll
0xe216eb63: {target: 0x695150d7, keep: []string{"flags", "spoiler", "photo", "ttl_seconds"}}, // messageMediaPhoto
0xe3af4434: {target: 0xb3ba0635, keep: []string{"flags", "spoiler", "id", "ttl_seconds"}}, // inputMediaPhoto
0xf8827ebf: {target: 0xe0955a3c, keep: []string{"store_product", "phone_code_hash", "support_email_address", "support_email_subject", "currency", "amount"}}, // auth.sentCodePaymentRequired
0xfc89f7f3: {target: 0xd58a08c6, keep: []string{"flags", "pinned", "unread_mark", "view_forum_as_messages", "peer", "top_message", "read_inbox_max_id", "read_outbox_max_id", "unread_count", "unread_mentions_count", "unread_reactions_count", "notify_settings", "pts", "draft", "folder_id", "ttl_period"}}, // dialog
0xfcdad815: {target: 0xcdff0eca, keep: []string{"flags", "my", "closed", "pinned", "short", "hidden", "title_missing", "id", "date", "peer", "title", "icon_color", "icon_emoji_id", "top_message", "read_inbox_max_id", "read_outbox_max_id", "unread_count", "unread_mentions_count", "unread_reactions_count", "from_id", "notify_settings", "draft"}}, // forumTopic
},
newTypes: []uint32{
0x004b572c, 0x01a9fbfc, 0x02ff29d3, 0x0652c1c5, 0x0773c080, 0x096b2aec,
0x0a617e7b, 0x0e6e47c4, 0x0efa0194, 0x140502d1, 0x16605e3e, 0x199fed96,
0x1fa01357, 0x1fe9a9bf, 0x24c26789, 0x2999beed, 0x2f51c337, 0x399674dc,
0x3c29a3e2, 0x3c60b621, 0x3e81e078, 0x402b4495, 0x445663a7, 0x4880ed9a,
0x4c2a5d62, 0x519524ea, 0x574b617f, 0x59080c20, 0x67e731ad, 0x682a41a9,
0x6c24f3dd, 0x6c9d0efe, 0x71777116, 0x7781fe18, 0x79eb8cb3, 0x7b9e1801,
0x83281dbd, 0x8c0f91fb, 0x90d7adfa, 0x933ca597, 0x98a3a840, 0x9b00622b,
0x9bad6414, 0x9d2eac97, 0x9da1cd6c, 0xa26156c0, 0xa2cb24f9, 0xa5b45e2b,
0xac6a83aa, 0xae152a69, 0xb22083a6, 0xb43df56c, 0xb532772b, 0xb956812d,
0xbaf39d8b, 0xbaff072f, 0xbdac7e70, 0xc1f46103, 0xc31c8f4e, 0xc39a2ade,
0xc556a45d, 0xc6c1e5a7, 0xcd24cf44, 0xcdd4093d, 0xcff63ea9, 0xd6e3b813,
0xdacb836a, 0xdbbe6c6a, 0xdd1fbf93, 0xe2b23b51, 0xe4c449fc, 0xf13bbcd7,
0xf1d628ec, 0xfa2bc90a,
},
},
224: {
rules: map[uint32]ruleRaw{
0x033ed001: {target: 0xcd64636c, keep: []string{"flags", "bot_id", "recipients", "rights"}}, // connectedBot
0x15031189: {target: 0x5e068047, structural: "pageListOrderedItemText"}, // field "num": conditional-ness changed
0x1fd6f6c1: {target: 0x9a8ae1e1, keep: []string{"items"}}, // pageBlockOrderedList
0x2f58683c: {target: 0xb92fb6cd, keep: []string{"text"}}, // pageListItemText
0x3fc18057: {target: 0x9bb2636d, keep: []string{"flags", "restore", "phone_number", "phone_code_hash", "currency", "amount"}}, // inputStorePaymentAuthCode
0x60fe3294: {target: 0x96eaa5eb, keep: []string{"flags", "no_webpage", "invert_media", "reply_to", "message", "entities", "media", "date", "effect", "suggested_post"}}, // draftMessage
0x63ca67aa: {target: 0x25e073fc, keep: []string{"blocks"}}, // pageListItemBlocks
0x7600b9d3: {target: 0x3ae56482, keep: []string{"flags", "out", "mentioned", "media_unread", "silent", "post", "from_scheduled", "legacy", "edit_hide", "pinned", "noforwards", "invert_media", "flags2", "offline", "video_processing_pending", "paid_suggested_post_stars", "paid_suggested_post_ton", "id", "from_id", "from_boosts_applied", "from_rank", "peer_id", "saved_peer_id", "fwd_from", "via_bot_id", "via_business_bot_id", "reply_to", "date", "message", "media", "reply_markup", "entities", "views", "forwards", "replies", "edit_date", "post_author", "grouped_id", "reactions", "restriction_reason", "ttl_period", "quick_reply_shortcut_id", "effect", "factcheck", "report_delivery_until_date", "paid_message_stars", "suggested_post", "schedule_repeat_period", "summary_from_language"}}, // message
0x7cb34d79: {target: 0x11dfa986, keep: []string{"peer", "date", "user_id", "about", "invite", "qts"}}, // updateBotChatInviteRequester
0x8ff2d5f0: {target: 0x98dd8936, structural: "pageListOrderedItemBlocks"}, // field "num": conditional-ness changed
0x966e2dbf: {target: 0xb8425be9, keep: []string{"id", "flags", "closed", "public_voters", "multiple_choice", "quiz", "open_answers", "revoting_disabled", "shuffle_answers", "hide_results_until_close", "creator", "question", "answers", "close_period", "close_date", "hash"}}, // poll
0xa04e8d3a: {target: 0xe4e0b29d, keep: []string{"flags", "can_view_participants", "can_set_username", "can_set_stickers", "hidden_prehistory", "can_set_location", "has_scheduled", "can_view_stats", "blocked", "flags2", "can_delete_channel", "antispam", "participants_hidden", "translations_disabled", "stories_pinned_available", "view_forum_as_messages", "restricted_sponsored", "can_view_revenue", "paid_media_allowed", "can_view_stars_revenue", "paid_reactions_available", "stargifts_available", "paid_messages_available", "id", "about", "participants_count", "admins_count", "kicked_count", "banned_count", "online_count", "read_inbox_max_id", "read_outbox_max_id", "unread_count", "chat_photo", "notify_settings", "exported_invite", "bot_info", "migrated_from_chat_id", "migrated_from_max_id", "pinned_msg_id", "stickerset", "available_min_id", "folder_id", "linked_chat_id", "location", "slowmode_seconds", "slowmode_next_send_date", "stats_dc", "pts", "call", "ttl_period", "pending_suggestions", "groupcall_default_join_as", "theme_emoticon", "requests_pending", "recent_requesters", "default_send_as", "available_reactions", "reactions_limit", "stories", "wallpaper", "boosts_applied", "boosts_unrestrict", "emojiset", "bot_verification", "stargifts_count", "send_paid_messages_stars", "main_tab"}}, // channelFull
0xf8827ebf: {target: 0xe0955a3c, keep: []string{"store_product", "phone_code_hash", "support_email_address", "support_email_subject", "currency", "amount"}}, // auth.sentCodePaymentRequired
},
newTypes: []uint32{
0x004b572c, 0x01a9fbfc, 0x02ff29d3, 0x0773c080, 0x096b2aec, 0x0a617e7b,
0x0e6e47c4, 0x0efa0194, 0x140502d1, 0x1fa01357, 0x1fe9a9bf, 0x24c26789,
0x2999beed, 0x2f51c337, 0x3c29a3e2, 0x445663a7, 0x4c2a5d62, 0x519524ea,
0x574b617f, 0x59080c20, 0x67e731ad, 0x682a41a9, 0x6c24f3dd, 0x6c9d0efe,
0x7781fe18, 0x79eb8cb3, 0x7b9e1801, 0x83281dbd, 0x8c0f91fb, 0x933ca597,
0x98a3a840, 0x9b00622b, 0x9bad6414, 0x9d2eac97, 0xa26156c0, 0xa2cb24f9,
0xa5b45e2b, 0xac6a83aa, 0xae152a69, 0xb22083a6, 0xb43df56c, 0xb532772b,
0xb956812d, 0xbaf39d8b, 0xbaff072f, 0xbdac7e70, 0xc1f46103, 0xc31c8f4e,
0xc39a2ade, 0xc556a45d, 0xcd24cf44, 0xcdd4093d, 0xcff63ea9, 0xd6e3b813,
0xdacb836a, 0xdbbe6c6a, 0xdd1fbf93, 0xe2b23b51, 0xe4c449fc, 0xf1d628ec,
},
},
225: {
rules: map[uint32]ruleRaw{
0x033ed001: {target: 0xcd64636c, keep: []string{"flags", "bot_id", "recipients", "rights"}}, // connectedBot
0x15031189: {target: 0x5e068047, structural: "pageListOrderedItemText"}, // field "num": conditional-ness changed
0x1fd6f6c1: {target: 0x9a8ae1e1, keep: []string{"items"}}, // pageBlockOrderedList
0x2f58683c: {target: 0xb92fb6cd, keep: []string{"text"}}, // pageListItemText
0x3fc18057: {target: 0x9bb2636d, keep: []string{"flags", "restore", "phone_number", "phone_code_hash", "currency", "amount"}}, // inputStorePaymentAuthCode
0x60fe3294: {target: 0x96eaa5eb, keep: []string{"flags", "no_webpage", "invert_media", "reply_to", "message", "entities", "media", "date", "effect", "suggested_post"}}, // draftMessage
0x63ca67aa: {target: 0x25e073fc, keep: []string{"blocks"}}, // pageListItemBlocks
0x7600b9d3: {target: 0x95ef6f2b, keep: []string{"flags", "out", "mentioned", "media_unread", "silent", "post", "from_scheduled", "legacy", "edit_hide", "pinned", "noforwards", "invert_media", "flags2", "offline", "video_processing_pending", "paid_suggested_post_stars", "paid_suggested_post_ton", "id", "from_id", "from_boosts_applied", "from_rank", "peer_id", "saved_peer_id", "fwd_from", "via_bot_id", "via_business_bot_id", "guestchat_via_from", "reply_to", "date", "message", "media", "reply_markup", "entities", "views", "forwards", "replies", "edit_date", "post_author", "grouped_id", "reactions", "restriction_reason", "ttl_period", "quick_reply_shortcut_id", "effect", "factcheck", "report_delivery_until_date", "paid_message_stars", "suggested_post", "schedule_repeat_period", "summary_from_language"}}, // message
0x7cb34d79: {target: 0x11dfa986, keep: []string{"peer", "date", "user_id", "about", "invite", "qts"}}, // updateBotChatInviteRequester
0x8ff2d5f0: {target: 0x98dd8936, structural: "pageListOrderedItemBlocks"}, // field "num": conditional-ness changed
0xa04e8d3a: {target: 0xe4e0b29d, keep: []string{"flags", "can_view_participants", "can_set_username", "can_set_stickers", "hidden_prehistory", "can_set_location", "has_scheduled", "can_view_stats", "blocked", "flags2", "can_delete_channel", "antispam", "participants_hidden", "translations_disabled", "stories_pinned_available", "view_forum_as_messages", "restricted_sponsored", "can_view_revenue", "paid_media_allowed", "can_view_stars_revenue", "paid_reactions_available", "stargifts_available", "paid_messages_available", "id", "about", "participants_count", "admins_count", "kicked_count", "banned_count", "online_count", "read_inbox_max_id", "read_outbox_max_id", "unread_count", "chat_photo", "notify_settings", "exported_invite", "bot_info", "migrated_from_chat_id", "migrated_from_max_id", "pinned_msg_id", "stickerset", "available_min_id", "folder_id", "linked_chat_id", "location", "slowmode_seconds", "slowmode_next_send_date", "stats_dc", "pts", "call", "ttl_period", "pending_suggestions", "groupcall_default_join_as", "theme_emoticon", "requests_pending", "recent_requesters", "default_send_as", "available_reactions", "reactions_limit", "stories", "wallpaper", "boosts_applied", "boosts_unrestrict", "emojiset", "bot_verification", "stargifts_count", "send_paid_messages_stars", "main_tab"}}, // channelFull
0xf8827ebf: {target: 0xe0955a3c, keep: []string{"store_product", "phone_code_hash", "support_email_address", "support_email_subject", "currency", "amount"}}, // auth.sentCodePaymentRequired
},
newTypes: []uint32{
0x004b572c, 0x01a9fbfc, 0x02ff29d3, 0x096b2aec, 0x0a617e7b, 0x0e6e47c4,
0x0efa0194, 0x140502d1, 0x24c26789, 0x2f51c337, 0x3c29a3e2, 0x445663a7,
0x4c2a5d62, 0x519524ea, 0x574b617f, 0x59080c20, 0x67e731ad, 0x682a41a9,
0x6c24f3dd, 0x79eb8cb3, 0x7b9e1801, 0x83281dbd, 0x933ca597, 0x98a3a840,
0x9b00622b, 0x9d2eac97, 0xa26156c0, 0xa2cb24f9, 0xa5b45e2b, 0xac6a83aa,
0xae152a69, 0xb22083a6, 0xb43df56c, 0xb532772b, 0xb956812d, 0xbaf39d8b,
0xbaff072f, 0xbdac7e70, 0xc31c8f4e, 0xc39a2ade, 0xc556a45d, 0xcd24cf44,
0xd6e3b813, 0xdacb836a, 0xdbbe6c6a, 0xe2b23b51, 0xe4c449fc,
},
},
226: {
rules: map[uint32]ruleRaw{
0x15031189: {target: 0x5e068047, structural: "pageListOrderedItemText"}, // field "num": conditional-ness changed
0x1fd6f6c1: {target: 0x9a8ae1e1, keep: []string{"items"}}, // pageBlockOrderedList
0x2f51c337: {target: 0x774bbdf4, structural: "messages.chatInviteJoinResultWebView"}, // target field "url" not found in canonical (reorder/insert)
0x2f58683c: {target: 0xb92fb6cd, keep: []string{"text"}}, // pageListItemText
0x60fe3294: {target: 0x96eaa5eb, keep: []string{"flags", "no_webpage", "invert_media", "reply_to", "message", "entities", "media", "date", "effect", "suggested_post"}}, // draftMessage
0x63ca67aa: {target: 0x25e073fc, keep: []string{"blocks"}}, // pageListItemBlocks
0x7600b9d3: {target: 0x95ef6f2b, keep: []string{"flags", "out", "mentioned", "media_unread", "silent", "post", "from_scheduled", "legacy", "edit_hide", "pinned", "noforwards", "invert_media", "flags2", "offline", "video_processing_pending", "paid_suggested_post_stars", "paid_suggested_post_ton", "id", "from_id", "from_boosts_applied", "from_rank", "peer_id", "saved_peer_id", "fwd_from", "via_bot_id", "via_business_bot_id", "guestchat_via_from", "reply_to", "date", "message", "media", "reply_markup", "entities", "views", "forwards", "replies", "edit_date", "post_author", "grouped_id", "reactions", "restriction_reason", "ttl_period", "quick_reply_shortcut_id", "effect", "factcheck", "report_delivery_until_date", "paid_message_stars", "suggested_post", "schedule_repeat_period", "summary_from_language"}}, // message
0x8ff2d5f0: {target: 0x98dd8936, structural: "pageListOrderedItemBlocks"}, // field "num": conditional-ness changed
},
newTypes: []uint32{
0x004b572c, 0x01a9fbfc, 0x02ff29d3, 0x096b2aec, 0x0a617e7b, 0x0e6e47c4,
0x24c26789, 0x3c29a3e2, 0x4c2a5d62, 0x519524ea, 0x574b617f, 0x59080c20,
0x67e731ad, 0x682a41a9, 0x7b9e1801, 0x83281dbd, 0x9b00622b, 0x9d2eac97,
0xa26156c0, 0xa2cb24f9, 0xa5b45e2b, 0xac6a83aa, 0xb43df56c, 0xb532772b,
0xb956812d, 0xbaf39d8b, 0xbaff072f, 0xc556a45d, 0xcd24cf44, 0xdacb836a,
0xdbbe6c6a, 0xe2b23b51, 0xe4c449fc,
},
},
}
// inboundMethodUpgrades maps an old client method constructor id to the
// canonical (227) id. Only upgrade-safe changes (all 227 additions flag-gated)
// are listed: rewriting the 4-byte id yields a valid 227 request body.
// NOT upgrade-safe as a pure id swap (declare a body transform in client-drift.tl when needed):
//
// channels.editAdmin: field "rank": conditional-ness changed
// channels.toggleJoinRequest: 227 inserts flags integer "flags"
// contacts.search: 227 inserts flags integer "flags"
// messages.composeMessageWithAI: target field "change_tone" not found in canonical (reorder/insert)
// messages.getPollResults: 227-only field "poll_hash" is non-conditional
// messages.sendBotRequestedPeer: field "msg_id": conditional-ness changed
// messages.toggleNoForwards: 227 inserts flags integer "flags"
var inboundMethodUpgrades = map[uint32]uint32{
0x052b08db: 0xb8f106e3, // messages.setBotGuestChatResult
0x198fb446: 0x894cc99c, // messages.requestUrlAuth
0x24b524c5: 0x7f6a1e22, // channels.joinChannel
0x2d0a0571: 0x60ed4229, // account.toggleWebBrowserSettingsException
0x51e842e1: 0xb106e66c, // messages.editMessage
0x545cd15a: 0xfef48f62, // messages.sendMessage
0x54ae308e: 0xad0fa15c, // messages.saveDraft
0x63183030: 0xa5eec345, // messages.translateText
0x6c50051c: 0xde91436e, // messages.importChatInvite
0x737fc2ec: 0x8f9e6898, // stories.sendStory
0x83557dba: 0xa423bb51, // messages.editInlineBotMessage
0x9d4104e2: 0xabbbd346, // messages.summarizeText
0xb12c7125: 0x67a3f0de, // messages.acceptUrlAuth
0xb583ba46: 0x2c63a72b, // stories.editStory
}

View file

@ -1,249 +0,0 @@
package layerwire
import (
"bytes"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/gotd/td/bin"
"github.com/gotd/td/tg"
)
// loadLayerModel parses a vendored historical schema (_schema/layer-N.tl) into a
// schemaModel used as an independent oracle: downgraded bytes must parse cleanly
// against the actual target-layer schema.
func loadLayerModel(t *testing.T, layer int) *schemaModel {
t.Helper()
src, err := os.ReadFile(filepath.Join("_schema", fmt.Sprintf("layer-%d.tl", layer)))
if err != nil {
t.Fatalf("read layer %d schema: %v", layer, err)
}
m, err := parseSchemaModel(string(src))
if err != nil {
t.Fatalf("parse layer %d schema: %v", layer, err)
}
return m
}
// TestTranscodeIdentity verifies that targeting the canonical layer (or above)
// is a pure passthrough — the transcoder must never mutate 227 bytes.
func TestTranscodeIdentity(t *testing.T) {
for _, o := range canonicalCorpus() {
raw := mustEncode(t, o)
out, err := Transcode(raw, CanonicalLayer)
if err != nil {
t.Fatalf("%T: identity transcode: %v", o, err)
}
if !bytes.Equal(out, raw) {
t.Errorf("%T: identity transcode changed bytes", o)
}
}
}
// TestTranscodeDowngradeValid downgrades the corpus to every supported layer and
// asserts the result parses cleanly (full byte consumption) against that layer's
// own schema. This is the core correctness oracle for the transcoder.
func TestTranscodeDowngradeValid(t *testing.T) {
for layer := SupportedFloor; layer < CanonicalLayer; layer++ {
model := loadLayerModel(t, layer)
for _, o := range canonicalCorpus() {
raw := mustEncode(t, o)
out, err := Transcode(raw, layer)
if err != nil {
t.Errorf("layer %d %T: transcode: %v", layer, o, err)
continue
}
b := &bin.Buffer{Buf: append([]byte(nil), out...)}
if err := model.skipObject(b); err != nil {
t.Errorf("layer %d %T: result invalid at target: %v", layer, o, err)
continue
}
if b.Len() != 0 {
t.Errorf("layer %d %T: %d trailing bytes in downgraded output", layer, o, b.Len())
}
}
}
}
// TestTranscodeMessageGolden checks that a message downgraded to 220 carries the
// 220 constructor id and is strictly shorter (dropped trailing fields).
func TestTranscodeMessageGolden(t *testing.T) {
const message220CRC = 0xb92f76cf
raw := mustEncode(t, canonicalCorpus()[1]) // the rich message
out, err := Transcode(raw, 220)
if err != nil {
t.Fatalf("transcode message->220: %v", err)
}
b := &bin.Buffer{Buf: append([]byte(nil), out...)}
id, err := b.PeekID()
if err != nil {
t.Fatalf("peek id: %v", err)
}
if id != message220CRC {
t.Fatalf("message@220 id = %#08x, want %#08x", id, message220CRC)
}
if len(out) >= len(raw) {
t.Errorf("downgraded message not shorter: %d >= %d", len(out), len(raw))
}
}
func TestTranscodeFormattedDateEntityLayerBoundary(t *testing.T) {
const formattedDateEntityCRC = 0x904ac7c7
entityCRC := func(crc uint32) []byte {
return []byte{byte(crc), byte(crc >> 8), byte(crc >> 16), byte(crc >> 24)}
}
msg := &tg.Message{
ID: 7,
PeerID: &tg.PeerUser{UserID: 2},
Date: 100,
Message: "Meet soon",
Entities: []tg.MessageEntityClass{
&tg.MessageEntityFormattedDate{Offset: 5, Length: 4, Date: 1773436800, ShortDate: true, ShortTime: true},
},
}
raw := mustEncode(t, msg)
out222, err := Transcode(raw, 222)
if err != nil {
t.Fatalf("transcode message->222: %v", err)
}
if bytes.Contains(out222, entityCRC(formattedDateEntityCRC)) {
t.Fatalf("layer 222 output leaked formatted-date entity")
}
if !bytes.Contains(out222, entityCRC(messageEntityUnknownID)) {
t.Fatalf("layer 222 output missing messageEntityUnknown fallback")
}
m222 := loadLayerModel(t, 222)
b222 := &bin.Buffer{Buf: append([]byte(nil), out222...)}
if err := m222.skipObject(b222); err != nil || b222.Len() != 0 {
t.Fatalf("layer 222 formatted-date fallback does not parse cleanly (err=%v left=%d)", err, b222.Len())
}
out223, err := Transcode(raw, 223)
if err != nil {
t.Fatalf("transcode message->223: %v", err)
}
if !bytes.Contains(out223, entityCRC(formattedDateEntityCRC)) {
t.Fatalf("layer 223 output did not preserve formatted-date entity")
}
m223 := loadLayerModel(t, 223)
b223 := &bin.Buffer{Buf: append([]byte(nil), out223...)}
if err := m223.skipObject(b223); err != nil || b223.Len() != 0 {
t.Fatalf("layer 223 formatted-date output does not parse cleanly (err=%v left=%d)", err, b223.Len())
}
}
// TestTranscodePassthroughNonAPI verifies that a top-level constructor absent
// from the tg schema (an MTProto control object such as rpc_error) passes
// through untouched at any layer.
func TestTranscodePassthroughNonAPI(t *testing.T) {
var b bin.Buffer
b.PutID(0xc4b9f9bb) // rpc_error#c4b9f9bb (mt.*), not a tg API constructor
b.PutInt(420)
b.PutString("FLOOD_WAIT")
raw := b.Copy()
out, err := Transcode(raw, 220)
if err != nil {
t.Fatalf("passthrough transcode: %v", err)
}
if !bytes.Equal(out, raw) {
t.Errorf("non-API object was modified by transcode")
}
}
// TestTranscodeChangedTypeNestedInUnchangedContainer is the case raised in
// review: an outer constructor whose CRC is IDENTICAL across 227 and the target
// layer (so a naive "same CRC ⇒ copy verbatim" would be wrong) but which nests a
// CHANGED type (message). The dirty closure must mark the outer container dirty
// purely because it can transitively reach a changed type, so the transcoder
// keeps the outer CRC yet recurses and rewrites the inner message to the target.
func TestTranscodeChangedTypeNestedInUnchangedContainer(t *testing.T) {
const (
message227CRC = 0x7600b9d3
message220CRC = 0xb92f76cf
)
// updates#... nests Vector<Update> → updateNewMessage → message:Message.
updates := &tg.Updates{
Updates: []tg.UpdateClass{
&tg.UpdateNewMessage{
Message: &tg.Message{ID: 7, PeerID: &tg.PeerUser{UserID: 2}, Date: 1, Message: "nested"},
Pts: 1, PtsCount: 1,
},
},
Users: []tg.UserClass{&tg.User{ID: 2, AccessHash: 5, FirstName: "A"}},
Chats: []tg.ChatClass{},
Date: 100, Seq: 1,
}
// Premise of the question: the OUTER container's CRC is unchanged at 220.
m220 := loadLayerModel(t, 220)
if canonical.byName["updates"].crc != m220.byName["updates"].crc {
t.Skip("updates CRC differs 220<->227; premise no longer holds")
}
raw := mustEncode(t, updates)
out, err := Transcode(raw, 220)
if err != nil {
t.Fatalf("transcode updates->220: %v", err)
}
// Outer CRC preserved (it really is unchanged).
if id, _ := (&bin.Buffer{Buf: out}).PeekID(); id != canonical.byName["updates"].crc {
t.Fatalf("outer updates id changed to %#08x", id)
}
// Inner message rewritten to the 220 constructor; the 227 one must be gone.
le := func(crc uint32) []byte { return []byte{byte(crc), byte(crc >> 8), byte(crc >> 16), byte(crc >> 24)} }
if bytes.Contains(out, le(message227CRC)) {
t.Errorf("downgraded output still contains the 227 message constructor")
}
if !bytes.Contains(out, le(message220CRC)) {
t.Errorf("downgraded output missing the 220 message constructor")
}
// Rigorous: the whole thing must parse cleanly against the real 220 schema —
// impossible if a 227-only nested constructor leaked through.
b := &bin.Buffer{Buf: append([]byte(nil), out...)}
if err := m220.skipObject(b); err != nil || b.Len() != 0 {
t.Fatalf("downgraded updates invalid at 220 (err=%v left=%d)", err, b.Len())
}
}
// TestTranscodePollResults exercises the pollAnswerVoters structural transform.
func TestTranscodePollResults(t *testing.T) {
raw := mustEncode(t, canonicalCorpus()[10]) // PollResults
for layer := SupportedFloor; layer < CanonicalLayer; layer++ {
out, err := Transcode(raw, layer)
if err != nil {
t.Fatalf("layer %d: pollResults transcode: %v", layer, err)
}
model := loadLayerModel(t, layer)
b := &bin.Buffer{Buf: append([]byte(nil), out...)}
if err := model.skipObject(b); err != nil || b.Len() != 0 {
t.Errorf("layer %d: pollResults invalid (err=%v left=%d)", layer, err, b.Len())
}
}
}
// TestTranscodePollAnswerVotersAbsentFlag exercises the structural transform's
// flag-bit-2-unset path: a pollAnswerVoters whose voters field is absent in 227
// (flags.2 clear) must still emit voters:0 (unconditional int) at older layers.
func TestTranscodePollAnswerVotersAbsentFlag(t *testing.T) {
// voters absent (flag bit 2 unset): Voters=0 ⇒ gotd SetFlags leaves flags.2 clear.
pr := &tg.PollResults{
Results: []tg.PollAnswerVoters{{Option: []byte{0}, Chosen: true}},
TotalVoters: 0,
}
raw := mustEncode(t, pr)
for layer := SupportedFloor; layer < CanonicalLayer; layer++ {
out, err := Transcode(raw, layer)
if err != nil {
t.Fatalf("layer %d: transcode: %v", layer, err)
}
model := loadLayerModel(t, layer)
b := &bin.Buffer{Buf: append([]byte(nil), out...)}
if err := model.skipObject(b); err != nil || b.Len() != 0 {
t.Errorf("layer %d: voters-absent pollResults invalid (err=%v left=%d)", layer, err, b.Len())
}
}
}

View file

@ -1,433 +0,0 @@
package layerwire
import (
"errors"
"fmt"
"io"
"math"
"github.com/gotd/td/bin"
)
// ErrMalformed identifies invalid or truncated TL wire data. Callers may use
// errors.Is to distinguish it from an otherwise well-formed request which was
// rejected by a walker resource limit.
var ErrMalformed = errors.New("layerwire: malformed TL")
// ErrResourceLimit identifies structurally valid-looking TL input which would
// exceed a walker resource budget.
var ErrResourceLimit = errors.New("layerwire: resource limit")
const (
defaultMaxVectorElements = 4096
defaultMaxWalkDepth = 32
defaultMaxWalkUnits = 131072 // constructors + declared vector elements
defaultMaxFieldBytes = 16 << 20
defaultMaxTotalBytes = 32 << 20
)
// A very small number of API methods have a documented limit above the
// package-wide default. Keeping overrides keyed by constructor and field makes
// every exception explicit and prevents a large vector in an unrelated method
// from inheriting the larger allowance.
type vectorLimitKey struct {
owner string
field string
}
var vectorElementLimitOverrides = map[vectorLimitKey]int{
{owner: "contacts.editCloseFriends", field: "id"}: 5000,
{owner: "contacts.setBlocked", field: "id"}: 5000,
}
type walkLimits struct {
maxVectorElements int
maxDepth int
maxUnits uint64
maxFieldBytes uint64
maxTotalBytes uint64
}
var defaultWalkLimits = walkLimits{
maxVectorElements: defaultMaxVectorElements,
maxDepth: defaultMaxWalkDepth,
maxUnits: defaultMaxWalkUnits,
maxFieldBytes: defaultMaxFieldBytes,
maxTotalBytes: defaultMaxTotalBytes,
}
// walkState is deliberately request-scoped. Every branch of one transform
// shares it, so splitting a large value across nested constructors or vectors
// cannot reset the aggregate budgets.
type walkState struct {
limits walkLimits
units uint64
bytes uint64
}
func newWalkState() *walkState {
return &walkState{limits: defaultWalkLimits}
}
func malformedf(format string, args ...any) error {
return fmt.Errorf("%w: %s", ErrMalformed, fmt.Sprintf(format, args...))
}
func limitf(format string, args ...any) error {
return fmt.Errorf("%w: %s", ErrResourceLimit, fmt.Sprintf(format, args...))
}
// classifyWalkError makes all public walker/transform failures classifiable,
// including errors returned by the low-level gotd bin decoder.
func classifyWalkError(err error) error {
if err == nil || errors.Is(err, ErrMalformed) || errors.Is(err, ErrResourceLimit) {
return err
}
return fmt.Errorf("%w: %v", ErrMalformed, err)
}
func (s *walkState) enter(depth int, what string) error {
if depth <= 0 || depth > s.limits.maxDepth {
return limitf("%s nesting depth %d exceeds limit %d", what, depth, s.limits.maxDepth)
}
return s.addUnits(1, what)
}
func (s *walkState) addUnits(n int, what string) error {
if n < 0 {
return malformedf("negative %s count %d", what, n)
}
u := uint64(n)
// Subtraction form avoids overflow even if limits are changed later.
if s.units > s.limits.maxUnits || u > s.limits.maxUnits-s.units {
return limitf("constructor/vector element budget exceeds %d at %s", s.limits.maxUnits, what)
}
s.units += u
return nil
}
func (s *walkState) addBytes(n uint64, what string) error {
if n > s.limits.maxFieldBytes {
return limitf("%s payload length %d exceeds per-field limit %d", what, n, s.limits.maxFieldBytes)
}
if s.bytes > s.limits.maxTotalBytes || n > s.limits.maxTotalBytes-s.bytes {
return limitf("string/bytes payload budget exceeds %d at %s", s.limits.maxTotalBytes, what)
}
s.bytes += n
return nil
}
func (s *walkState) vectorLimit(owner *ctorLayout, f *fieldLayout) int {
if owner != nil && f != nil {
if n := vectorElementLimitOverrides[vectorLimitKey{owner: owner.name, field: f.name}]; n > 0 {
return n
}
}
return s.limits.maxVectorElements
}
const maxConstructorFlagWords = 8
type constructorFlagWord struct {
name string
value uint32
}
// ValidateCanonicalRequest performs a complete, allocation-free structural
// preflight of one canonical Layer 227 method request. It is intended for the
// router seam immediately before typed dispatch. A successful result means the
// walker consumed exactly one known function constructor and all of its body.
func ValidateCanonicalRequest(body []byte) error {
b := &bin.Buffer{Buf: body}
id, err := b.PeekID()
if err != nil {
return classifyWalkError(err)
}
cl := canonical.byCRC[id]
if cl == nil {
return malformedf("unknown canonical request constructor %#08x", id)
}
if !cl.isFunc {
return malformedf("constructor %s (%#08x) is not a method", cl.name, id)
}
return validateRequestLayout(canonical, cl, body)
}
func validateRequestLayout(m *schemaModel, cl *ctorLayout, body []byte) error {
b := &bin.Buffer{Buf: body}
s := newWalkState()
if err := s.skipObject(m, b, 1); err != nil {
return classifyWalkError(err)
}
if b.Len() != 0 {
return malformedf("%d trailing bytes after canonical request %s", b.Len(), cl.name)
}
return nil
}
// skipObject advances b past one boxed object (CRC + body), resolving the
// constructor from m. This compatibility wrapper creates a fresh budget; all
// production transforms call the stateful variant directly.
func (m *schemaModel) skipObject(b *bin.Buffer) error {
return classifyWalkError(newWalkState().skipObject(m, b, 1))
}
func (s *walkState) skipObject(m *schemaModel, b *bin.Buffer, depth int) error {
if err := s.enter(depth, "constructor"); err != nil {
return err
}
id, err := b.PeekID()
if err != nil {
return err
}
cl, ok := m.byCRC[id]
if !ok {
return malformedf("unknown constructor %#08x", id)
}
if err := b.ConsumeID(id); err != nil {
return err
}
return s.skipCtorBody(m, b, cl, depth)
}
// skipCtorBody advances b past a constructor body (no leading CRC), evaluating
// flag integers so conditional fields are read iff present. The constructor's
// unit and depth have already been charged by the caller.
func (s *walkState) skipCtorBody(m *schemaModel, b *bin.Buffer, cl *ctorLayout, depth int) error {
// Layer 227 constructors currently use at most flags + flags2. Keep generous fixed stack
// storage so the allocation-free preflight remains allocation-free on the hottest flagged
// methods; the explicit bound also prevents a future malformed/generated layout from turning
// every request into an attacker-amplified map allocation.
var flags [maxConstructorFlagWords]constructorFlagWord
flagCount := 0
for i := range cl.fields {
f := &cl.fields[i]
if f.isFlags {
v, err := b.Uint32()
if err != nil {
return fmt.Errorf("%s.%s: %w", cl.name, f.name, err)
}
if flagCount >= len(flags) {
return limitf("constructor %s has more than %d flags words", cl.name, len(flags))
}
flags[flagCount] = constructorFlagWord{name: f.name, value: v}
flagCount++
continue
}
if f.conditional() {
var (
flagValue uint32
found bool
)
for j := 0; j < flagCount; j++ {
if flags[j].name == f.flagName {
flagValue = flags[j].value
found = true
break
}
}
if !found {
return malformedf("constructor %s conditional field %s references missing flags word %s", cl.name, f.name, f.flagName)
}
if flagValue&(1<<uint(f.flagBit)) == 0 {
continue
}
}
if err := s.skipValue(m, b, f, cl, depth); err != nil {
return fmt.Errorf("%s.%s: %w", cl.name, f.name, err)
}
}
return nil
}
// skipValue advances b past one already-known-present field value.
func (s *walkState) skipValue(m *schemaModel, b *bin.Buffer, f *fieldLayout, owner *ctorLayout, depth int) error {
switch f.kind {
case kindInt:
return skipFixed(b, 4)
case kindLong, kindDouble:
return skipFixed(b, 8)
case kindInt128:
return skipFixed(b, 16)
case kindInt256:
return skipFixed(b, 32)
case kindBytes:
return s.skipTLBytes(b, "bytes")
case kindString:
return s.skipTLBytes(b, "string")
case kindBool:
if err := s.addUnits(1, "Bool constructor"); err != nil {
return err
}
id, err := b.Uint32()
if err != nil {
return err
}
if id != bin.TypeTrue && id != bin.TypeFalse {
return malformedf("invalid Bool constructor %#08x", id)
}
return nil
case kindTrue:
return nil
case kindVector, kindVectorBare:
vectorDepth := depth + 1
if vectorDepth <= 0 || vectorDepth > s.limits.maxDepth {
return limitf("vector nesting depth %d exceeds limit %d", vectorDepth, s.limits.maxDepth)
}
if f.kind == kindVector {
id, err := b.Uint32()
if err != nil {
return err
}
if id != vectorTypeID {
return malformedf("expected vector id, got %#08x", id)
}
}
n, err := b.Int()
if err != nil {
return err
}
if n < 0 {
return malformedf("negative vector length %d", n)
}
if max := s.vectorLimit(owner, f); n > max {
return limitf("vector %s.%s length %d exceeds limit %d", ownerName(owner), fieldName(f), n, max)
}
if err := s.addUnits(n, "vector "+ownerName(owner)+"."+fieldName(f)); err != nil {
return err
}
if width, ok := fixedWireWidth(f.elem); ok {
total, ok := checkedMulInt(n, width)
if !ok {
return malformedf("vector byte length overflow: %d * %d", n, width)
}
return skipFixed(b, total)
}
for i := 0; i < n; i++ {
if err := s.skipValue(m, b, f.elem, nil, vectorDepth); err != nil {
return fmt.Errorf("vector element %d: %w", i, err)
}
}
return nil
case kindObject:
return s.skipObject(m, b, depth+1)
case kindBareObject:
bareDepth := depth + 1
if err := s.enter(bareDepth, "bare constructor"); err != nil {
return err
}
cl, ok := m.bareByT[f.typeName]
if !ok {
return malformedf("unknown bare type %q", f.typeName)
}
return s.skipCtorBody(m, b, cl, bareDepth)
default:
return malformedf("bad wire kind %d", f.kind)
}
}
// skipTLBytes parses TL's 1/4-byte length prefix directly and advances the
// input slice. Unlike bin.Buffer.Bytes it never copies payload data.
func (s *walkState) skipTLBytes(b *bin.Buffer, what string) error {
if len(b.Buf) == 0 {
return io.ErrUnexpectedEOF
}
var header, payload uint64
switch b.Buf[0] {
case 254:
if len(b.Buf) < 4 {
return io.ErrUnexpectedEOF
}
header = 4
payload = uint64(b.Buf[1]) | uint64(b.Buf[2])<<8 | uint64(b.Buf[3])<<16
case 255:
return malformedf("invalid %s length prefix 255", what)
default:
header = 1
payload = uint64(b.Buf[0])
}
if err := s.addBytes(payload, what); err != nil {
return err
}
encoded, ok := checkedAddUint64(header, payload)
if !ok {
return malformedf("%s encoded length overflow", what)
}
withPadding, ok := checkedAddUint64(encoded, 3)
if !ok {
return malformedf("%s padded length overflow", what)
}
padded := withPadding &^ uint64(3)
if padded > uint64(math.MaxInt) {
return malformedf("%s padded length %d overflows int", what, padded)
}
if uint64(len(b.Buf)) < padded {
return io.ErrUnexpectedEOF
}
b.Buf = b.Buf[int(padded):]
return nil
}
func skipFixed(b *bin.Buffer, n int) error {
if n < 0 {
return malformedf("negative fixed-width skip %d", n)
}
if len(b.Buf) < n {
return io.ErrUnexpectedEOF
}
b.Buf = b.Buf[n:]
return nil
}
func fixedWireWidth(f *fieldLayout) (int, bool) {
if f == nil {
return 0, false
}
switch f.kind {
case kindInt:
return 4, true
case kindLong, kindDouble:
return 8, true
case kindInt128:
return 16, true
case kindInt256:
return 32, true
case kindTrue:
return 0, true
default:
// Bool deliberately stays on the element loop so constructor ids are
// validated and charged to the aggregate constructor budget.
return 0, false
}
}
func checkedMulInt(a, b int) (int, bool) {
if a < 0 || b < 0 {
return 0, false
}
if a != 0 && b > math.MaxInt/a {
return 0, false
}
return a * b, true
}
func checkedAddUint64(a, b uint64) (uint64, bool) {
if b > math.MaxUint64-a {
return 0, false
}
return a + b, true
}
func ownerName(cl *ctorLayout) string {
if cl == nil || cl.name == "" {
return "<nested>"
}
return cl.name
}
func fieldName(f *fieldLayout) string {
if f == nil || f.name == "" {
return "<element>"
}
return f.name
}

View file

@ -1,263 +0,0 @@
package layerwire
import (
"errors"
"math"
"testing"
"github.com/gotd/td/bin"
"github.com/gotd/td/tg"
)
func TestValidateCanonicalRequestFlaggedHotPathAllocatesNothing(t *testing.T) {
var body bin.Buffer
req := &tg.MessagesSendMessageRequest{
Peer: &tg.InputPeerSelf{},
Message: "hello",
RandomID: 7,
}
if err := req.Encode(&body); err != nil {
t.Fatalf("encode request: %v", err)
}
if err := ValidateCanonicalRequest(body.Buf); err != nil {
t.Fatalf("validate request: %v", err)
}
if allocs := testing.AllocsPerRun(1000, func() {
if err := ValidateCanonicalRequest(body.Buf); err != nil {
panic(err)
}
}); allocs != 0 {
t.Fatalf("canonical request preflight allocations = %.2f, want 0", allocs)
}
}
func TestValidateCanonicalRequestVectorLimits(t *testing.T) {
editCloseFriends := canonical.byName["contacts.editCloseFriends"]
if editCloseFriends == nil {
t.Fatal("contacts.editCloseFriends missing from canonical schema")
}
t.Run("explicit_5000_override", func(t *testing.T) {
var body bin.Buffer
body.PutID(editCloseFriends.crc)
body.PutVectorHeader(5000)
for i := 0; i < 5000; i++ {
body.PutLong(int64(i))
}
if err := ValidateCanonicalRequest(body.Buf); err != nil {
t.Fatalf("validate legal 5000-element close-friends request: %v", err)
}
})
t.Run("override_stops_at_5000", func(t *testing.T) {
var body bin.Buffer
body.PutID(editCloseFriends.crc)
body.PutVectorHeader(5001)
err := ValidateCanonicalRequest(body.Buf)
if !errors.Is(err, ErrResourceLimit) {
t.Fatalf("error = %v, want ErrResourceLimit", err)
}
})
t.Run("default_4096", func(t *testing.T) {
getMessages := canonical.byName["messages.getMessages"]
var body bin.Buffer
body.PutID(getMessages.crc)
body.PutVectorHeader(defaultMaxVectorElements + 1)
err := ValidateCanonicalRequest(body.Buf)
if !errors.Is(err, ErrResourceLimit) {
t.Fatalf("error = %v, want ErrResourceLimit", err)
}
})
t.Run("max_int32_count_rejected_before_iteration", func(t *testing.T) {
var body bin.Buffer
body.PutID(editCloseFriends.crc)
body.PutID(vectorTypeID)
body.PutInt32(math.MaxInt32)
err := ValidateCanonicalRequest(body.Buf)
if !errors.Is(err, ErrResourceLimit) {
t.Fatalf("error = %v, want ErrResourceLimit", err)
}
})
}
func TestValidateCanonicalRequestDepthLimit(t *testing.T) {
invoke := canonical.byName["invokeWithoutUpdates"]
leaf := canonical.byName["help.getConfig"]
if invoke == nil || leaf == nil {
t.Fatal("generic wrapper methods missing from canonical schema")
}
request := func(wrappers int) []byte {
var body bin.Buffer
for i := 0; i < wrappers; i++ {
body.PutID(invoke.crc)
}
body.PutID(leaf.crc)
return body.Buf
}
if err := ValidateCanonicalRequest(request(defaultMaxWalkDepth - 1)); err != nil {
t.Fatalf("depth exactly %d rejected: %v", defaultMaxWalkDepth, err)
}
err := ValidateCanonicalRequest(request(defaultMaxWalkDepth))
if !errors.Is(err, ErrResourceLimit) {
t.Fatalf("depth %d error = %v, want ErrResourceLimit", defaultMaxWalkDepth+1, err)
}
}
func TestTLBytesSkipIsZeroCopyAndBounded(t *testing.T) {
var encoded bin.Buffer
encoded.PutBytes([]byte("payload"))
fieldLen := len(encoded.Buf)
raw := append(encoded.Copy(), 0xaa, 0xbb, 0xcc, 0xdd)
b := &bin.Buffer{Buf: raw}
walk := newWalkState()
if err := walk.skipTLBytes(b, "bytes"); err != nil {
t.Fatalf("skip bytes: %v", err)
}
if len(b.Buf) != 4 || &b.Buf[0] != &raw[fieldLen] {
t.Fatalf("walker did not retain the original backing buffer")
}
t.Run("per_field_budget", func(t *testing.T) {
limited := newWalkState()
limited.limits.maxFieldBytes = 3
probe := &bin.Buffer{Buf: encoded.Copy()}
err := limited.skipTLBytes(probe, "bytes")
if !errors.Is(err, ErrResourceLimit) {
t.Fatalf("error = %v, want ErrResourceLimit", err)
}
})
t.Run("aggregate_budget", func(t *testing.T) {
limited := newWalkState()
limited.limits.maxTotalBytes = 10
first := &bin.Buffer{Buf: encoded.Copy()}
if err := limited.skipTLBytes(first, "bytes"); err != nil {
t.Fatalf("first field: %v", err)
}
second := &bin.Buffer{Buf: encoded.Copy()}
err := limited.skipTLBytes(second, "bytes")
if !errors.Is(err, ErrResourceLimit) {
t.Fatalf("second error = %v, want ErrResourceLimit", err)
}
})
t.Run("truncated_payload_is_malformed", func(t *testing.T) {
importAuth := canonical.byName["auth.importAuthorization"]
var body bin.Buffer
body.PutID(importAuth.crc)
body.PutLong(1)
body.Put([]byte{5, 'a', 'b'}) // declares five bytes, lacks payload/padding
err := ValidateCanonicalRequest(body.Buf)
if !errors.Is(err, ErrMalformed) || errors.Is(err, ErrResourceLimit) {
t.Fatalf("error = %v, want only ErrMalformed", err)
}
})
}
func TestInboundTransformsShareWalkerBudgets(t *testing.T) {
t.Run("canonical_alias", func(t *testing.T) {
var body bin.Buffer
body.PutID(0x41d41ade) // DrKLO messages.forwardMessages alias
body.PutUint32(0)
body.PutID(canonical.byName["inputPeerEmpty"].crc)
body.PutID(vectorTypeID)
body.PutInt32(math.MaxInt32)
_, ok, err := UpgradeInbound(0x41d41ade, &body)
if !ok || !errors.Is(err, ErrResourceLimit) {
t.Fatalf("ok=%v error=%v, want matched ErrResourceLimit", ok, err)
}
})
t.Run("drift_body_transform", func(t *testing.T) {
var body bin.Buffer
body.PutID(0x2e1ee318) // DrKLO langpack.getStrings body transform
body.PutString("en")
body.PutID(vectorTypeID)
body.PutInt32(math.MaxInt32)
_, ok, err := UpgradeInbound(0x2e1ee318, &body)
if !ok || !errors.Is(err, ErrResourceLimit) {
t.Fatalf("ok=%v error=%v, want matched ErrResourceLimit", ok, err)
}
})
t.Run("outbound_structural_transform", func(t *testing.T) {
poll := canonical.byName["pollAnswerVoters"]
var body bin.Buffer
body.PutID(poll.crc)
body.PutUint32(1 << 2)
body.PutBytes(nil)
body.PutInt(1)
body.PutID(vectorTypeID)
body.PutInt32(math.MaxInt32)
_, err := Transcode(body.Buf, CanonicalLayer-1)
if !errors.Is(err, ErrResourceLimit) {
t.Fatalf("error = %v, want ErrResourceLimit", err)
}
})
}
func TestWalkerArithmeticAndMalformedClassification(t *testing.T) {
if defaultMaxWalkUnits != 131072 {
t.Fatalf("default constructor/vector budget = %d, want 131072", defaultMaxWalkUnits)
}
if _, ok := checkedMulInt(math.MaxInt, 2); ok {
t.Fatal("checkedMulInt accepted overflow")
}
if _, ok := checkedAddUint64(math.MaxUint64, 1); ok {
t.Fatal("checkedAddUint64 accepted overflow")
}
t.Run("aggregate_constructor_and_vector_units", func(t *testing.T) {
editCloseFriends := canonical.byName["contacts.editCloseFriends"]
var body bin.Buffer
body.PutID(editCloseFriends.crc)
body.PutVectorHeader(4)
for i := 0; i < 4; i++ {
body.PutLong(int64(i))
}
walk := newWalkState()
walk.limits.maxUnits = 4 // top constructor + four elements needs five
probe := &bin.Buffer{Buf: body.Buf}
err := walk.skipObject(canonical, probe, 1)
if !errors.Is(err, ErrResourceLimit) {
t.Fatalf("error = %v, want ErrResourceLimit", err)
}
})
tests := []struct {
name string
body []byte
}{
{name: "empty"},
{name: "unknown_constructor", body: []byte{1, 2, 3, 4}},
{name: "trailing_bytes", body: append(methodIDBytes(canonical.byName["help.getConfig"].crc), 0, 0, 0, 0)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateCanonicalRequest(tt.body)
if !errors.Is(err, ErrMalformed) || errors.Is(err, ErrResourceLimit) {
t.Fatalf("error = %v, want only ErrMalformed", err)
}
})
}
}
func methodIDBytes(id uint32) []byte {
var b bin.Buffer
b.PutID(id)
return b.Buf
}
func FuzzValidateCanonicalRequest(f *testing.F) {
f.Add(methodIDBytes(canonical.byName["help.getConfig"].crc))
f.Add([]byte{})
f.Add([]byte{1, 2, 3, 4})
f.Fuzz(func(t *testing.T, body []byte) {
err := ValidateCanonicalRequest(body)
if err != nil && !errors.Is(err, ErrMalformed) && !errors.Is(err, ErrResourceLimit) {
t.Fatalf("unclassified walker error: %v", err)
}
})
}

View file

@ -1,35 +0,0 @@
package layerwire
import (
"testing"
"github.com/gotd/td/bin"
)
func mustEncode(t *testing.T, o bin.Encoder) []byte {
t.Helper()
var b bin.Buffer
if err := o.Encode(&b); err != nil {
t.Fatalf("encode %T: %v", o, err)
}
return b.Copy()
}
// TestWalkConsumesCanonicalObjects encodes a diverse corpus of canonical (gotd,
// Layer 227) objects and asserts the generic walker consumes every byte. Full
// consumption proves the layout handles each field's wire kind (flags,
// multi-flags, conditionals, vectors, nested boxed/bare objects) exactly as gotd
// encoded them.
func TestWalkConsumesCanonicalObjects(t *testing.T) {
for _, o := range canonicalCorpus() {
raw := mustEncode(t, o)
b := &bin.Buffer{Buf: append([]byte(nil), raw...)}
if err := canonical.skipObject(b); err != nil {
t.Errorf("%T: walk error: %v", o, err)
continue
}
if b.Len() != 0 {
t.Errorf("%T: %d/%d bytes left after walk", o, b.Len(), len(raw))
}
}
}

View file

@ -5,7 +5,7 @@ import (
"telesrv/internal/seed/appearance"
"github.com/gotd/td/tg"
"github.com/iamxvbaba/td/tg"
)
const appearanceSeedDCID = 2
@ -18,16 +18,16 @@ var peerColorOptionsCache = struct {
profile []tg.HelpPeerColorOption
}{}
func seedWallPapers() []tg.WallPaperClass {
func DefaultWallPapers() []tg.WallPaperClass {
catalog := appearance.Default()
out := make([]tg.WallPaperClass, 0, len(catalog.Wallpapers))
for _, wallpaper := range catalog.Wallpapers {
out = append(out, seedWallPaper(wallpaper))
out = append(out, DefaultWallPaper(wallpaper))
}
return out
}
// LookupWallPaper resolves a cloud wallpaper from the default seed catalog.
// LookupWallPaper resolves a cloud wallpaper from the Default seed catalog.
func LookupWallPaper(input tg.InputWallPaperClass) (tg.WallPaperClass, bool) {
if in, ok := input.(*tg.InputWallPaperNoFile); ok {
return &tg.WallPaperNoFile{ID: in.ID}, true
@ -35,13 +35,13 @@ func LookupWallPaper(input tg.InputWallPaperClass) (tg.WallPaperClass, bool) {
catalog := appearance.Default()
for _, wallpaper := range catalog.Wallpapers {
if inputWallPaperMatches(input, wallpaper) {
return seedWallPaper(wallpaper), true
return DefaultWallPaper(wallpaper), true
}
}
return nil, false
}
// LookupWallPapers resolves multiple wallpapers from the default seed catalog.
// LookupWallPapers resolves multiple wallpapers from the Default seed catalog.
func LookupWallPapers(inputs []tg.InputWallPaperClass) ([]tg.WallPaperClass, bool) {
out := make([]tg.WallPaperClass, 0, len(inputs))
for _, input := range inputs {
@ -65,28 +65,28 @@ func inputWallPaperMatches(input tg.InputWallPaperClass, wallpaper appearance.Wa
}
}
func seedWallPaper(in appearance.Wallpaper) tg.WallPaperClass {
func DefaultWallPaper(in appearance.Wallpaper) tg.WallPaperClass {
if in.Type == 1 || in.Document.ID == 0 {
out := &tg.WallPaperNoFile{ID: in.ID}
out.SetDefault(in.Default)
out.SetDark(in.Dark)
out.SetSettings(seedWallPaperSettings(in.Settings))
out.SetSettings(DefaultWallPaperSettings(in.Settings))
return out
}
out := &tg.WallPaper{
ID: in.ID,
AccessHash: in.AccessHash,
Slug: in.Slug,
Document: seedDocument(in.Document),
Document: DefaultDocument(in.Document),
}
out.SetDefault(in.Default)
out.SetPattern(in.Pattern)
out.SetDark(in.Dark)
out.SetSettings(seedWallPaperSettings(in.Settings))
out.SetSettings(DefaultWallPaperSettings(in.Settings))
return out
}
func seedWallPaperSettings(in appearance.WallpaperSettings) tg.WallPaperSettings {
func DefaultWallPaperSettings(in appearance.WallpaperSettings) tg.WallPaperSettings {
var out tg.WallPaperSettings
out.SetBlur(in.Blur)
out.SetMotion(in.Motion)
@ -111,7 +111,7 @@ func seedWallPaperSettings(in appearance.WallpaperSettings) tg.WallPaperSettings
return out
}
func seedDocument(in appearance.Document) tg.DocumentClass {
func DefaultDocument(in appearance.Document) tg.DocumentClass {
if in.ID == 0 {
return &tg.DocumentEmpty{}
}
@ -121,14 +121,14 @@ func seedDocument(in appearance.Document) tg.DocumentClass {
Date: in.Date,
MimeType: in.MimeType,
Size: in.Size,
Thumbs: seedPhotoSizes(in.Thumbs),
Thumbs: DefaultPhotoSizes(in.Thumbs),
DCID: appearanceSeedDCID,
Attributes: seedDocumentAttributes(in.Attributes),
Attributes: DefaultDocumentAttributes(in.Attributes),
FileReference: nil,
}
}
func seedPhotoSizes(in []appearance.PhotoSize) []tg.PhotoSizeClass {
func DefaultPhotoSizes(in []appearance.PhotoSize) []tg.PhotoSizeClass {
out := make([]tg.PhotoSizeClass, 0, len(in))
for _, size := range in {
switch size.Kind {
@ -144,7 +144,7 @@ func seedPhotoSizes(in []appearance.PhotoSize) []tg.PhotoSizeClass {
return out
}
func seedDocumentAttributes(in []appearance.DocumentAttribute) []tg.DocumentAttributeClass {
func DefaultDocumentAttributes(in []appearance.DocumentAttribute) []tg.DocumentAttributeClass {
out := make([]tg.DocumentAttributeClass, 0, len(in))
for _, attr := range in {
switch attr.Kind {
@ -159,20 +159,20 @@ func seedDocumentAttributes(in []appearance.DocumentAttribute) []tg.DocumentAttr
return out
}
func seedPeerColorOptions(profile bool) []tg.HelpPeerColorOption {
func DefaultPeerColorOptions(profile bool) []tg.HelpPeerColorOption {
if profile {
peerColorOptionsCache.profileOnce.Do(func() {
peerColorOptionsCache.profile = buildSeedPeerColorOptions(true)
peerColorOptionsCache.profile = buildDefaultPeerColorOptions(true)
})
return clonePeerColorOptions(peerColorOptionsCache.profile)
}
peerColorOptionsCache.regularOnce.Do(func() {
peerColorOptionsCache.regular = buildSeedPeerColorOptions(false)
peerColorOptionsCache.regular = buildDefaultPeerColorOptions(false)
})
return clonePeerColorOptions(peerColorOptionsCache.regular)
}
func buildSeedPeerColorOptions(profile bool) []tg.HelpPeerColorOption {
func buildDefaultPeerColorOptions(profile bool) []tg.HelpPeerColorOption {
catalog := appearance.Default()
source := catalog.PeerColors
if profile {
@ -193,10 +193,10 @@ func buildSeedPeerColorOptions(profile bool) []tg.HelpPeerColorOption {
if groupMin > 0 {
option.SetGroupMinLevel(groupMin)
}
if colors := seedPeerColorSet(color.Colors); colors != nil {
if colors := DefaultPeerColorSet(color.Colors); colors != nil {
option.SetColors(colors)
}
if colors := seedPeerColorSet(color.DarkColors); colors != nil {
if colors := DefaultPeerColorSet(color.DarkColors); colors != nil {
option.SetDarkColors(colors)
}
out = append(out, option)
@ -246,7 +246,7 @@ func boundedPeerColorMinLevel(level int) int {
return level
}
func seedPeerColorID(id int, profile bool) (bool, bool) {
func DefaultPeerColorID(id int, profile bool) (bool, bool) {
catalog := appearance.Default()
source := catalog.PeerColors
if profile {
@ -263,7 +263,7 @@ func seedPeerColorID(id int, profile bool) (bool, bool) {
return false, true
}
func seedPeerColorSet(in *appearance.ColorSet) tg.HelpPeerColorSetClass {
func DefaultPeerColorSet(in *appearance.ColorSet) tg.HelpPeerColorSetClass {
if in == nil {
return nil
}

View file

@ -3,7 +3,7 @@ package tdesktop
import (
"time"
"github.com/gotd/td/tg"
"github.com/iamxvbaba/td/tg"
"telesrv/internal/links"
)

View file

@ -1,7 +1,7 @@
package tdesktop
import (
"github.com/gotd/td/tg"
"github.com/iamxvbaba/td/tg"
"telesrv/internal/seed/catalog"
)

View file

@ -3,7 +3,7 @@ package tdesktop
import (
"time"
"github.com/gotd/td/tg"
"github.com/iamxvbaba/td/tg"
"telesrv/internal/seed/appearance"
"telesrv/internal/seed/catalog"
@ -147,7 +147,7 @@ func catalogThemeSettings(s appearance.ThemeSettings, base tg.BaseThemeClass) tg
ts.SetMessageColors(append([]int(nil), s.MessageColors...))
}
if s.Wallpaper.ID != 0 || s.Wallpaper.Document.ID != 0 {
ts.SetWallpaper(seedWallPaper(s.Wallpaper))
ts.SetWallpaper(DefaultWallPaper(s.Wallpaper))
}
return ts
}
@ -194,13 +194,13 @@ func UniqueGiftChatThemes(hash int64) tg.AccountChatThemesClass {
}
}
// WallPapers returns the read-only default wallpaper catalog. User wallpaper
// WallPapers returns the read-only Default wallpaper catalog. User wallpaper
// upload/save/install remains outside the current TDesktop compatibility scope.
func WallPapers(hash int64) tg.AccountWallPapersClass {
if hash == wallPapersHash {
return &tg.AccountWallPapersNotModified{}
}
wallpapers := seedWallPapers()
wallpapers := DefaultWallPapers()
return &tg.AccountWallPapers{
Hash: wallPapersHash,
Wallpapers: wallpapers,
@ -425,7 +425,7 @@ var defaultPeerColors = []defaultPeerColor{
// IsPeerColorID reports whether id is in the TDesktop-compatible peer color palette.
func IsPeerColorID(id int) bool {
if found, seeded := seedPeerColorID(id, false); seeded {
if found, seeded := DefaultPeerColorID(id, false); seeded {
return found
}
for _, color := range defaultPeerColors {
@ -438,7 +438,7 @@ func IsPeerColorID(id int) bool {
// IsPeerProfileColorID reports whether id is in the profile background palette.
func IsPeerProfileColorID(id int) bool {
if found, seeded := seedPeerColorID(id, true); seeded {
if found, seeded := DefaultPeerColorID(id, true); seeded {
return found
}
return IsPeerColorID(id)
@ -448,7 +448,7 @@ func PeerColors(hash int) tg.HelpPeerColorsClass {
if hash == peerColorsHash {
return &tg.HelpPeerColorsNotModified{}
}
colors := seedPeerColorOptions(false)
colors := DefaultPeerColorOptions(false)
if len(colors) == 0 {
colors = make([]tg.HelpPeerColorOption, 0, len(defaultPeerColors))
for _, color := range defaultPeerColors {
@ -468,7 +468,7 @@ func PeerProfileColors(hash int) tg.HelpPeerColorsClass {
if hash == peerProfileColorsHash {
return &tg.HelpPeerColorsNotModified{}
}
colors := seedPeerColorOptions(true)
colors := DefaultPeerColorOptions(true)
if len(colors) == 0 {
colors = make([]tg.HelpPeerColorOption, 0, len(defaultPeerColors))
for _, color := range defaultPeerColors {

View file

@ -4,7 +4,7 @@ import (
"math"
"testing"
"github.com/gotd/td/tg"
"github.com/iamxvbaba/td/tg"
)
func TestNotifySettingsDefaultIsAudible(t *testing.T) {
@ -233,7 +233,7 @@ func TestUniqueGiftChatThemesIsEmptyHashableStub(t *testing.T) {
}
}
func TestWallPapersUsesOrangeFileCatalog(t *testing.T) {
func TestWallPapersUsesDefaultFileCatalog(t *testing.T) {
got, ok := WallPapers(0).(*tg.AccountWallPapers)
if !ok {
t.Fatalf("WallPapers(0) = %T, want modified list", got)
@ -249,14 +249,14 @@ func TestWallPapersUsesOrangeFileCatalog(t *testing.T) {
t.Fatalf("WallPapers(0).Wallpapers[0] = %T, want *tg.WallPaper", got.Wallpapers[0])
}
if wallpaper.ID == 0 || wallpaper.AccessHash == 0 || wallpaper.Slug == "" {
t.Fatalf("wallpaper identity = id %d hash %d slug %q, want seed ids", wallpaper.ID, wallpaper.AccessHash, wallpaper.Slug)
t.Fatalf("wallpaper identity = id %d hash %d slug %q, want Default ids", wallpaper.ID, wallpaper.AccessHash, wallpaper.Slug)
}
doc, ok := wallpaper.Document.(*tg.Document)
if !ok {
t.Fatalf("wallpaper document = %T, want *tg.Document", wallpaper.Document)
}
if doc.ID == 0 || doc.AccessHash == 0 || doc.Size == 0 || doc.MimeType == "" || doc.DCID != appearanceSeedDCID {
t.Fatalf("wallpaper document = id %d hash %d size %d mime %q dc %d, want downloadable seed document",
t.Fatalf("wallpaper document = id %d hash %d size %d mime %q dc %d, want downloadable Default document",
doc.ID, doc.AccessHash, doc.Size, doc.MimeType, doc.DCID)
}
if len(doc.Thumbs) == 0 {
@ -392,7 +392,7 @@ func TestPeerColorsAreNonEmptyHashableAccentSets(t *testing.T) {
t.Fatalf("PeerColors(0) = hash %d colors %d, want non-empty stable list", got.Hash, len(got.Colors))
}
if len(got.Colors) != 21 {
t.Fatalf("PeerColors(0).Colors length = %d, want seed palette count 21", len(got.Colors))
t.Fatalf("PeerColors(0).Colors length = %d, want Default palette count 21", len(got.Colors))
}
withExplicitColors := 0
for i, option := range got.Colors {
@ -412,7 +412,7 @@ func TestPeerColorsAreNonEmptyHashableAccentSets(t *testing.T) {
withExplicitColors++
}
if withExplicitColors == 0 {
t.Fatal("PeerColors() has no explicit seed color sets")
t.Fatal("PeerColors() has no explicit Default color sets")
}
if _, ok := PeerColors(got.Hash).(*tg.HelpPeerColorsNotModified); !ok {
t.Fatalf("PeerColors(hash) = %#v, want notModified", PeerColors(got.Hash))
@ -428,7 +428,7 @@ func TestPeerProfileColorsAreNonEmptyHashableProfileSets(t *testing.T) {
t.Fatalf("PeerProfileColors(0) = hash %d colors %d, want non-empty stable list", got.Hash, len(got.Colors))
}
if len(got.Colors) != 16 {
t.Fatalf("PeerProfileColors(0).Colors length = %d, want seed profile palette count 16", len(got.Colors))
t.Fatalf("PeerProfileColors(0).Colors length = %d, want Default profile palette count 16", len(got.Colors))
}
for i, option := range got.Colors {
if !IsPeerProfileColorID(option.ColorID) {