diff --git a/multiversal b/multiversal index 05cdec2f..fdd79600 160000 --- a/multiversal +++ b/multiversal @@ -1 +1 @@ -Subproject commit 05cdec2fa261b21712597549d08481b79b7b1d9b +Subproject commit fdd796001bcabf83990b7dc865dff5325ec2cec7 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 12f0b96c..47ce764e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -466,6 +466,12 @@ set(prefs_sources source_group(Prefs FILES ${prefs_sources}) +set(mactcp_sources + mactcp/mactcp.cpp +) + +source_group(MacTCP FILES ${mactcp_sources}) + set(util_sources util/macstrings.h util/macstrings.cpp @@ -536,6 +542,7 @@ set(include_sources include/rsys/icon.h include/rsys/keyboard.h include/rsys/launch.h + include/rsys/mactcp.h include/rsys/macros.h include/rsys/noreturn.h include/rsys/osutil.h @@ -567,6 +574,7 @@ set(sources ${base_sources} ${mman_sources} ${vdriver_sources} ${sound_sources} ${num_sources} ${misc_sources} ${file_sources} ${hfs_sources} ${time_sources} ${osevent_sources} ${error_sources} ${commandline_sources} ${prefs_sources} + ${mactcp_sources} ${util_sources} ${debug_sources} ${api_headers} ${trap_instance_sources} ${host_os_sources} ${mpw_sources} diff --git a/src/desk.cpp b/src/desk.cpp index d9b3d559..073cd032 100644 --- a/src/desk.cpp +++ b/src/desk.cpp @@ -164,6 +164,17 @@ void Executor::C_SystemTask() for(i = 0; i < LM(UnitNtryCnt); ++i) { dctlh = LM(UTableBase)[i]; + /* The unit table is sparse: native drivers install themselves at + * fixed unit numbers (serial at 5..8) and leave the slots in + * between empty. Skip empty slots rather than dereference them. + * + * NOTE: LM(UnitNtryCnt) is set to 0 in init.cpp and never + * updated, so this loop never actually runs -- meaning accRun is + * never delivered to any driver. Fixing that is a Device + * Manager design question; this guard is what makes raising + * UnitNtryCnt safe when someone does. */ + if(!dctlh || !*dctlh) + continue; if(((*dctlh)->dCtlFlags & NEEDTIMEBIT) && TickCount() >= (*dctlh)->dCtlCurTicks) { Control(itorn(i), accRun, (Ptr)0); @@ -241,6 +252,8 @@ void Executor::C_SystemMenu(LONGINT menu) for(i = 0; i < LM(UnitNtryCnt); ++i) { dctlh = LM(UTableBase)[i]; + if(!dctlh || !*dctlh) /* sparse unit table -- see C_SystemTask */ + continue; if((*dctlh)->dCtlMenu == LM(MBarEnable)) { menu_s = menu; diff --git a/src/device.cpp b/src/device.cpp index 03504aac..3d8e6b46 100644 --- a/src/device.cpp +++ b/src/device.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include using namespace Executor; @@ -258,9 +260,20 @@ void Executor::RegisterDriver(const driverinfo& di) knowndrivers.push_back(di); } +/* Re-enter emulated code to run a driver call's completion routine. + * A0 = param block, A1 = the routine, D0 = result. */ +void Executor::callcomp(ParmBlkPtr pbp, ProcPtr comp, OSErr err) +{ + EM_A0 = US_TO_SYN68K(pbp); + EM_A1 = US_TO_SYN68K(comp); + EM_D0 = (unsigned short)err; /* TODO: unsigned short ? */ + execute68K((syn68k_addr_t)(uintptr_t)comp); +} + static void InitBuiltinDrivers() { InitSerialDriver(); + InitMacTCPDriver(); } /* diff --git a/src/include/rsys/device.h b/src/include/rsys/device.h index 91b27e99..42c1938d 100644 --- a/src/include/rsys/device.h +++ b/src/include/rsys/device.h @@ -17,4 +17,10 @@ struct driverinfo }; void RegisterDriver(const driverinfo& di); + +/* Re-enter emulated code to run a driver call's completion routine. + * A0 = param block, A1 = the routine, D0 = result -- the completion + * routine ABI. Any native driver implementing asynchronous calls + * needs this, so it lives here rather than in one driver's .cpp. */ +void callcomp(ParmBlkPtr pbp, ProcPtr comp, OSErr err); } diff --git a/src/include/rsys/mactcp.h b/src/include/rsys/mactcp.h new file mode 100644 index 00000000..d0fc848c --- /dev/null +++ b/src/include/rsys/mactcp.h @@ -0,0 +1,11 @@ +/* + * rsys/mactcp.h — native MacTCP (.IPP) driver for Executor 2000. + */ +#if !defined(_RSYS_MACTCP_H_) +#define _RSYS_MACTCP_H_ + +namespace Executor +{ +void InitMacTCPDriver(); +} +#endif diff --git a/src/mactcp/mactcp.cpp b/src/mactcp/mactcp.cpp new file mode 100644 index 00000000..b2171dfc --- /dev/null +++ b/src/mactcp/mactcp.cpp @@ -0,0 +1,700 @@ +/* mactcp.cpp — native `.IPP` (MacTCP) driver for Executor 2000. + * + * Phase 1 skeleton: driver registration, csCode dispatch, stream + * table, and synchronous TCP client operations over non-blocking + * POSIX sockets. Modeled on src/serial.cpp. + * + * Design notes: + * - Async model: like serial.cpp, operations currently execute + * synchronously and then "complete" (set ioResult, invoke the + * ioCompletion routine via callcomp if the async trap bit is + * set). Phase 2 replaces the bodies with a pending-op queue + * drained from the accRun pump. + * - Pump: driver open sets NEEDTIMEBIT/dCtlDelay=0 on the DCE, which + * is what C_SystemTask() (desk.cpp) looks for when deciding whom to + * send accRun to. + * + * BLOCKER FOR PHASE 2: that pump does not currently run at all. + * LM(UnitNtryCnt) is set to 0 in init.cpp and is never updated by + * anything, so C_SystemTask's loop body never executes and no + * driver has ever received accRun. Raising it naively would then + * null-deref on the empty slots between installed units, so + * C_SystemTask and C_SystemMenu now skip empty slots. Deciding + * how UnitNtryCnt should actually be maintained is a Device + * Manager question to settle with upstream before phase 2 depends + * on it; phase 1 is unaffected because it is fully synchronous. + * - Apps that spin-poll ioResult without calling WaitNextEvent will + * starve even once the pump works; a secondary pump point in the + * trap path may be needed. + * - StreamPtr: an opaque cookie (never a real pointer); apps are + * documented to treat it as opaque. + * - Byte order: guest is big-endian == network order; GUEST<> + * reads yield host-order values, so htonl/htons at the sockaddr + * boundary is both necessary and sufficient. + */ + +#include +#include +#include +#include +#include +#include /* accRun */ +#include /* generated from defs/MacTCP.yaml */ +#include +#include +/* Required for REGISTER_FUNCTION_PTR below: it expands to an explicit + * template instantiation of WrappedFunction<>, whose member definitions + * live here. Without it everything compiles and the link fails on + * WrappedFunction<...>::init(). serial.cpp includes it for the same + * reason. */ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include /* writev, struct iovec */ +#include + +#include +#include +#include + +using namespace Executor; + +/* ---- ABI guard rails ------------------------------------------------ + * If the generated headers disagree with the documented MacTCP ABI, + * fail the build rather than corrupt guest memory at run time. + */ +static_assert(offsetof(TCPiopb, ioCompletion) == 12, "TCPiopb ABI"); +static_assert(offsetof(TCPiopb, ioResult) == 16, "TCPiopb ABI"); +static_assert(offsetof(TCPiopb, ioCRefNum) == 24, "TCPiopb ABI"); +static_assert(offsetof(TCPiopb, csCode) == 26, "TCPiopb ABI"); +static_assert(offsetof(TCPiopb, tcpStream) == 28, "TCPiopb ABI"); +static_assert(offsetof(TCPiopb, csParam) == 32, "TCPiopb ABI"); +static_assert(offsetof(TCPOpenPB, remoteHost) == 4, "TCPOpenPB ABI"); +static_assert(offsetof(TCPOpenPB, localHost) == 10, "TCPOpenPB ABI"); +static_assert(offsetof(TCPSendPB, wdsPtr) == 6, "TCPSendPB ABI"); +static_assert(offsetof(TCPReceivePB, rcvBuff) == 4, "TCPReceivePB ABI"); +static_assert(offsetof(TCPReceivePB, markFlag) == 1, "TCPReceivePB ABI"); +static_assert(offsetof(TCPReceivePB, urgentFlag) == 2, "TCPReceivePB ABI"); +static_assert(sizeof(TCPStatusPB) == 70, "TCPStatusPB ABI"); +static_assert(sizeof(TCPiopb) == 102, "TCPiopb ABI"); +static_assert(offsetof(GetAddrParamBlock, ourAddress) == 28, + "GetAddrParamBlock ABI"); +static_assert(sizeof(wdsEntry) == 6, "wdsEntry ABI"); + +/* callcomp() (re-enter emulated code to run a completion routine) is + * declared in rsys/device.h and defined in device.cpp. */ + +namespace +{ + +/* ---- Stream table -------------------------------------------------- */ + +enum class StreamState : uint8_t +{ + created, /* TCPCreate done, no connection yet */ + connecting, /* reserved for phase 2 async open */ + established, + closing, /* we sent FIN (TCPClose) */ + terminated, /* connection gone, stream not yet released */ +}; + +struct MacTCPStream +{ + int fd = -1; + StreamState state = StreamState::created; + bool remoteClosed = false; /* peer FIN seen */ + + /* App-supplied receive area (guest memory), from TCPCreate. + * Phase 1 (TCPRcv only) copies straight into the caller's + * buffer; phase 2's TCPNoCopyRcv stages data here and hands + * out rdsEntry pointers into it. */ + Ptr rcvBuff = nullptr; + uint32_t rcvBuffLen = 0; + + /* ASR + per-stream user data, from TCPCreate. */ + ProcPtr notifyProc = nullptr; + Ptr userDataPtr = nullptr; + + /* TODO(phase2): pending async op queue; rds bookkeeping. */ +}; + +/* StreamPtr cookies: 'TCP\0' | id. Never dereferenced. */ +constexpr uint32_t STREAM_COOKIE_BASE = 0x54435000; +uint32_t next_stream_id = 1; +std::unordered_map streams; + +MacTCPStream *lookup(GUEST sp, OSErr *err) +{ + auto it = streams.find(guest_cast(sp)); + if(it == streams.end()) + { + *err = invalidStreamPtr; + return nullptr; + } + return &it->second; +} + +/* ---- errno -> MacTCP OSErr ---------------------------------------- */ + +OSErr map_socket_errno(int e) +{ + switch(e) + { + case 0: + return noErr; + case ECONNREFUSED: + case EHOSTUNREACH: + case ENETUNREACH: + return openFailed; + case ETIMEDOUT: + return commandTimeout; + case ECONNRESET: + case EPIPE: + return connectionTerminated; + case EADDRINUSE: + return duplicateSocket; + case ENOBUFS: + case ENOMEM: + case EMFILE: + case ENFILE: + return insufficientResources; + default: + return ipBadAddr; /* generic; refine as cases surface */ + } +} + +/* ---- completion ---------------------------------------------------- */ + +OSErr complete(TCPiopb *pb, OSErr err) +{ + pb->ioResult = err; + if((pb->ioTrap & asyncTrpBit) && pb->ioCompletion) + callcomp(guest_cast(pb), pb->ioCompletion, err); + return err; +} + +/* ---- ASR delivery (phase 2) --------------------------------------- + * pascal void notifyProc(StreamPtr, u16 eventCode, Ptr userDataPtr, + * u16 terminReason, ICMPReport *icmpMsg) + * + * TODO(phase2): push args right-to-left on the emulated stack + * (EM_A7), push a magic return address, execute68K, restore A7. + * Check whether Executor already has a generic pascal-call helper + * before hand-rolling one here (the menu/control defproc callers + * are the place to look). + */ +[[maybe_unused]] void call_asr(uint32_t cookie, MacTCPStream &s, + uint16_t event, uint16_t reason) +{ + if(!s.notifyProc) + return; + (void)cookie; + (void)event; + (void)reason; + warning_unimplemented("MacTCP ASR delivery (event %d)", event); +} + +/* ---- helpers ------------------------------------------------------- */ + +int set_nonblocking(int fd) +{ + int fl = fcntl(fd, F_GETFL, 0); + return fl < 0 ? fl : fcntl(fd, F_SETFL, fl | O_NONBLOCK); +} + +/* Wait for fd readiness with a MacTCP-style timeout (0 = default). */ +int poll_one(int fd, short events, uint8_t timeout_secs, + uint8_t default_secs) +{ + struct pollfd p = { fd, events, 0 }; + int secs = timeout_secs ? timeout_secs : default_secs; + return poll(&p, 1, secs * 1000); +} + +/* ---- csCode implementations (phase 1: synchronous) ---------------- */ + +OSErr do_getaddr(GetAddrParamBlock *pb) +{ + /* First non-loopback IPv4 interface. TODO: make configurable. */ + struct ifaddrs *ifa0; + OSErr err = ipBadCnfgErr; + + pb->ourAddress = 0; + pb->ourNetMask = 0; + if(getifaddrs(&ifa0) == 0) + { + for(struct ifaddrs *ifa = ifa0; ifa; ifa = ifa->ifa_next) + { + if(!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_INET) + continue; + auto *sin = (struct sockaddr_in *)ifa->ifa_addr; + uint32_t a = ntohl(sin->sin_addr.s_addr); + if((a >> 24) == 127) + continue; + pb->ourAddress = a; + if(ifa->ifa_netmask) + pb->ourNetMask = ntohl( + ((struct sockaddr_in *)ifa->ifa_netmask) + ->sin_addr.s_addr); + err = noErr; + break; + } + freeifaddrs(ifa0); + } + return err; +} + +OSErr do_tcp_create(TCPiopb *pb) +{ + auto &create = pb->csParam.create; + + if(!create.rcvBuff || create.rcvBuffLen < 4096) + return invalidBufPtr; /* MacTCP demanded >= 4K rcv buffer */ + + uint32_t cookie = STREAM_COOKIE_BASE + next_stream_id++; + MacTCPStream &s = streams[cookie]; + s.rcvBuff = create.rcvBuff; + s.rcvBuffLen = create.rcvBuffLen; + s.notifyProc = guest_cast(create.notifyProc); + s.userDataPtr = create.userDataPtr; + + pb->tcpStream = guest_cast(cookie); + return noErr; +} + +OSErr do_tcp_active_open(TCPiopb *pb) +{ + OSErr err = noErr; + MacTCPStream *s = lookup(pb->tcpStream, &err); + if(!s) + return err; + if(s->fd >= 0) + return connectionExists; + + auto &open = pb->csParam.open; + + int fd = socket(AF_INET, SOCK_STREAM, 0); + if(fd < 0) + return insufficientResources; + set_nonblocking(fd); + + if(open.localPort) + { + struct sockaddr_in la = {}; + la.sin_family = AF_INET; + la.sin_port = htons(open.localPort); + la.sin_addr.s_addr = htonl(open.localHost); + int one = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + if(bind(fd, (struct sockaddr *)&la, sizeof(la)) < 0) + { + err = map_socket_errno(errno); + close(fd); + return err; + } + } + + struct sockaddr_in ra = {}; + ra.sin_family = AF_INET; + ra.sin_port = htons(open.remotePort); + ra.sin_addr.s_addr = htonl(open.remoteHost); + + if(connect(fd, (struct sockaddr *)&ra, sizeof(ra)) < 0 + && errno != EINPROGRESS) + { + err = map_socket_errno(errno); + close(fd); + return err; + } + + /* Phase 1: block right here until connected or ULP timeout. + * Phase 2: return inProgress, finish from the accRun pump. */ + int r = poll_one(fd, POLLOUT, open.ulpTimeoutValue, 60); + if(r <= 0) + { + close(fd); + return r == 0 ? openFailed : map_socket_errno(errno); + } + int soerr = 0; + socklen_t slen = sizeof(soerr); + getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerr, &slen); + if(soerr) + { + close(fd); + return map_socket_errno(soerr); + } + + /* Report the resolved local endpoint back, as MacTCP did. */ + struct sockaddr_in la = {}; + socklen_t lalen = sizeof(la); + if(getsockname(fd, (struct sockaddr *)&la, &lalen) == 0) + { + open.localHost = ntohl(la.sin_addr.s_addr); + open.localPort = ntohs(la.sin_port); + } + + s->fd = fd; + s->state = StreamState::established; + return noErr; +} + +OSErr do_tcp_send(TCPiopb *pb) +{ + OSErr err = noErr; + MacTCPStream *s = lookup(pb->tcpStream, &err); + if(!s) + return err; + if(s->fd < 0 || s->state != StreamState::established) + return connectionDoesntExist; + + wdsEntry *w = guest_cast(pb->csParam.send.wdsPtr); + if(!w) + return invalidWDS; + + /* Gather the WDS (zero-length entry terminates the list). */ + struct iovec iov[16]; + int n = 0; + uint32_t total = 0; + for(; w[n].length && n < 16; ++n) + { + iov[n].iov_base = (void *)guest_cast(w[n].ptr); + iov[n].iov_len = w[n].length; + total += w[n].length; + } + if(w[n].length) + return invalidWDS; /* TODO: dynamically sized iovec */ + + /* Phase 1: loop until fully written (sockets are non-blocking). */ + uint32_t written = 0; + while(written < total) + { + ssize_t r = writev(s->fd, iov, n); + if(r < 0) + { + if(errno == EAGAIN || errno == EWOULDBLOCK) + { + if(poll_one(s->fd, POLLOUT, + pb->csParam.send.ulpTimeoutValue, 60) + <= 0) + return commandTimeout; + continue; + } + return map_socket_errno(errno); + } + written += r; + /* Advance iov past what was written. */ + while(r > 0 && n > 0) + { + if((size_t)r >= iov[0].iov_len) + { + r -= iov[0].iov_len; + std::memmove(&iov[0], &iov[1], + sizeof(iov[0]) * --n); + } + else + { + iov[0].iov_base = (char *)iov[0].iov_base + r; + iov[0].iov_len -= r; + r = 0; + } + } + } + if(pb->csParam.send.pushFlag) + { + int one = 1; + setsockopt(s->fd, IPPROTO_TCP, TCP_NODELAY, &one, + sizeof(one)); + } + return noErr; +} + +OSErr do_tcp_rcv(TCPiopb *pb) +{ + OSErr err = noErr; + MacTCPStream *s = lookup(pb->tcpStream, &err); + if(!s) + return err; + if(s->fd < 0) + return connectionDoesntExist; + + auto &rcv = pb->csParam.receive; + Ptr buf = rcv.rcvBuff; + uint16_t want = rcv.rcvBuffLen; + if(!buf || !want) + return invalidBufPtr; + + if(poll_one(s->fd, POLLIN, rcv.commandTimeoutValue, 60) <= 0) + return commandTimeout; + + ssize_t r = read(s->fd, (void *)buf, want); + if(r < 0) + return map_socket_errno(errno); + if(r == 0) + { + /* Peer FIN: report closing; further reads after drain + * report connectionTerminated per MacTCP semantics. */ + s->remoteClosed = true; + rcv.rcvBuffLen = 0; + return connectionClosing; + } + rcv.rcvBuffLen = (uint16_t)r; + rcv.urgentFlag = 0; + rcv.markFlag = 0; + return noErr; +} + +OSErr do_tcp_close(TCPiopb *pb) +{ + OSErr err = noErr; + MacTCPStream *s = lookup(pb->tcpStream, &err); + if(!s) + return err; + if(s->fd < 0) + return connectionDoesntExist; + shutdown(s->fd, SHUT_WR); /* half-close; stream stays readable */ + s->state = StreamState::closing; + return noErr; +} + +OSErr do_tcp_abort(TCPiopb *pb) +{ + OSErr err = noErr; + MacTCPStream *s = lookup(pb->tcpStream, &err); + if(!s) + return err; + if(s->fd >= 0) + { + struct linger lg = { 1, 0 }; /* RST on close */ + setsockopt(s->fd, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg)); + close(s->fd); + s->fd = -1; + } + s->state = StreamState::terminated; + return noErr; +} + +OSErr do_tcp_status(TCPiopb *pb) +{ + OSErr err = noErr; + MacTCPStream *s = lookup(pb->tcpStream, &err); + if(!s) + return err; + + auto &st = pb->csParam.status; + std::memset((void *)&st, 0, sizeof(st)); + + switch(s->state) + { + case StreamState::established: + st.connectionState = TCPSEstablished; + break; + case StreamState::closing: + st.connectionState = s->remoteClosed ? TCPSClosing + : TCPSFinWait1; + break; + case StreamState::terminated: + case StreamState::created: + default: + st.connectionState = TCPSClosed; + break; + } + if(s->fd >= 0) + { + int avail = 0; + if(ioctl(s->fd, FIONREAD, &avail) == 0) + st.amtUnreadData = (uint16_t)std::min(avail, 0xffff); + + struct sockaddr_in a = {}; + socklen_t alen = sizeof(a); + if(getpeername(s->fd, (struct sockaddr *)&a, &alen) == 0) + { + st.remoteHost = ntohl(a.sin_addr.s_addr); + st.remotePort = ntohs(a.sin_port); + } + alen = sizeof(a); + if(getsockname(s->fd, (struct sockaddr *)&a, &alen) == 0) + { + st.localHost = ntohl(a.sin_addr.s_addr); + st.localPort = ntohs(a.sin_port); + } + } + return noErr; +} + +OSErr do_tcp_release(TCPiopb *pb) +{ + OSErr err = noErr; + MacTCPStream *s = lookup(pb->tcpStream, &err); + if(!s) + return err; + if(s->fd >= 0) + close(s->fd); + /* Hand the receive area back to the app, as MacTCP documented. */ + pb->csParam.create.rcvBuff = s->rcvBuff; + pb->csParam.create.rcvBuffLen = s->rcvBuffLen; + streams.erase(guest_cast(pb->tcpStream)); + return noErr; +} + +/* ---- accRun pump --------------------------------------------------- + * Called every SystemTask via NEEDTIMEBIT (see driver open). + * Phase 1: nothing to do — everything is synchronous. + * Phase 2: poll(2) all live fds; complete pending async ops; + * deliver TCPDataArrival / TCPClosing / TCPTerminate ASRs. + */ +void pump() +{ + /* TODO(phase2) */ +} + +} /* anonymous namespace */ + +/* ---- driver entry points ------------------------------------------ + * These live at file scope rather than in the anonymous namespace + * above because REGISTER_FUNCTION_PTR expands to an explicit template + * instantiation, which has to be at namespace scope. Same shape as + * serial.cpp: a C_-prefixed implementation plus a wrapper object of + * the register-convention calling convention the Device Manager + * dispatches through. + */ + +static OSErr C_ROMlib_ippopen(ParmBlkPtr pbp, DCtlPtr dce); +REGISTER_FUNCTION_PTR(ROMlib_ippopen, D0(A0, A1)); +static OSErr C_ROMlib_ippprime(ParmBlkPtr pbp, DCtlPtr dce); +REGISTER_FUNCTION_PTR(ROMlib_ippprime, D0(A0, A1)); +static OSErr C_ROMlib_ippctl(ParmBlkPtr pbp, DCtlPtr dce); +REGISTER_FUNCTION_PTR(ROMlib_ippctl, D0(A0, A1)); +static OSErr C_ROMlib_ippstatus(ParmBlkPtr pbp, DCtlPtr dce); +REGISTER_FUNCTION_PTR(ROMlib_ippstatus, D0(A0, A1)); +static OSErr C_ROMlib_ippclose(ParmBlkPtr pbp, DCtlPtr dce); +REGISTER_FUNCTION_PTR(ROMlib_ippclose, D0(A0, A1)); + +static OSErr C_ROMlib_ippopen(ParmBlkPtr pbp, DCtlPtr dce) +{ + (void)pbp; + /* Ask C_SystemTask to deliver accRun to us every pass. */ + dce->dCtlFlags |= NEEDTIMEBIT; + dce->dCtlDelay = 0; + return noErr; +} + +static OSErr C_ROMlib_ippprime(ParmBlkPtr pbp, DCtlPtr dce) +{ + /* MacTCP has no Read/Write interface; everything is Control. */ + (void)pbp; + (void)dce; + return controlErr; +} + +static OSErr C_ROMlib_ippctl(ParmBlkPtr pbp, DCtlPtr dce) +{ + (void)dce; + TCPiopb *pb = (TCPiopb *)pbp; + OSErr err; + + switch(pb->csCode) + { + case accRun: + pump(); + return noErr; /* housekeeping: no completion semantics */ + + case killCode: + /* Phase 1 has no queued ops to kill. */ + return complete(pb, noErr); + + case ipctlGetAddr: + err = do_getaddr((GetAddrParamBlock *)pbp); + break; + + case TCPCreate: + err = do_tcp_create(pb); + break; + case TCPActiveOpen: + err = do_tcp_active_open(pb); + break; + case TCPSend: + err = do_tcp_send(pb); + break; + case TCPRcv: + err = do_tcp_rcv(pb); + break; + case TCPClose: + err = do_tcp_close(pb); + break; + case TCPAbort: + err = do_tcp_abort(pb); + break; + case TCPStatus: + err = do_tcp_status(pb); + break; + case TCPRelease: + err = do_tcp_release(pb); + break; + + case TCPPassiveOpen: /* phase 3 */ + case TCPNoCopyRcv: /* phase 2 */ + case TCPRcvBfrReturn: /* phase 2 */ + case udpCreate: /* phase 3 ... */ + case udpRead: + case udpBfrReturn: + case udpWrite: + case udpRelease: + case udpMaxMTUSize: + case udpStatus: + warning_unimplemented("MacTCP csCode %d", (int)pb->csCode); + err = invalidLength; /* TODO: most fitting stub error? */ + break; + + default: + warning_unexpected("MacTCP unknown csCode %d", + (int)pb->csCode); + err = controlErr; + break; + } + return complete(pb, err); +} + +static OSErr C_ROMlib_ippstatus(ParmBlkPtr pbp, DCtlPtr dce) +{ + /* MacTCP routes its status-ish calls (TCPStatus etc.) through + * Control; a bare PBStatus on .IPP has nothing to report. */ + (void)dce; + return complete((TCPiopb *)pbp, controlErr); +} + +static OSErr C_ROMlib_ippclose(ParmBlkPtr pbp, DCtlPtr dce) +{ + (void)pbp; + (void)dce; + for(auto &kv : streams) + if(kv.second.fd >= 0) + close(kv.second.fd); + streams.clear(); + return noErr; +} + +/* ---- registration -------------------------------------------------- + * WIRE-UP: add `InitMacTCPDriver();` to InitBuiltinDrivers() in + * device.cpp. + * + * Unit slot: 47 (refnum -48), the top of the 48-entry unit table — + * far from .Sony (-5) and the serial units (-6..-9). Real MacTCP + * grabbed whatever free unit the Device Manager gave it, and no + * application may depend on the number: they must PBOpen by name. + */ +void Executor::InitMacTCPDriver() +{ + RegisterDriver({ + &ROMlib_ippopen, &ROMlib_ippprime, &ROMlib_ippctl, + &ROMlib_ippstatus, &ROMlib_ippclose, + (StringPtr) "\04.IPP", -48, + }); +} diff --git a/src/serial.cpp b/src/serial.cpp index 3a3b70cc..7af1ac59 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -286,16 +286,10 @@ static const char *specialname(ParmBlkPtr pbp) typedef void (*compfuncp)(void); +/* callcomp() now lives in device.cpp and is declared in rsys/device.h, + * so that other native drivers can complete asynchronous calls too. */ -void callcomp(ParmBlkPtr pbp, ProcPtr comp, OSErr err) -{ - EM_A0 = US_TO_SYN68K(pbp); - EM_A1 = US_TO_SYN68K(comp); - EM_D0 = (unsigned short)err; /* TODO: unsigned short ? */ - execute68K((syn68k_addr_t)(uintptr_t)comp); -} - -#define DOCOMPLETION(pbp, err) \ +#define DOCOMPLETION(pbp, err) \ (pbp)->ioParam.ioResult = err; \ if(((pbp)->ioParam.ioTrap & asyncTrpBit) && (pbp)->ioParam.ioCompletion) \ callcomp(pbp, (pbp)->ioParam.ioCompletion, err); \ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2d52cc19..bf1456f5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -39,10 +39,20 @@ endfunction() set(NATIVE_TEST_SOURCES files_internal.cpp guestvalues.cpp cpu.cpp sane.cpp + # Native-only: drives the .IPP driver through the Device Manager. + # The dual-mode ABI comparison against Apple's headers belongs in + # TEST_SOURCES later, once the Retro68 side has MacTCP.h. + mactcp.cpp ) set(TEST_SOURCES files.cpp quickdraw.cpp listmgr.cpp resources.cpp events.cpp + # Dual-mode on purpose: natively this checks the generated + # headers against defs/MacTCP.yaml, and under Retro68 -- built + # against Apple's Universal Interfaces rather than generated + # CIncludes -- it checks the documented ABI against Apple's. + # Compiles to nothing where there is no MacTCP.h. + mactcp_abi.cpp ) if(CMAKE_SYSTEM_NAME STREQUAL Retro68) list(APPEND TEST_SOURCES diff --git a/tests/mactcp.cpp b/tests/mactcp.cpp new file mode 100644 index 00000000..53eae32c --- /dev/null +++ b/tests/mactcp.cpp @@ -0,0 +1,558 @@ +/* Runtime tests for the native MacTCP (.IPP) driver. + * + * Native-only: these drive the driver through the real Device Manager + * entry points (OpenDriver / PBControl) inside the test harness set up + * by main_executor.cpp, so no 68k application and no emulator window + * are involved. + * + * The TCP tests are hermetic -- the test process itself listens on a + * loopback socket and the driver connects back to it, so nothing here + * depends on the machine having a network, a route, or a name server. + */ + +#include "gtest/gtest.h" + +#include "compat.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace Executor; + +namespace +{ + +/* Buffers reachable by the driver have to live in guest memory: the + * driver turns the GUEST fields back into host pointers with + * guest_cast, which is only meaningful for addresses inside the guest + * address space. NewPtr gives us that; the host stack would not. */ +Ptr guestBuffer(uint32_t size) +{ + Ptr p = NewPtrClear(size); + EXPECT_NE(nullptr, p); + return p; +} + +INTEGER openIPP() +{ + GUEST refnum = 0; + OSErr err = OpenDriver(PSTR(".IPP"), &refnum); + EXPECT_EQ(noErr, err) << "OpenDriver(\".IPP\") failed"; + return refnum; +} + +/* Issue one PBControl against .IPP. The parameter block may live on + * the host stack: it is never handed to emulated code, because these + * are synchronous calls with no completion routine. */ +OSErr control(INTEGER refnum, TCPiopb& pb, INTEGER csCode) +{ + pb.ioCRefNum = refnum; + pb.csCode = csCode; + return PBControl((ParmBlkPtr)&pb, false); +} + +/* A host-side listening socket for the driver to connect to. */ +class LoopbackListener +{ +public: + LoopbackListener() + { + fd_ = socket(AF_INET, SOCK_STREAM, 0); + EXPECT_GE(fd_, 0); + + struct sockaddr_in a = {}; + a.sin_family = AF_INET; + a.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + a.sin_port = 0; /* let the kernel pick */ + EXPECT_EQ(0, bind(fd_, (struct sockaddr *)&a, sizeof(a))); + EXPECT_EQ(0, listen(fd_, 1)); + + socklen_t len = sizeof(a); + EXPECT_EQ(0, getsockname(fd_, (struct sockaddr *)&a, &len)); + port_ = ntohs(a.sin_port); + } + + ~LoopbackListener() + { + if(accepted_ >= 0) + close(accepted_); + if(fd_ >= 0) + close(fd_); + } + + uint16_t port() const { return port_; } + + int accepted() + { + if(accepted_ < 0) + accepted_ = accept(fd_, nullptr, nullptr); + return accepted_; + } + +private: + int fd_ = -1; + int accepted_ = -1; + uint16_t port_ = 0; +}; + +/* Open a stream and connect it. MacTCP has no resolver of its own -- + * names go through the DNR, a separate code resource and phase 3 work + * -- so every connect here is by numeric address, exactly as an + * application of the era would have done it. */ +void createAndConnect(INTEGER refnum, TCPiopb& pb, Ptr rcvBuff, + uint32_t rcvBuffLen, uint16_t port) +{ + std::memset(&pb, 0, sizeof(pb)); + pb.csParam.create.rcvBuff = rcvBuff; + pb.csParam.create.rcvBuffLen = rcvBuffLen; + ASSERT_EQ(noErr, control(refnum, pb, TCPCreate)); + ASSERT_NE(nullptr, pb.tcpStream) << "TCPCreate returned a null stream"; + + auto stream = pb.tcpStream; + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + pb.csParam.open.remoteHost = 0x7F000001; /* 127.0.0.1, numeric */ + pb.csParam.open.remotePort = port; + pb.csParam.open.ulpTimeoutValue = 5; + ASSERT_EQ(noErr, control(refnum, pb, TCPActiveOpen)); +} + +} /* namespace */ + +/* ---- step 2: the driver is reachable at all ----------------------- */ + +TEST(MacTCP, OpenDriverByName) +{ + GUEST refnum = 0; + + ASSERT_EQ(noErr, OpenDriver(PSTR(".IPP"), &refnum)); + /* Registered at refnum -48 == unit 47, the top of the unit table. */ + EXPECT_EQ(-48, (INTEGER)refnum); + EXPECT_NE(nullptr, GetDCtlEntry(refnum)); +} + +TEST(MacTCP, OpenDriverIsCaseInsensitive) +{ + GUEST refnum = 0; + + /* ROMlib_driveropen matches driver names case-insensitively. */ + ASSERT_EQ(noErr, OpenDriver(PSTR(".ipp"), &refnum)); + EXPECT_EQ(-48, (INTEGER)refnum); +} + +TEST(MacTCP, GetAddrReportsAnInterface) +{ + INTEGER refnum = openIPP(); + + GetAddrParamBlock pb; + std::memset(&pb, 0, sizeof(pb)); + pb.ioCRefNum = refnum; + pb.csCode = ipctlGetAddr; + + OSErr err = PBControl((ParmBlkPtr)&pb, false); + + /* A machine with no non-loopback IPv4 interface is a legitimate + * configuration (CI containers, in particular), and the driver + * reports that rather than inventing an address. */ + if(err == ipBadCnfgErr) + { + EXPECT_EQ(0u, (uint32_t)pb.ourAddress); + GTEST_SKIP() << "no non-loopback IPv4 interface on this host"; + } + + ASSERT_EQ(noErr, err); + uint32_t addr = pb.ourAddress; + EXPECT_NE(0u, addr) << "noErr but no address filled in"; + EXPECT_NE(127u, addr >> 24) << "loopback should have been skipped"; +} + +TEST(MacTCP, UnknownCsCodeIsRejected) +{ + INTEGER refnum = openIPP(); + + TCPiopb pb; + std::memset(&pb, 0, sizeof(pb)); + EXPECT_EQ(controlErr, control(refnum, pb, 999)); +} + +/* ---- step 3: a hermetic loopback round trip ----------------------- */ + +TEST(MacTCP, CreateRejectsUndersizedBuffer) +{ + INTEGER refnum = openIPP(); + Ptr small = guestBuffer(1024); + + TCPiopb pb; + std::memset(&pb, 0, sizeof(pb)); + pb.csParam.create.rcvBuff = small; + pb.csParam.create.rcvBuffLen = 1024; + + /* MacTCP required a receive buffer of at least 4K. */ + EXPECT_EQ(invalidBufPtr, control(refnum, pb, TCPCreate)); + + DisposePtr(small); +} + +TEST(MacTCP, OperationsOnAnUnknownStreamFail) +{ + INTEGER refnum = openIPP(); + + TCPiopb pb; + std::memset(&pb, 0, sizeof(pb)); + + /* A StreamPtr is an opaque cookie, not a real address, so it has to + * be built with guest_cast the same way the driver builds it. + * Assigning a fabricated host pointer instead would send + * US_TO_SYN68K an address outside the guest space and abort. */ + pb.tcpStream = guest_cast(0xDEADBEEFu); + + EXPECT_EQ(invalidStreamPtr, control(refnum, pb, TCPStatus)); + EXPECT_EQ(invalidStreamPtr, control(refnum, pb, TCPClose)); + EXPECT_EQ(invalidStreamPtr, control(refnum, pb, TCPRelease)); + + /* Zero is what a parameter block that was never filled in holds. */ + std::memset(&pb, 0, sizeof(pb)); + EXPECT_EQ(invalidStreamPtr, control(refnum, pb, TCPStatus)); +} + +TEST(MacTCP, LoopbackRoundTrip) +{ + INTEGER refnum = openIPP(); + LoopbackListener listener; + + const uint32_t kRcvBuffLen = 8192; + Ptr rcvBuff = guestBuffer(kRcvBuffLen); + + TCPiopb pb; + ASSERT_NO_FATAL_FAILURE( + createAndConnect(refnum, pb, rcvBuff, kRcvBuffLen, listener.port())); + auto stream = pb.tcpStream; + + int peer = listener.accepted(); + ASSERT_GE(peer, 0) << "driver never connected"; + + /* TCPActiveOpen should have reported the local endpoint back. */ + EXPECT_NE(0u, (uint32_t)pb.csParam.open.localPort); + + /* --- send, exercising the WDS gather list ---------------------- */ + const char part1[] = "GET / "; + const char part2[] = "HTTP/1.0\r\n\r\n"; + const char whole[] = "GET / HTTP/1.0\r\n\r\n"; + + Ptr b1 = guestBuffer(sizeof(part1)); + Ptr b2 = guestBuffer(sizeof(part2)); + std::memcpy(b1, part1, sizeof(part1) - 1); + std::memcpy(b2, part2, sizeof(part2) - 1); + + /* Three entries: two of data, then the zero-length terminator. */ + Ptr wdsMem = guestBuffer(sizeof(wdsEntry) * 3); + auto *wds = (wdsEntry *)wdsMem; + wds[0].length = sizeof(part1) - 1; + wds[0].ptr = b1; + wds[1].length = sizeof(part2) - 1; + wds[1].ptr = b2; + wds[2].length = 0; + wds[2].ptr = nullptr; + + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + pb.csParam.send.wdsPtr = wdsMem; + pb.csParam.send.pushFlag = 1; + pb.csParam.send.ulpTimeoutValue = 5; + ASSERT_EQ(noErr, control(refnum, pb, TCPSend)); + + char got[64] = {}; + ssize_t n = recv(peer, got, sizeof(got), 0); + ASSERT_EQ((ssize_t)(sizeof(whole) - 1), n) + << "gathered send did not arrive whole"; + EXPECT_EQ(0, std::memcmp(got, whole, sizeof(whole) - 1)); + + /* --- receive --------------------------------------------------- */ + const char reply[] = "HTTP/1.0 200 OK\r\n"; + ASSERT_EQ((ssize_t)(sizeof(reply) - 1), + send(peer, reply, sizeof(reply) - 1, 0)); + + Ptr appBuff = guestBuffer(256); + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + pb.csParam.receive.rcvBuff = appBuff; + pb.csParam.receive.rcvBuffLen = 256; + pb.csParam.receive.commandTimeoutValue = 5; + ASSERT_EQ(noErr, control(refnum, pb, TCPRcv)); + + EXPECT_EQ(sizeof(reply) - 1, (size_t)(uint16_t)pb.csParam.receive.rcvBuffLen); + EXPECT_EQ(0, std::memcmp(appBuff, reply, sizeof(reply) - 1)); + + /* --- status ---------------------------------------------------- */ + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + ASSERT_EQ(noErr, control(refnum, pb, TCPStatus)); + EXPECT_EQ(TCPSEstablished, (int)pb.csParam.status.connectionState); + EXPECT_EQ(0x7F000001u, (uint32_t)pb.csParam.status.remoteHost); + EXPECT_EQ(listener.port(), (uint16_t)pb.csParam.status.remotePort); + + /* --- close ----------------------------------------------------- */ + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + ASSERT_EQ(noErr, control(refnum, pb, TCPClose)); + + /* Our FIN should reach the peer as end-of-stream. */ + char drain[16]; + EXPECT_EQ(0, recv(peer, drain, sizeof(drain), 0)); + + /* --- release --------------------------------------------------- */ + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + ASSERT_EQ(noErr, control(refnum, pb, TCPRelease)); + /* Release hands the receive area back to the application. */ + EXPECT_EQ(rcvBuff, (Ptr)pb.csParam.create.rcvBuff); + EXPECT_EQ(kRcvBuffLen, (uint32_t)pb.csParam.create.rcvBuffLen); + + /* The stream is gone afterwards. */ + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + EXPECT_EQ(invalidStreamPtr, control(refnum, pb, TCPStatus)); + + DisposePtr(appBuff); + DisposePtr(wdsMem); + DisposePtr(b2); + DisposePtr(b1); + DisposePtr(rcvBuff); +} + +TEST(MacTCP, PeerCloseIsReportedAsConnectionClosing) +{ + INTEGER refnum = openIPP(); + LoopbackListener listener; + + const uint32_t kRcvBuffLen = 8192; + Ptr rcvBuff = guestBuffer(kRcvBuffLen); + + TCPiopb pb; + ASSERT_NO_FATAL_FAILURE( + createAndConnect(refnum, pb, rcvBuff, kRcvBuffLen, listener.port())); + auto stream = pb.tcpStream; + + int peer = listener.accepted(); + ASSERT_GE(peer, 0); + + /* Peer hangs up without sending anything. */ + shutdown(peer, SHUT_WR); + + Ptr appBuff = guestBuffer(256); + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + pb.csParam.receive.rcvBuff = appBuff; + pb.csParam.receive.rcvBuffLen = 256; + pb.csParam.receive.commandTimeoutValue = 5; + + EXPECT_EQ(connectionClosing, control(refnum, pb, TCPRcv)); + EXPECT_EQ(0, (uint16_t)pb.csParam.receive.rcvBuffLen); + + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + EXPECT_EQ(noErr, control(refnum, pb, TCPAbort)); + + DisposePtr(appBuff); + DisposePtr(rcvBuff); +} + +/* ---- a real HTTP/1.0 fetch ---------------------------------------- + * The phase 1 exit criterion was "a hand-rolled port-80 fetch works". + * This is that exchange, driven entirely through the .IPP driver: + * a genuine HTTP request goes out, a genuine response with headers and + * a body larger than the application's receive buffer comes back, and + * the server closes the connection to signal the end of the entity -- + * HTTP/1.0 semantics, which is what a 90s Mac client would have spoken. + * + * It talks to a server inside the test process rather than out to the + * internet, for three reasons. MacTCP has no resolver yet, so a real + * host would have to be a hardcoded IP address that rots. A test that + * needs egress cannot run in CI or on a developer's laptop offline. + * And the driver cannot tell the difference: the same socket calls run + * either way, and the parts that would differ -- routing, DNS -- are + * not in the driver. Set EXECUTOR_MACTCP_TEST_ADDR (dotted quad) and + * optionally EXECUTOR_MACTCP_TEST_PORT to point the same exchange at a + * real server. + */ + +namespace +{ + +/* Read from a stream until the peer hangs up, appending to `out`. + * Returns the OSErr that ended the loop. */ +OSErr drainStream(INTEGER refnum, TCPiopb& pb, GUEST stream, + Ptr appBuff, uint16_t appBuffLen, std::string& out) +{ + for(;;) + { + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + pb.csParam.receive.rcvBuff = appBuff; + pb.csParam.receive.rcvBuffLen = appBuffLen; + pb.csParam.receive.commandTimeoutValue = 10; + + OSErr err = control(refnum, pb, TCPRcv); + if(err != noErr) + return err; + + uint16_t got = pb.csParam.receive.rcvBuffLen; + if(got == 0) + return noErr; + out.append((const char *)appBuff, got); + } +} + +} /* namespace */ + +TEST(MacTCP, HttpFetch) +{ + INTEGER refnum = openIPP(); + + /* A body deliberately larger than the receive buffer below, so the + * response cannot arrive in one TCPRcv. + * + * Keep it comfortably inside the socket buffers. Driver calls here + * are synchronous and the server side is the same thread, so the + * server writes its whole response before the driver reads any of + * it; a response too large to sit in the kernel buffers would block + * the send and deadlock the test. Growing this much beyond a few + * tens of KB needs a thread or a poll loop on the server side. */ + std::string body; + for(int i = 0; body.size() < 10000; ++i) + body += "line " + std::to_string(i) + " of the response body\n"; + + std::string response = + "HTTP/1.0 200 OK\r\n" + "Content-Type: text/plain\r\n" + "Content-Length: " + std::to_string(body.size()) + "\r\n" + "\r\n" + body; + + uint32_t host = 0x7F000001; + uint16_t port = 0; + std::unique_ptr listener; + + if(const char *addr = getenv("EXECUTOR_MACTCP_TEST_ADDR")) + { + struct in_addr in; + ASSERT_EQ(1, inet_pton(AF_INET, addr, &in)) << "bad test address"; + host = ntohl(in.s_addr); + port = 80; + if(const char *p = getenv("EXECUTOR_MACTCP_TEST_PORT")) + port = (uint16_t)atoi(p); + } + else + { + listener = std::make_unique(); + port = listener->port(); + } + + const uint32_t kRcvBuffLen = 8192; + Ptr rcvBuff = guestBuffer(kRcvBuffLen); + + TCPiopb pb; + std::memset(&pb, 0, sizeof(pb)); + pb.csParam.create.rcvBuff = rcvBuff; + pb.csParam.create.rcvBuffLen = kRcvBuffLen; + ASSERT_EQ(noErr, control(refnum, pb, TCPCreate)); + auto stream = pb.tcpStream; + + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + pb.csParam.open.remoteHost = host; + pb.csParam.open.remotePort = port; + pb.csParam.open.ulpTimeoutValue = 10; + ASSERT_EQ(noErr, control(refnum, pb, TCPActiveOpen)) + << "TCPActiveOpen failed"; + + /* --- send the request --------------------------------------- */ + const char request[] = + "GET / HTTP/1.0\r\nHost: localhost\r\nConnection: close\r\n\r\n"; + + Ptr reqBuff = guestBuffer(sizeof(request)); + std::memcpy(reqBuff, request, sizeof(request) - 1); + Ptr wdsMem = guestBuffer(sizeof(wdsEntry) * 2); + auto *wds = (wdsEntry *)wdsMem; + wds[0].length = sizeof(request) - 1; + wds[0].ptr = reqBuff; + wds[1].length = 0; + wds[1].ptr = nullptr; + + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + pb.csParam.send.wdsPtr = wdsMem; + pb.csParam.send.pushFlag = 1; + pb.csParam.send.ulpTimeoutValue = 10; + ASSERT_EQ(noErr, control(refnum, pb, TCPSend)); + + /* --- the server side ----------------------------------------- */ + if(listener) + { + int peer = listener->accepted(); + ASSERT_GE(peer, 0); + + char req[512] = {}; + ssize_t n = recv(peer, req, sizeof(req) - 1, 0); + ASSERT_GT(n, 0); + EXPECT_NE(nullptr, std::strstr(req, "GET / HTTP/1.0")) + << "server did not receive a well-formed request"; + + ASSERT_EQ((ssize_t)response.size(), + send(peer, response.data(), response.size(), 0)); + shutdown(peer, SHUT_WR); /* HTTP/1.0: close ends the entity */ + } + + /* --- read the response --------------------------------------- */ + Ptr appBuff = guestBuffer(1024); + std::string got; + OSErr err = drainStream(refnum, pb, stream, appBuff, 1024, got); + + /* A clean server hangup surfaces as connectionClosing, which is + * the normal end of an HTTP/1.0 response, not a failure. */ + EXPECT_TRUE(err == noErr || err == connectionClosing) + << "unexpected error draining response: " << err; + + ASSERT_FALSE(got.empty()) << "no response received"; + EXPECT_EQ(0u, got.rfind("HTTP/1.", 0)) << "not an HTTP response"; + + size_t hdrEnd = got.find("\r\n\r\n"); + ASSERT_NE(std::string::npos, hdrEnd) << "no header terminator"; + + std::string gotBody = got.substr(hdrEnd + 4); + if(!listener) + { + /* Against a real server we cannot predict the body, so just + * insist we got a plausible amount of it. */ + EXPECT_GT(got.size(), 16u); + } + else + { + EXPECT_EQ(body.size(), gotBody.size()) + << "body truncated: needed several TCPRcv calls"; + EXPECT_EQ(body, gotBody); + } + + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + control(refnum, pb, TCPClose); + std::memset(&pb.csParam, 0, sizeof(pb.csParam)); + pb.tcpStream = stream; + ASSERT_EQ(noErr, control(refnum, pb, TCPRelease)); + + DisposePtr(appBuff); + DisposePtr(wdsMem); + DisposePtr(reqBuff); + DisposePtr(rcvBuff); +} diff --git a/tests/mactcp_abi.cpp b/tests/mactcp_abi.cpp new file mode 100644 index 00000000..49152f4c --- /dev/null +++ b/tests/mactcp_abi.cpp @@ -0,0 +1,337 @@ +/* MacTCP ABI conformance: sizes and byte offsets of the parameter + * blocks an application shares with the .IPP driver. + * + * This file is deliberately dual-mode, and the two modes answer two + * *different* questions: + * + * Native (-DEXECUTOR), against multiversal's generated MacTCP.h: + * does the header the generator produced actually lay out the + * way defs/MacTCP.yaml says it does? The `size:` assertions in + * the YAML only pin total sizes, so a mac68k alignment mistake + * in the middle of a struct that happens to preserve the total + * can slip through. These per-field offsets catch that. + * + * Retro68, against Apple's Universal Interfaces MacTCP.h: + * are those numbers actually correct? This is the ground truth. + * + * The expected values below have been confirmed field by field against + * Universal Interfaces 3.4.2, by compiling Apple's MacTCP.h for a + * 4-byte-pointer target with 2-byte packing (which is what mac68k + * alignment amounts to for these types) and reading back the offsets + * the compiler computed. Two blocks did *not* match the MacTCP + * Programmer's Guide, and both are called out where they appear below: + * TCPReceivePB's field order, and TCPStatusPB's connStatPtr, whose + * omission also made TCPiopb 98 bytes instead of 102. + * + * IMPORTANT: the Retro68 half only means something when it compiles + * against *Apple's* headers. Retro68 generates its CIncludes from the + * same multiversal YAML that Executor uses, so if MacTCP.yaml is ever + * added to Retro68's multiversal too, building this against those + * generated CIncludes would compare our definitions to themselves and + * prove nothing. Genuine Universal Interfaces must come first on the + * include path. + * + * Today that happens by default: Retro68's multiversal has no MacTCP + * definitions at all, so there can only be Apple's, which + * arrives via Retro68's interfaces-and-libraries.sh from a copy of + * Apple's Universal Interfaces you supply yourself. Retro68's own + * LaunchAPPL/Server/MacTCPStream.cc is a working MacTCP client built + * that way, and every parameter block field it touches agrees with the + * names used here. + * + * To re-confirm after a change: + * + * ./tests --gtest_filter=MacTCPABI.DumpLayout # native, ours + * ... same test built as the Retro68 application, run under + * Basilisk II or on real hardware via LaunchAPPL ... + * diff ours.txt apples.txt + * + * A compile failure on the Retro68 side is itself a result: it means a + * field name in defs/MacTCP.yaml does not match Apple's. + * + * The expected values are written out here rather than derived from + * the YAML, so that a transcription error in the YAML shows up as a + * failure instead of being quietly mirrored. + */ + +#include "gtest/gtest.h" + +#include "compat.h" + +/* Retro68 builds that have no MacTCP.h at all shouldn't fail to build; + * they just can't answer the question. */ +#if !defined(__has_include) +#define MACTCP_ABI_HAVE_HEADER 1 +#elif __has_include() +#define MACTCP_ABI_HAVE_HEADER 1 +#else +#define MACTCP_ABI_HAVE_HEADER 0 +#endif + +#if MACTCP_ABI_HAVE_HEADER + +#include + +#include +#include + +#ifdef EXECUTOR +using namespace Executor; +#endif + +/* Absolute offsets inside TCPiopb. Reaching the csParam members + * through the enclosing block rather than through each sub-struct + * keeps this independent of what the sub-structs are named, and + * absolute offsets are what the ABI actually constrains. + * + * X(field-expression, expected-offset) + */ +#define MACTCP_IOPB_FIELDS(X) \ + X(qLink, 0) \ + X(qType, 4) \ + X(ioTrap, 6) \ + X(ioCmdAddr, 8) \ + X(ioCompletion, 12) \ + X(ioResult, 16) \ + X(ioNamePtr, 18) \ + X(ioVRefNum, 22) \ + X(ioCRefNum, 24) \ + X(csCode, 26) \ + X(tcpStream, 28) \ + /* TCPCreate */ \ + X(csParam.create.rcvBuff, 32) \ + X(csParam.create.rcvBuffLen, 36) \ + X(csParam.create.notifyProc, 40) \ + X(csParam.create.userDataPtr, 44) \ + /* TCPActiveOpen / TCPPassiveOpen */ \ + X(csParam.open.ulpTimeoutValue, 32) \ + X(csParam.open.ulpTimeoutAction, 33) \ + X(csParam.open.validityFlags, 34) \ + X(csParam.open.commandTimeoutValue, 35)\ + X(csParam.open.remoteHost, 36) \ + X(csParam.open.remotePort, 40) \ + X(csParam.open.localHost, 42) \ + X(csParam.open.localPort, 46) \ + X(csParam.open.tosFlags, 48) \ + X(csParam.open.precedence, 49) \ + X(csParam.open.dontFrag, 50) \ + X(csParam.open.timeToLive, 51) \ + X(csParam.open.security, 52) \ + X(csParam.open.optionCnt, 53) \ + X(csParam.open.options, 54) \ + X(csParam.open.userDataPtr, 94) \ + /* TCPSend -- filler at 37, confirmed against Apple */ \ + X(csParam.send.ulpTimeoutValue, 32) \ + X(csParam.send.ulpTimeoutAction, 33) \ + X(csParam.send.validityFlags, 34) \ + X(csParam.send.pushFlag, 35) \ + X(csParam.send.urgentFlag, 36) \ + X(csParam.send.wdsPtr, 38) \ + X(csParam.send.sendFree, 42) \ + X(csParam.send.sendLength, 46) \ + X(csParam.send.userDataPtr, 48) \ + /* TCPRcv / TCPNoCopyRcv / TCPRcvBfrReturn. markFlag and \ + * urgentFlag are at the front, not the tail: the Programmer's \ + * Guide is wrong about this block and Apple's header says so. */ \ + X(csParam.receive.commandTimeoutValue, 32) \ + X(csParam.receive.markFlag, 33) \ + X(csParam.receive.urgentFlag, 34) \ + X(csParam.receive.rcvBuff, 36) \ + X(csParam.receive.rcvBuffLen, 40) \ + X(csParam.receive.rdsPtr, 42) \ + X(csParam.receive.rdsLength, 46) \ + X(csParam.receive.secondTimeStamp, 48) \ + X(csParam.receive.userDataPtr, 50) \ + /* TCPClose */ \ + X(csParam.close.ulpTimeoutValue, 32) \ + X(csParam.close.ulpTimeoutAction, 33) \ + X(csParam.close.validityFlags, 34) \ + X(csParam.close.userDataPtr, 36) \ + /* TCPAbort */ \ + X(csParam.abort.userDataPtr, 32) \ + /* TCPStatus. connStatPtr at 94 is absent from the Guide's \ + * parameter table; leaving it out shortens the block to 66 and \ + * TCPiopb to 98. */ \ + X(csParam.status.ulpTimeoutValue, 32) \ + X(csParam.status.ulpTimeoutAction, 33) \ + X(csParam.status.remoteHost, 38) \ + X(csParam.status.remotePort, 42) \ + X(csParam.status.localHost, 44) \ + X(csParam.status.localPort, 48) \ + X(csParam.status.tosFlags, 50) \ + X(csParam.status.precedence, 51) \ + X(csParam.status.connectionState, 52) \ + X(csParam.status.sendWindow, 54) \ + X(csParam.status.rcvWindow, 56) \ + X(csParam.status.amtUnackedData, 58) \ + X(csParam.status.amtUnreadData, 60) \ + X(csParam.status.securityLevelPtr, 62) \ + X(csParam.status.sendUnacked, 66) \ + X(csParam.status.sendNext, 70) \ + X(csParam.status.congestionWindow, 74) \ + X(csParam.status.rcvNext, 78) \ + X(csParam.status.srtt, 82) \ + X(csParam.status.lastRTT, 86) \ + X(csParam.status.sendMaxSegSize, 90) \ + X(csParam.status.connStatPtr, 94) \ + X(csParam.status.userDataPtr, 98) + +#define MACTCP_GETADDR_FIELDS(X) \ + X(ourAddress, 28) \ + X(ourNetMask, 32) + +/* Compile-time is the strongest form: a mismatch stops the build + * rather than waiting for someone to run the suite. */ +#define ASSERT_IOPB_OFFSET(field, expected) \ + static_assert(offsetof(TCPiopb, field) == (expected), "TCPiopb::" #field); +MACTCP_IOPB_FIELDS(ASSERT_IOPB_OFFSET) +#undef ASSERT_IOPB_OFFSET + +#define ASSERT_GETADDR_OFFSET(field, expected) \ + static_assert(offsetof(GetAddrParamBlock, field) == (expected), \ + "GetAddrParamBlock::" #field); +MACTCP_GETADDR_FIELDS(ASSERT_GETADDR_OFFSET) +#undef ASSERT_GETADDR_OFFSET + +static_assert(sizeof(TCPiopb) == 102, "TCPiopb size"); +static_assert(sizeof(GetAddrParamBlock) == 36, "GetAddrParamBlock size"); +static_assert(sizeof(wdsEntry) == 6, "wdsEntry size"); +static_assert(sizeof(rdsEntry) == 6, "rdsEntry size"); +static_assert(sizeof(ICMPReport) == 24, "ICMPReport size"); + +/* The same checks again at runtime. Compile-time assertions stop at + * the first failure and print no numbers; these report every + * discrepancy with both values, which is what you want when diffing + * our headers against Apple's. */ +TEST(MacTCPABI, TCPiopbFieldOffsets) +{ +#define CHECK_IOPB_OFFSET(field, expected) \ + EXPECT_EQ((size_t)(expected), offsetof(TCPiopb, field)) << "TCPiopb::" #field; + MACTCP_IOPB_FIELDS(CHECK_IOPB_OFFSET) +#undef CHECK_IOPB_OFFSET +} + +TEST(MacTCPABI, GetAddrParamBlockFieldOffsets) +{ +#define CHECK_GETADDR_OFFSET(field, expected) \ + EXPECT_EQ((size_t)(expected), offsetof(GetAddrParamBlock, field)) \ + << "GetAddrParamBlock::" #field; + MACTCP_GETADDR_FIELDS(CHECK_GETADDR_OFFSET) +#undef CHECK_GETADDR_OFFSET +} + +TEST(MacTCPABI, StructSizes) +{ + EXPECT_EQ(102u, (unsigned)sizeof(TCPiopb)); + EXPECT_EQ(36u, (unsigned)sizeof(GetAddrParamBlock)); + EXPECT_EQ(6u, (unsigned)sizeof(wdsEntry)); + EXPECT_EQ(6u, (unsigned)sizeof(rdsEntry)); + EXPECT_EQ(24u, (unsigned)sizeof(ICMPReport)); + + /* csParam covers its largest member, TCPStatusPB at 70 bytes, + * which is what makes TCPiopb 102. TCPOpenPB is 66. */ + EXPECT_EQ(70u, (unsigned)(sizeof(TCPiopb) - 32)); +} + +/* Result codes are as much a part of the ABI as the layouts: an + * application switching on them cares about the exact values. */ +TEST(MacTCPABI, ResultCodes) +{ + EXPECT_EQ(1, (int)inProgress); + EXPECT_EQ(-23000, (int)ipBadLapErr); + EXPECT_EQ(-23001, (int)ipBadCnfgErr); + EXPECT_EQ(-23002, (int)ipNoCnfgErr); + EXPECT_EQ(-23003, (int)ipLoadErr); + EXPECT_EQ(-23004, (int)ipBadAddr); + EXPECT_EQ(-23005, (int)connectionClosing); + EXPECT_EQ(-23006, (int)invalidLength); + EXPECT_EQ(-23007, (int)connectionExists); + EXPECT_EQ(-23008, (int)connectionDoesntExist); + EXPECT_EQ(-23009, (int)insufficientResources); + EXPECT_EQ(-23010, (int)invalidStreamPtr); + EXPECT_EQ(-23011, (int)streamAlreadyOpen); + EXPECT_EQ(-23012, (int)connectionTerminated); + EXPECT_EQ(-23013, (int)invalidBufPtr); + EXPECT_EQ(-23014, (int)invalidRDS); + EXPECT_EQ(-23014, (int)invalidWDS); + EXPECT_EQ(-23015, (int)openFailed); + EXPECT_EQ(-23016, (int)commandTimeout); + EXPECT_EQ(-23017, (int)duplicateSocket); + EXPECT_EQ(-23041, (int)nameSyntaxErr); + EXPECT_EQ(-23042, (int)cacheFault); + EXPECT_EQ(-23043, (int)noResultProc); + EXPECT_EQ(-23044, (int)noNameServer); + EXPECT_EQ(-23045, (int)authNameErr); + EXPECT_EQ(-23046, (int)noAnsErr); + EXPECT_EQ(-23047, (int)dnrErr); +} + +TEST(MacTCPABI, CsCodes) +{ + EXPECT_EQ(15, (int)ipctlGetAddr); + EXPECT_EQ(30, (int)TCPCreate); + EXPECT_EQ(31, (int)TCPPassiveOpen); + EXPECT_EQ(32, (int)TCPActiveOpen); + EXPECT_EQ(34, (int)TCPSend); + EXPECT_EQ(35, (int)TCPNoCopyRcv); + EXPECT_EQ(36, (int)TCPRcvBfrReturn); + EXPECT_EQ(37, (int)TCPRcv); + EXPECT_EQ(38, (int)TCPClose); + EXPECT_EQ(39, (int)TCPAbort); + EXPECT_EQ(40, (int)TCPStatus); + EXPECT_EQ(41, (int)TCPExtendedStat); + EXPECT_EQ(42, (int)TCPRelease); + EXPECT_EQ(43, (int)TCPGlobalInfo); +} + +TEST(MacTCPABI, ConnectionStates) +{ + /* All even; the odd values were never used. */ + EXPECT_EQ(0, (int)TCPSClosed); + EXPECT_EQ(2, (int)TCPSListen); + EXPECT_EQ(4, (int)TCPSSynReceived); + EXPECT_EQ(6, (int)TCPSSynSent); + EXPECT_EQ(8, (int)TCPSEstablished); + EXPECT_EQ(10, (int)TCPSFinWait1); + EXPECT_EQ(12, (int)TCPSFinWait2); + EXPECT_EQ(14, (int)TCPSCloseWait); + EXPECT_EQ(16, (int)TCPSClosing); + EXPECT_EQ(18, (int)TCPSLastAck); + EXPECT_EQ(20, (int)TCPSTimeWait); +} + +/* A diffable dump. Run this on both sides -- natively, and as the + * Retro68 application against Apple's headers -- and diff the two + * transcripts. Where the assertions above only say "wrong", this says + * what the other side actually thinks the layout is. + * + * ./tests --gtest_filter=MacTCPABI.DumpLayout > ours.txt + */ +TEST(MacTCPABI, DumpLayout) +{ + printf("sizeof(TCPiopb) = %u\n", (unsigned)sizeof(TCPiopb)); + printf("sizeof(GetAddrParamBlock) = %u\n", + (unsigned)sizeof(GetAddrParamBlock)); + printf("sizeof(wdsEntry) = %u\n", (unsigned)sizeof(wdsEntry)); + printf("sizeof(rdsEntry) = %u\n", (unsigned)sizeof(rdsEntry)); + printf("sizeof(ICMPReport) = %u\n", (unsigned)sizeof(ICMPReport)); + +#define DUMP_IOPB_OFFSET(field, expected) \ + printf("TCPiopb.%-40s = %u\n", #field, (unsigned)offsetof(TCPiopb, field)); + MACTCP_IOPB_FIELDS(DUMP_IOPB_OFFSET) +#undef DUMP_IOPB_OFFSET + +#define DUMP_GETADDR_OFFSET(field, expected) \ + printf("GetAddrParamBlock.%-31s = %u\n", #field, \ + (unsigned)offsetof(GetAddrParamBlock, field)); + MACTCP_GETADDR_FIELDS(DUMP_GETADDR_OFFSET) +#undef DUMP_GETADDR_OFFSET +} + +#else /* !MACTCP_ABI_HAVE_HEADER */ + +TEST(MacTCPABI, DISABLED_NoMacTCPHeader) +{ +} + +#endif