package ca import ( "net" "testing" "github.com/uopi/goca/proto" ) // buildOldSearchReply builds a CA_PROTO_SEARCH UDP reply using the old // payload format (CA < 4.9): [minor_version(2)][pad(2)][IP(4)]. // When ip is nil, the IP field is 0xFFFFFFFF (use-sender convention). func buildOldSearchReply(searchID uint32, serverIP net.IP, port int) []byte { payload := make([]byte, 8) payload[0] = 0x00 payload[1] = proto.MinorVersion // e.g. 0x0D = 13 // payload[2:4] = 0x00 0x00 (pad) if serverIP == nil { payload[4] = 0xFF payload[5] = 0xFF payload[6] = 0xFF payload[7] = 0xFF } else { copy(payload[4:], serverIP.To4()) } h := proto.Header{ Command: proto.CmdSearch, DataType: uint16(port), Parameter1: proto.MinorVersion, Parameter2: searchID, } return proto.BuildMessage(h, payload) } // TestParseReplyOldFormatAllFF verifies the old format with 0xFFFFFFFF IP. func TestParseReplyOldFormatAllFF(t *testing.T) { se := &searchEngine{waiters: make(map[uint32]*searchWaiter)} const searchID = uint32(42) result := make(chan searchResult, 1) se.mu.Lock() se.waiters[searchID] = &searchWaiter{pvName: "TEST:PV", ch: result} se.mu.Unlock() pkt := buildOldSearchReply(searchID, nil, 5064) // nil → 0xFFFFFFFF src := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5064} se.parseReply(pkt, src) select { case r := <-result: if r.addr != "127.0.0.1:5064" { t.Errorf("addr = %q, want 127.0.0.1:5064", r.addr) } default: t.Fatal("parseReply did not deliver a result") } } // TestParseReplyOldFormatAllZero verifies the old format with 0.0.0.0 IP // (used by some EPICS Base versions to also mean "use sender IP"). func TestParseReplyOldFormatAllZero(t *testing.T) { se := &searchEngine{waiters: make(map[uint32]*searchWaiter)} const searchID = uint32(43) result := make(chan searchResult, 1) se.mu.Lock() se.waiters[searchID] = &searchWaiter{pvName: "TEST:PV", ch: result} se.mu.Unlock() pkt := buildOldSearchReply(searchID, net.IPv4(0, 0, 0, 0).To4(), 5064) src := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5064} se.parseReply(pkt, src) select { case r := <-result: if r.addr != "127.0.0.1:5064" { t.Errorf("addr = %q, want 127.0.0.1:5064", r.addr) } default: t.Fatal("parseReply did not deliver a result") } } // TestParseReplyOldFormatExplicitIP verifies that an explicit server IP in // the old payload format is read from the correct offset (bytes [4:8]). func TestParseReplyOldFormatExplicitIP(t *testing.T) { se := &searchEngine{ waiters: make(map[uint32]*searchWaiter), } const searchID = uint32(7) result := make(chan searchResult, 1) se.mu.Lock() se.waiters[searchID] = &searchWaiter{pvName: "TEST:PV2", ch: result} se.mu.Unlock() explicitIP := net.ParseIP("10.0.0.5").To4() pkt := buildOldSearchReply(searchID, explicitIP, 5064) src := &net.UDPAddr{IP: net.ParseIP("10.0.0.5"), Port: 5064} se.parseReply(pkt, src) select { case r := <-result: if r.addr != "10.0.0.5:5064" { t.Errorf("addr = %q, want 10.0.0.5:5064", r.addr) } default: t.Fatal("parseReply did not deliver a result") } } // TestParseReplyNewFormat verifies the new payload format (IP at [0:4]). func TestParseReplyNewFormat(t *testing.T) { se := &searchEngine{ waiters: make(map[uint32]*searchWaiter), } const searchID = uint32(99) result := make(chan searchResult, 1) se.mu.Lock() se.waiters[searchID] = &searchWaiter{pvName: "TEST:PV3", ch: result} se.mu.Unlock() // New format: IP = 0xFFFFFFFF at [0:4]. pkt := EncodeSearchReply(searchID, nil, 5064) src := &net.UDPAddr{IP: net.ParseIP("192.168.1.10"), Port: 5064} se.parseReply(pkt, src) select { case r := <-result: if r.addr != "192.168.1.10:5064" { t.Errorf("addr = %q, want 192.168.1.10:5064", r.addr) } default: t.Fatal("parseReply did not deliver a result") } }