From 6b91bf25ed955316a095cfc616e654a90d7340f0 Mon Sep 17 00:00:00 2001 From: Mikio Hara Date: Sat, 16 Feb 2013 13:02:07 +0900 Subject: [PATCH] go.net/ipv4: fix sprious lookahead on IPConn-based PacketConn Also improves test coverage for both payload and datagram I/O. R=golang-dev, dave CC=golang-dev https://golang.org/cl/7304091 --- ipv4/header_test.go | 38 ++++----- ipv4/mockicmp_test.go | 138 +++++++++++++++++++++++++-------- ipv4/mocktransponder_test.go | 123 ++++++++++++++--------------- ipv4/multicast_test.go | 62 +++++++++++++-- ipv4/multicastlistener_test.go | 10 +-- ipv4/payload.go | 4 +- ipv4/unicast_test.go | 65 +++++++++++++--- 7 files changed, 297 insertions(+), 143 deletions(-) diff --git a/ipv4/header_test.go b/ipv4/header_test.go index 0ac02ed3..cc2fd8e0 100644 --- a/ipv4/header_test.go +++ b/ipv4/header_test.go @@ -44,27 +44,24 @@ var ( } // TODO(mikio): Add platform dependent wire header formats when // we support new platforms. + + testHeader = &ipv4.Header{ + Version: ipv4.Version, + Len: ipv4.HeaderLen, + TOS: 1, + TotalLen: 0xbeef, + ID: 0xcafe, + FragOff: 1500, + TTL: 255, + Protocol: 1, + Checksum: 0xdead, + Src: net.IPv4(172, 16, 254, 254), + Dst: net.IPv4(192, 168, 0, 1), + } ) -func testHeader() *ipv4.Header { - h := &ipv4.Header{} - h.Version = ipv4.Version - h.Len = ipv4.HeaderLen - h.TOS = 1 - h.TotalLen = 0xbeef - h.ID = 0xcafe - h.FragOff = 1500 - h.TTL = 255 - h.Protocol = 1 - h.Checksum = 0xdead - h.Src = net.IPv4(172, 16, 254, 254) - h.Dst = net.IPv4(192, 168, 0, 1) - return h -} - func TestMarshalHeader(t *testing.T) { - th := testHeader() - b, err := th.Marshal() + b, err := testHeader.Marshal() if err != nil { t.Fatalf("ipv4.Header.Marshal failed: %v", err) } @@ -92,8 +89,7 @@ func TestParseHeader(t *testing.T) { if err != nil { t.Fatalf("ipv4.ParseHeader failed: %v", err) } - th := testHeader() - if !reflect.DeepEqual(h, th) { - t.Fatalf("ipv4.ParseHeader failed: %#v not equal %#v", h, th) + if !reflect.DeepEqual(h, testHeader) { + t.Fatalf("ipv4.ParseHeader failed: %#v not equal %#v", h, testHeader) } } diff --git a/ipv4/mockicmp_test.go b/ipv4/mockicmp_test.go index 5bf89209..7b9ee066 100644 --- a/ipv4/mockicmp_test.go +++ b/ipv4/mockicmp_test.go @@ -5,43 +5,117 @@ package ipv4_test import ( - "bytes" + "errors" "flag" ) var testExternal = flag.Bool("external", true, "allow use of external networks during long test") -func newICMPEchoRequest(id, seqnum, msglen int, filler []byte) []byte { - b := newICMPInfoMessage(id, seqnum, msglen, filler) - b[0] = 8 - // calculate ICMP checksum - cklen := len(b) - s := uint32(0) - for i := 0; i < cklen-1; i += 2 { - s += uint32(b[i+1])<<8 | uint32(b[i]) - } - if cklen&1 == 1 { - s += uint32(b[cklen-1]) - } - s = (s >> 16) + (s & 0xffff) - s = s + (s >> 16) - // place checksum back in header; using ^= avoids the - // assumption the checksum bytes are zero - b[2] ^= byte(^s & 0xff) - b[3] ^= byte(^s >> 8) - return b +const ( + icmpv4EchoRequest = 8 + icmpv4EchoReply = 0 + icmpv6EchoRequest = 128 + icmpv6EchoReply = 129 +) + +// icmpMessage represents an ICMP message. +type icmpMessage struct { + Type int // type + Code int // code + Checksum int // checksum + Body icmpMessageBody // body } -func newICMPInfoMessage(id, seqnum, msglen int, filler []byte) []byte { - b := make([]byte, msglen) - copy(b[8:], bytes.Repeat(filler, (msglen-8)/len(filler)+1)) - b[0] = 0 // type - b[1] = 0 // code - b[2] = 0 // checksum - b[3] = 0 // checksum - b[4] = byte(id >> 8) // identifier - b[5] = byte(id & 0xff) // identifier - b[6] = byte(seqnum >> 8) // sequence number - b[7] = byte(seqnum & 0xff) // sequence number - return b +// icmpMessageBody represents an ICMP message body. +type icmpMessageBody interface { + Len() int + Marshal() ([]byte, error) +} + +// Marshal returns the binary enconding of the ICMP echo request or +// reply message m. +func (m *icmpMessage) Marshal() ([]byte, error) { + b := []byte{byte(m.Type), byte(m.Code), 0, 0} + if m.Body != nil && m.Body.Len() != 0 { + mb, err := m.Body.Marshal() + if err != nil { + return nil, err + } + b = append(b, mb...) + } + switch m.Type { + case icmpv6EchoRequest, icmpv6EchoReply: + return b, nil + } + csumcv := len(b) - 1 // checksum coverage + s := uint32(0) + for i := 0; i < csumcv; i += 2 { + s += uint32(b[i+1])<<8 | uint32(b[i]) + } + if csumcv&1 == 0 { + s += uint32(b[csumcv]) + } + s = s>>16 + s&0xffff + s = s + s>>16 + // Place checksum back in header; using ^= avoids the + // assumption the checksum bytes are zero. + b[2] ^= byte(^s & 0xff) + b[3] ^= byte(^s >> 8) + return b, nil +} + +// parseICMPMessage parses b as an ICMP message. +func parseICMPMessage(b []byte) (*icmpMessage, error) { + msglen := len(b) + if msglen < 4 { + return nil, errors.New("message too short") + } + m := &icmpMessage{Type: int(b[0]), Code: int(b[1]), Checksum: int(b[2])<<8 | int(b[3])} + if msglen > 4 { + var err error + switch m.Type { + case icmpv4EchoRequest, icmpv4EchoReply, icmpv6EchoRequest, icmpv6EchoReply: + m.Body, err = parseICMPEcho(b[4:]) + if err != nil { + return nil, err + } + } + } + return m, nil +} + +// imcpEcho represenets an ICMP echo request or reply message body. +type icmpEcho struct { + ID int // identifier + Seq int // sequence number + Data []byte // data +} + +func (p *icmpEcho) Len() int { + if p == nil { + return 0 + } + return 4 + len(p.Data) +} + +// Marshal returns the binary enconding of the ICMP echo request or +// reply message body p. +func (p *icmpEcho) Marshal() ([]byte, error) { + b := make([]byte, 4+len(p.Data)) + b[0], b[1] = byte(p.ID>>8), byte(p.ID&0xff) + b[2], b[3] = byte(p.Seq>>8), byte(p.Seq&0xff) + copy(b[4:], p.Data) + return b, nil +} + +// parseICMPEcho parses b as an ICMP echo request or reply message +// body. +func parseICMPEcho(b []byte) (*icmpEcho, error) { + bodylen := len(b) + p := &icmpEcho{ID: int(b[0])<<8 | int(b[1]), Seq: int(b[2])<<8 | int(b[3])} + if bodylen > 4 { + p.Data = make([]byte, bodylen-4) + copy(p.Data, b[4:]) + } + return p, nil } diff --git a/ipv4/mocktransponder_test.go b/ipv4/mocktransponder_test.go index ef682bd2..357721d8 100644 --- a/ipv4/mocktransponder_test.go +++ b/ipv4/mocktransponder_test.go @@ -13,77 +13,70 @@ import ( "time" ) -// runPayloadTransponder transmits IPv4 datagram payloads to the +// writeThenReadPayload transmits IPv4 datagram payloads to the // loopback address or interface and captures the loopback'd datagram // payloads. -func runPayloadTransponder(t *testing.T, c *ipv4.PacketConn, wb []byte, dst net.Addr) { - cf := ipv4.FlagTTL | ipv4.FlagDst | ipv4.FlagInterface +func writeThenReadPayload(t *testing.T, i int, c *ipv4.PacketConn, wb []byte, dst net.Addr) []byte { rb := make([]byte, 1500) - for i, toggle := range []bool{true, false, true} { - if err := c.SetControlMessage(cf, toggle); err != nil { - t.Fatalf("ipv4.PacketConn.SetControlMessage failed: %v", err) - } - c.SetTOS(i + 1) - var ip net.IP - switch v := dst.(type) { - case *net.UDPAddr: - ip = v.IP - case *net.IPAddr: - ip = v.IP - } - if ip.IsMulticast() { - c.SetMulticastTTL(i + 1) - } else { - c.SetTTL(i + 1) - } - c.SetDeadline(time.Now().Add(100 * time.Millisecond)) - if _, err := c.WriteTo(wb, nil, dst); err != nil { - t.Fatalf("ipv4.PacketConn.WriteTo failed: %v", err) - } - _, cm, _, err := c.ReadFrom(rb) - if err != nil { - t.Fatalf("ipv4.PacketConn.ReadFrom failed: %v", err) - } - t.Logf("rcvd cmsg: %v", cm) + c.SetTOS(i + 1) + var ip net.IP + switch v := dst.(type) { + case *net.UDPAddr: + ip = v.IP + case *net.IPAddr: + ip = v.IP } + if ip.IsMulticast() { + c.SetMulticastTTL(i + 1) + } else { + c.SetTTL(i + 1) + } + c.SetDeadline(time.Now().Add(100 * time.Millisecond)) + if _, err := c.WriteTo(wb, nil, dst); err != nil { + t.Fatalf("ipv4.PacketConn.WriteTo failed: %v", err) + } + n, cm, _, err := c.ReadFrom(rb) + if err != nil { + t.Fatalf("ipv4.PacketConn.ReadFrom failed: %v", err) + } + t.Logf("rcvd cmsg: %v", cm) + return rb[:n] } -// runDatagramTransponder transmits ICMP for IPv4 datagrams to the +// writeThenReadDatagram transmits ICMP for IPv4 datagrams to the // loopback address or interface and captures the response datagrams // from the protocol stack within the kernel. -func runDatagramTransponder(t *testing.T, c *ipv4.RawConn, wb []byte, src, dst net.Addr) { - cf := ipv4.FlagTTL | ipv4.FlagDst | ipv4.FlagInterface +func writeThenReadDatagram(t *testing.T, i int, c *ipv4.RawConn, wb []byte, src, dst net.Addr) []byte { rb := make([]byte, ipv4.HeaderLen+len(wb)) - for i, toggle := range []bool{true, false, true} { - if err := c.SetControlMessage(cf, toggle); err != nil { - t.Fatalf("ipv4.RawConn.SetControlMessage failed: %v", err) - } - wh := &ipv4.Header{} - wh.Version = ipv4.Version - wh.Len = ipv4.HeaderLen - wh.TOS = i + 1 - wh.TotalLen = ipv4.HeaderLen + len(wb) - wh.TTL = i + 1 - wh.Protocol = 1 - if src != nil { - wh.Src = src.(*net.IPAddr).IP - } - if dst != nil { - wh.Dst = dst.(*net.IPAddr).IP - } - c.SetDeadline(time.Now().Add(100 * time.Millisecond)) - if err := c.WriteTo(wh, wb, nil); err != nil { - t.Fatalf("ipv4.RawConn.WriteTo failed: %v", err) - } - rh, _, cm, err := c.ReadFrom(rb) - if err != nil { - t.Fatalf("ipv4.RawConn.ReadFrom failed: %v", err) - } - t.Logf("rcvd cmsg: %v", cm.String()) - t.Logf("rcvd hdr: %v", rh.String()) + wh := &ipv4.Header{ + Version: ipv4.Version, + Len: ipv4.HeaderLen, + TOS: i + 1, + TotalLen: ipv4.HeaderLen + len(wb), + TTL: i + 1, + Protocol: 1, } + if src != nil { + wh.Src = src.(*net.IPAddr).IP + } + if dst != nil { + wh.Dst = dst.(*net.IPAddr).IP + } + c.SetDeadline(time.Now().Add(100 * time.Millisecond)) + if err := c.WriteTo(wh, wb, nil); err != nil { + t.Fatalf("ipv4.RawConn.WriteTo failed: %v", err) + } + rh, b, cm, err := c.ReadFrom(rb) + if err != nil { + t.Fatalf("ipv4.RawConn.ReadFrom failed: %v", err) + } + t.Logf("rcvd cmsg: %v", cm.String()) + t.Logf("rcvd hdr: %v", rh.String()) + return b } +// LoopbackInterface returns a logical network interface for loopback +// tests. func loopbackInterface() *net.Interface { ift, err := net.Interfaces() if err != nil { @@ -97,12 +90,13 @@ func loopbackInterface() *net.Interface { return nil } -func isGoodForMulticast(ifi *net.Interface) (net.IP, bool) { - if ifi.Flags&net.FlagUp == 0 { +// isMulticastAvailable returns true if ifi is a multicast access +// enabled network interface. It also returns a unicast IPv4 address +// that can be used for listening on ifi. +func isMulticastAvailable(ifi *net.Interface) (net.IP, bool) { + if ifi.Flags&net.FlagUp == 0 || ifi.Flags&net.FlagMulticast == 0 { return nil, false } - // We need a unicast IPv4 address that can be used to specify - // the IPv4 multicast interface. ifat, err := ifi.Addrs() if err != nil { return nil, false @@ -126,8 +120,5 @@ func isGoodForMulticast(ifi *net.Interface) (net.IP, bool) { } break } - if ip == nil { - return nil, false - } return ip, true } diff --git a/ipv4/multicast_test.go b/ipv4/multicast_test.go index 01cc2a13..a5c470de 100644 --- a/ipv4/multicast_test.go +++ b/ipv4/multicast_test.go @@ -45,7 +45,13 @@ func TestReadWriteMulticastIPPayloadUDP(t *testing.T) { if err := p.SetMulticastLoopback(true); err != nil { t.Fatalf("ipv4.PacketConn.SetMulticastLoopback failed: %v", err) } - runPayloadTransponder(t, p, []byte("HELLO-R-U-THERE"), dst) + cf := ipv4.FlagTTL | ipv4.FlagDst | ipv4.FlagInterface + for i, toggle := range []bool{true, false, true} { + if err := p.SetControlMessage(cf, toggle); err != nil { + t.Fatalf("ipv4.PacketConn.SetControlMessage failed: %v", err) + } + writeThenReadPayload(t, i, p, []byte("HELLO-R-U-THERE"), dst) + } } func TestReadWriteMulticastIPPayloadICMP(t *testing.T) { @@ -81,9 +87,30 @@ func TestReadWriteMulticastIPPayloadICMP(t *testing.T) { if err := p.SetMulticastInterface(ifi); err != nil { t.Fatalf("ipv4.PacketConn.SetMulticastInterface failed: %v", err) } - id := os.Getpid() & 0xffff - pld := newICMPEchoRequest(id, 1, 128, []byte("HELLO-R-U-THERE")) - runPayloadTransponder(t, p, pld, dst) + cf := ipv4.FlagTTL | ipv4.FlagDst | ipv4.FlagInterface + for i, toggle := range []bool{true, false, true} { + wb, err := (&icmpMessage{ + Type: icmpv4EchoRequest, Code: 0, + Body: &icmpEcho{ + ID: os.Getpid() & 0xffff, Seq: i + 1, + Data: []byte("HELLO-R-U-THERE"), + }, + }).Marshal() + if err != nil { + t.Fatalf("icmpMessage.Marshal failed: %v", err) + } + if err := p.SetControlMessage(cf, toggle); err != nil { + t.Fatalf("ipv4.PacketConn.SetControlMessage failed: %v", err) + } + rb := writeThenReadPayload(t, i, p, wb, dst) + m, err := parseICMPMessage(rb) + if err != nil { + t.Fatalf("parseICMPMessage failed: %v", err) + } + if m.Type != icmpv4EchoReply || m.Code != 0 { + t.Fatalf("got type=%v, code=%v; expected type=%v, code=%v", m.Type, m.Code, icmpv4EchoReply, 0) + } + } } func TestReadWriteMulticastIPDatagram(t *testing.T) { @@ -122,7 +149,28 @@ func TestReadWriteMulticastIPDatagram(t *testing.T) { if err := r.SetMulticastInterface(ifi); err != nil { t.Fatalf("ipv4.PacketConn.SetMulticastInterface failed: %v", err) } - id := os.Getpid() & 0xffff - pld := newICMPEchoRequest(id, 1, 128, []byte("HELLO-R-U-THERE")) - runDatagramTransponder(t, r, pld, nil, dst) + cf := ipv4.FlagTTL | ipv4.FlagDst | ipv4.FlagInterface + for i, toggle := range []bool{true, false, true} { + wb, err := (&icmpMessage{ + Type: icmpv4EchoRequest, Code: 0, + Body: &icmpEcho{ + ID: os.Getpid() & 0xffff, Seq: i + 1, + Data: []byte("HELLO-R-U-THERE"), + }, + }).Marshal() + if err != nil { + t.Fatalf("icmpMessage.Marshal failed: %v", err) + } + if err := r.SetControlMessage(cf, toggle); err != nil { + t.Fatalf("ipv4.RawConn.SetControlMessage failed: %v", err) + } + rb := writeThenReadDatagram(t, i, r, wb, nil, dst) + m, err := parseICMPMessage(rb) + if err != nil { + t.Fatalf("parseICMPMessage failed: %v", err) + } + if m.Type != icmpv4EchoReply || m.Code != 0 { + t.Fatalf("got type=%v, code=%v; expected type=%v, code=%v", m.Type, m.Code, icmpv4EchoReply, 0) + } + } } diff --git a/ipv4/multicastlistener_test.go b/ipv4/multicastlistener_test.go index 4199c056..2ece9a7f 100644 --- a/ipv4/multicastlistener_test.go +++ b/ipv4/multicastlistener_test.go @@ -43,7 +43,7 @@ func TestUDPSingleConnWithMultipleGroupListeners(t *testing.T) { t.Fatalf("net.Interfaces failed: %v", err) } for i, ifi := range ift { - if _, ok := isGoodForMulticast(&ifi); !ok { + if _, ok := isMulticastAvailable(&ifi); !ok { continue } if err := p.JoinGroup(&ifi, tt.gaddr); err != nil { @@ -90,7 +90,7 @@ func TestUDPMultipleConnWithMultipleGroupListeners(t *testing.T) { t.Fatalf("net.Interfaces failed: %v", err) } for i, ifi := range ift { - if _, ok := isGoodForMulticast(&ifi); !ok { + if _, ok := isMulticastAvailable(&ifi); !ok { continue } for _, p := range ps { @@ -139,7 +139,7 @@ func TestIPSingleConnWithSingleGroupListener(t *testing.T) { t.Fatalf("net.Interfaces failed: %v", err) } for i, ifi := range ift { - if _, ok := isGoodForMulticast(&ifi); !ok { + if _, ok := isMulticastAvailable(&ifi); !ok { continue } if err := r.JoinGroup(&ifi, gaddr); err != nil { @@ -172,7 +172,7 @@ func TestUDPPerInterfaceSingleConnWithSingleGroupListener(t *testing.T) { t.Fatalf("net.Interfaces failed: %v", err) } for i, ifi := range ift { - ip, ok := isGoodForMulticast(&ifi) + ip, ok := isMulticastAvailable(&ifi) if !ok { continue } @@ -217,7 +217,7 @@ func TestIPPerInterfaceSingleConnWithSingleGroupListener(t *testing.T) { t.Fatalf("net.Interfaces failed: %v", err) } for i, ifi := range ift { - ip, ok := isGoodForMulticast(&ifi) + ip, ok := isMulticastAvailable(&ifi) if !ok { continue } diff --git a/ipv4/payload.go b/ipv4/payload.go index 9b3c2c9b..7580d788 100644 --- a/ipv4/payload.go +++ b/ipv4/payload.go @@ -33,11 +33,11 @@ func (c *payloadHandler) ReadFrom(b []byte) (n int, cm *ControlMessage, src net. return 0, nil, nil, err } case *net.IPConn: - nb := make([]byte, len(b)+maxHeaderLen) + nb := make([]byte, maxHeaderLen+len(b)) if n, oobn, _, src, err = rd.ReadMsgIP(nb, oob); err != nil { return 0, nil, nil, err } - hdrlen := (int(b[0]) & 0x0f) << 2 + hdrlen := int(nb[0]&0x0f) << 2 copy(b, nb[hdrlen:]) n -= hdrlen default: diff --git a/ipv4/unicast_test.go b/ipv4/unicast_test.go index 3b905a04..bddfb971 100644 --- a/ipv4/unicast_test.go +++ b/ipv4/unicast_test.go @@ -24,9 +24,14 @@ func TestReadWriteUnicastIPPayloadUDP(t *testing.T) { if err != nil { t.Fatalf("net.ResolveUDPAddr failed: %v", err) } - p := ipv4.NewPacketConn(c) - runPayloadTransponder(t, p, []byte("HELLO-R-U-THERE"), dst) + cf := ipv4.FlagTTL | ipv4.FlagDst | ipv4.FlagInterface + for i, toggle := range []bool{true, false, true} { + if err := p.SetControlMessage(cf, toggle); err != nil { + t.Fatalf("ipv4.PacketConn.SetControlMessage failed: %v", err) + } + writeThenReadPayload(t, i, p, []byte("HELLO-R-U-THERE"), dst) + } } func TestReadWriteUnicastIPPayloadICMP(t *testing.T) { @@ -45,11 +50,31 @@ func TestReadWriteUnicastIPPayloadICMP(t *testing.T) { if err != nil { t.Fatalf("ResolveIPAddr failed: %v", err) } - p := ipv4.NewPacketConn(c) - id := os.Getpid() & 0xffff - pld := newICMPEchoRequest(id, 1, 128, []byte("HELLO-R-U-THERE")) - runPayloadTransponder(t, p, pld, dst) + cf := ipv4.FlagTTL | ipv4.FlagDst | ipv4.FlagInterface + for i, toggle := range []bool{true, false, true} { + wb, err := (&icmpMessage{ + Type: icmpv4EchoRequest, Code: 0, + Body: &icmpEcho{ + ID: os.Getpid() & 0xffff, Seq: i + 1, + Data: []byte("HELLO-R-U-THERE"), + }, + }).Marshal() + if err != nil { + t.Fatalf("icmpMessage.Marshal failed: %v", err) + } + if err := p.SetControlMessage(cf, toggle); err != nil { + t.Fatalf("ipv4.PacketConn.SetControlMessage failed: %v", err) + } + rb := writeThenReadPayload(t, i, p, wb, dst) + m, err := parseICMPMessage(rb) + if err != nil { + t.Fatalf("parseICMPMessage failed: %v", err) + } + if m.Type != icmpv4EchoReply || m.Code != 0 { + t.Fatalf("got type=%v, code=%v; expected type=%v, code=%v", m.Type, m.Code, icmpv4EchoReply, 0) + } + } } func TestReadWriteUnicastIPDatagram(t *testing.T) { @@ -68,12 +93,32 @@ func TestReadWriteUnicastIPDatagram(t *testing.T) { if err != nil { t.Fatalf("ResolveIPAddr failed: %v", err) } - r, err := ipv4.NewRawConn(c) if err != nil { t.Fatalf("ipv4.NewRawConn failed: %v", err) } - id := os.Getpid() & 0xffff - pld := newICMPEchoRequest(id, 1, 128, []byte("HELLO-R-U-THERE")) - runDatagramTransponder(t, r, pld, nil, dst) + cf := ipv4.FlagTTL | ipv4.FlagDst | ipv4.FlagInterface + for i, toggle := range []bool{true, false, true} { + wb, err := (&icmpMessage{ + Type: icmpv4EchoRequest, Code: 0, + Body: &icmpEcho{ + ID: os.Getpid() & 0xffff, Seq: i + 1, + Data: []byte("HELLO-R-U-THERE"), + }, + }).Marshal() + if err != nil { + t.Fatalf("icmpMessage.Marshal failed: %v", err) + } + if err := r.SetControlMessage(cf, toggle); err != nil { + t.Fatalf("ipv4.RawConn.SetControlMessage failed: %v", err) + } + rb := writeThenReadDatagram(t, i, r, wb, nil, dst) + m, err := parseICMPMessage(rb) + if err != nil { + t.Fatalf("parseICMPMessage failed: %v", err) + } + if m.Type != icmpv4EchoReply || m.Code != 0 { + t.Fatalf("got type=%v, code=%v; expected type=%v, code=%v", m.Type, m.Code, icmpv4EchoReply, 0) + } + } }