203 lines
7.2 KiB
Go
203 lines
7.2 KiB
Go
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))
|
|
}
|
|
}
|
|
|
|
// 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())
|
|
}
|
|
}
|
|
}
|