225 lines
6.7 KiB
Go
225 lines
6.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
type declaration struct {
|
|
Name string `json:"name"`
|
|
Kind string `json:"kind"`
|
|
File string `json:"file"`
|
|
SHA256 string `json:"sha256"`
|
|
}
|
|
|
|
type manifest struct {
|
|
Version string `json:"version"`
|
|
Source string `json:"source"`
|
|
Declarations []declaration `json:"declarations"`
|
|
}
|
|
|
|
var requiredFiles = []string{
|
|
"server.go",
|
|
"authentication.go",
|
|
"submit.go",
|
|
"sessions.go",
|
|
"delivery.go",
|
|
"acknowledgement.go",
|
|
"pending_recovery.go",
|
|
"protocol_log.go",
|
|
"transport.go",
|
|
}
|
|
|
|
var requiredTests = []string{
|
|
"TestInboundServerAuthenticatesAndSubmits",
|
|
"TestInboundServerProcessesSubmitWithinAuthenticatedConnectionWindow",
|
|
"TestBoundedSubmitWindowAndAggregateSlotSnapshot",
|
|
"TestInboundServerForwardsLongMessageFragmentsWithoutUDHAndAcknowledgesEachSubmit",
|
|
"TestSubmitResponsePrecedesQueuedFailureReceipt",
|
|
"TestDailyLimitRejectsSubmitSynchronouslyWithoutPendingReceipt",
|
|
"TestInboundServerNegotiatesCMPP2AndUsesAuthenticatedAccountForSubmit",
|
|
"TestNormalizeInboundSubmitSupportsCMPP2AndCMPP3",
|
|
"TestDownstreamDeliveryRequiresAcknowledgement",
|
|
"TestReceiptLookupDoesNotFallbackToAccountBeforeSubmitMappingExists",
|
|
"TestDownstreamDeliveryReportsAckTimeout",
|
|
"TestRecoverPendingCandidatesWritesWaitingConnectionStatus",
|
|
"TestSubmitResponseProtocolLoggerEmitsActualPacketDirection",
|
|
"TestDownstreamDeliverProtocolLoggerEmitsReceiptPacket",
|
|
}
|
|
|
|
func main() {
|
|
root, err := os.Getwd()
|
|
must(err)
|
|
manifestPath := filepath.Join(root, "docs", "contracts", "inbound-r6-declarations.json")
|
|
payload, err := os.ReadFile(manifestPath)
|
|
must(err)
|
|
var contract manifest
|
|
must(json.Unmarshal(payload, &contract))
|
|
if contract.Version != "R6" {
|
|
fail("unexpected contract version %q", contract.Version)
|
|
}
|
|
|
|
inboundDir := filepath.Join(root, "gateway", "internal", "inbound")
|
|
actual := map[string]declaration{}
|
|
for _, name := range requiredFiles {
|
|
path := filepath.Join(inboundDir, name)
|
|
source, err := os.ReadFile(path)
|
|
must(err)
|
|
if !bytes.HasPrefix(source, []byte("package inbound")) {
|
|
fail("%s must remain in package inbound", name)
|
|
}
|
|
for _, item := range declarationsInFile(path, source) {
|
|
key := item.Kind + ":" + item.Name
|
|
if previous, exists := actual[key]; exists {
|
|
fail("duplicate declaration %s in %s and %s", key, previous.File, item.File)
|
|
}
|
|
actual[key] = item
|
|
}
|
|
}
|
|
if os.Getenv("UPDATE_INBOUND_R6_CONTRACT") == "1" {
|
|
contract.Declarations = contract.Declarations[:0]
|
|
for _, item := range actual {
|
|
contract.Declarations = append(contract.Declarations, item)
|
|
}
|
|
sort.Slice(contract.Declarations, func(i, j int) bool {
|
|
if contract.Declarations[i].File != contract.Declarations[j].File {
|
|
return contract.Declarations[i].File < contract.Declarations[j].File
|
|
}
|
|
if contract.Declarations[i].Name == contract.Declarations[j].Name {
|
|
return contract.Declarations[i].Kind < contract.Declarations[j].Kind
|
|
}
|
|
return contract.Declarations[i].Name < contract.Declarations[j].Name
|
|
})
|
|
updated, err := json.MarshalIndent(contract, "", " ")
|
|
must(err)
|
|
must(os.WriteFile(manifestPath, append(updated, '\n'), 0o644))
|
|
fmt.Printf("R6 inbound contract updated with %d declarations.\n", len(contract.Declarations))
|
|
}
|
|
|
|
for _, expected := range contract.Declarations {
|
|
key := expected.Kind + ":" + expected.Name
|
|
item, ok := actual[key]
|
|
if !ok {
|
|
fail("missing declaration %s expected in %s", key, expected.File)
|
|
}
|
|
if item.File != expected.File {
|
|
fail("%s moved to %s, expected %s", key, item.File, expected.File)
|
|
}
|
|
if item.SHA256 != expected.SHA256 {
|
|
fail("%s implementation changed: %s != %s", key, item.SHA256, expected.SHA256)
|
|
}
|
|
}
|
|
|
|
serverSource, err := os.ReadFile(filepath.Join(inboundDir, "server.go"))
|
|
must(err)
|
|
if lines := bytes.Count(serverSource, []byte{'\n'}); lines > 100 {
|
|
fail("stable server.go grew to %d lines", lines)
|
|
}
|
|
assertContains(serverSource, "func (s Server) ListenAndServe() error")
|
|
assertContains(serverSource, "s.handleLogin")
|
|
assertContains(serverSource, "s.handleSubmit")
|
|
assertContains(serverSource, "s.handleActivity")
|
|
assertContains(serverSource, "s.handleConnectionClosed")
|
|
|
|
controlSource, err := os.ReadFile(filepath.Join(root, "gateway", "internal", "control", "server.go"))
|
|
must(err)
|
|
for _, entry := range []string{
|
|
"inbound.DisconnectAccount(",
|
|
"inbound.PushReceiptWithResult(",
|
|
"inbound.PushUplinkWithResult(",
|
|
} {
|
|
assertContains(controlSource, entry)
|
|
}
|
|
|
|
var tests bytes.Buffer
|
|
for _, name := range []string{"server_test.go", "protocol_log_test.go", "presence_test.go", "recovery_test.go"} {
|
|
source, err := os.ReadFile(filepath.Join(inboundDir, name))
|
|
must(err)
|
|
tests.Write(source)
|
|
}
|
|
for _, test := range requiredTests {
|
|
assertContains(tests.Bytes(), "func "+test+"(")
|
|
}
|
|
|
|
fmt.Printf(
|
|
"R6 inbound facade verified: %d declarations across %d focused files; stable entries and %d critical tests preserved.\n",
|
|
len(contract.Declarations), len(requiredFiles), len(requiredTests),
|
|
)
|
|
}
|
|
|
|
func declarationsInFile(path string, source []byte) []declaration {
|
|
fset := token.NewFileSet()
|
|
file, err := parser.ParseFile(fset, path, source, parser.ParseComments)
|
|
must(err)
|
|
var result []declaration
|
|
for _, decl := range file.Decls {
|
|
if gen, ok := decl.(*ast.GenDecl); ok && gen.Tok == token.IMPORT {
|
|
continue
|
|
}
|
|
name, kind := declarationName(decl)
|
|
start := decl.Pos()
|
|
switch typed := decl.(type) {
|
|
case *ast.FuncDecl:
|
|
if typed.Doc != nil {
|
|
start = typed.Doc.Pos()
|
|
}
|
|
case *ast.GenDecl:
|
|
if typed.Doc != nil {
|
|
start = typed.Doc.Pos()
|
|
}
|
|
}
|
|
code := strings.TrimSpace(string(source[fset.Position(start).Offset:fset.Position(decl.End()).Offset]))
|
|
hash := sha256.Sum256([]byte(strings.ReplaceAll(code, "\r\n", "\n")))
|
|
result = append(result, declaration{
|
|
Name: name, Kind: kind, File: filepath.Base(path), SHA256: hex.EncodeToString(hash[:]),
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func declarationName(decl ast.Decl) (string, string) {
|
|
switch typed := decl.(type) {
|
|
case *ast.FuncDecl:
|
|
return typed.Name.Name, "func"
|
|
case *ast.GenDecl:
|
|
if len(typed.Specs) == 0 {
|
|
fail("empty declaration")
|
|
}
|
|
switch spec := typed.Specs[0].(type) {
|
|
case *ast.TypeSpec:
|
|
return spec.Name.Name, "type"
|
|
case *ast.ValueSpec:
|
|
return spec.Names[0].Name, strings.ToLower(typed.Tok.String())
|
|
}
|
|
}
|
|
fail("unsupported declaration %T", decl)
|
|
return "", ""
|
|
}
|
|
|
|
func assertContains(source []byte, fragment string) {
|
|
if !bytes.Contains(source, []byte(fragment)) {
|
|
fail("required invariant not found: %s", fragment)
|
|
}
|
|
}
|
|
|
|
func must(err error) {
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func fail(format string, args ...any) {
|
|
fmt.Fprintf(os.Stderr, "R6 verification failed: "+format+"\n", args...)
|
|
os.Exit(1)
|
|
}
|