From 9b0a1b65840d1ac2392ba2fc3ed8c371209e8aad Mon Sep 17 00:00:00 2001 From: tannevaled Date: Tue, 7 Jul 2026 00:02:35 +0200 Subject: [PATCH] feat(socket): Addrinfo + Socket.getaddrinfo + sockaddr pack/unpack Extend rbgo's socket transport (require "socket") with the name-resolution and address utilities the connected/datagram socket work (#173/#192) deferred, all backed by Go's net package (no cgo): - Socket.getaddrinfo(host, port[, family, socktype, protocol, flags]) -> MRI's [afamily, port, host, addr, pfamily, socktype, protocol] tuples, resolved through net.Resolver with no reverse lookup (hermetic). - Addrinfo value class: .tcp / .udp / .ip / .getaddrinfo / .new (packed sockaddr String or [afamily, port, host, addr] array), plus ip_address / ip_port / afamily / pfamily / socktype / protocol / ipv4? / ipv6? / to_sockaddr / inspect. - sockaddr pack/unpack: Socket.pack_sockaddr_in / sockaddr_in / unpack_sockaddr_in (IPv4 + IPv6) and pack_sockaddr_un / sockaddr_un / unpack_sockaddr_un (AF_UNIX, non-Windows; NotImplementedError stubs on Windows, mirroring the UNIXSocket transport). The sockaddr layout is the Linux one (no BSD sa_len byte): the family field is host native byte order (a C sa_family_t) while port and address are network byte order, so the [port, host] round-trip is byte-exact on every arch. Verified on big-endian s390x under emulation. Tests exercise the whole surface through the rbgo engine against loopback-resolvable inputs (127.0.0.1 / ::1 / localhost); the resolver and service-name port lookup are driven through injectable seams (resolveIPs / lookupPort) so the suite never touches live DNS. 100% coverage of the new code, including every error and platform arm. Co-Authored-By: Claude Opus 4.8 --- internal/vm/object_model.go | 2 + internal/vm/socket.go | 9 +- internal/vm/socket_addrinfo.go | 543 +++++++++++++++++++++++++ internal/vm/socket_addrinfo_test.go | 320 +++++++++++++++ internal/vm/socket_addrinfo_unix.go | 65 +++ internal/vm/socket_addrinfo_windows.go | 29 ++ 6 files changed, 966 insertions(+), 2 deletions(-) create mode 100644 internal/vm/socket_addrinfo.go create mode 100644 internal/vm/socket_addrinfo_test.go create mode 100644 internal/vm/socket_addrinfo_unix.go create mode 100644 internal/vm/socket_addrinfo_windows.go diff --git a/internal/vm/object_model.go b/internal/vm/object_model.go index 8b434a8a..a6e8d846 100644 --- a/internal/vm/object_model.go +++ b/internal/vm/object_model.go @@ -1212,6 +1212,8 @@ func (vm *VM) classOf(v object.Value) *RClass { return x.cls case *sslServer: return x.cls + case *addrinfo: + return x.cls case *opensslDigest: return x.cls case *DigestObj: diff --git a/internal/vm/socket.go b/internal/vm/socket.go index 9b8fd9b1..ba2b8d6f 100644 --- a/internal/vm/socket.go +++ b/internal/vm/socket.go @@ -26,8 +26,10 @@ import ( // instance methods assume an *IOObj receiver and would panic on a socket value, // and the socket surface below re-implements the IO methods that matter // (read/gets/write/puts/...) directly. UDPSocket (socket_udp.go) and -// UNIXSocket/UNIXServer (socket_unix.go) extend this ancestry; raw Socket.new -// and full Addrinfo are deferred follow-ups. +// UNIXSocket/UNIXServer (socket_unix.go) extend this ancestry; the name- +// resolution + address utilities (Socket.getaddrinfo, the Addrinfo value class, +// and the sockaddr pack/unpack helpers) live in socket_addrinfo.go. Raw +// Socket.new remains a deferred follow-up. // tcpSocket is a connected stream socket (TCPSocket): a live net.Conn plus a // buffered reader so gets / readpartial / eof? can peek without losing bytes. @@ -83,6 +85,9 @@ func (vm *VM) registerSocket() { vm.registerUDPSocket(ip) vm.registerUnixSockets(basic) vm.registerSocketClass(basic) + // The address utilities (Socket.getaddrinfo, Addrinfo, sockaddr pack/unpack) + // build on the Socket class registered just above (socket_addrinfo.go). + vm.registerSocketAddr() // Upgrade the OpenSSL::SSL TLS shell to a real transport (socket_bind.go). vm.registerSSLTransport() diff --git a/internal/vm/socket_addrinfo.go b/internal/vm/socket_addrinfo.go new file mode 100644 index 00000000..7e170b53 --- /dev/null +++ b/internal/vm/socket_addrinfo.go @@ -0,0 +1,543 @@ +// Copyright (c) the go-embedded-ruby/ruby authors +// +// SPDX-License-Identifier: BSD-3-Clause + +package vm + +import ( + "context" + binpkg "encoding/binary" + "net" + "strconv" + + "github.com/go-embedded-ruby/ruby/internal/object" +) + +// This file is the name-resolution + address-utility half of rbgo's socket +// transport, the follow-up the connected/datagram socket work (socket.go / +// socket_udp.go / socket_unix.go) deferred: Socket.getaddrinfo, the Addrinfo +// value class, and the sockaddr pack/unpack helpers. All of it is backed by Go's +// net package (net.Resolver for lookups) and pure byte manipulation for the +// sockaddr layout — no cgo, no live network beyond the resolver the caller asks +// for. +// +// Endianness note: the sockaddr byte layout modelled here is the Linux one (no +// BSD sa_len byte). sin_family / sin6_family are written in the host's native +// byte order (a C sa_family_t is a host-endian short), while sin_port and the +// address bytes are network byte order (big-endian) regardless of host — exactly +// the C semantics. pack and unpack use the same native order for the family +// field, so the [port, host] round-trip is byte-exact on every arch, including +// big-endian s390x (where the family short lands as 00 02 rather than 02 00). + +// Address-family / socket-type / protocol numbers rbgo uses across the address +// utilities. They match the Socket:: constants registered in socket.go, so a +// tuple's family integer (2 / 30) and an Addrinfo#afamily agree with what a +// script reads from Socket::AF_INET etc. v4-vs-v6 is decided by sockaddr length, +// not this number, so the exact AF_INET6 value (BSD 30 here) never affects a +// round-trip. +const ( + afINET = 2 + afINET6 = 30 + afUNIX = 1 + sockStream = 1 + sockDgram = 2 + ipprotoTCP = 6 + ipprotoUDP = 17 +) + +// resolveIPs is the net.Resolver.LookupIP seam Socket.getaddrinfo / Addrinfo +// resolve a hostname through. It is a package var so a test can inject a +// resolution failure (a numeric-literal host never reaches the resolver, so its +// error arm has no natural trigger). network is "ip" / "ip4" / "ip6". +var resolveIPs = func(network, host string) ([]net.IP, error) { + return net.DefaultResolver.LookupIP(context.Background(), network, host) +} + +// lookupPort is the net.LookupPort seam a service-name port ("http") resolves +// through. It is a package var so a test can drive both its success and failure +// arms deterministically without depending on the host's /etc/services. +var lookupPort = net.LookupPort + +// addrinfo is a resolved address (MRI's Addrinfo): an address family plus the +// numeric IP, port, and the socket-type / protocol that qualify it (0 when the +// Addrinfo is a bare IP). It is a value object — construction resolves the host, +// the readers below just report the stored fields. +type addrinfo struct { + cls *RClass + afamily int + ip string + port int + socktype int + protocol int +} + +func (a *addrinfo) ToS() string { return a.inspect() } +func (a *addrinfo) Inspect() string { return a.inspect() } +func (a *addrinfo) Truthy() bool { return true } + +// inspect renders the MRI Addrinfo#inspect form: "#" for a bare +// IP, "#" with a " TCP" / " UDP" suffix when a protocol +// qualifies it. An IPv6 host is bracketed ("[::1]:443"). +func (a *addrinfo) inspect() string { + host := a.ip + if a.afamily == afINET6 { + host = "[" + a.ip + "]" + } + s := "#" +} + +// registerSocketAddr installs the address-utility surface on the Socket class +// (Socket.getaddrinfo + the sockaddr pack/unpack methods) and the Addrinfo value +// class. It runs from registerSocket after registerSocketClass has published the +// Socket constant. +func (vm *VM) registerSocketAddr() { + sock := vm.consts["Socket"].(*RClass) + + // Socket.getaddrinfo(host, port [, family, socktype, protocol, flags]) → + // [[afamily_string, port, host, addr, pfamily, socktype, protocol], ...], + // MRI's tuple shape. host is the numeric address (no reverse lookup, keeping + // the result hermetic). + sock.smethods["getaddrinfo"] = &Method{name: "getaddrinfo", owner: sock, + native: func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value { + if len(args) < 2 { + raise("ArgumentError", "wrong number of arguments (given %d, expected 2..7)", len(args)) + } + list := resolveAddrinfo(sock, args) + tuples := make([]object.Value, len(list)) + for i, a := range list { + tuples[i] = object.NewArray( + object.NewString(afamilyName(a.afamily)), + object.IntValue(int64(a.port)), + object.NewString(a.ip), + object.NewString(a.ip), + object.IntValue(int64(a.afamily)), + object.IntValue(int64(a.socktype)), + object.IntValue(int64(a.protocol)), + ) + } + return object.NewArrayFromSlice(tuples) + }} + + // Socket.pack_sockaddr_in(port, host) / Socket.sockaddr_in(port, host) → the + // packed sockaddr_in / sockaddr_in6 bytes for host:port. + packIn := &Method{name: "pack_sockaddr_in", owner: sock, + native: func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value { + if len(args) < 2 { + raise("ArgumentError", "wrong number of arguments (given %d, expected 2)", len(args)) + } + return object.NewStringBytesEnc(packSockaddrIn(int(intArg(args[0])), strArg(args[1])), "ASCII-8BIT") + }} + sock.smethods["pack_sockaddr_in"] = packIn + sock.smethods["sockaddr_in"] = &Method{name: "sockaddr_in", owner: sock, native: packIn.native} + + // Socket.unpack_sockaddr_in(sockaddr) → [port, host]. A sockaddr that is + // neither AF_INET nor AF_INET6 raises ArgumentError, as MRI does. + sock.smethods["unpack_sockaddr_in"] = &Method{name: "unpack_sockaddr_in", owner: sock, + native: func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value { + if len(args) < 1 { + raise("ArgumentError", "wrong number of arguments (given %d, expected 1)", len(args)) + } + port, host := unpackSockaddrIn(sockaddrBytes(args[0])) + return object.NewArray(object.IntValue(int64(port)), object.NewString(host)) + }} + + // The AF_UNIX sockaddr helpers (pack_sockaddr_un / unpack_sockaddr_un) are + // platform-gated: real on non-Windows, NotImplementedError stubs on Windows + // (socket_addrinfo_unix.go / socket_addrinfo_windows.go), mirroring the + // UNIXSocket transport. + registerSockaddrUn(sock) + + vm.registerAddrinfo(sock) +} + +// registerAddrinfo installs the Addrinfo value class: the .tcp / .udp / .ip / +// .getaddrinfo / .new constructors and the ip_address / ip_port / afamily / +// pfamily / socktype / protocol / ipv4? / ipv6? / to_sockaddr / inspect readers. +func (vm *VM) registerAddrinfo(sock *RClass) { + cls := newClass("Addrinfo", vm.cObject) + vm.consts["Addrinfo"] = cls + + // Addrinfo.tcp(host, port) / Addrinfo.udp(host, port): an Addrinfo qualified + // by SOCK_STREAM+TCP / SOCK_DGRAM+UDP. + mkQualified := func(socktype, protocol int) NativeFn { + return func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value { + if len(args) < 2 { + raise("ArgumentError", "wrong number of arguments (given %d, expected 2)", len(args)) + } + ip, af := resolveOneIP("ip", strArg(args[0])) + return &addrinfo{cls: cls, afamily: af, ip: ip, port: int(intArg(args[1])), socktype: socktype, protocol: protocol} + } + } + cls.smethods["tcp"] = &Method{name: "tcp", owner: cls, native: mkQualified(sockStream, ipprotoTCP)} + cls.smethods["udp"] = &Method{name: "udp", owner: cls, native: mkQualified(sockDgram, ipprotoUDP)} + + // Addrinfo.ip(host): a bare-IP Addrinfo (no port, socktype 0, protocol 0). + cls.smethods["ip"] = &Method{name: "ip", owner: cls, + native: func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value { + if len(args) < 1 { + raise("ArgumentError", "wrong number of arguments (given %d, expected 1)", len(args)) + } + ip, af := resolveOneIP("ip", strArg(args[0])) + return &addrinfo{cls: cls, afamily: af, ip: ip} + }} + + // Addrinfo.getaddrinfo(host, port [, family, socktype, protocol, flags]) → + // an array of Addrinfo, the object form of Socket.getaddrinfo. + cls.smethods["getaddrinfo"] = &Method{name: "getaddrinfo", owner: cls, + native: func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value { + if len(args) < 2 { + raise("ArgumentError", "wrong number of arguments (given %d, expected 2..7)", len(args)) + } + list := resolveAddrinfo(cls, args) + out := make([]object.Value, len(list)) + for i := range list { + out[i] = list[i] + } + return object.NewArrayFromSlice(out) + }} + + // Addrinfo.new(sockaddr [, family, socktype, protocol]): sockaddr is either a + // packed sockaddr_in / sockaddr_in6 String or a [afamily, port, host, addr] + // array (MRI accepts both). The optional family/socktype/protocol override the + // derived ones. + cls.smethods["new"] = &Method{name: "new", owner: cls, + native: func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value { + if len(args) < 1 { + raise("ArgumentError", "wrong number of arguments (given %d, expected 1..4)", len(args)) + } + a := addrinfoFromSockaddr(cls, args[0]) + if len(args) > 1 && !object.IsNil(args[1]) { + a.afamily = familyNumber(args[1]) + } + if len(args) > 2 && !object.IsNil(args[2]) { + a.socktype = socktypeNumber(args[2]) + } + if len(args) > 3 && !object.IsNil(args[3]) { + a.protocol = int(intArg(args[3])) + } + return a + }} + + cls.define("ip_address", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value { + return object.NewString(asAddrinfo(self).ip) + }) + cls.define("ip_port", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value { + return object.IntValue(int64(asAddrinfo(self).port)) + }) + cls.define("afamily", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value { + return object.IntValue(int64(asAddrinfo(self).afamily)) + }) + // pfamily mirrors afamily for the IP families rbgo models (PF_INET == AF_INET). + cls.define("pfamily", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value { + return object.IntValue(int64(asAddrinfo(self).afamily)) + }) + cls.define("socktype", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value { + return object.IntValue(int64(asAddrinfo(self).socktype)) + }) + cls.define("protocol", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value { + return object.IntValue(int64(asAddrinfo(self).protocol)) + }) + cls.define("ipv4?", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value { + return object.Bool(asAddrinfo(self).afamily == afINET) + }) + cls.define("ipv6?", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value { + return object.Bool(asAddrinfo(self).afamily == afINET6) + }) + // to_sockaddr (alias to_s) returns the packed sockaddr bytes for this address. + toSockaddr := func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value { + a := asAddrinfo(self) + return object.NewStringBytesEnc(packSockaddrIn(a.port, a.ip), "ASCII-8BIT") + } + cls.define("to_sockaddr", toSockaddr) + cls.define("to_s", toSockaddr) + cls.define("inspect", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value { + return object.NewString(asAddrinfo(self).inspect()) + }) +} + +// resolveAddrinfo is the shared resolution engine behind Socket.getaddrinfo and +// Addrinfo.getaddrinfo: it resolves args[0] (host) to one or more IPs, honouring +// an optional family filter (args[2]) and socket-type (args[3]) / protocol +// (args[4]), and returns one addrinfo per resolved IP. args[1] is the port +// (Integer, numeric / service-name String, or nil). +func resolveAddrinfo(cls *RClass, args []object.Value) []*addrinfo { + network := "ip" + if len(args) > 2 && !object.IsNil(args[2]) { + switch familyNumber(args[2]) { + case afINET: + network = "ip4" + case afINET6: + network = "ip6" + } + } + socktype := 0 + if len(args) > 3 && !object.IsNil(args[3]) { + socktype = socktypeNumber(args[3]) + } + protocol := 0 + if len(args) > 4 && !object.IsNil(args[4]) { + protocol = int(intArg(args[4])) + } else { + switch socktype { + case sockStream: + protocol = ipprotoTCP + case sockDgram: + protocol = ipprotoUDP + } + } + port := resolvePort(args[1], socktype) + + ips := lookupHost(network, strArg(args[0])) + out := make([]*addrinfo, 0, len(ips)) + for _, ip := range ips { + out = append(out, &addrinfo{cls: cls, afamily: familyOf(ip), ip: ip.String(), port: port, socktype: socktype, protocol: protocol}) + } + return out +} + +// lookupHost resolves host to its IPs on the requested network ("ip" / "ip4" / +// "ip6"), short-circuiting a numeric literal through net.ParseIP (so it never +// touches the resolver) and filtering it to the requested family. A resolution +// failure raises SocketError, matching MRI's getaddrinfo error. +func lookupHost(network, host string) []net.IP { + if ip := net.ParseIP(host); ip != nil { + if network == "ip4" && ip.To4() == nil { + raise("SocketError", "getaddrinfo: Address family for hostname not supported") + } + if network == "ip6" && ip.To4() != nil { + raise("SocketError", "getaddrinfo: Address family for hostname not supported") + } + return []net.IP{ip} + } + ips, err := resolveIPs(network, host) + if err != nil { + raise("SocketError", "getaddrinfo: %s", err.Error()) + } + return ips +} + +// resolveOneIP resolves host to a single numeric IP + family for the singular +// Addrinfo constructors (.tcp / .udp / .ip), taking the first resolved address. +func resolveOneIP(network, host string) (string, int) { + ips := lookupHost(network, host) + if len(ips) == 0 { + raise("SocketError", "getaddrinfo: no address for %s", host) + } + return ips[0].String(), familyOf(ips[0]) +} + +// resolvePort resolves a port argument to an integer: an Integer is taken as-is, +// a numeric String is parsed, and a service-name String ("http") is looked up +// through net (the socktype selecting tcp / udp). nil is port 0. +func resolvePort(v object.Value, socktype int) int { + switch p := v.(type) { + case object.Integer: + return int(p) + case *object.String: + s := p.Str() + if n, err := strconv.Atoi(s); err == nil { + return n + } + proto := "tcp" + if socktype == sockDgram { + proto = "udp" + } + n, err := lookupPort(proto, s) + if err != nil { + raise("SocketError", "getaddrinfo: %s", err.Error()) + } + return n + default: + if object.IsNil(v) { + return 0 + } + raise("TypeError", "no implicit conversion of %s into Integer", v.Inspect()) + return 0 + } +} + +// familyOf reports the rbgo address-family number for a resolved IP. +func familyOf(ip net.IP) int { + if ip.To4() != nil { + return afINET + } + return afINET6 +} + +// familyNumber resolves a family argument (an Integer such as Socket::AF_INET, +// or a Symbol / String such as :INET / "AF_INET6") to the rbgo family number. +func familyNumber(v object.Value) int { + switch x := v.(type) { + case object.Integer: + // Accept both the BSD (30) and Linux (10) AF_INET6 spellings. + if int(x) == 10 { + return afINET6 + } + return int(x) + case object.Symbol: + return familyFromName(string(x)) + case *object.String: + return familyFromName(x.Str()) + default: + raise("TypeError", "invalid address family %s", v.Inspect()) + return 0 + } +} + +// familyFromName maps a family name (with or without the AF_ / PF_ prefix) to its +// number: INET → AF_INET, INET6 → AF_INET6, UNIX / LOCAL → AF_UNIX. +func familyFromName(name string) int { + switch name { + case "INET", "AF_INET", "PF_INET": + return afINET + case "INET6", "AF_INET6", "PF_INET6": + return afINET6 + case "UNIX", "LOCAL", "AF_UNIX": + return afUNIX + default: + raise("SocketError", "getaddrinfo: unknown address family %q", name) + return 0 + } +} + +// socktypeNumber resolves a socket-type argument (an Integer such as +// Socket::SOCK_STREAM, or a Symbol / String such as :STREAM) to its number. +func socktypeNumber(v object.Value) int { + switch x := v.(type) { + case object.Integer: + return int(x) + case object.Symbol: + return socktypeFromName(string(x)) + case *object.String: + return socktypeFromName(x.Str()) + default: + raise("TypeError", "invalid socktype %s", v.Inspect()) + return 0 + } +} + +// socktypeFromName maps a socket-type name (with or without the SOCK_ prefix) to +// its number: STREAM → SOCK_STREAM, DGRAM → SOCK_DGRAM. +func socktypeFromName(name string) int { + switch name { + case "STREAM", "SOCK_STREAM": + return sockStream + case "DGRAM", "SOCK_DGRAM": + return sockDgram + default: + raise("SocketError", "getaddrinfo: unknown socket type %q", name) + return 0 + } +} + +// afamilyName renders an address-family number as its MRI string ("AF_INET" / +// "AF_INET6" / "AF_UNIX"), the first element of a getaddrinfo tuple. +func afamilyName(af int) string { + switch af { + case afINET: + return "AF_INET" + case afINET6: + return "AF_INET6" + case afUNIX: + return "AF_UNIX" + default: + return "AF_UNSPEC" + } +} + +// packSockaddrIn packs host:port as a sockaddr_in (IPv4) or sockaddr_in6 (IPv6). +// The family field is native byte order; the port and address are network byte +// order. An unresolvable host raises SocketError. +func packSockaddrIn(port int, host string) []byte { + ip := net.ParseIP(host) + if ip == nil { + ips, err := resolveIPs("ip", host) + if err != nil || len(ips) == 0 { + raise("SocketError", "getaddrinfo: %s", host) + } + ip = ips[0] + } + if v4 := ip.To4(); v4 != nil { + buf := make([]byte, 16) + binpkg.NativeEndian.PutUint16(buf[0:2], uint16(afINET)) + binpkg.BigEndian.PutUint16(buf[2:4], uint16(port)) + copy(buf[4:8], v4) + return buf + } + buf := make([]byte, 28) + binpkg.NativeEndian.PutUint16(buf[0:2], uint16(afINET6)) + binpkg.BigEndian.PutUint16(buf[2:4], uint16(port)) + copy(buf[8:24], ip.To16()) + return buf +} + +// unpackSockaddrIn reads a packed sockaddr_in / sockaddr_in6 back into its port +// and numeric host. The v4-vs-v6 shape is decided by length (16 vs 28); any +// other length raises ArgumentError, as MRI does for a non-AF_INET sockaddr. +func unpackSockaddrIn(sa []byte) (int, string) { + switch len(sa) { + case 16: + port := binpkg.BigEndian.Uint16(sa[2:4]) + ip := net.IP(sa[4:8]) + return int(port), ip.String() + case 28: + port := binpkg.BigEndian.Uint16(sa[2:4]) + ip := net.IP(sa[8:24]) + return int(port), ip.String() + default: + raise("ArgumentError", "not an AF_INET/AF_INET6 sockaddr") + return 0, "" + } +} + +// addrinfoFromSockaddr builds an Addrinfo from the argument to Addrinfo.new: +// either a packed sockaddr String or a [afamily, port, host, addr] array. +func addrinfoFromSockaddr(cls *RClass, v object.Value) *addrinfo { + if s, ok := v.(*object.String); ok { + port, host := unpackSockaddrIn(s.Bytes()) + af := afINET + if len(s.Bytes()) == 28 { + af = afINET6 + } + return &addrinfo{cls: cls, afamily: af, ip: host, port: port} + } + if arr, ok := v.(*object.Array); ok { + if len(arr.Elems) < 4 { + raise("ArgumentError", "array address must have 4 elements [afamily, port, host, addr]") + } + af := familyNumber(arr.Elems[0]) + return &addrinfo{cls: cls, afamily: af, ip: strArg(arr.Elems[3]), port: int(intArg(arr.Elems[1]))} + } + raise("TypeError", "expected a packed sockaddr String or an address Array") + return nil +} + +// sockaddrBytes extracts the raw bytes of a sockaddr String argument, raising +// TypeError for a non-String (matching MRI's implicit-conversion error). +func sockaddrBytes(v object.Value) []byte { + if s, ok := v.(*object.String); ok { + return s.Bytes() + } + raise("TypeError", "no implicit conversion of %s into String", v.Inspect()) + return nil +} + +// asAddrinfo narrows a receiver to *addrinfo, raising TypeError otherwise so a +// mis-typed self surfaces as a Ruby error rather than a Go panic. +func asAddrinfo(v object.Value) *addrinfo { + if a, ok := v.(*addrinfo); ok { + return a + } + raise("TypeError", "not an Addrinfo") + return nil +} diff --git a/internal/vm/socket_addrinfo_test.go b/internal/vm/socket_addrinfo_test.go new file mode 100644 index 00000000..90fddf77 --- /dev/null +++ b/internal/vm/socket_addrinfo_test.go @@ -0,0 +1,320 @@ +// Copyright (c) the go-embedded-ruby/ruby authors +// +// SPDX-License-Identifier: BSD-3-Clause + +package vm + +import ( + binpkg "encoding/binary" + "fmt" + "net" + "runtime" + "testing" + + "github.com/go-embedded-ruby/ruby/internal/object" +) + +// The address utilities (Socket.getaddrinfo, the Addrinfo value class, and the +// sockaddr pack/unpack helpers) are proven through the rbgo engine against +// hermetic, loopback-resolvable inputs (127.0.0.1 / ::1 / localhost). The name +// resolver and the service-name port lookup are reached only for non-numeric +// inputs; their success and failure arms are driven deterministically through +// the resolveIPs / lookupPort seams so the suite never depends on live DNS. + +// TestGetaddrinfoLoopback covers Socket.getaddrinfo's tuple shape and its +// family / socktype / protocol arguments against loopback inputs. +func TestGetaddrinfoLoopback(t *testing.T) { + cases := []struct{ src, want string }{ + // Numeric IPv4 + explicit family/socktype: one AF_INET STREAM/TCP tuple. + {`p Socket.getaddrinfo("127.0.0.1", 80, Socket::AF_INET, Socket::SOCK_STREAM)`, + `[["AF_INET", 80, "127.0.0.1", "127.0.0.1", 2, 1, 6]]`}, + // SOCK_DGRAM defaults the protocol to IPPROTO_UDP. + {`p Socket.getaddrinfo("127.0.0.1", 53, Socket::AF_INET, Socket::SOCK_DGRAM)`, + `[["AF_INET", 53, "127.0.0.1", "127.0.0.1", 2, 2, 17]]`}, + // No socktype -> socktype 0, protocol 0. + {`p Socket.getaddrinfo("127.0.0.1", 80, Socket::AF_INET)`, + `[["AF_INET", 80, "127.0.0.1", "127.0.0.1", 2, 0, 0]]`}, + // An explicit protocol argument overrides the socktype-derived default. + {`p Socket.getaddrinfo("127.0.0.1", 80, Socket::AF_INET, Socket::SOCK_STREAM, Socket::IPPROTO_TCP)`, + `[["AF_INET", 80, "127.0.0.1", "127.0.0.1", 2, 1, 6]]`}, + // The AF_INET filter over the localhost lookup yields just the v4 loopback. + {`p Socket.getaddrinfo("localhost", 80, Socket::AF_INET, Socket::SOCK_STREAM)`, + `[["AF_INET", 80, "127.0.0.1", "127.0.0.1", 2, 1, 6]]`}, + // A numeric IPv6 literal with the AF_INET6 filter. + {`p Socket.getaddrinfo("::1", 443, Socket::AF_INET6, Socket::SOCK_STREAM)`, + `[["AF_INET6", 443, "::1", "::1", 30, 1, 6]]`}, + // A String port that names a numeric value is parsed. + {`p Socket.getaddrinfo("127.0.0.1", "8080", Socket::AF_INET, Socket::SOCK_STREAM)`, + `[["AF_INET", 8080, "127.0.0.1", "127.0.0.1", 2, 1, 6]]`}, + // A nil port is port 0. + {`p Socket.getaddrinfo("127.0.0.1", nil, Socket::AF_INET)`, + `[["AF_INET", 0, "127.0.0.1", "127.0.0.1", 2, 0, 0]]`}, + } + for _, c := range cases { + if got := runSrc(t, "require \"socket\"\n"+c.src); got != c.want { + t.Errorf("src=%q\n got=%q\nwant=%q", c.src, got, c.want) + } + } +} + +// TestAddrinfoSurface covers the Addrinfo constructors and instance readers +// through the engine against loopback inputs. +func TestAddrinfoSurface(t *testing.T) { + cases := []struct{ src, want string }{ + // tcp: the full reader surface. + {`a=Addrinfo.tcp("127.0.0.1", 8080) +p [a.ip_address, a.ip_port, a.afamily, a.pfamily, a.socktype, a.protocol, a.ipv4?, a.ipv6?]`, + `["127.0.0.1", 8080, 2, 2, 1, 6, true, false]`}, + // tcp / udp / ip inspect forms (v4). + {`p Addrinfo.tcp("127.0.0.1", 8080).inspect`, `"#"`}, + {`p Addrinfo.udp("127.0.0.1", 53).inspect`, `"#"`}, + {`p Addrinfo.ip("127.0.0.1").inspect`, `"#"`}, + // A bare-IP Addrinfo has port 0 and socktype/protocol 0. + {`a=Addrinfo.ip("127.0.0.1"); p [a.ip_port, a.socktype, a.protocol]`, `[0, 0, 0]`}, + // v6 inspect brackets the host, and ipv6? flips. + {`a=Addrinfo.tcp("::1", 443); p a.inspect; p [a.ipv4?, a.ipv6?, a.afamily]`, + "\"#\"\n[false, true, 30]"}, + // to_sockaddr / to_s return the same packed bytes; a round-trip recovers it. + {`a=Addrinfo.tcp("1.2.3.4", 443); p(a.to_sockaddr == a.to_s) +p Socket.unpack_sockaddr_in(a.to_sockaddr)`, "true\n[443, \"1.2.3.4\"]"}, + // getaddrinfo yields Addrinfo objects. + {`p Addrinfo.getaddrinfo("127.0.0.1", 80, Socket::AF_INET, Socket::SOCK_STREAM).map(&:inspect)`, + `["#"]`}, + // getaddrinfo accepts Symbol family / socktype. + {`p Addrinfo.getaddrinfo("127.0.0.1", 80, :INET, :STREAM).map(&:inspect)`, + `["#"]`}, + // new(packed sockaddr) reconstructs the address. + {`b=Addrinfo.new(Socket.pack_sockaddr_in(80, "127.0.0.1")) +p [b.ip_address, b.ip_port, b.afamily]`, `["127.0.0.1", 80, 2]`}, + // new(packed v6 sockaddr) sets AF_INET6. + {`b=Addrinfo.new(Socket.pack_sockaddr_in(443, "::1")); p [b.ip_address, b.ip_port, b.afamily]`, + `["::1", 443, 30]`}, + // new([afamily, port, host, addr]) accepts the array address form. + {`b=Addrinfo.new(["AF_INET", 25, "127.0.0.1", "127.0.0.1"]) +p [b.ip_address, b.ip_port, b.afamily]`, `["127.0.0.1", 25, 2]`}, + // new(sockaddr, family, socktype, protocol) overrides the derived fields. + {`b=Addrinfo.new(Socket.pack_sockaddr_in(80, "127.0.0.1"), Socket::AF_INET, Socket::SOCK_STREAM, Socket::IPPROTO_TCP) +p [b.socktype, b.protocol]; p b.inspect`, "[1, 6]\n\"#\""}, + // value protocol: inspect / to truthiness. + {`a=Addrinfo.tcp("127.0.0.1", 1); p a; p(!!a)`, "#\ntrue"}, + } + for _, c := range cases { + if got := runSrc(t, "require \"socket\"\n"+c.src); got != c.want { + t.Errorf("src=%q\n got=%q\nwant=%q", c.src, got, c.want) + } + } +} + +// TestSockaddrPackUnpack is the byte-exact round-trip proof for the sockaddr +// helpers, asserting the concrete IPv4 / IPv6 layout as well as the round-trip. +func TestSockaddrPackUnpack(t *testing.T) { + cases := []struct{ src, want string }{ + // The headline round-trip. + {`p(Socket.unpack_sockaddr_in(Socket.pack_sockaddr_in(443, "1.2.3.4")) == [443, "1.2.3.4"])`, "true"}, + {`p Socket.unpack_sockaddr_in(Socket.pack_sockaddr_in(443, "1.2.3.4"))`, `[443, "1.2.3.4"]`}, + // sockaddr_in is the alias for pack_sockaddr_in. + {`p(Socket.sockaddr_in(80, "127.0.0.1") == Socket.pack_sockaddr_in(80, "127.0.0.1"))`, "true"}, + // The IPv4 packed form is 16 bytes; port is network order (443 -> 1,187), + // address is the natural dotted order. + {`p Socket.pack_sockaddr_in(443, "1.2.3.4").bytes[2..7]`, `[1, 187, 1, 2, 3, 4]`}, + {`p Socket.pack_sockaddr_in(443, "1.2.3.4").bytesize`, "16"}, + // The IPv6 packed form is 28 bytes and round-trips. + {`p Socket.pack_sockaddr_in(8080, "::1").bytesize`, "28"}, + {`p Socket.unpack_sockaddr_in(Socket.pack_sockaddr_in(8080, "::1"))`, `[8080, "::1"]`}, + } + for _, c := range cases { + if got := runSrc(t, "require \"socket\"\n"+c.src); got != c.want { + t.Errorf("src=%q\n got=%q\nwant=%q", c.src, got, c.want) + } + } + + // The family field is the host's native byte order (a C sa_family_t), while + // the port is always network order — verified against the running arch's + // endianness so the s390x big-endian lane checks the same invariant. + sa := packSockaddrIn(443, "1.2.3.4") + var fam [2]byte + binpkg.NativeEndian.PutUint16(fam[:], uint16(afINET)) + if sa[0] != fam[0] || sa[1] != fam[1] { + t.Errorf("family bytes = %v, want native-order %v", sa[0:2], fam) + } + if got := binpkg.BigEndian.Uint16(sa[2:4]); got != 443 { + t.Errorf("port (network order) = %d, want 443", got) + } +} + +// TestSockaddrUnPackUnpack covers the AF_UNIX sockaddr helpers on non-Windows. +func TestSockaddrUnPackUnpack(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("AF_UNIX unsupported on Windows") + } + cases := []struct{ src, want string }{ + {`p Socket.unpack_sockaddr_un(Socket.pack_sockaddr_un("/tmp/rbgo.sock"))`, `"/tmp/rbgo.sock"`}, + // sockaddr_un is the alias for pack_sockaddr_un. + {`p(Socket.sockaddr_un("/x") == Socket.pack_sockaddr_un("/x"))`, "true"}, + // The packed form is family (2 bytes) + sun_path (108 bytes) = 110. + {`p Socket.pack_sockaddr_un("/x").bytesize`, "110"}, + // Arity guards on both helpers. + {`begin; Socket.pack_sockaddr_un; rescue ArgumentError; puts "packarity"; end`, "packarity"}, + {`begin; Socket.unpack_sockaddr_un; rescue ArgumentError; puts "unarity"; end`, "unarity"}, + // A path that overflows sun_path is rejected. + {`begin; Socket.pack_sockaddr_un("/" + ("x" * 200)); rescue ArgumentError; puts "toolong"; end`, "toolong"}, + // A too-short sockaddr (no family field) is rejected. + {`begin; Socket.unpack_sockaddr_un("x"); rescue ArgumentError; puts "tooshort"; end`, "tooshort"}, + } + for _, c := range cases { + if got := runSrc(t, "require \"socket\"\n"+c.src); got != c.want { + t.Errorf("src=%q\n got=%q\nwant=%q", c.src, got, c.want) + } + } +} + +// TestSockaddrUnWindowsStub confirms the AF_UNIX sockaddr helpers raise a clean, +// rescuable NotImplementedError on Windows (the byte layout is not compiled there). +func TestSockaddrUnWindowsStub(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("Windows-only AF_UNIX stub") + } + cases := []struct{ src, want string }{ + {`begin; Socket.pack_sockaddr_un("/x"); rescue NotImplementedError; puts "pack"; end`, "pack"}, + {`begin; Socket.sockaddr_un("/x"); rescue NotImplementedError; puts "alias"; end`, "alias"}, + {`begin; Socket.unpack_sockaddr_un("x"); rescue NotImplementedError; puts "unpack"; end`, "unpack"}, + } + for _, c := range cases { + if got := runSrc(t, "require \"socket\"\n"+c.src); got != c.want { + t.Errorf("src=%q got=%q want=%q", c.src, got, c.want) + } + } +} + +// TestAddrinfoErrors covers the raising arms of the address surface through the +// engine: argument arities, bad family / socktype names, unpack of a malformed +// sockaddr, and the family/type guards. +func TestAddrinfoErrors(t *testing.T) { + cases := []struct{ src, want string }{ + // Arities. + {`begin; Socket.getaddrinfo("h"); rescue ArgumentError; puts "gai"; end`, "gai"}, + {`begin; Socket.pack_sockaddr_in(80); rescue ArgumentError; puts "pin"; end`, "pin"}, + {`begin; Socket.unpack_sockaddr_in; rescue ArgumentError; puts "uin"; end`, "uin"}, + {`begin; Addrinfo.tcp("127.0.0.1"); rescue ArgumentError; puts "tcp"; end`, "tcp"}, + {`begin; Addrinfo.udp("127.0.0.1"); rescue ArgumentError; puts "udp"; end`, "udp"}, + {`begin; Addrinfo.ip; rescue ArgumentError; puts "ip"; end`, "ip"}, + {`begin; Addrinfo.getaddrinfo("h"); rescue ArgumentError; puts "agai"; end`, "agai"}, + {`begin; Addrinfo.new; rescue ArgumentError; puts "new"; end`, "new"}, + // A numeric IPv4 literal filtered to AF_INET6 (and the reverse) fails. + {`begin; Socket.getaddrinfo("127.0.0.1", 80, Socket::AF_INET6); rescue SocketError; puts "v6filter"; end`, "v6filter"}, + {`begin; Socket.getaddrinfo("::1", 80, Socket::AF_INET); rescue SocketError; puts "v4filter"; end`, "v4filter"}, + // Unknown family / socktype names. + {`begin; Addrinfo.getaddrinfo("127.0.0.1", 80, "AF_BOGUS"); rescue SocketError; puts "badfam"; end`, "badfam"}, + {`begin; Addrinfo.getaddrinfo("127.0.0.1", 80, :INET, "SOCK_BOGUS"); rescue SocketError; puts "badsock"; end`, "badsock"}, + // A non-Integer/Symbol/String family / socktype is a TypeError. + {`begin; Addrinfo.getaddrinfo("127.0.0.1", 80, []); rescue TypeError; puts "famtype"; end`, "famtype"}, + {`begin; Addrinfo.getaddrinfo("127.0.0.1", 80, :INET, []); rescue TypeError; puts "socktype"; end`, "socktype"}, + // A non-Integer/String port is a TypeError. + {`begin; Socket.getaddrinfo("127.0.0.1", []); rescue TypeError; puts "porttype"; end`, "porttype"}, + // unpack of a wrong-length sockaddr raises ArgumentError. + {`begin; Socket.unpack_sockaddr_in("short"); rescue ArgumentError; puts "unlen"; end`, "unlen"}, + // unpack of a non-String sockaddr is a TypeError. + {`begin; Socket.unpack_sockaddr_in(42); rescue TypeError; puts "unstr"; end`, "unstr"}, + // Addrinfo.new with a too-short array / a non-String,non-Array argument. + {`begin; Addrinfo.new(["AF_INET", 80]); rescue ArgumentError; puts "arrlen"; end`, "arrlen"}, + {`begin; Addrinfo.new(42); rescue TypeError; puts "argtype"; end`, "argtype"}, + // Family names / the Linux AF_INET6 spelling round-trip through new's array form. + {`b=Addrinfo.new(["AF_INET6", 443, "::1", "::1"]); p b.afamily`, "30"}, + {`p Addrinfo.getaddrinfo("::1", 443, 10, :STREAM).map(&:ipv6?)`, "[true]"}, + // The :DGRAM / SOCK_DGRAM socket-type names map to SOCK_DGRAM+UDP. + {`p Addrinfo.getaddrinfo("127.0.0.1", 53, :INET, :DGRAM).map(&:inspect)`, + `["#"]`}, + {`p Addrinfo.getaddrinfo("127.0.0.1", 53, "AF_INET", "SOCK_DGRAM")[0].protocol`, "17"}, + } + for _, c := range cases { + if got := runSrc(t, "require \"socket\"\n"+c.src); got != c.want { + t.Errorf("src=%q\n got=%q\nwant=%q", c.src, got, c.want) + } + } +} + +// TestAddrinfoResolveSeams drives the resolver-backed arms deterministically +// through the resolveIPs / lookupPort seams: a resolution failure, an empty +// result, a hostname packed into a sockaddr, and both service-name port arms. +func TestAddrinfoResolveSeams(t *testing.T) { + origIP, origPort := resolveIPs, lookupPort + defer func() { resolveIPs, lookupPort = origIP, origPort }() + + // A resolution failure surfaces as SocketError from getaddrinfo. + resolveIPs = func(string, string) ([]net.IP, error) { return nil, fmt.Errorf("injected resolve failure") } + if got := runSrc(t, `require "socket" +begin; Socket.getaddrinfo("no.such.host.invalid", 80); rescue SocketError; puts "resfail"; end`); got != "resfail" { + t.Errorf("resolve failure got %q", got) + } + // The same failure from pack_sockaddr_in's hostname resolution. + if got := runSrc(t, `require "socket" +begin; Socket.pack_sockaddr_in(80, "no.such.host.invalid"); rescue SocketError; puts "packfail"; end`); got != "packfail" { + t.Errorf("pack resolve failure got %q", got) + } + // An empty (but error-free) result from the singular Addrinfo.tcp constructor. + resolveIPs = func(string, string) ([]net.IP, error) { return nil, nil } + if got := runSrc(t, `require "socket" +begin; Addrinfo.tcp("no.such.host.invalid", 80); rescue SocketError; puts "empty"; end`); got != "empty" { + t.Errorf("empty resolve got %q", got) + } + + // A hostname resolving to a concrete IP is packed into a sockaddr. + resolveIPs = func(string, string) ([]net.IP, error) { return []net.IP{net.ParseIP("9.8.7.6")}, nil } + if got := runSrc(t, `require "socket" +p Socket.unpack_sockaddr_in(Socket.pack_sockaddr_in(80, "myhost"))`); got != `[80, "9.8.7.6"]` { + t.Errorf("hostname pack got %q", got) + } + // getaddrinfo over a resolved hostname (default network, no family filter). + if got := runSrc(t, `require "socket" +p Socket.getaddrinfo("myhost", 80, nil, Socket::SOCK_STREAM)`); got != `[["AF_INET", 80, "9.8.7.6", "9.8.7.6", 2, 1, 6]]` { + t.Errorf("hostname getaddrinfo got %q", got) + } + + // A service-name port: success and failure through the lookupPort seam. + lookupPort = func(network, service string) (int, error) { + if service == "http" { + return 80, nil + } + return 0, fmt.Errorf("unknown service %q", service) + } + if got := runSrc(t, `require "socket" +p Socket.getaddrinfo("127.0.0.1", "http", Socket::AF_INET, Socket::SOCK_STREAM)[0][1]`); got != "80" { + t.Errorf("service port got %q", got) + } + if got := runSrc(t, `require "socket" +begin; Socket.getaddrinfo("127.0.0.1", "bogus-service", Socket::AF_INET, Socket::SOCK_DGRAM); rescue SocketError; puts "svcfail"; end`); got != "svcfail" { + t.Errorf("service failure got %q", got) + } +} + +// TestAddrinfoHelperArms covers the pure helper branches that normal dispatch +// cannot reach (the AF_UNIX / AF_UNSPEC family names, the value-protocol methods, +// and the type-narrowing guard). +func TestAddrinfoHelperArms(t *testing.T) { + // afamilyName over every arm, including AF_UNIX and the unspecified default. + for af, want := range map[int]string{afINET: "AF_INET", afINET6: "AF_INET6", afUNIX: "AF_UNIX", 99: "AF_UNSPEC"} { + if got := afamilyName(af); got != want { + t.Errorf("afamilyName(%d) = %q, want %q", af, got, want) + } + } + // familyFromName / socktypeFromName over the AF_UNIX / LOCAL and prefixed forms. + for _, name := range []string{"UNIX", "LOCAL", "AF_UNIX"} { + if got := familyFromName(name); got != afUNIX { + t.Errorf("familyFromName(%q) = %d, want afUNIX", name, got) + } + } + // The addrinfo value's Go-level ToS / Inspect / Truthy. + a := &addrinfo{afamily: afINET, ip: "127.0.0.1", port: 80, socktype: sockStream, protocol: ipprotoTCP} + if a.ToS() != "#" || a.Inspect() != a.ToS() || !a.Truthy() { + t.Errorf("addrinfo value protocol: ToS=%q Truthy=%v", a.ToS(), a.Truthy()) + } + // The type-narrowing guard raises TypeError on a mis-typed receiver. + func() { + defer func() { + if re, ok := recover().(RubyError); !ok || re.Class != "TypeError" { + t.Errorf("asAddrinfo recover = %v", recover()) + } + }() + asAddrinfo(object.NilV) + }() +} diff --git a/internal/vm/socket_addrinfo_unix.go b/internal/vm/socket_addrinfo_unix.go new file mode 100644 index 00000000..9ff3bc38 --- /dev/null +++ b/internal/vm/socket_addrinfo_unix.go @@ -0,0 +1,65 @@ +// Copyright (c) the go-embedded-ruby/ruby authors +// +// SPDX-License-Identifier: BSD-3-Clause + +//go:build !windows + +package vm + +import ( + "bytes" + binpkg "encoding/binary" + + "github.com/go-embedded-ruby/ruby/internal/object" +) + +// This is the non-Windows half of the sockaddr utilities: the AF_UNIX +// pack_sockaddr_un / unpack_sockaddr_un helpers. AF_UNIX is unsupported on +// Windows (matching the UNIXSocket transport in socket_unix.go), so these are +// registered only here; the Windows build supplies NotImplementedError stubs +// (socket_addrinfo_windows.go). + +// sunPathLen is the sun_path capacity of a Linux sockaddr_un (108 bytes); the +// packed form is the 2-byte family followed by the NUL-terminated path padded to +// this length. +const sunPathLen = 108 + +// registerSockaddrUn installs Socket.pack_sockaddr_un / .unpack_sockaddr_un on +// non-Windows platforms. +func registerSockaddrUn(sock *RClass) { + // Socket.pack_sockaddr_un(path) / Socket.sockaddr_un(path) → the packed + // sockaddr_un bytes (family + NUL-terminated path, sun_path-padded). + packUn := &Method{name: "pack_sockaddr_un", owner: sock, + native: func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value { + if len(args) < 1 { + raise("ArgumentError", "wrong number of arguments (given %d, expected 1)", len(args)) + } + path := strArg(args[0]) + if len(path) >= sunPathLen { + raise("ArgumentError", "too long unix socket path (%d bytes given but %d bytes max)", len(path), sunPathLen-1) + } + buf := make([]byte, 2+sunPathLen) + binpkg.NativeEndian.PutUint16(buf[0:2], uint16(afUNIX)) + copy(buf[2:], path) + return object.NewStringBytesEnc(buf, "ASCII-8BIT") + }} + sock.smethods["pack_sockaddr_un"] = packUn + sock.smethods["sockaddr_un"] = &Method{name: "sockaddr_un", owner: sock, native: packUn.native} + + // Socket.unpack_sockaddr_un(sockaddr) → the path (bytes up to the first NUL). + sock.smethods["unpack_sockaddr_un"] = &Method{name: "unpack_sockaddr_un", owner: sock, + native: func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value { + if len(args) < 1 { + raise("ArgumentError", "wrong number of arguments (given %d, expected 1)", len(args)) + } + sa := sockaddrBytes(args[0]) + if len(sa) < 2 { + raise("ArgumentError", "not an AF_UNIX sockaddr") + } + path := sa[2:] + if i := bytes.IndexByte(path, 0); i >= 0 { + path = path[:i] + } + return object.NewString(string(path)) + }} +} diff --git a/internal/vm/socket_addrinfo_windows.go b/internal/vm/socket_addrinfo_windows.go new file mode 100644 index 00000000..bb1ee391 --- /dev/null +++ b/internal/vm/socket_addrinfo_windows.go @@ -0,0 +1,29 @@ +// Copyright (c) the go-embedded-ruby/ruby authors +// +// SPDX-License-Identifier: BSD-3-Clause + +//go:build windows + +package vm + +import "github.com/go-embedded-ruby/ruby/internal/object" + +// This is the Windows counterpart to socket_addrinfo_unix.go: AF_UNIX is +// unsupported on Windows (matching the UNIXSocket transport stub in +// socket_unix_windows.go), so Socket.pack_sockaddr_un / .unpack_sockaddr_un exist +// but raise a clean, rescuable NotImplementedError rather than compiling the +// AF_UNIX byte layout there. + +// registerSockaddrUn installs Socket.pack_sockaddr_un / .unpack_sockaddr_un stubs +// on Windows whose bodies raise NotImplementedError. It has the same signature as +// the non-Windows implementation so registerSocketAddr calls it uniformly. +func registerSockaddrUn(sock *RClass) { + unsupported := func(name string) NativeFn { + return func(_ *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value { + return raise("NotImplementedError", "%s (AF_UNIX) is not supported on Windows", name) + } + } + for _, name := range []string{"pack_sockaddr_un", "sockaddr_un", "unpack_sockaddr_un"} { + sock.smethods[name] = &Method{name: name, owner: sock, native: unsupported("Socket." + name)} + } +}