From aba40a53532fcb49a6d49dafea8646b077bc6c29 Mon Sep 17 00:00:00 2001 From: Maksim Tiushev Date: Tue, 14 Jan 2025 14:09:47 +0000 Subject: [PATCH 1/3] WASI: VirtualSocket interface This patch provides core socket operations such as creating, binding, connecting, listening, sending, and receiving data, configuring socket options (broadcast and reuse) and retrieving peer and socket names. This interface is specific to the WASI Wasm and is not supported for JS or non-WASI Wasm. --- .../org/teavm/backend/wasm/WasmTarget.java | 8 +- .../wasm/runtime/net/WasiVirtualSocket.java | 430 ++++++++++++++++++ .../wasm/runtime/net/impl/AddrInfo.java | 125 +++++ .../wasm/runtime/net/impl/AddrInfoHints.java | 64 +++ .../wasm/runtime/net/impl/SockAddrInet4.java | 106 +++++ .../wasm/runtime/net/impl/SockAddrInet6.java | 125 +++++ .../WasiSocketProviderTransformer.java | 30 ++ .../org/teavm/backend/wasm/wasi/Wasi.java | 96 ++++ .../java/org/teavm/runtime/net/SockAddr.java | 30 ++ .../org/teavm/runtime/net/VirtualSocket.java | 74 +++ .../runtime/net/VirtualSocketProvider.java | 42 ++ .../net/VirtualSocketProviderTransformer.java | 36 ++ .../java/org/teavm/interop/Platforms.java | 2 + 13 files changed, 1167 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/org/teavm/backend/wasm/runtime/net/WasiVirtualSocket.java create mode 100644 core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/AddrInfo.java create mode 100644 core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/AddrInfoHints.java create mode 100644 core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/SockAddrInet4.java create mode 100644 core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/SockAddrInet6.java create mode 100644 core/src/main/java/org/teavm/backend/wasm/transformation/WasiSocketProviderTransformer.java create mode 100644 core/src/main/java/org/teavm/runtime/net/SockAddr.java create mode 100644 core/src/main/java/org/teavm/runtime/net/VirtualSocket.java create mode 100644 core/src/main/java/org/teavm/runtime/net/VirtualSocketProvider.java create mode 100644 core/src/main/java/org/teavm/runtime/net/VirtualSocketProviderTransformer.java diff --git a/core/src/main/java/org/teavm/backend/wasm/WasmTarget.java b/core/src/main/java/org/teavm/backend/wasm/WasmTarget.java index c94060dbd4..71df84054e 100644 --- a/core/src/main/java/org/teavm/backend/wasm/WasmTarget.java +++ b/core/src/main/java/org/teavm/backend/wasm/WasmTarget.java @@ -119,6 +119,7 @@ import org.teavm.backend.wasm.transformation.IndirectCallTraceTransformation; import org.teavm.backend.wasm.transformation.MemoryAccessTraceTransformation; import org.teavm.backend.wasm.transformation.WasiFileSystemProviderTransformer; +import org.teavm.backend.wasm.transformation.WasiSocketProviderTransformer; import org.teavm.backend.wasm.transformation.WasiSupportClassTransformer; import org.teavm.backend.wasm.transformation.WasmExceptionHandlingTransform; import org.teavm.common.ServiceRepository; @@ -247,6 +248,7 @@ public List getTransformers() { if (runtimeType == WasmRuntimeType.WASI) { transformers.add(new WasiSupportClassTransformer()); transformers.add(new WasiFileSystemProviderTransformer()); + transformers.add(new WasiSocketProviderTransformer()); } if (exceptionsUsed) { transformers.add(new WasmExceptionHandlingTransform()); @@ -1081,7 +1083,11 @@ private VirtualTableProvider createVirtualTableProvider(ListableClassHolderSourc @Override public String[] getPlatformTags() { - return new String[] { Platforms.WEBASSEMBLY, Platforms.LOW_LEVEL }; + return new String[] { + Platforms.WEBASSEMBLY, + runtimeType == WasmRuntimeType.TEAVM ? Platforms.WEBASSEMBLY_BROWSER : Platforms.WEBASSEMBLY_WASI, + Platforms.LOW_LEVEL + }; } @Override diff --git a/core/src/main/java/org/teavm/backend/wasm/runtime/net/WasiVirtualSocket.java b/core/src/main/java/org/teavm/backend/wasm/runtime/net/WasiVirtualSocket.java new file mode 100644 index 0000000000..6f677062e7 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/runtime/net/WasiVirtualSocket.java @@ -0,0 +1,430 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.backend.wasm.runtime.net; + +import static org.teavm.backend.wasm.wasi.Wasi.AF_INET; +import static org.teavm.backend.wasm.wasi.Wasi.AF_INET6; +import static org.teavm.backend.wasm.wasi.Wasi.AF_UNIX; +import static org.teavm.backend.wasm.wasi.Wasi.SOCK_ANY; +import static org.teavm.backend.wasm.wasi.Wasi.SOCK_DGRAM; +import static org.teavm.backend.wasm.wasi.Wasi.SOCK_STREAM; +import java.net.SocketException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import org.teavm.backend.wasm.runtime.WasiBuffer; +import org.teavm.backend.wasm.runtime.net.impl.*; +import org.teavm.backend.wasm.wasi.Wasi; +import org.teavm.interop.Address; +import org.teavm.interop.Structure; +import org.teavm.runtime.net.SockAddr; +import org.teavm.runtime.net.VirtualSocket; + +public class WasiVirtualSocket implements VirtualSocket { + + private static final int MAX_RESOLVED_ADDRESSES = 16; + private static final int ADDR_SIZE = 22; + private static final int IPV4_ADDR_SIZE = 4; + private static final int IPV6_ADDR_SIZE = 8; + private static final int ADDR_INFO_BUFFER_SIZE = AddrInfo.getBufferSize(); + private static final int ADDR_INFO_ADDR_BUFFER_SIZE = AddrInfo.getAddrBufferSize(); + public static final int HINTS_ENABLED = 1; + public static final int HINTS_DISABLED = 2; + + public static class CIOVec extends Structure { + public int address; + public int len; + } + + @Override + public int socket(int proto, int sotype) throws SocketException { + validateProto(proto); + validateSotype(sotype); + + int[] newFd = new int[1]; + int errno = Wasi.sockOpen(0, proto, sotype, Address.ofData(newFd)); + if (errno != 0) { + throw new SocketException("socket: " + errno); + } + return newFd[0]; + } + + @Override + public void connect(int fd, SockAddr sa) throws SocketException { + try { + Address rawAddr = sa.sockAddr(); + int errno = Wasi.sockConnect(fd, rawAddr); + if (errno != 0) { + throw new SocketException("сonnect: " + errno); + } + } catch (Exception e) { + throw new SocketException("Failed to get SockAddr: " + e.getMessage()); + } + } + + @Override + public void bind(int fd, SockAddr sa) throws SocketException { + try { + Address rawAddr = sa.sockAddr(); + int errno = Wasi.sockBind(fd, rawAddr); + if (errno != 0) { + throw new SocketException("bind: " + errno); + } + } catch (Exception e) { + throw new SocketException("Failed to get SockAddr: " + e.getMessage()); + } + } + + @Override + public void listen(int fd, int backlog) throws SocketException { + int errno = Wasi.sockListen(fd, backlog); + if (errno != 0) { + throw new SocketException("listen: " + errno); + } + } + + @Override + public int sendTo(int fd, byte[] buf, int len, SockAddr sa) throws SocketException { + Address rawAddr; + + try { + rawAddr = sa.sockAddr(); + } catch (Exception e) { + throw new SocketException("Failed to get SockAddr: " + e.getMessage()); + } + + Address argsAddress = WasiBuffer.getBuffer(); + argsAddress = Address.align(argsAddress.add(sa.sockAddrLen()), 16); + CIOVec s = argsAddress.toStructure(); + s.address = (int) Address.ofData(buf).toLong(); + s.len = len; + + int[] dataLen = new int[1]; + int errno = Wasi.sockSendTo(fd, argsAddress, 1, 0, rawAddr, Address.ofData(dataLen)); + + if (errno != 0) { + throw new SocketException("send_to: " + errno); + } + return dataLen[0]; + } + + @Override + public int recvFrom(int fd, byte[] buf, int len, SockAddr sa) throws SocketException { + byte[] addr = new byte[ADDR_SIZE]; + Address rawAddr = Address.ofData(addr); + + Address argsAddress = WasiBuffer.getBuffer(); + CIOVec s = argsAddress.toStructure(); + s.address = (int) Address.ofData(buf).toLong(); + s.len = len; + + int[] dataLen = new int[1]; + + int errno = Wasi.sockRecvFrom(fd, argsAddress, 1, 0, rawAddr, Address.ofData(dataLen)); + if (errno != 0) { + throw new SocketException("recv_from: " + errno); + } + return dataLen[0]; + } + + @Override + public void shutdown(int fd, int how) throws SocketException { + int errno = Wasi.sockShutdown(fd, how); + if (errno != 0) { + throw new SocketException("shutdown: " + errno); + } + } + + @Override + public int accept(int fd, int flags) throws SocketException { + int[] newFd = new int[1]; + int errno = Wasi.sockAccept(fd, flags, Address.ofData(newFd)); + if (errno != 0) { + throw new SocketException("accept: " + errno); + } + return newFd[0]; + } + + @Override + public SockAddr getSockName(int fd) throws SocketException { + byte[] addr = new byte[ADDR_SIZE]; + int errno = Wasi.sockAddrLocal(fd, Address.ofData(addr)); + if (errno != 0) { + throw new SocketException("get_sock_name: " + errno); + } + return parseSockAddr(addr); + } + + @Override + public SockAddr getPeerName(int fd) throws SocketException { + byte[] addr = new byte[ADDR_SIZE]; + int errno = Wasi.sockAddrRemote(fd, Address.ofData(addr)); + if (errno != 0) { + throw new SocketException("get_peer_name: " + errno); + } + return parseSockAddr(addr); + } + + @Override + public void setSockBroadcast(int fd, int value) throws SocketException { + int errno = Wasi.sockSetBroadcast(fd, value); + if (errno != 0) { + throw new SocketException("sockopt_set_broadcast: " + errno); + } + } + + public SockAddr[] getAddrInfo( + String name, String service, int proto, int sotype, int hintsEnabled) + throws SocketException { + byte[] nameNT = toByteArrayWithNullTerminator(name); + byte[] serviceNT = toByteArrayWithNullTerminator(service); + + if (hintsEnabled == HINTS_ENABLED) { + validateProto(proto); + validateSotype(sotype); + } + + AddrInfoHints hints = new AddrInfoHints(sotype, proto, hintsEnabled); + byte[] resultBuffer = new byte[ADDR_INFO_BUFFER_SIZE * MAX_RESOLVED_ADDRESSES]; + int[] resolvedCount = new int[1]; + + int errno = Wasi.sockAddrResolve( + Address.ofData(nameNT), + Address.ofData(serviceNT), + hints.getAddress(), + Address.ofData(resultBuffer), + resultBuffer.length, + Address.ofData(resolvedCount)); + + if (errno != 0) { + throw new SocketException("get_addr_info: " + errno); + } + + SockAddr[] addresses = new SockAddr[resolvedCount[0]]; + + for (int i = 0; i < resolvedCount[0]; i++) { + ByteBuffer buffer = + ByteBuffer.wrap(resultBuffer, i * ADDR_INFO_BUFFER_SIZE, ADDR_INFO_BUFFER_SIZE) + .order(ByteOrder.nativeOrder()); + + int sockKind = buffer.getInt(); + byte[] addrBuf = new byte[ADDR_INFO_ADDR_BUFFER_SIZE]; + buffer.get(addrBuf); + int sockType = buffer.getInt(); + + AddrInfo addrInfo = new AddrInfo(sockKind, addrBuf, sockType); + addresses[i] = parseSockAddr(addrInfoToRaw(addrInfo)); + } + + return addresses; + } + + @Override + public int getSockKeepAlive(int fd) throws SocketException { + int[] res = new int[1]; + int errno = Wasi.sockGetKeepAlive(fd, Address.ofData(res)); + if (errno != 0) { + throw new SocketException("sock_get_keep_alive: " + errno); + } + return res[0]; + } + + @Override + public void setSockKeepAlive(int fd, int value) throws SocketException { + int errno = Wasi.sockSetKeepAlive(fd, value); + if (errno != 0) { + throw new SocketException("sock_set_keep_alive: " + errno); + } + } + + @Override + public int getSockReuseAddr(int fd) throws SocketException { + int[] res = new int[1]; + int errno = Wasi.sockGetReuseAddr(fd, Address.ofData(res)); + if (errno != 0) { + throw new SocketException("sock_get_reuse_addr: " + errno); + } + return res[0]; + } + + @Override + public void setSockReuseAddr(int fd, int value) throws SocketException { + int errno = Wasi.sockSetReuseAddr(fd, value); + if (errno != 0) { + throw new SocketException("sockopt_set_reuse_addr: " + errno); + } + } + + @Override + public int getSockRecvBufSize(int fd) throws SocketException { + int[] res = new int[1]; + int errno = Wasi.sockGetRecvBufSize(fd, Address.ofData(res)); + if (errno != 0) { + throw new SocketException("sock_get_recv_buf_size: " + errno); + } + return res[0]; + } + + @Override + public void setSockRecvBufSize(int fd, int size) throws SocketException { + int errno = Wasi.sockSetRecvBufSize(fd, size); + if (errno != 0) { + throw new SocketException("sock_set_recv_buf_size: " + errno); + } + } + + @Override + public int getSockSendBufSize(int fd) throws SocketException { + int[] res = new int[1]; + int errno = Wasi.sockGetSendBufSize(fd, Address.ofData(res)); + if (errno != 0) { + throw new SocketException("sock_get_send_buf_size: " + errno); + } + return res[0]; + } + + @Override + public void setSockSendBufSize(int fd, int size) throws SocketException { + int errno = Wasi.sockSetSendBufSize(fd, size); + if (errno != 0) { + throw new SocketException("sock_set_send_buf_size: " + errno); + } + } + + @Override + public int getSockLinger(int fd) throws SocketException { + int[] isEnabled = new int[1]; + int[] linger = new int[1]; + int errno = Wasi.sockGetLinger(fd, Address.ofData(isEnabled), Address.ofData(linger)); + if (errno != 0) { + throw new SocketException("sock_get_linger: " + errno); + } + if (isEnabled[0] == 0) { + return linger[0]; + } + return -1; + } + + @Override + public void setSockLinger(int fd, int value, int linger) throws SocketException { + int errno = Wasi.sockSetLinger(fd, value, linger); + if (errno != 0) { + throw new SocketException("sock_set_linger: " + errno); + } + } + + @Override + public int getSockRecvTimeout(int fd) throws SocketException { + int[] res = new int[1]; + int errno = Wasi.sockGetRecvTimeout(fd, Address.ofData(res)); + if (errno != 0) { + throw new SocketException("sock_get_recv_timeout: " + errno); + } + return res[0] / 1000; + } + + @Override + public void setSockRecvTimeout(int fd, int value) throws SocketException { + int errno = Wasi.sockSetRecvTimeout(fd, value * 1000); + if (errno != 0) { + throw new SocketException("sock_set_recv_timeout: " + errno); + } + } + + @Override + public int getSockTcpNoDelay(int fd) throws SocketException { + int[] res = new int[1]; + int errno = Wasi.sockGetTcpNoDelay(fd, Address.ofData(res)); + if (errno != 0) { + throw new SocketException("sock_get_tcp_no_delay: " + errno); + } + return res[0]; + } + + @Override + public void setSockTcpNoDelay(int fd, int value) throws SocketException { + int errno = Wasi.sockSetTcpNoDelay(fd, value); + if (errno != 0) { + throw new SocketException("sock_set_tcp_no_delay: " + errno); + } + } + + // ============================== Helpers ============================== + + private void validateProto(int proto) throws SocketException { + if (!isValidProto(proto)) { + throw new SocketException("Invalid protocol type: " + proto); + } + } + + private void validateSotype(int sotype) throws SocketException { + if (!isValidSotype(sotype)) { + throw new SocketException("Invalid socket type: " + sotype); + } + } + + private boolean isValidProto(int proto) { + return proto == AF_INET || proto == AF_INET6 || proto == AF_UNIX; + } + + private boolean isValidSotype(int sotype) { + return sotype == SOCK_ANY || sotype == SOCK_DGRAM || sotype == SOCK_STREAM; + } + + public static byte[] toByteArrayWithNullTerminator(String input) { + if (input == null) { + return new byte[] {0}; + } + + byte[] originalBytes = input.getBytes(); + byte[] result = new byte[originalBytes.length + 1]; + System.arraycopy(originalBytes, 0, result, 0, originalBytes.length); + result[result.length - 1] = 0; + return result; + } + + private static SockAddr parseSockAddr(byte[] data) throws SocketException { + if (data.length != ADDR_SIZE) { + throw new SocketException( + "Expected " + ADDR_SIZE + " bytes of data, but got " + data.length); + } + + ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder()); + int kind = buffer.getInt(); + + if (kind == AF_INET) { + byte[] addr = new byte[IPV4_ADDR_SIZE]; + buffer.get(addr); + short port = buffer.getShort(); + return new SockAddrInet4(addr, port); + } else if (kind == AF_INET6) { + short[] addr = new short[IPV6_ADDR_SIZE]; + for (int i = 0; i < IPV6_ADDR_SIZE; i++) { + addr[i] = buffer.getShort(); + } + short port = buffer.getShort(); + return new SockAddrInet6(addr, port); + } else { + throw new SocketException("Unknown address family: " + kind); + } + } + + private byte[] addrInfoToRaw(AddrInfo addrInfo) { + ByteBuffer buffer = ByteBuffer.allocate(ADDR_SIZE).order(ByteOrder.nativeOrder()); + buffer.putInt(addrInfo.getSockKind()); + buffer.put(addrInfo.getAddrBuf()); + return buffer.array(); + } +} diff --git a/core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/AddrInfo.java b/core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/AddrInfo.java new file mode 100644 index 0000000000..1efcc7bda6 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/AddrInfo.java @@ -0,0 +1,125 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.backend.wasm.runtime.net.impl; + +import org.teavm.backend.wasm.runtime.WasiBuffer; +import org.teavm.interop.Address; +import org.teavm.interop.Structure; + +public class AddrInfo { + private static final int ADDR_BUF_SIZE = 18; + + public static class AddrInfoStruct extends Structure { + public int sockKind; + public byte b0; + public byte b1; + public byte b2; + public byte b3; + public byte b4; + public byte b5; + public byte b6; + public byte b7; + public byte b8; + public byte b9; + public byte b10; + public byte b11; + public byte b12; + public byte b13; + public byte b14; + public byte b15; + public byte b16; + public byte b17; + public int sockType; + } + + private final int sockKind; + private final byte[] addrBuf; + private final int sockType; + + public AddrInfo() { + this(0, new byte[ADDR_BUF_SIZE], 0); + } + + public AddrInfo(int sockKind, byte[] addrBuf, int sockType) { + if (addrBuf == null || addrBuf.length != ADDR_BUF_SIZE) { + throw new IllegalArgumentException( + "addrBuf must be exactly " + ADDR_BUF_SIZE + " bytes long."); + } + this.sockKind = sockKind; + this.addrBuf = addrBuf.clone(); + this.sockType = sockType; + } + + public static int getBufferSize() { + return Structure.sizeOf(AddrInfoStruct.class); + } + + public static int getAddrBufferSize() { + return ADDR_BUF_SIZE; + } + + public int getSockKind() { + return sockKind; + } + + public byte[] getAddrBuf() { + return addrBuf.clone(); + } + + public int getSockType() { + return sockType; + } + + public Address getAddress() { + Address argsAddress = WasiBuffer.getBuffer(); + AddrInfoStruct s = argsAddress.toStructure(); + s.sockKind = sockKind; + s.b0 = addrBuf[0]; + s.b1 = addrBuf[1]; + s.b2 = addrBuf[2]; + s.b3 = addrBuf[3]; + s.b4 = addrBuf[4]; + s.b5 = addrBuf[5]; + s.b6 = addrBuf[6]; + s.b7 = addrBuf[7]; + s.b8 = addrBuf[8]; + s.b9 = addrBuf[9]; + s.b10 = addrBuf[10]; + s.b11 = addrBuf[11]; + s.b12 = addrBuf[12]; + s.b13 = addrBuf[13]; + s.b14 = addrBuf[14]; + s.b15 = addrBuf[15]; + s.b16 = addrBuf[16]; + s.b17 = addrBuf[17]; + s.sockType = sockType; + return argsAddress; + } + + @Override + public String toString() { + StringBuilder addrBufString = new StringBuilder(); + for (byte b : addrBuf) { + if (addrBufString.length() > 0) { + addrBufString.append(" "); + } + addrBufString.append(String.format("%02X", b)); + } + return "AddrInfo{sockKind=" + sockKind + + ", addrBuf=[" + addrBufString + + "], sockType=" + sockType + "}"; + } +} diff --git a/core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/AddrInfoHints.java b/core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/AddrInfoHints.java new file mode 100644 index 0000000000..4dd4c4c140 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/AddrInfoHints.java @@ -0,0 +1,64 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.backend.wasm.runtime.net.impl; + +import org.teavm.backend.wasm.runtime.WasiBuffer; +import org.teavm.interop.Address; +import org.teavm.interop.Structure; + +public class AddrInfoHints { + private final int type; + private final int family; + private final int hintsEnabled; + + public static class AddrInfoHintsStruct extends Structure { + public int type; + public int family; + public int hintsEnabled; + } + + public AddrInfoHints(int type, int family, int hintsEnabled) { + this.type = type; + this.family = family; + this.hintsEnabled = hintsEnabled; + } + + public int getType() { + return type; + } + + public int getFamily() { + return family; + } + + public int getHintsEnabled() { + return hintsEnabled; + } + + public Address getAddress() { + Address argsAddress = WasiBuffer.getBuffer(); + AddrInfoHintsStruct s = argsAddress.toStructure(); + s.type = type; + s.family = family; + s.hintsEnabled = hintsEnabled; + return argsAddress; + } + + @Override + public String toString() { + return "AddrInfoHints{type=" + type + ", family=" + family + ", hintsEnabled=" + hintsEnabled + "}"; + } +} diff --git a/core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/SockAddrInet4.java b/core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/SockAddrInet4.java new file mode 100644 index 0000000000..ecdea48fd6 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/SockAddrInet4.java @@ -0,0 +1,106 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.backend.wasm.runtime.net.impl; + +import static org.teavm.backend.wasm.wasi.Wasi.INET4; +import org.teavm.backend.wasm.runtime.WasiBuffer; +import org.teavm.interop.Address; +import org.teavm.interop.Structure; +import org.teavm.runtime.net.SockAddr; + +public class SockAddrInet4 implements SockAddr { + private final byte[] addr; + private final int port; + + public static class SockAddrInet4Struct extends Structure { + public int family; + public byte a0; + public byte a1; + public byte a2; + public byte a3; + public short port; + } + + public SockAddrInet4(byte[] addr, int port) { + validateIPv4Address(addr); + this.addr = addr; + this.port = validatePort(port & 0xFFFF); + } + + public SockAddrInet4(int addr, int port) { + this(intToIPv4(addr), port); + } + + private static byte[] intToIPv4(int addr) { + return new byte[] { + (byte) ((addr >> 24) & 0xFF), + (byte) ((addr >> 16) & 0xFF), + (byte) ((addr >> 8) & 0xFF), + (byte) (addr & 0xFF) + }; + } + + private static void validateIPv4Address(byte[] addr) { + if (addr.length != 4) { + throw new IllegalArgumentException("IPv4 address must be exactly 4 bytes long."); + } + } + + private static int validatePort(int port) { + if (port < 0 || port > 65535) { + throw new IllegalArgumentException("Port number out of range: " + port); + } + return port; + } + + @Override + public Address sockAddr() { + Address argsAddress = WasiBuffer.getBuffer(); + SockAddrInet4Struct s = argsAddress.toStructure(); + s.family = INET4; + s.a0 = addr[0]; + s.a1 = addr[1]; + s.a2 = addr[2]; + s.a3 = addr[3]; + s.port = (short) port; + return argsAddress; + } + + @Override + public int sockPort() { + return port; + } + + @Override + public int sockFamily() { + return INET4; + } + + public byte[] getAddr() { + return addr; + } + + @Override + public String toString() { + return (addr[0] & 0xFF) + "." + (addr[1] & 0xFF) + "." + (addr[2] & 0xFF) + "." + + (addr[3] & 0xFF) + ":" + port; + } + + @Override + public int sockAddrLen() { + return Structure.sizeOf(SockAddrInet4Struct.class); + } +} diff --git a/core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/SockAddrInet6.java b/core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/SockAddrInet6.java new file mode 100644 index 0000000000..4f4f954482 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/SockAddrInet6.java @@ -0,0 +1,125 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.backend.wasm.runtime.net.impl; + +import static org.teavm.backend.wasm.wasi.Wasi.INET6; +import org.teavm.backend.wasm.runtime.WasiBuffer; +import org.teavm.interop.Address; +import org.teavm.interop.Structure; +import org.teavm.runtime.net.SockAddr; + +public class SockAddrInet6 implements SockAddr { + private final short[] addr; + private final int port; + + public static class SockAddrInet6Struct extends Structure { + public int family; + public short a0; + public short a1; + public short a2; + public short a3; + public short a4; + public short a5; + public short a6; + public short a7; + public short port; + } + + public SockAddrInet6(short[] addr, int port) { + validateIPv6Address(addr); + this.addr = addr; + this.port = validatePort(port & 0xFFFF); + } + + public SockAddrInet6(byte[] addr, int port) { + if (addr == null || addr.length != 16) { + throw new IllegalArgumentException("Invalid IPv6 address. Expected a 16-byte array."); + } + short[] shortAddr = new short[8]; + for (int i = 0; i < 8; i++) { + shortAddr[i] = (short) (((addr[2 * i] & 0xFF) << 8) | (addr[2 * i + 1] & 0xFF)); + } + validateIPv6Address(shortAddr); + this.addr = shortAddr; + this.port = validatePort(port & 0xFFFF); + } + + private static void validateIPv6Address(short[] addr) { + if (addr.length != 8) { + throw new IllegalArgumentException("IPv6 address must be exactly 16 bytes long."); + } + } + + private static int validatePort(int port) { + if (port < 0 || port > 65535) { + throw new IllegalArgumentException("Port number out of range: " + port); + } + return port; + } + + @Override + public Address sockAddr() { + Address argsAddress = WasiBuffer.getBuffer(); + SockAddrInet6Struct s = argsAddress.toStructure(); + s.family = INET6; + s.a0 = addr[0]; + s.a1 = addr[1]; + s.a2 = addr[2]; + s.a3 = addr[3]; + s.a4 = addr[4]; + s.a5 = addr[5]; + s.a6 = addr[6]; + s.a7 = addr[7]; + s.port = (short) port; + return argsAddress; + } + + @Override + public int sockPort() { + return port; + } + + @Override + public int sockFamily() { + return INET6; + } + + public byte[] getAddr() { + byte[] byteArray = new byte[addr.length * 2]; + for (int i = 0; i < addr.length; i++) { + byteArray[i * 2] = (byte) ((addr[i] >> 8) & 0xFF); + byteArray[i * 2 + 1] = (byte) (addr[i] & 0xFF); + } + return byteArray; + } + + @Override + public String toString() { + StringBuilder ipBuilder = new StringBuilder(); + for (int i = 0; i < addr.length; i++) { + ipBuilder.append(String.format("%04X", addr[i] & 0xFFFF)); + if (i < addr.length - 1) { + ipBuilder.append(":"); + } + } + return "[" + ipBuilder + "]:" + sockPort(); + } + + @Override + public int sockAddrLen() { + return Structure.sizeOf(SockAddrInet6Struct.class); + } +} diff --git a/core/src/main/java/org/teavm/backend/wasm/transformation/WasiSocketProviderTransformer.java b/core/src/main/java/org/teavm/backend/wasm/transformation/WasiSocketProviderTransformer.java new file mode 100644 index 0000000000..9a2dd2a4de --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/transformation/WasiSocketProviderTransformer.java @@ -0,0 +1,30 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.backend.wasm.transformation; + +import org.teavm.backend.wasm.runtime.net.WasiVirtualSocket; +import org.teavm.model.ClassHolderTransformerContext; +import org.teavm.model.MethodHolder; +import org.teavm.model.emit.ProgramEmitter; +import org.teavm.runtime.net.VirtualSocketProviderTransformer; + +public class WasiSocketProviderTransformer extends VirtualSocketProviderTransformer { + @Override + protected void transformCreateMethod(MethodHolder method, ClassHolderTransformerContext context) { + ProgramEmitter pe = ProgramEmitter.create(method, context.getHierarchy()); + pe.construct(WasiVirtualSocket.class).returnValue(); + } +} diff --git a/core/src/main/java/org/teavm/backend/wasm/wasi/Wasi.java b/core/src/main/java/org/teavm/backend/wasm/wasi/Wasi.java index 0899955ff6..d09443865d 100644 --- a/core/src/main/java/org/teavm/backend/wasm/wasi/Wasi.java +++ b/core/src/main/java/org/teavm/backend/wasm/wasi/Wasi.java @@ -56,6 +56,22 @@ public final class Wasi { public static final byte WHENCE_CURRENT = 1; public static final byte WHENCE_END = 2; + public static final int AF_INET = 0; + public static final int AF_INET6 = 1; + public static final int AF_UNIX = 2; + + public static final int INET4 = 0; + public static final int INET6 = 1; + public static final int INET_UNSPEC = 2; + + public static final int SOCK_ANY = -1; + public static final int SOCK_DGRAM = 0; + public static final int SOCK_STREAM = 1; + + public static final int SHUT_RD = 0; + public static final int SHUT_WR = 1; + public static final int SHUT_RDWR = 2; + private Wasi() { } @@ -132,4 +148,84 @@ public static native short pathFilestatSetTimes(int fd, int lookupFlags, Address @Import(name = "random_get", module = "wasi_snapshot_preview1") public static native short randomGet(Address buffer, int bufferLength); + @Import(name = "sock_open", module = "wasi_snapshot_preview1") + public static native int sockOpen(int fd, int af, int socktype, Address sockfd); + + @Import(name = "sock_bind", module = "wasi_snapshot_preview1") + public static native int sockBind(int fd, Address addr); + + @Import(name = "sock_listen", module = "wasi_snapshot_preview1") + public static native int sockListen(int fd, int backlog); + + @Import(name = "sock_connect", module = "wasi_snapshot_preview1") + public static native int sockConnect(int fd, Address addr); + + @Import(name = "sock_set_reuse_addr", module = "wasi_snapshot_preview1") + public static native int sockSetReuseAddr(int fd, int reuse); + + @Import(name = "sock_set_broadcast", module = "wasi_snapshot_preview1") + public static native int sockSetBroadcast(int fd, int option); + + @Import(name = "sock_addr_local", module = "wasi_snapshot_preview1") + public static native int sockAddrLocal(int fd, Address addr); + + @Import(name = "sock_addr_remote", module = "wasi_snapshot_preview1") + public static native int sockAddrRemote(int fd, Address addr); + + @Import(name = "sock_recv_from", module = "wasi_snapshot_preview1") + public static native int sockRecvFrom(int fd, Address riData, int riDataLen, + int riFlags, Address srcAddr, Address addrLen); + + @Import(name = "sock_send_to", module = "wasi_snapshot_preview1") + public static native int sockSendTo(int fd, Address siData, int siDataLen, + int siFlags, Address destAddr, Address addrLen); + + @Import(name = "sock_addr_resolve", module = "wasi_snapshot_preview1") + public static native int sockAddrResolve(Address node, Address service, Address hints, + Address res, int maxResLen, Address resLen); + + @Import(name = "sock_shutdown", module = "wasi_snapshot_preview1") + public static native int sockShutdown(int fd, int how); + + @Import(name = "sock_accept", module = "wasi_snapshot_preview1") + public static native int sockAccept(int fd, int flags, Address fdNew); + + @Import(name = "sock_get_keep_alive", module = "wasi_snapshot_preview1") + public static native int sockGetKeepAlive(int fd, Address option); + + @Import(name = "sock_set_keep_alive", module = "wasi_snapshot_preview1") + public static native int sockSetKeepAlive(int fd, int option); + + @Import(name = "sock_get_reuse_addr", module = "wasi_snapshot_preview1") + public static native int sockGetReuseAddr(int fd, Address option); + + @Import(name = "sock_get_recv_buf_size", module = "wasi_snapshot_preview1") + public static native int sockGetRecvBufSize(int fd, Address size); + + @Import(name = "sock_set_recv_buf_size", module = "wasi_snapshot_preview1") + public static native int sockSetRecvBufSize(int fd, int size); + + @Import(name = "sock_get_send_buf_size", module = "wasi_snapshot_preview1") + public static native int sockGetSendBufSize(int fd, Address size); + + @Import(name = "sock_set_send_buf_size", module = "wasi_snapshot_preview1") + public static native int sockSetSendBufSize(int fd, int size); + + @Import(name = "sock_get_linger", module = "wasi_snapshot_preview1") + public static native int sockGetLinger(int fd, Address isEnabled, Address linger); + + @Import(name = "sock_set_linger", module = "wasi_snapshot_preview1") + public static native int sockSetLinger(int fd, int isEnabled, int linger); + + @Import(name = "sock_get_recv_timeout", module = "wasi_snapshot_preview1") + public static native int sockGetRecvTimeout(int fd, Address timeout); + + @Import(name = "sock_set_recv_timeout", module = "wasi_snapshot_preview1") + public static native int sockSetRecvTimeout(int fd, int timeout); + + @Import(name = "sock_get_tcp_no_delay", module = "wasi_snapshot_preview1") + public static native int sockGetTcpNoDelay(int fd, Address option); + + @Import(name = "sock_set_tcp_no_delay", module = "wasi_snapshot_preview1") + public static native int sockSetTcpNoDelay(int fd, int option); } diff --git a/core/src/main/java/org/teavm/runtime/net/SockAddr.java b/core/src/main/java/org/teavm/runtime/net/SockAddr.java new file mode 100644 index 0000000000..9ec2cb7e30 --- /dev/null +++ b/core/src/main/java/org/teavm/runtime/net/SockAddr.java @@ -0,0 +1,30 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.runtime.net; + +import java.net.SocketException; +import org.teavm.interop.Address; + +public interface SockAddr { + + Address sockAddr() throws SocketException; + + int sockPort(); + + int sockFamily(); + + int sockAddrLen(); +} diff --git a/core/src/main/java/org/teavm/runtime/net/VirtualSocket.java b/core/src/main/java/org/teavm/runtime/net/VirtualSocket.java new file mode 100644 index 0000000000..040de1c30a --- /dev/null +++ b/core/src/main/java/org/teavm/runtime/net/VirtualSocket.java @@ -0,0 +1,74 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.runtime.net; + +import java.net.SocketException; + +public interface VirtualSocket { + + int socket(int proto, int sotype) throws SocketException; + + void connect(int fd, SockAddr sa) throws SocketException; + + void bind(int fd, SockAddr sa) throws SocketException; + + void listen(int fd, int backlog) throws SocketException; + + int sendTo(int fd, byte[] buf, int len, SockAddr sa) throws SocketException; + + int recvFrom(int fd, byte[] buf, int len, SockAddr sa) throws SocketException; + + void shutdown(int fd, int how) throws SocketException; + + int accept(int fd, int flags) throws SocketException; + + SockAddr getSockName(int fd) throws SocketException; + + SockAddr getPeerName(int fd) throws SocketException; + + void setSockBroadcast(int fd, int value) throws SocketException; + + void setSockReuseAddr(int fd, int value) throws SocketException; + + int getSockReuseAddr(int fd) throws SocketException; + + SockAddr[] getAddrInfo(String name, String service, int proto, int sotype, int hintsEnabled) + throws SocketException; + + int getSockKeepAlive(int fd) throws SocketException; + + void setSockKeepAlive(int fd, int value) throws SocketException; + + int getSockRecvBufSize(int fd) throws SocketException; + + void setSockRecvBufSize(int fd, int size) throws SocketException; + + int getSockSendBufSize(int fd) throws SocketException; + + void setSockSendBufSize(int fd, int size) throws SocketException; + + int getSockLinger(int fd) throws SocketException; + + void setSockLinger(int fd, int value, int linger) throws SocketException; + + int getSockRecvTimeout(int fd) throws SocketException; + + void setSockRecvTimeout(int fd, int value) throws SocketException; + + int getSockTcpNoDelay(int fd) throws SocketException; + + void setSockTcpNoDelay(int fd, int value) throws SocketException; +} diff --git a/core/src/main/java/org/teavm/runtime/net/VirtualSocketProvider.java b/core/src/main/java/org/teavm/runtime/net/VirtualSocketProvider.java new file mode 100644 index 0000000000..eca9fd2740 --- /dev/null +++ b/core/src/main/java/org/teavm/runtime/net/VirtualSocketProvider.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.runtime.net; + +import org.teavm.interop.Platforms; +import org.teavm.interop.SupportedOn; + +public final class VirtualSocketProvider { + private static VirtualSocket instance; + + private VirtualSocketProvider() { + } + + public static VirtualSocket getInstance() { + if (instance == null) { + instance = create(); + } + return instance; + } + + @SupportedOn(Platforms.WEBASSEMBLY_WASI) + private static VirtualSocket create() { + throw new UnsupportedOperationException(); + } + + public static void setInstance(VirtualSocket instance) { + VirtualSocketProvider.instance = instance; + } +} diff --git a/core/src/main/java/org/teavm/runtime/net/VirtualSocketProviderTransformer.java b/core/src/main/java/org/teavm/runtime/net/VirtualSocketProviderTransformer.java new file mode 100644 index 0000000000..66ebc6b456 --- /dev/null +++ b/core/src/main/java/org/teavm/runtime/net/VirtualSocketProviderTransformer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.runtime.net; + +import org.teavm.model.ClassHolder; +import org.teavm.model.ClassHolderTransformer; +import org.teavm.model.ClassHolderTransformerContext; +import org.teavm.model.MethodDescriptor; +import org.teavm.model.MethodHolder; + +public abstract class VirtualSocketProviderTransformer implements ClassHolderTransformer { + @Override + public void transformClass(ClassHolder cls, ClassHolderTransformerContext context) { + if (cls.getName().equals(VirtualSocketProvider.class.getName())) { + MethodHolder method = + cls.getMethod(new MethodDescriptor("create", VirtualSocket.class)); + transformCreateMethod(method, context); + } + } + + protected abstract void transformCreateMethod( + MethodHolder method, ClassHolderTransformerContext context); +} diff --git a/interop/core/src/main/java/org/teavm/interop/Platforms.java b/interop/core/src/main/java/org/teavm/interop/Platforms.java index c752d451a8..84cb019cb6 100644 --- a/interop/core/src/main/java/org/teavm/interop/Platforms.java +++ b/interop/core/src/main/java/org/teavm/interop/Platforms.java @@ -21,6 +21,8 @@ private Platforms() { public static final String JAVASCRIPT = "javascript"; public static final String WEBASSEMBLY = "webassembly"; + public static final String WEBASSEMBLY_BROWSER = "webassembly-browser"; + public static final String WEBASSEMBLY_WASI = "webassembly-wasi"; public static final String C = "c"; public static final String LOW_LEVEL = "low_level"; public static final String WEBASSEMBLY_GC = "webassembly-gc"; From cf13ca167f7d1f8bb1ea997d55d14bf211980fc6 Mon Sep 17 00:00:00 2001 From: Maksim Tiushev Date: Fri, 31 Jan 2025 15:38:20 +0000 Subject: [PATCH 2/3] classlib: implement java.net.Socket This patch introduces the implementation of the java.net.Socket class, providing functionality for creating, connecting, and managing sockets in a WASI-based environment. Additionally, it includes the implementation of several related networking classes: - java.net.ServerSocket - java.net.InetAddress - java.net.Inet4Address - java.net.Inet6Address - java.net.SocketAddress - java.net.InetSocketAddress - java.net.Proxy - java.net.NetworkInterface - java.net.SocketException - java.net.UnknownHostException --- .../classlib/java/net/TInet4Address.java | 83 +++ .../classlib/java/net/TInet6Address.java | 154 +++++ .../teavm/classlib/java/net/TInetAddress.java | 182 ++++++ .../classlib/java/net/TInetSocketAddress.java | 100 ++++ .../classlib/java/net/TNetworkInterface.java | 23 + .../org/teavm/classlib/java/net/TProxy.java | 32 ++ .../classlib/java/net/TServerSocket.java | 155 ++++++ .../org/teavm/classlib/java/net/TSocket.java | 526 ++++++++++++++++++ .../classlib/java/net/TSocketAddress.java | 26 + .../classlib/java/net/TSocketException.java | 35 ++ .../java/net/TUnknownHostException.java | 27 + .../classlib/java/net/InetAddressTest.java | 158 ++++++ .../java/net/InetSocketAddressTest.java | 78 +++ 13 files changed, 1579 insertions(+) create mode 100644 classlib/src/main/java/org/teavm/classlib/java/net/TInet4Address.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/net/TInet6Address.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/net/TInetAddress.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/net/TInetSocketAddress.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/net/TNetworkInterface.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/net/TProxy.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/net/TServerSocket.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/net/TSocket.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/net/TSocketAddress.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/net/TSocketException.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/net/TUnknownHostException.java create mode 100644 tests/src/test/java/org/teavm/classlib/java/net/InetAddressTest.java create mode 100644 tests/src/test/java/org/teavm/classlib/java/net/InetSocketAddressTest.java diff --git a/classlib/src/main/java/org/teavm/classlib/java/net/TInet4Address.java b/classlib/src/main/java/org/teavm/classlib/java/net/TInet4Address.java new file mode 100644 index 0000000000..052e8bcff8 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/net/TInet4Address.java @@ -0,0 +1,83 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.classlib.java.net; + +public final class TInet4Address extends TInetAddress { + static final int INADDRSZ = 4; + + TInet4Address(byte[] addr, String hostname) { + super(addr, hostname, IPv4); + validateIPv4Address(addr); + } + + private static void validateIPv4Address(byte[] addr) { + if (addr.length != INADDRSZ) { + throw new IllegalArgumentException("IPv4 address must be exactly 4 bytes long."); + } + } + + @Override + public boolean isAnyLocalAddress() { + return address[0] == 0 && address[1] == 0 && address[2] == 0 && address[3] == 0; + } + + @Override + public boolean isLoopbackAddress() { + return address[0] == 127; + } + + @Override + public boolean isLinkLocalAddress() { + return (address[0] & 0xFF) == 169 && (address[1] & 0xFF) == 254; + } + + @Override + public boolean isMCGlobal() { + return (address[0] & 0xFF) >= 224 + && (address[0] & 0xFF) <= 238 + && !(address[0] == 224 && address[1] == 0 && address[2] == 0); + } + + @Override + public boolean isMCLinkLocal() { + return (address[0] & 0xFF) == 224 && (address[1] & 0xFF) == 0; + } + + @Override + public boolean isMCOrgLocal() { + return (address[0] & 0xFF) == 239 && (address[1] & 0xFF) >= 192; + } + + @Override + public boolean isMCSiteLocal() { + return (address[0] & 0xFF) == 239 && (address[1] & 0xFF) == 255; + } + + @Override + public boolean isMulticastAddress() { + return (address[0] & 0xFF) >= 224 && (address[0] & 0xFF) <= 239; + } + + @Override + public boolean isSiteLocalAddress() { + int firstOctet = address[0] & 0xFF; + int secondOctet = address[1] & 0xFF; + + return (firstOctet == 10) + || (firstOctet == 172 && secondOctet >= 16 && secondOctet <= 31) + || (firstOctet == 192 && secondOctet == 168); + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/net/TInet6Address.java b/classlib/src/main/java/org/teavm/classlib/java/net/TInet6Address.java new file mode 100644 index 0000000000..e41942c0a0 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/net/TInet6Address.java @@ -0,0 +1,154 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.classlib.java.net; + +public final class TInet6Address extends TInetAddress { + static final int INADDRSZ = 16; + + TInet6Address(byte[] addr, String hostname) { + super(addr, hostname, IPv6); + validateIPv6Address(addr); + } + + private static void validateIPv6Address(byte[] addr) { + if (addr.length != INADDRSZ) { + throw new IllegalArgumentException("IPv6 address must be exactly 16 bytes long."); + } + } + + @Override + public String getHostAddress() { + StringBuilder sb = new StringBuilder(); + int[] segments = new int[8]; + + for (int i = 0; i < 8; i++) { + segments[i] = ((address[i * 2] & 0xFF) << 8) | (address[i * 2 + 1] & 0xFF); + } + + int startZero = -1; + int maxZeroLength = 0; + int zeroLength = 0; + int currentStart = -1; + for (int i = 0; i < segments.length; i++) { + if (segments[i] == 0) { + if (currentStart == -1) { + currentStart = i; + } + zeroLength++; + } else { + if (zeroLength > maxZeroLength) { + maxZeroLength = zeroLength; + startZero = currentStart; + } + zeroLength = 0; + currentStart = -1; + } + } + if (zeroLength > maxZeroLength) { + maxZeroLength = zeroLength; + startZero = currentStart; + } + + for (int i = 0; i < segments.length; i++) { + if (startZero == i && maxZeroLength > 1) { + if (i == 0) { + sb.append("::"); + } else { + sb.append(':'); + } + i += maxZeroLength - 1; + continue; + } + sb.append(Integer.toHexString(segments[i])); + if (i < segments.length - 1) { + sb.append(':'); + } + } + return sb.toString(); + } + + @Override + public boolean isAnyLocalAddress() { + for (byte b : address) { + if (b != 0) { + return false; + } + } + return true; + } + + public boolean isIPv4CompatibleAddress() { + for (int i = 0; i < 12; i++) { + if (address[i] != 0) { + return false; + } + } + return true; + } + + @Override + public boolean isLinkLocalAddress() { + return (address[0] & 0xFF) == 0xFE && (address[1] & 0xC0) == 0x80; + } + + @Override + public boolean isLoopbackAddress() { + if (address[15] != 1) { + return false; + } + for (int i = 0; i < 15; i++) { + if (address[i] != 0) { + return false; + } + } + return true; + } + + @Override + public boolean isMCGlobal() { + return (address[0] & 0xFF) == 0xFF && (address[1] & 0x0F) == 0x0E; + } + + @Override + public boolean isMCLinkLocal() { + return (address[0] & 0xFF) == 0xFF && (address[1] & 0x0F) == 0x02; + } + + @Override + public boolean isMCNodeLocal() { + return (address[0] & 0xFF) == 0xFF && (address[1] & 0x0F) == 0x01; + } + + @Override + public boolean isMCOrgLocal() { + return (address[0] & 0xFF) == 0xFF && (address[1] & 0x0F) == 0x08; + } + + @Override + public boolean isMCSiteLocal() { + return (address[0] & 0xFF) == 0xFF && (address[1] & 0x0F) == 0x05; + } + + @Override + public boolean isMulticastAddress() { + return (address[0] & 0xFF) == 0xFF; + } + + @Override + public boolean isSiteLocalAddress() { + return (address[0] & 0xFF) == 0xFE && (address[1] & 0xC0) == 0xC0; + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/net/TInetAddress.java b/classlib/src/main/java/org/teavm/classlib/java/net/TInetAddress.java new file mode 100644 index 0000000000..708868e239 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/net/TInetAddress.java @@ -0,0 +1,182 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.classlib.java.net; + +import static org.teavm.backend.wasm.wasi.Wasi.INET4; +import static org.teavm.backend.wasm.wasi.Wasi.INET6; +import static org.teavm.backend.wasm.wasi.Wasi.INET_UNSPEC; +import static org.teavm.backend.wasm.wasi.Wasi.SOCK_ANY; +import org.teavm.backend.wasm.runtime.net.impl.SockAddrInet4; +import org.teavm.backend.wasm.runtime.net.impl.SockAddrInet6; +import org.teavm.runtime.net.SockAddr; +import org.teavm.runtime.net.VirtualSocket; +import org.teavm.runtime.net.VirtualSocketProvider; + +public class TInetAddress { + protected String hostname; + protected byte[] address; + protected int family; + + static final int IPv4 = 1; + static final int IPv6 = 2; + + private static VirtualSocket vs() { + return VirtualSocketProvider.getInstance(); + } + + protected TInetAddress() { + } + + protected TInetAddress(byte[] address, String hostname, int family) { + this.address = address; + this.hostname = hostname; + this.family = family; + } + + public byte[] getAddress() { + return address; + } + + public int getFamily() { + return family; + } + + public static TInetAddress[] getAllByName(String host) throws TUnknownHostException { + if (host == null || host.isEmpty()) { + TInetAddress[] ret = new TInetAddress[1]; + ret[0] = getLoopbackAddress(); + return ret; + } + try { + SockAddr[] addresses = vs().getAddrInfo(host, null, INET_UNSPEC, SOCK_ANY, 0); + if (addresses == null || addresses.length == 0) { + throw new TUnknownHostException("No addresses found for host: " + host); + } + + TInetAddress[] result = new TInetAddress[addresses.length]; + for (int i = 0; i < addresses.length; i++) { + SockAddr addr = addresses[i]; + if (addr.sockFamily() == INET4) { + result[i] = new TInet4Address(((SockAddrInet4) addr).getAddr(), host); + } else if (addr.sockFamily() == INET6) { + result[i] = new TInet6Address(((SockAddrInet6) addr).getAddr(), host); + } + } + return result; + } catch (Exception e) { + throw new TUnknownHostException( + "Failed to resolve host: " + host + " " + e.getMessage()); + } + } + + public static TInetAddress getByAddress(byte[] addr) throws TUnknownHostException { + return getByAddress(null, addr); + } + + public static TInetAddress getByAddress(String host, byte[] addr) throws TUnknownHostException { + if (addr != null) { + if (addr.length == TInet4Address.INADDRSZ) { + return new TInet4Address(addr, host); + } else if (addr.length == TInet6Address.INADDRSZ) { + return new TInet6Address(addr, host); + } + } + throw new TUnknownHostException("Addr is of illegal length"); + } + + public static TInetAddress getByName(String host) throws TUnknownHostException { + TInetAddress[] addresses = getAllByName(host); + return addresses[0]; + } + + public String getCanonicalHostName() { + return hostname != null ? hostname : getHostAddress(); + } + + public String getHostAddress() { + StringBuilder sb = new StringBuilder(); + for (byte b : address) { + sb.append(b & 0xFF).append('.'); + } + sb.setLength(sb.length() - 1); + return sb.toString(); + } + + public String getHostName() { + return hostname != null ? hostname : getHostAddress(); + } + + public static TInetAddress getLocalHost() throws TUnknownHostException { + return new TInetAddress(new byte[] {127, 0, 0, 1}, "localhost", IPv4); + } + + public static TInetAddress getLoopbackAddress() { + return new TInetAddress(new byte[] {127, 0, 0, 1}, "localhost", IPv4); + } + + public boolean isAnyLocalAddress() { + return false; + } + + public boolean isLinkLocalAddress() { + return false; + } + + public boolean isLoopbackAddress() { + return false; + } + + public boolean isMulticastAddress() { + return false; + } + + public boolean isReachable(int timeout) { + return false; + } + + public boolean isReachable(TNetworkInterface netif, int ttl, int timeout) { + return false; + } + + public boolean isSiteLocalAddress() { + return false; + } + + public boolean isMCGlobal() { + return false; + } + + public boolean isMCNodeLocal() { + return false; + } + + public boolean isMCLinkLocal() { + return false; + } + + public boolean isMCSiteLocal() { + return false; + } + + public boolean isMCOrgLocal() { + return false; + } + + @Override + public String toString() { + return (hostname != null ? hostname : "") + "/" + getHostAddress(); + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/net/TInetSocketAddress.java b/classlib/src/main/java/org/teavm/classlib/java/net/TInetSocketAddress.java new file mode 100644 index 0000000000..ca3be6e074 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/net/TInetSocketAddress.java @@ -0,0 +1,100 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.classlib.java.net; + +import java.io.Serializable; + +public class TInetSocketAddress extends TSocketAddress implements Serializable { + private TInetAddress address; + private String hostname; + private int port; + private boolean unresolved; + + public TInetSocketAddress(TInetAddress addr, int port) { + validatePort(port); + this.address = addr; + this.port = port; + this.unresolved = addr == null; + } + + public TInetSocketAddress(int port) { + this((String) null, port); + } + + public TInetSocketAddress(String hostname, int port) { + validatePort(port); + this.hostname = hostname; + this.port = port; + try { + this.address = TInetAddress.getByName(hostname); + this.unresolved = false; + } catch (TUnknownHostException e) { + this.address = null; + this.unresolved = true; + } + } + + public static TInetSocketAddress createUnresolved(String host, int port) { + validatePort(port); + TInetSocketAddress socketAddress = new TInetSocketAddress(host, port); + socketAddress.unresolved = true; + return socketAddress; + } + + public TInetAddress getAddress() { + return address; + } + + public String getHostName() { + return hostname; + } + + public String getHostString() { + if (hostname != null) { + return hostname; + } + return address != null ? address.getHostAddress() : null; + } + + public int getPort() { + return port; + } + + public boolean isUnresolved() { + return unresolved; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (hostname != null) { + sb.append(hostname); + } else if (address != null) { + sb.append(address.getHostAddress()); + } else { + sb.append(""); + } + sb.append(":").append(port); + return sb.toString(); + } + + private static int validatePort(int port) { + if (port < 0 || port > 65535) { + throw new IllegalArgumentException("Port number out of range: " + port); + } + return port; + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/net/TNetworkInterface.java b/classlib/src/main/java/org/teavm/classlib/java/net/TNetworkInterface.java new file mode 100644 index 0000000000..4f8d579377 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/net/TNetworkInterface.java @@ -0,0 +1,23 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.classlib.java.net; + +/** + * This is a placeholder class required only to maintain type compatibility. + * The actual implementation will be provided later. + */ +public class TNetworkInterface { +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/net/TProxy.java b/classlib/src/main/java/org/teavm/classlib/java/net/TProxy.java new file mode 100644 index 0000000000..b7912e3b41 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/net/TProxy.java @@ -0,0 +1,32 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.classlib.java.net; + +/** + * This is a placeholder class required only to maintain type compatibility. + * The actual implementation will be provided later. + */ +public class TProxy { + + public enum Type { + DIRECT, + HTTP, + SOCKS + }; + + public TProxy(Type type, TSocketAddress sa) { + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/net/TServerSocket.java b/classlib/src/main/java/org/teavm/classlib/java/net/TServerSocket.java new file mode 100644 index 0000000000..610dcf6466 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/net/TServerSocket.java @@ -0,0 +1,155 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.classlib.java.net; + +import java.io.IOException; +import org.teavm.runtime.net.VirtualSocket; +import org.teavm.runtime.net.VirtualSocketProvider; + +public class TServerSocket { + private TSocket socket; + private boolean bound; + + private static final int FD_CLOSED = -1; + + private static VirtualSocket vs() { + return VirtualSocketProvider.getInstance(); + } + + public TServerSocket() throws IOException { + this.socket = new TSocket(); + this.bound = false; + } + + public TServerSocket(int port) throws IOException { + this(); + bind(new TInetSocketAddress(port)); + } + + public TServerSocket(int port, int backlog) throws IOException { + this(); + bind(new TInetSocketAddress(port), backlog); + } + + public TServerSocket(int port, int backlog, TInetAddress bindAddr) throws IOException { + this(); + bind(new TInetSocketAddress(bindAddr, port), backlog); + } + + public void bind(TSocketAddress endpoint) throws IOException { + bind(endpoint, 50); + } + + public void bind(TSocketAddress endpoint, int backlog) throws IOException { + if (bound) { + throw new IOException("Socket is already bound"); + } + if (!(endpoint instanceof TInetSocketAddress)) { + throw new IllegalArgumentException("Unsupported address type"); + } + socket.bind(endpoint); + vs().listen(socket.getFd(), backlog); + this.bound = true; + } + + public TSocket accept() throws IOException { + ensureSocketOpen(); + ensureSocketBound(); + int clientFd = vs().accept(socket.getFd(), 0); + return new TSocket(clientFd); + } + + public void close() throws IOException { + if (!isClosed()) { + socket.close(); + } + } + + public boolean isBound() { + return bound; + } + + public boolean isClosed() { + return socket.isClosed(); + } + + public TInetAddress getInetAddress() throws IOException { + return socket.getInetAddress(); + } + + public int getLocalPort() throws IOException { + return socket.getLocalPort(); } + + public TSocketAddress getLocalSocketAddress() throws IOException { + return socket.getLocalSocketAddress(); + } + + public void setReuseAddress(boolean on) throws IOException { + socket.setReuseAddress(on); + } + + public boolean getReuseAddress() throws IOException { + return socket.getReuseAddress(); + } + + public void setSoTimeout(int timeout) throws IOException { + socket.setSoTimeout(timeout); + } + + public int getSoTimeout() throws IOException { + return socket.getSoTimeout(); + } + + public int getReceiveBufferSize() throws IOException { + return socket.getReceiveBufferSize(); + } + + public void setReceiveBufferSize(int size) throws IOException { + socket.setReceiveBufferSize(size); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("ServerSocket["); + + if (!socket.isConnected()) { + sb.append("unconnected"); + } else { + try { + sb.append("addr=").append(getInetAddress()); + sb.append(",port=").append(getLocalPort()); + } catch (IOException e) { + sb.append("error while retrieving socket information"); + } + } + + sb.append("]"); + return sb.toString(); + } + + private void ensureSocketOpen() throws IOException { + if (isClosed()) { + throw new IOException("Socket is closed"); + } + } + + private void ensureSocketBound() throws IOException { + if (!isBound()) { + throw new IOException("Socket is not bound"); + } + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/net/TSocket.java b/classlib/src/main/java/org/teavm/classlib/java/net/TSocket.java new file mode 100644 index 0000000000..4c032127cc --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/net/TSocket.java @@ -0,0 +1,526 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.classlib.java.net; + +import static org.teavm.backend.wasm.wasi.Wasi.INET4; +import static org.teavm.backend.wasm.wasi.Wasi.INET6; +import static org.teavm.backend.wasm.wasi.Wasi.INET_UNSPEC; +import static org.teavm.backend.wasm.wasi.Wasi.SHUT_RD; +import static org.teavm.backend.wasm.wasi.Wasi.SHUT_RDWR; +import static org.teavm.backend.wasm.wasi.Wasi.SHUT_WR; +import static org.teavm.backend.wasm.wasi.Wasi.SOCK_DGRAM; +import static org.teavm.backend.wasm.wasi.Wasi.SOCK_STREAM; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import org.teavm.backend.wasm.runtime.net.impl.SockAddrInet4; +import org.teavm.backend.wasm.runtime.net.impl.SockAddrInet6; +import org.teavm.runtime.net.SockAddr; +import org.teavm.runtime.net.VirtualSocket; +import org.teavm.runtime.net.VirtualSocketProvider; + +public class TSocket implements Closeable { + protected int fd; + protected boolean created; + + private static final int FD_CLOSED = -1; + + private static VirtualSocket vs() { + return VirtualSocketProvider.getInstance(); + } + + public TSocket(int fd) { + this.fd = fd; + this.created = true; + } + + public int getFd() { + return fd; + } + + public TSocket() throws IOException { + this.fd = vs().socket(INET_UNSPEC, SOCK_STREAM); + this.created = true; + } + + public TSocket(TInetAddress address, int port) throws IOException { + this(address, port, true); + } + + public TSocket(TInetAddress host, int port, boolean stream) throws IOException { + this(host != null ? new TInetSocketAddress(host, port) : null, + (TSocketAddress) null, stream); + } + + public TSocket(TInetAddress address, int port, TInetAddress localAddr, int localPort) + throws IOException { + this(address != null ? new TInetSocketAddress(address, port) : null, + new TInetSocketAddress(localAddr, localPort), true); + } + + public TSocket(TProxy proxy) throws IOException { + throw new UnsupportedOperationException(); + } + + public TSocket(String host, int port) throws IOException { + this(host, port, true); + } + + public TSocket(String host, int port, boolean stream) throws IOException { + this(host != null ? new TInetSocketAddress(host, port) + : new TInetSocketAddress(TInetAddress.getByName(null), port), + (TSocketAddress) null, stream); + } + + public TSocket(String host, int port, TInetAddress localAddr, int localPort) + throws IOException { + this(host != null ? new TInetSocketAddress(host, port) + : new TInetSocketAddress(TInetAddress.getByName(null), port), + new TInetSocketAddress(localAddr, localPort), true); + } + + private TSocket(TSocketAddress addr, TSocketAddress localAddr, boolean stream) + throws IOException { + assert addr instanceof TInetSocketAddress; + int sotype = stream ? SOCK_STREAM : SOCK_DGRAM; + + SockAddr sa = getSockAddr(addr); + int family = sa.sockFamily(); + this.fd = vs().socket(family, sotype); + this.created = true; + + if (localAddr != null) { + bind(localAddr); + } + connect(sa); + } + + private SockAddr getSockAddr(TSocketAddress addr) throws TSocketException { + assert addr instanceof TInetSocketAddress; + TInetSocketAddress isa = (TInetSocketAddress) addr; + return getSockAddr(isa.getAddress(), isa.getPort()); + } + + private SockAddr getSockAddr(TInetAddress addr, int port) throws TSocketException { + if (addr.getFamily() == TInetAddress.IPv4) { + return new SockAddrInet4(addr.getAddress(), port); + } else if (addr.getFamily() == TInetAddress.IPv6) { + return new SockAddrInet6(addr.getAddress(), port); + } + throw new TSocketException("Unknown address family"); + } + + public void bind(TSocketAddress bindpoint) throws IOException { + SockAddr sa = getSockAddr(bindpoint); + vs().bind(fd, sa); + } + + private void connect(SockAddr sa) throws IOException { + vs().connect(fd, sa); + } + + public void connect(TSocketAddress sa) throws IOException { + assert sa instanceof TInetSocketAddress; + SockAddr sAddr = getSockAddr(sa); + connect(sAddr); + } + + public void connect(TSocketAddress endpoint, int timeout) throws IOException { + connect(endpoint); + setSoTimeout(timeout); + } + + public void close() throws IOException { + ensureSocketOpen(); + vs().shutdown(fd, SHUT_RDWR); + this.fd = FD_CLOSED; + } + + public TInetAddress getInetAddress() throws IOException { + ensureSocketOpen(); + SockAddr remoteAddress = vs().getSockName(fd); + + if (remoteAddress.sockFamily() == INET4) { + SockAddrInet4 ipv4Address = (SockAddrInet4) remoteAddress; + return new TInet4Address(ipv4Address.getAddr(), (String) null); + } else if (remoteAddress.sockFamily() == INET6) { + SockAddrInet6 ipv6Address = (SockAddrInet6) remoteAddress; + return new TInet6Address(ipv6Address.getAddr(), (String) null); + } + throw new TSocketException("Unsupported address type: " + remoteAddress.sockFamily()); + } + + public TInetAddress getLocalAddress() throws IOException { + ensureSocketOpen(); + SockAddr localAddress = vs().getSockName(fd); + + if (localAddress.sockFamily() == INET4) { + SockAddrInet4 ipv4Address = (SockAddrInet4) localAddress; + return new TInet4Address(ipv4Address.getAddr(), (String) null); + } else if (localAddress.sockFamily() == INET6) { + SockAddrInet6 ipv6Address = (SockAddrInet6) localAddress; + return new TInet6Address(ipv6Address.getAddr(), (String) null); + } + throw new TSocketException("Unsupported address type: " + localAddress.sockFamily()); + } + + public int getLocalPort() throws IOException { + ensureSocketOpen(); + SockAddr localAddress = vs().getSockName(fd); + return localAddress.sockPort(); + } + + public TSocketAddress getLocalSocketAddress() throws IOException { + ensureSocketOpen(); + SockAddr localAddress = vs().getSockName(fd); + + TInetAddress inetAddress; + if (localAddress.sockFamily() == INET4) { + SockAddrInet4 ipv4Address = (SockAddrInet4) localAddress; + inetAddress = new TInet4Address(ipv4Address.getAddr(), (String) null); + } else if (localAddress.sockFamily() == INET6) { + SockAddrInet6 ipv6Address = (SockAddrInet6) localAddress; + inetAddress = new TInet6Address(ipv6Address.getAddr(), (String) null); + } else { + throw new TSocketException("Unsupported address type: " + localAddress.sockFamily()); + } + return new TInetSocketAddress(inetAddress, localAddress.sockPort()); + } + + public int getPort() throws IOException { + ensureSocketOpen(); + SockAddr remoteAddress = vs().getPeerName(fd); + return remoteAddress.sockPort(); + } + + public TSocketAddress getRemoteSocketAddress() throws IOException { + ensureSocketOpen(); + SockAddr remoteAddress = vs().getPeerName(fd); + + TInetAddress inetAddress; + if (remoteAddress.sockFamily() == INET4) { + SockAddrInet4 ipv4Address = (SockAddrInet4) remoteAddress; + inetAddress = new TInet4Address(ipv4Address.getAddr(), (String) null); + } else if (remoteAddress.sockFamily() == INET6) { + SockAddrInet6 ipv6Address = (SockAddrInet6) remoteAddress; + inetAddress = new TInet6Address(ipv6Address.getAddr(), (String) null); + } else { + throw new TSocketException("Unsupported address type: " + remoteAddress.sockFamily()); + } + return new TInetSocketAddress(inetAddress, remoteAddress.sockPort()); + } + + public InputStream getInputStream() throws IOException { + ensureSocketOpen(); + final SockAddr remoteAddress = vs().getPeerName(fd); + + return new InputStream() { + private static final int MAX_LENGTH = 8192; + private final byte[] buffer = new byte[MAX_LENGTH]; + + @Override + public int read() throws IOException { + int bytesRead = read(buffer, 0, 1); + return (bytesRead == -1) ? -1 : (buffer[0] & 0xFF); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (b == null) { + throw new NullPointerException("Buffer is null"); + } + if (off < 0 || len < 0 || off + len > b.length) { + throw new IndexOutOfBoundsException("Invalid offset or length"); + } + if (len == 0) { + return 0; + } + + try { + int bytesRead = vs().recvFrom(fd, buffer, len, remoteAddress); + if (bytesRead <= 0) { + return -1; + } + + System.arraycopy(buffer, 0, b, off, bytesRead); + return bytesRead; + } catch (Exception e) { + throw new IOException("Error reading from socket", e); + } + } + + @Override + public int available() throws IOException { + return vs().getSockRecvBufSize(fd); + } + }; + } + + public OutputStream getOutputStream() throws IOException { + ensureSocketOpen(); + final SockAddr remoteAddress = vs().getPeerName(fd); + + return new OutputStream() { + @Override + public void write(int b) throws IOException { + byte[] singleByte = {(byte) b}; + write(singleByte, 0, 1); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (b == null) { + throw new NullPointerException("Buffer is null"); + } + if (off < 0 || len < 0 || off + len > b.length) { + throw new IndexOutOfBoundsException("Invalid offset or length"); + } + if (len == 0) { + return; + } + + try { + int bytesSent = vs().sendTo(fd, b, len, remoteAddress); + if (bytesSent != len) { + throw new IOException("Failed to send all data to socket"); + } + } catch (Exception e) { + throw new IOException("Error writing to socket", e); + } + } + }; + } + + public boolean getKeepAlive() throws IOException { + ensureSocketOpen(); + return vs().getSockKeepAlive(fd) == 1; + } + + public void setKeepAlive(boolean on) throws IOException { + ensureSocketOpen(); + vs().setSockKeepAlive(fd, on ? 1 : 0); + } + + public boolean isBound() { + return fd > 0 && created; + } + + public boolean isClosed() { + return fd == FD_CLOSED || !created; + } + + public boolean isConnected() { + try { + return isBound() && !isAddressZero(vs().getPeerName(fd)); + } catch (Exception e) { + return false; + } + } + + public boolean isInputShutdown() { + if (isClosed()) { + return true; + } + try { + return vs().recvFrom(fd, new byte[1], 0, null) == -1; + } catch (Exception e) { + return true; + } + } + + public boolean isOutputShutdown() { + if (isClosed()) { + return true; + } + try { + SockAddr remoteAddress = vs().getPeerName(fd); + vs().sendTo(fd, new byte[0], 0, remoteAddress); + return false; + } catch (Exception e) { + return true; + } + } + + public void shutdownInput() throws IOException { + if (!isClosed()) { + vs().shutdown(fd, SHUT_RD); + } + } + + public void shutdownOutput() throws IOException { + if (!isClosed()) { + vs().shutdown(fd, SHUT_WR); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Socket["); + + if (!isConnected()) { + sb.append("unconnected"); + } else { + try { + sb.append("addr=").append(getInetAddress()); + sb.append(",port=").append(getPort()); + sb.append(",localport=").append(getLocalPort()); + } catch (IOException e) { + sb.append("error while retrieving socket information"); + } + } + + sb.append("]"); + return sb.toString(); + } + + public void setReuseAddress(boolean on) throws IOException { + ensureSocketOpen(); + vs().setSockReuseAddr(fd, on ? 1 : 0); + } + + public boolean getReuseAddress() throws IOException { + ensureSocketOpen(); + return vs().getSockReuseAddr(fd) == 1; + } + + public int getReceiveBufferSize() throws IOException { + ensureSocketOpen(); + return vs().getSockRecvBufSize(fd); + } + + public void setReceiveBufferSize(int size) throws IOException { + if (size <= 0) { + throw new IllegalArgumentException("invalid receive size"); + } + ensureSocketOpen(); + vs().setSockRecvBufSize(fd, size); + } + + public int getSendBufferSize() throws IOException { + ensureSocketOpen(); + return vs().getSockSendBufSize(fd); + } + + public void setSendBufferSize(int size) throws IOException { + if (size <= 0) { + throw new IllegalArgumentException("invalid receive size"); + } + ensureSocketOpen(); + vs().setSockSendBufSize(fd, size); + } + + public int getSoLinger() throws IOException { + ensureSocketOpen(); + return vs().getSockLinger(fd); + } + + public void setSoLinger(boolean on, int linger) throws IOException { + ensureSocketOpen(); + if (linger < 0) { + throw new IllegalArgumentException("invalid value for SO_LINGER"); + } + if (linger > 65535) { + linger = 65535; + } + vs().setSockLinger(fd, on ? 1 : 0, linger); + } + + public int getSoTimeout() throws IOException { + ensureSocketOpen(); + return vs().getSockRecvTimeout(fd); + } + + public void setSoTimeout(int timeout) throws IOException { + if (timeout <= 0) { + throw new IllegalArgumentException("invalid timeout"); + } + ensureSocketOpen(); + vs().setSockRecvTimeout(fd, timeout); + } + + public boolean getTcpNoDelay() throws IOException { + ensureSocketOpen(); + return vs().getSockTcpNoDelay(fd) == 1; + } + + public void setTcpNoDelay(boolean on) throws IOException { + ensureSocketOpen(); + vs().setSockTcpNoDelay(fd, on ? 1 : 0); + } + + /** + * Sends a single byte of data on the socket. This method is intended to simulate the behavior + * of sending urgent data but does not implement true TCP Urgent Data (out-of-band data with the + * URG flag) due to limitations in the WASI environment. + * + *

Note: True TCP Urgent Data requires support for the URG flag in the TCP header, which is + * not currently available in WASI. This implementation simply sends the byte as normal in-band + * data. + */ + public void sendUrgentData(int data) throws IOException { + ensureSocketOpen(); + if (data < 0 || data > 255) { + throw new IllegalArgumentException("Data must be a single byte (0-255)."); + } + SockAddr remoteAddress = vs().getPeerName(fd); + byte[] b = new byte[] {(byte) data}; + int len = 1; + int bytesSent = vs().sendTo(fd, b, len, remoteAddress); + if (bytesSent != len) { + throw new IOException("Failed to send all data to socket"); + } + } + + public static void printByteArrayAsChars(byte[] data) { + if (data == null) { + throw new IllegalArgumentException("Data must not be null."); + } + + for (byte b : data) { + System.out.print((char) b); + } + System.out.println(); + } + + private static boolean isAddressZero(SockAddr addr) { + if (addr instanceof SockAddrInet4) { + SockAddrInet4 inet4 = (SockAddrInet4) addr; + byte[] address = inet4.getAddr(); + for (byte b : address) { + if (b != 0) { + return false; + } + } + } else if (addr instanceof SockAddrInet6) { + SockAddrInet6 inet6 = (SockAddrInet6) addr; + byte[] address = inet6.getAddr(); + for (byte s : address) { + if (s != 0) { + return false; + } + } + } + return addr.sockPort() == 0; + } + + private void ensureSocketOpen() throws TSocketException { + if (isClosed()) { + throw new TSocketException("Socket is closed"); + } + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/net/TSocketAddress.java b/classlib/src/main/java/org/teavm/classlib/java/net/TSocketAddress.java new file mode 100644 index 0000000000..c29bfa7dd9 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/net/TSocketAddress.java @@ -0,0 +1,26 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.classlib.java.net; + +import java.io.Serializable; + +public abstract class TSocketAddress implements Serializable { + /** + * Constructor for subclasses to call. + */ + public TSocketAddress() { + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/net/TSocketException.java b/classlib/src/main/java/org/teavm/classlib/java/net/TSocketException.java new file mode 100644 index 0000000000..760b0019b2 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/net/TSocketException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.classlib.java.net; + +import java.io.IOException; + +public class TSocketException extends IOException { + public TSocketException(String msg) { + super(msg); + } + + public TSocketException() { + } + + public TSocketException(String msg, Throwable cause) { + super(msg, cause); + } + + public TSocketException(Throwable cause) { + super(cause); + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/net/TUnknownHostException.java b/classlib/src/main/java/org/teavm/classlib/java/net/TUnknownHostException.java new file mode 100644 index 0000000000..454745c76d --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/net/TUnknownHostException.java @@ -0,0 +1,27 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.classlib.java.net; + +import java.io.IOException; + +public class TUnknownHostException extends IOException { + public TUnknownHostException(String message) { + super(message); + } + + public TUnknownHostException() { + } +} diff --git a/tests/src/test/java/org/teavm/classlib/java/net/InetAddressTest.java b/tests/src/test/java/org/teavm/classlib/java/net/InetAddressTest.java new file mode 100644 index 0000000000..2768abbb56 --- /dev/null +++ b/tests/src/test/java/org/teavm/classlib/java/net/InetAddressTest.java @@ -0,0 +1,158 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.classlib.java.net; + +import static org.junit.Assert.*; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.teavm.junit.TeaVMTestRunner; + +@RunWith(TeaVMTestRunner.class) +public class InetAddressTest { + + @Test + public void localhostInet4Address() throws Exception { + var addr1 = InetAddress.getByName("localhost"); + + assertTrue("1. should be an instance of Inet4Address", addr1 instanceof Inet4Address); + assertEquals("2. host address should be 127.0.0.1", "127.0.0.1", addr1.getHostAddress()); + assertEquals("3. toString() should return localhost/127.0.0.1", "localhost/127.0.0.1", addr1.toString()); + assertArrayEquals("4. address bytes should be [127, 0, 0, 1]", new byte[]{127, 0, 0, 1}, addr1.getAddress()); + assertEquals("5. canonical host name should be localhost", "localhost", addr1.getCanonicalHostName()); + assertEquals("6. host name should be localhost", "localhost", addr1.getHostName()); + assertFalse("7. should not be any local address", addr1.isAnyLocalAddress()); + assertFalse("8. should not be a link-local address", addr1.isLinkLocalAddress()); + assertTrue("9. should be a loopback address", addr1.isLoopbackAddress()); + assertFalse("10. should not be a global multicast address", addr1.isMCGlobal()); + assertFalse("11. should not be a node-local multicast address", addr1.isMCNodeLocal()); + assertFalse("12. should not be an organization-local multicast address", addr1.isMCOrgLocal()); + assertFalse("13. should not be a site-local multicast address", addr1.isMCSiteLocal()); + assertFalse("14. should not be a multicast address", addr1.isMulticastAddress()); + assertFalse("15. should not be a site-local address", addr1.isSiteLocalAddress()); + } + + @Test + public void inet4AddressBasicProperties() throws Exception { + var addr2 = (Inet4Address) InetAddress.getByAddress(new byte[]{127, 0, 0, 1}); + + assertTrue("1. should be an instance of Inet4Address", addr2 instanceof Inet4Address); + assertEquals("2. host address should be 127.0.0.1", "127.0.0.1", addr2.getHostAddress()); + assertEquals("3. toString() should return /127.0.0.1", "/127.0.0.1", addr2.toString()); + assertArrayEquals("4. address bytes should be [127, 0, 0, 1]", new byte[]{127, 0, 0, 1}, addr2.getAddress()); + assertFalse("5. should not be any local address", addr2.isAnyLocalAddress()); + assertFalse("6. should not be a link-local address", addr2.isLinkLocalAddress()); + assertTrue("7. should be a loopback address", addr2.isLoopbackAddress()); + assertFalse("8. should not be a global multicast address", addr2.isMCGlobal()); + assertFalse("9. should not be a node-local multicast address", addr2.isMCNodeLocal()); + assertFalse("10. should not be an organization-local multicast address", addr2.isMCOrgLocal()); + assertFalse("11. should not be a site-local multicast address", addr2.isMCSiteLocal()); + assertFalse("12. should not be a multicast address", addr2.isMulticastAddress()); + assertFalse("13. should not be a site-local address", addr2.isSiteLocalAddress()); + } + + @Test + public void inet4AddressCustomBytes() throws Exception { + var addr3 = (Inet4Address) InetAddress.getByAddress(new byte[]{(byte) 192, (byte) 168, 1, 1}); + + assertTrue("1. should be an instance of Inet4Address", addr3 instanceof Inet4Address); + assertEquals("2. host address should be 192.168.1.1", "192.168.1.1", addr3.getHostAddress()); + assertEquals("3. toString() should return /192.168.1.1", "/192.168.1.1", addr3.toString()); + assertArrayEquals("4. address bytes should be [192, 168, 1, 1]", + new byte[]{(byte) 192, (byte) 168, 1, 1}, addr3.getAddress()); + assertFalse("5. should not be any local address", addr3.isAnyLocalAddress()); + assertFalse("6. should not be a link-local address", addr3.isLinkLocalAddress()); + assertFalse("7. should not be a loopback address", addr3.isLoopbackAddress()); + assertFalse("8. should not be a multicast address", addr3.isMulticastAddress()); + assertTrue("9. should be a site-local address", addr3.isSiteLocalAddress()); + } + + @Test + public void loopbackAddressIPv6() throws Exception { + var addr4 = (Inet6Address) InetAddress.getByAddress(new byte[]{ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 + }); + + assertTrue("1. should be an instance of Inet6Address", addr4 instanceof Inet6Address); + assertEquals("2. host address should be ::1", "::1", addr4.getHostAddress()); + assertEquals("3. toString() should return /::1", "/::1", addr4.toString()); + assertArrayEquals("4. address bytes should be [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]", + new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, addr4.getAddress()); + assertFalse("5. should not be any local address", addr4.isAnyLocalAddress()); + assertFalse("6. should not be a link-local address", addr4.isLinkLocalAddress()); + assertTrue("7. should be a loopback address", addr4.isLoopbackAddress()); + assertFalse("8. should not be a global multicast address", addr4.isMCGlobal()); + assertFalse("9. should not be a node-local multicast address", addr4.isMCNodeLocal()); + assertFalse("10. should not be an organization-local multicast address", addr4.isMCOrgLocal()); + assertFalse("11. should not be a site-local multicast address", addr4.isMCSiteLocal()); + assertFalse("12. should not be a multicast address", addr4.isMulticastAddress()); + assertFalse("13. should not be a site-local address", addr4.isSiteLocalAddress()); + } + + @Test + public void customAddressIPv6() throws Exception { + var addr5 = (Inet6Address) InetAddress.getByAddress(new byte[]{ + (byte) 0x20, (byte) 0x01, 0x0d, (byte) 0xb8, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 + }); + + assertTrue("1. should be an instance of Inet6Address", addr5 instanceof Inet6Address); + assertEquals("2. host address should be 2001:db8::1", "2001:db8::1", addr5.getHostAddress()); + assertEquals("3. toString() should return /2001:db8::1", "/2001:db8::1", addr5.toString()); + assertArrayEquals("4. address bytes should be [32, 1, 13, -72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]", + new byte[]{ + (byte) 0x20, (byte) 0x01, 0x0d, (byte) 0xb8, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 + }, addr5.getAddress()); + assertFalse("5. should not be any local address", addr5.isAnyLocalAddress()); + assertFalse("6. should not be a link-local address", addr5.isLinkLocalAddress()); + assertFalse("7. should not be a loopback address", addr5.isLoopbackAddress()); + assertFalse("8. should not be a global multicast address", addr5.isMCGlobal()); + assertFalse("9. should not be a node-local multicast address", addr5.isMCNodeLocal()); + assertFalse("10. should not be an organization-local multicast address", addr5.isMCOrgLocal()); + assertFalse("11. should not be a site-local multicast address", addr5.isMCSiteLocal()); + assertFalse("12. should not be a multicast address", addr5.isMulticastAddress()); + assertFalse("13. should not be a site-local address", addr5.isSiteLocalAddress()); + } + + + @Test + public void multicastAddressIPv6() throws Exception { + var addr6 = (Inet6Address) InetAddress.getByAddress(new byte[]{ + (byte) 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 + }); + assertTrue("1. should be an instance of Inet6Address", addr6 instanceof Inet6Address); + assertEquals("2. host address should be ff02::1", "ff02::1", addr6.getHostAddress()); + assertEquals("3. toString() should return /ff02::1", "/ff02::1", addr6.toString()); + assertArrayEquals("4. address bytes should be [255, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]", + new byte[]{ + (byte) 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 + }, addr6.getAddress()); + assertFalse("5. should not be any local address", addr6.isAnyLocalAddress()); + assertFalse("6. should not be a link-local address", addr6.isLinkLocalAddress()); + assertFalse("7. should not be a loopback address", addr6.isLoopbackAddress()); + assertFalse("8. should not be a global multicast address", addr6.isMCGlobal()); + assertFalse("9. should not be a node-local multicast address", addr6.isMCNodeLocal()); + assertFalse("10. should not be an organization-local multicast address", addr6.isMCOrgLocal()); + assertFalse("11. should not be a site-local multicast address", addr6.isMCSiteLocal()); + assertTrue("12. should be a multicast address", addr6.isMulticastAddress()); + assertFalse("13. should not be a site-local address", addr6.isSiteLocalAddress()); + } +} diff --git a/tests/src/test/java/org/teavm/classlib/java/net/InetSocketAddressTest.java b/tests/src/test/java/org/teavm/classlib/java/net/InetSocketAddressTest.java new file mode 100644 index 0000000000..6408be96ba --- /dev/null +++ b/tests/src/test/java/org/teavm/classlib/java/net/InetSocketAddressTest.java @@ -0,0 +1,78 @@ +/* + * Copyright 2025 Maksim Tiushev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.classlib.java.net; + +import static org.junit.Assert.*; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.teavm.junit.TeaVMTestRunner; + +@RunWith(TeaVMTestRunner.class) +public class InetSocketAddressTest { + + @Test + public void loopbackAddressWithPort() throws Exception { + var addr1 = new InetSocketAddress(InetAddress.getByAddress(new byte[]{127, 0, 0, 1}), 8080); + + assertTrue("1. should be an instance of InetSocketAddress", addr1 instanceof InetSocketAddress); + assertEquals("2. host string should be 127.0.0.1", "127.0.0.1", addr1.getHostString()); + assertEquals("3. port should be 8080", 8080, addr1.getPort()); + assertArrayEquals("4. address bytes should be [127, 0, 0, 1]", + new byte[]{127, 0, 0, 1}, addr1.getAddress().getAddress()); + assertTrue("5. should be a loopback address", addr1.getAddress().isLoopbackAddress()); + assertFalse("6. should not be any local address", addr1.getAddress().isAnyLocalAddress()); + assertFalse("7. should not be a multicast address", addr1.getAddress().isMulticastAddress()); + assertFalse("8. should not be a site-local address", addr1.getAddress().isSiteLocalAddress()); + } + + @Test + public void customIpv4AddressWithPort() throws Exception { + var addr2 = new InetSocketAddress(InetAddress.getByAddress(new byte[]{(byte) 192, (byte) 168, 1, 1}), 9090); + + assertTrue("1. should be an instance of InetSocketAddress", addr2 instanceof InetSocketAddress); + assertEquals("2. host string should be 192.168.1.1", "192.168.1.1", addr2.getHostString()); + assertEquals("3. port should be 9090", 9090, addr2.getPort()); + assertArrayEquals("4. address bytes should be [192, 168, 1, 1]", + new byte[]{(byte) 192, (byte) 168, 1, 1}, addr2.getAddress().getAddress()); + assertFalse("5. should not be a loopback address", addr2.getAddress().isLoopbackAddress()); + assertFalse("6. should not be any local address", addr2.getAddress().isAnyLocalAddress()); + assertFalse("7. should not be a multicast address", addr2.getAddress().isMulticastAddress()); + assertTrue("8. should be a site-local address", addr2.getAddress().isSiteLocalAddress()); + } + + @Test + public void ipv6AddressWithPort() throws Exception { + var addr3 = new InetSocketAddress(InetAddress.getByAddress(new byte[]{ + (byte) 0x20, (byte) 0x01, 0x0d, (byte) 0xb8, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 + }), 443); + + assertTrue("1. should be an instance of InetSocketAddress", addr3 instanceof InetSocketAddress); + assertEquals("2. host string should be 2001:db8::1", "2001:db8::1", addr3.getHostString()); + assertEquals("3. port should be 443", 443, addr3.getPort()); + assertArrayEquals("4. address bytes should match 2001:db8::1", + new byte[]{ + (byte) 0x20, (byte) 0x01, 0x0d, (byte) 0xb8, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 + }, addr3.getAddress().getAddress()); + assertFalse("5. should not be a loopback address", addr3.getAddress().isLoopbackAddress()); + assertFalse("6. should not be any local address", addr3.getAddress().isAnyLocalAddress()); + assertFalse("7. should not be a multicast address", addr3.getAddress().isMulticastAddress()); + assertFalse("8. should not be a site-local address", addr3.getAddress().isSiteLocalAddress()); + } +} From b132783a3d6baddf5bcd8188de7d0d6f197458fc Mon Sep 17 00:00:00 2001 From: Maksim Tiushev Date: Fri, 7 Feb 2025 11:50:14 +0000 Subject: [PATCH 3/3] [Patch 1/2] Refactoring Simplification of `AddrInfo` and `AddrInfoHints` from classes to structures. --- .../wasm/runtime/net/WasiVirtualSocket.java | 50 ++++--- .../wasm/runtime/net/impl/AddrInfo.java | 125 ------------------ .../wasm/runtime/net/impl/AddrInfoHints.java | 64 --------- 3 files changed, 29 insertions(+), 210 deletions(-) delete mode 100644 core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/AddrInfo.java delete mode 100644 core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/AddrInfoHints.java diff --git a/core/src/main/java/org/teavm/backend/wasm/runtime/net/WasiVirtualSocket.java b/core/src/main/java/org/teavm/backend/wasm/runtime/net/WasiVirtualSocket.java index 6f677062e7..b09310e781 100644 --- a/core/src/main/java/org/teavm/backend/wasm/runtime/net/WasiVirtualSocket.java +++ b/core/src/main/java/org/teavm/backend/wasm/runtime/net/WasiVirtualSocket.java @@ -25,7 +25,8 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import org.teavm.backend.wasm.runtime.WasiBuffer; -import org.teavm.backend.wasm.runtime.net.impl.*; +import org.teavm.backend.wasm.runtime.net.impl.SockAddrInet4; +import org.teavm.backend.wasm.runtime.net.impl.SockAddrInet6; import org.teavm.backend.wasm.wasi.Wasi; import org.teavm.interop.Address; import org.teavm.interop.Structure; @@ -34,15 +35,22 @@ public class WasiVirtualSocket implements VirtualSocket { - private static final int MAX_RESOLVED_ADDRESSES = 16; - private static final int ADDR_SIZE = 22; private static final int IPV4_ADDR_SIZE = 4; private static final int IPV6_ADDR_SIZE = 8; - private static final int ADDR_INFO_BUFFER_SIZE = AddrInfo.getBufferSize(); - private static final int ADDR_INFO_ADDR_BUFFER_SIZE = AddrInfo.getAddrBufferSize(); + private static final int ADDR_SIZE = 22; + private static final int ADDR_INFO_BUFFER_SIZE = 28; + private static final int ADDR_INFO_ADDR_BUFFER_SIZE = 18; + private static final int MAX_RESOLVED_ADDRESSES = 16; + public static final int HINTS_ENABLED = 1; public static final int HINTS_DISABLED = 2; + public class AddrInfoHints extends Structure { + public int type; + public int family; + public int hintsEnabled; + } + public static class CIOVec extends Structure { public int address; public int len; @@ -196,14 +204,20 @@ public SockAddr[] getAddrInfo( validateSotype(sotype); } - AddrInfoHints hints = new AddrInfoHints(sotype, proto, hintsEnabled); + Address hintsAddr = WasiBuffer.getBuffer(); + AddrInfoHints hints = hintsAddr.toStructure(); + + hints.type = sotype; + hints.family = proto; + hints.hintsEnabled = hintsEnabled; + byte[] resultBuffer = new byte[ADDR_INFO_BUFFER_SIZE * MAX_RESOLVED_ADDRESSES]; int[] resolvedCount = new int[1]; int errno = Wasi.sockAddrResolve( Address.ofData(nameNT), Address.ofData(serviceNT), - hints.getAddress(), + hintsAddr, Address.ofData(resultBuffer), resultBuffer.length, Address.ofData(resolvedCount)); @@ -215,17 +229,18 @@ public SockAddr[] getAddrInfo( SockAddr[] addresses = new SockAddr[resolvedCount[0]]; for (int i = 0; i < resolvedCount[0]; i++) { - ByteBuffer buffer = - ByteBuffer.wrap(resultBuffer, i * ADDR_INFO_BUFFER_SIZE, ADDR_INFO_BUFFER_SIZE) - .order(ByteOrder.nativeOrder()); - + ByteBuffer buffer = ByteBuffer.wrap(resultBuffer, i * ADDR_INFO_BUFFER_SIZE, ADDR_INFO_BUFFER_SIZE) + .order(ByteOrder.nativeOrder()); int sockKind = buffer.getInt(); byte[] addrBuf = new byte[ADDR_INFO_ADDR_BUFFER_SIZE]; buffer.get(addrBuf); - int sockType = buffer.getInt(); + buffer.getInt(); - AddrInfo addrInfo = new AddrInfo(sockKind, addrBuf, sockType); - addresses[i] = parseSockAddr(addrInfoToRaw(addrInfo)); + ByteBuffer rawBuffer = ByteBuffer.allocate(ADDR_SIZE).order(ByteOrder.nativeOrder()); + rawBuffer.putInt(sockKind); + rawBuffer.put(addrBuf); + + addresses[i] = parseSockAddr(rawBuffer.array()); } return addresses; @@ -420,11 +435,4 @@ private static SockAddr parseSockAddr(byte[] data) throws SocketException { throw new SocketException("Unknown address family: " + kind); } } - - private byte[] addrInfoToRaw(AddrInfo addrInfo) { - ByteBuffer buffer = ByteBuffer.allocate(ADDR_SIZE).order(ByteOrder.nativeOrder()); - buffer.putInt(addrInfo.getSockKind()); - buffer.put(addrInfo.getAddrBuf()); - return buffer.array(); - } } diff --git a/core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/AddrInfo.java b/core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/AddrInfo.java deleted file mode 100644 index 1efcc7bda6..0000000000 --- a/core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/AddrInfo.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2025 Maksim Tiushev. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.teavm.backend.wasm.runtime.net.impl; - -import org.teavm.backend.wasm.runtime.WasiBuffer; -import org.teavm.interop.Address; -import org.teavm.interop.Structure; - -public class AddrInfo { - private static final int ADDR_BUF_SIZE = 18; - - public static class AddrInfoStruct extends Structure { - public int sockKind; - public byte b0; - public byte b1; - public byte b2; - public byte b3; - public byte b4; - public byte b5; - public byte b6; - public byte b7; - public byte b8; - public byte b9; - public byte b10; - public byte b11; - public byte b12; - public byte b13; - public byte b14; - public byte b15; - public byte b16; - public byte b17; - public int sockType; - } - - private final int sockKind; - private final byte[] addrBuf; - private final int sockType; - - public AddrInfo() { - this(0, new byte[ADDR_BUF_SIZE], 0); - } - - public AddrInfo(int sockKind, byte[] addrBuf, int sockType) { - if (addrBuf == null || addrBuf.length != ADDR_BUF_SIZE) { - throw new IllegalArgumentException( - "addrBuf must be exactly " + ADDR_BUF_SIZE + " bytes long."); - } - this.sockKind = sockKind; - this.addrBuf = addrBuf.clone(); - this.sockType = sockType; - } - - public static int getBufferSize() { - return Structure.sizeOf(AddrInfoStruct.class); - } - - public static int getAddrBufferSize() { - return ADDR_BUF_SIZE; - } - - public int getSockKind() { - return sockKind; - } - - public byte[] getAddrBuf() { - return addrBuf.clone(); - } - - public int getSockType() { - return sockType; - } - - public Address getAddress() { - Address argsAddress = WasiBuffer.getBuffer(); - AddrInfoStruct s = argsAddress.toStructure(); - s.sockKind = sockKind; - s.b0 = addrBuf[0]; - s.b1 = addrBuf[1]; - s.b2 = addrBuf[2]; - s.b3 = addrBuf[3]; - s.b4 = addrBuf[4]; - s.b5 = addrBuf[5]; - s.b6 = addrBuf[6]; - s.b7 = addrBuf[7]; - s.b8 = addrBuf[8]; - s.b9 = addrBuf[9]; - s.b10 = addrBuf[10]; - s.b11 = addrBuf[11]; - s.b12 = addrBuf[12]; - s.b13 = addrBuf[13]; - s.b14 = addrBuf[14]; - s.b15 = addrBuf[15]; - s.b16 = addrBuf[16]; - s.b17 = addrBuf[17]; - s.sockType = sockType; - return argsAddress; - } - - @Override - public String toString() { - StringBuilder addrBufString = new StringBuilder(); - for (byte b : addrBuf) { - if (addrBufString.length() > 0) { - addrBufString.append(" "); - } - addrBufString.append(String.format("%02X", b)); - } - return "AddrInfo{sockKind=" + sockKind - + ", addrBuf=[" + addrBufString - + "], sockType=" + sockType + "}"; - } -} diff --git a/core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/AddrInfoHints.java b/core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/AddrInfoHints.java deleted file mode 100644 index 4dd4c4c140..0000000000 --- a/core/src/main/java/org/teavm/backend/wasm/runtime/net/impl/AddrInfoHints.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2025 Maksim Tiushev. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.teavm.backend.wasm.runtime.net.impl; - -import org.teavm.backend.wasm.runtime.WasiBuffer; -import org.teavm.interop.Address; -import org.teavm.interop.Structure; - -public class AddrInfoHints { - private final int type; - private final int family; - private final int hintsEnabled; - - public static class AddrInfoHintsStruct extends Structure { - public int type; - public int family; - public int hintsEnabled; - } - - public AddrInfoHints(int type, int family, int hintsEnabled) { - this.type = type; - this.family = family; - this.hintsEnabled = hintsEnabled; - } - - public int getType() { - return type; - } - - public int getFamily() { - return family; - } - - public int getHintsEnabled() { - return hintsEnabled; - } - - public Address getAddress() { - Address argsAddress = WasiBuffer.getBuffer(); - AddrInfoHintsStruct s = argsAddress.toStructure(); - s.type = type; - s.family = family; - s.hintsEnabled = hintsEnabled; - return argsAddress; - } - - @Override - public String toString() { - return "AddrInfoHints{type=" + type + ", family=" + family + ", hintsEnabled=" + hintsEnabled + "}"; - } -}