From fedaeefac4831c5338f8aa49cbf0055ef67c4d4e Mon Sep 17 00:00:00 2001 From: Leumor <116955025+leumor@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:30:56 +0000 Subject: [PATCH 1/3] refactor(interop-wire): Extract wire message nucleus Add the new :interop-wire leaf and move the narrow message/schema/version/probe subset into it. Introduce MessageSource as the minimal seam between leaf-owned Message types and root-owned PeerContext, then update the affected root handlers to cast only where transport access is still required. Also wire the new leaf into root build aggregation and add selective ownership metadata for non-clean build safety. --- build.gradle.kts | 2 + interop-wire/build.gradle.kts | 66 + interop-wire/gradle/owned-output-patterns.txt | 33 + .../crypta/io/comm/AsyncMessageCallback.java | 38 + .../network/crypta/io/comm/ByteCounter.java | 47 + .../main/java/network/crypta/io/comm/DMT.java | 2924 +++++++++++++++++ .../crypta/io/comm/DisconnectedException.java | 26 + .../network/crypta/io/comm/Dispatcher.java | 25 + .../comm/DuplicateMessageTypeException.java | 22 + .../crypta/io/comm/FreenetInetAddress.java | 559 ++++ .../io/comm/IncorrectTypeException.java | 20 + .../java/network/crypta/io/comm/Message.java | 762 +++++ .../network/crypta/io/comm/MessageSource.java | 11 + .../network/crypta/io/comm/MessageType.java | 297 ++ .../crypta/io/comm/NotConnectedException.java | 50 + .../io/comm/OpennetAnnounceRequest.java | 14 + .../java/network/crypta/io/comm/Peer.java | 415 +++ .../crypta/io/comm/PeerParseException.java | 39 + .../io/comm/PeerRestartedException.java | 25 + ...ferenceSignatureVerificationException.java | 39 + .../crypta/io/comm/RetrievalException.java | 151 + .../network/crypta/io/comm/TrafficClass.java | 72 + .../java/network/crypta/node/Version.java | 506 +++ .../crypta/node/VersionParseException.java | 27 + .../java/network/crypta/node/probe/Error.java | 151 + .../java/network/crypta/node/probe/Type.java | 112 + .../network/crypta/support/Serializer.java | 412 +++ settings.gradle.kts | 1 + .../network/crypta/io/comm/PeerContext.java | 33 +- .../crypta/node/NodeDataRequestHandler.java | 4 +- .../crypta/node/NodeNotRoutableHandler.java | 18 +- 31 files changed, 6867 insertions(+), 34 deletions(-) create mode 100644 interop-wire/build.gradle.kts create mode 100644 interop-wire/gradle/owned-output-patterns.txt create mode 100644 interop-wire/src/main/java/network/crypta/io/comm/AsyncMessageCallback.java create mode 100644 interop-wire/src/main/java/network/crypta/io/comm/ByteCounter.java create mode 100644 interop-wire/src/main/java/network/crypta/io/comm/DMT.java create mode 100644 interop-wire/src/main/java/network/crypta/io/comm/DisconnectedException.java create mode 100644 interop-wire/src/main/java/network/crypta/io/comm/Dispatcher.java create mode 100644 interop-wire/src/main/java/network/crypta/io/comm/DuplicateMessageTypeException.java create mode 100644 interop-wire/src/main/java/network/crypta/io/comm/FreenetInetAddress.java create mode 100644 interop-wire/src/main/java/network/crypta/io/comm/IncorrectTypeException.java create mode 100644 interop-wire/src/main/java/network/crypta/io/comm/Message.java create mode 100644 interop-wire/src/main/java/network/crypta/io/comm/MessageSource.java create mode 100644 interop-wire/src/main/java/network/crypta/io/comm/MessageType.java create mode 100644 interop-wire/src/main/java/network/crypta/io/comm/NotConnectedException.java create mode 100644 interop-wire/src/main/java/network/crypta/io/comm/OpennetAnnounceRequest.java create mode 100644 interop-wire/src/main/java/network/crypta/io/comm/Peer.java create mode 100644 interop-wire/src/main/java/network/crypta/io/comm/PeerParseException.java create mode 100644 interop-wire/src/main/java/network/crypta/io/comm/PeerRestartedException.java create mode 100644 interop-wire/src/main/java/network/crypta/io/comm/ReferenceSignatureVerificationException.java create mode 100644 interop-wire/src/main/java/network/crypta/io/comm/RetrievalException.java create mode 100644 interop-wire/src/main/java/network/crypta/io/comm/TrafficClass.java create mode 100644 interop-wire/src/main/java/network/crypta/node/Version.java create mode 100644 interop-wire/src/main/java/network/crypta/node/VersionParseException.java create mode 100644 interop-wire/src/main/java/network/crypta/node/probe/Error.java create mode 100644 interop-wire/src/main/java/network/crypta/node/probe/Type.java create mode 100644 interop-wire/src/main/java/network/crypta/support/Serializer.java diff --git a/build.gradle.kts b/build.gradle.kts index 4d61f85907b..45f77e0ff68 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,6 +20,7 @@ val internalLeafProjects = project(":foundation-store"), project(":foundation-store-contracts"), project(":foundation-crypto-keys"), + project(":interop-wire"), project(":foundation-config"), project(":foundation-fs"), project(":foundation-compat"), @@ -41,6 +42,7 @@ dependencies { implementation(project(":foundation-store")) implementation(project(":foundation-store-contracts")) implementation(project(":foundation-crypto-keys")) + implementation(project(":interop-wire")) implementation(project(":foundation-config")) implementation(project(":foundation-fs")) implementation(project(":foundation-compat")) diff --git a/interop-wire/build.gradle.kts b/interop-wire/build.gradle.kts new file mode 100644 index 00000000000..20e8e889ee6 --- /dev/null +++ b/interop-wire/build.gradle.kts @@ -0,0 +1,66 @@ +import java.io.IOException +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.compile.JavaCompile + +plugins { + id("cryptad.java-kotlin-conventions") + id("cryptad.spotless") + `java-library` +} + +version = rootProject.version + +dependencies { + api(project(":foundation-support")) + api(project(":foundation-crypto-keys")) + + implementation(libs.slf4jApi) + + compileOnly(libs.jetbrainsAnnotations) +} + +val versionBuildDir = file("$projectDir/build/tmp/compileVersion/") +val versionSrc = "network/crypta/node/Version.java" + +val gitrev: String = + try { + val cmd = "git rev-parse --short HEAD" + ProcessBuilder(cmd.split(" ")) + .directory(rootDir) + .start() + .inputStream + .bufferedReader() + .readText() + .trim() + } catch (_: IOException) { + "@unknown@" + } + +val sourceSetsContainer: SourceSetContainer = extensions.getByType(SourceSetContainer::class.java) + +val generateVersionSource by + tasks.registering(Copy::class) { + val buildVersion = project.version.toString() + val javaSrcDirs = sourceSetsContainer["main"].java.srcDirs + val templateInputs = javaSrcDirs.map { it.resolve(versionSrc) } + inputs.files(templateInputs) + inputs.property("buildVersion", buildVersion) + inputs.property("gitRevision", gitrev) + outputs.file(file(versionBuildDir.resolve(versionSrc))) + + from(javaSrcDirs) { + include(versionSrc) + filter { line: String -> + line.replace("@build_number@", buildVersion).replace("@git_rev@", gitrev) + } + } + into(versionBuildDir) + } + +tasks.named("compileJava") { + dependsOn(generateVersionSource) + source(versionBuildDir) + inputs.property("buildNumber", project.version.toString()) + inputs.property("gitRevision", gitrev) + inputs.files(generateVersionSource) +} diff --git a/interop-wire/gradle/owned-output-patterns.txt b/interop-wire/gradle/owned-output-patterns.txt new file mode 100644 index 00000000000..f1a685f0308 --- /dev/null +++ b/interop-wire/gradle/owned-output-patterns.txt @@ -0,0 +1,33 @@ +# Aggregated main outputs that must be pruned on non-clean builds because :interop-wire owns them now. +# Keep this list limited to the narrow message/schema/version/probe nucleus extracted in PR-75. + +network/crypta/io/comm/AsyncMessageCallback* +network/crypta/io/comm/ByteCounter* +network/crypta/io/comm/DMT* +network/crypta/io/comm/Dispatcher* +network/crypta/io/comm/DisconnectedException* +network/crypta/io/comm/DuplicateMessageTypeException* +network/crypta/io/comm/FreenetInetAddress* +network/crypta/io/comm/IncorrectTypeException* +network/crypta/io/comm/Message.class +network/crypta/io/comm/Message$*.class +network/crypta/io/comm/MessageSource.class +network/crypta/io/comm/MessageSource$*.class +network/crypta/io/comm/MessageType.class +network/crypta/io/comm/MessageType$*.class +network/crypta/io/comm/NotConnectedException* +network/crypta/io/comm/OpennetAnnounceRequest* +network/crypta/io/comm/Peer.class +network/crypta/io/comm/Peer$*.class +network/crypta/io/comm/PeerParseException* +network/crypta/io/comm/PeerRestartedException* +network/crypta/io/comm/ReferenceSignatureVerificationException* +network/crypta/io/comm/RetrievalException* +network/crypta/io/comm/TrafficClass* +network/crypta/node/Version.class +network/crypta/node/Version$*.class +network/crypta/node/VersionParseException* +network/crypta/node/probe/Error* +network/crypta/node/probe/Type* +network/crypta/support/Serializer.class +network/crypta/support/Serializer$*.class diff --git a/interop-wire/src/main/java/network/crypta/io/comm/AsyncMessageCallback.java b/interop-wire/src/main/java/network/crypta/io/comm/AsyncMessageCallback.java new file mode 100644 index 00000000000..2a1388cd4d8 --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/io/comm/AsyncMessageCallback.java @@ -0,0 +1,38 @@ +package network.crypta.io.comm; + +/** + * Receives progress and completion notifications for an asynchronously sent packet/message. + * + *

The sender uses this callback to report when a packet has been handed off to the transport and + * when the transmission completes or fails. On a reliable (non-lossy) transport, the peer's + * acknowledgment may follow immediately after {@link #sent()}; on a lossy transport, only {@link + * #acknowledged()} confirms reception by the remote node. + * + *

Implementations should return quickly and avoid long blocking operations; callback methods may + * be invoked from internal networking or scheduler threads. + */ +public interface AsyncMessageCallback { + + /** + * Invoked when the packet actually leaves the local node (i.e., handed to the transport or + * written to the socket/output queue). This does not imply the remote node has received it on a + * lossy transport. + */ + void sent(); + + /** + * Invoked when the remote node acknowledges receipt of the packet. This marks the end of the + * transmission for this packet. On a reliable (non-lossy) transport this may be called + * immediately after {@link #sent()}. + */ + void acknowledged(); + + /** + * Invoked if the connection is lost while the packet is queued or after it has been sent. + * Terminal. + */ + void disconnected(); + + /** Invoked if the packet is lost due to an unrecoverable internal error. Terminal. */ + void fatalError(); +} diff --git a/interop-wire/src/main/java/network/crypta/io/comm/ByteCounter.java b/interop-wire/src/main/java/network/crypta/io/comm/ByteCounter.java new file mode 100644 index 00000000000..d7544389d63 --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/io/comm/ByteCounter.java @@ -0,0 +1,47 @@ +package network.crypta.io.comm; + +/** + * Reports byte counts for network I/O. + * + *

This minimal callback interface is used by messaging and transfer layers to report the + * quantity of data that has been sent or received. Counts are expressed in bytes. Implementations + * typically update statistics, throttling, or diagnostic views; the specific side effects are + * implementation-defined. + * + *

Threading: Callers may invoke these methods from arbitrary threads (for example, I/O or worker + * threads). Implementations should document their own thread-safety guarantees if they are shared + * across threads. + */ +public interface ByteCounter { + + /** + * Records raw bytes transmitted on the wire. + * + *

The count includes all protocol overhead (headers, framing, etc.) and may also include bytes + * that a higher-level throttle has already accounted for. + * + * @param x number of bytes sent (bytes) + */ + void sentBytes(int x); + + /** + * Records raw bytes received from the wire. + * + *

The count includes all protocol overhead (headers, framing, etc.). + * + * @param x number of bytes received (bytes) + */ + void receivedBytes(int x); + + /** + * Records application payload bytes transmitted. + * + *

Only report the size of user-visible data and exclude protocol overhead. For the same + * transfer the caller is expected to also report the raw size via {@link #sentBytes(int)}; do not + * sum the totals from {@code sentBytes(...)} and {@code sentPayload(...)} externally, as they + * represent overlapping measurements and will double-count. + * + * @param x number of payload bytes sent (bytes) + */ + void sentPayload(int x); +} diff --git a/interop-wire/src/main/java/network/crypta/io/comm/DMT.java b/interop-wire/src/main/java/network/crypta/io/comm/DMT.java new file mode 100644 index 00000000000..5f356999437 --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/io/comm/DMT.java @@ -0,0 +1,2924 @@ +package network.crypta.io.comm; + +import network.crypta.crypt.DSAPublicKey; +import network.crypta.keys.Key; +import network.crypta.keys.NodeCHK; +import network.crypta.keys.NodeSSK; +import network.crypta.node.probe.Error; +import network.crypta.node.probe.Type; +import network.crypta.support.BitArray; +import network.crypta.support.Buffer; +import network.crypta.support.Fields; +import network.crypta.support.ShortBuffer; + +/** + * Registry of data message types (DMT) and field keys used by the Crypta node-to-node communication + * layer. + * + *

This utility exposes: + * + *

+ * + *

All members are static; the class is non-instantiable. Priorities indicate relative scheduling + * on the link; lower numeric values are more urgent. See the {@code PRIORITY_*} constants for + * semantics. + * + *

Thread-safety: message type definitions are initialized once and then immutable. Factory + * methods allocate new {@link Message} objects and are thread-safe. + */ +@SuppressWarnings("unused") +public class DMT { + + private DMT() {} + + public static final String UID = "uid"; + /* + * Field name constants used by {@link MessageType#addField(String, Class)} and by callers when + * reading/writing {@link Message} values. Names are part of the on-wire schema; avoid renaming. + */ + public static final String SEND_TIME = "sendTime"; + public static final String EXTERNAL_ADDRESS = "externalAddress"; + public static final String BUILD = "build"; + public static final String FIRST_GOOD_BUILD = "firstGoodBuild"; + public static final String JOINER = "joiner"; + public static final String REASON = "reason"; + public static final String DESCRIPTION = "description"; + public static final String TTL = "ttl"; + public static final String PEERS = "peers"; + public static final String URL = "url"; + public static final String FORWARDERS = "forwarders"; + public static final String FILE_LENGTH = "fileLength"; + public static final String LAST_MODIFIED = "lastModified"; + public static final String CHUNK_NO = "chunkNo"; + public static final String DATA_SOURCE = "dataSource"; + public static final String CACHED = "cached"; + public static final String PACKET_NO = "packetNo"; + public static final String DATA = "data"; + public static final String IS_HASH = "isHash"; + public static final String HASH = "hash"; + public static final String SENT = "sent"; + public static final String MISSING = "missing"; + public static final String KEY = "key"; + public static final String CHK_HEADER = "chkHeader"; + public static final String FREENET_URI = "freenetURI"; + public static final String FREENET_ROUTING_KEY = "freenetRoutingKey"; + public static final String TEST_CHK_HEADERS = "testCHKHeaders"; + public static final String HTL = "hopsToLive"; + public static final String SUCCESS = "success"; + public static final String FNP_SOURCE_PEERNODE = "sourcePeerNode"; + public static final String PING_SEQNO = "pingSequenceNumber"; + public static final String LOCATION = "location"; + public static final String NEAREST_LOCATION = "nearestLocation"; + public static final String BEST_LOCATION = "bestLocation"; + public static final String TARGET_LOCATION = "targetLocation"; + public static final String TYPE = "type"; + public static final String PAYLOAD = "payload"; + public static final String COUNTER = "counter"; + public static final String UNIQUE_COUNTER = "uniqueCounter"; + public static final String LINEAR_COUNTER = "linearCounter"; + public static final String RETURN_LOCATION = "returnLocation"; + public static final String BLOCK_HEADERS = "blockHeaders"; + public static final String DATA_INSERT_REJECTED_REASON = "dataInsertRejectedReason"; + public static final String STREAM_SEQNO = "streamSequenceNumber"; + public static final String IS_LOCAL = "isLocal"; + public static final String ANY_TIMED_OUT = "anyTimedOut"; + public static final String PUBKEY_HASH = "pubkeyHash"; + public static final String NEED_PUB_KEY = "needPubKey"; + public static final String PUBKEY_AS_BYTES = "pubkeyAsBytes"; + public static final String SOURCE_NODENAME = "sourceNodename"; + public static final String TARGET_NODENAME = "targetNodename"; + public static final String NODE_TO_NODE_MESSAGE_TYPE = "nodeToNodeMessageType"; + public static final String NODE_TO_NODE_MESSAGE_TEXT = "nodeToNodeMessageText"; + public static final String NODE_TO_NODE_MESSAGE_DATA = "nodeToNodeMessageData"; + public static final String NODE_UIDS = "nodeUIDs"; + public static final String MY_UID = "myUID"; + public static final String PEER_LOCATIONS = "peerLocations"; + public static final String PEER_UIDS = "peerUIDs"; + public static final String BEST_LOCATIONS_NOT_VISITED = "bestLocationsNotVisited"; + public static final String CORE_PACKAGE_KEY = "mainJarKey"; + public static final String EXTRA_JAR_KEY = "extraJarKey"; + public static final String REVOCATION_KEY = "revocationKey"; + public static final String HAVE_REVOCATION_KEY = "haveRevocationKey"; + // Wire field name intentionally preserved for protocol compatibility. + public static final String CORE_PACKAGE_VERSION = "mainJarVersion"; + public static final String EXTRA_JAR_VERSION = "extJarVersion"; + public static final String REVOCATION_KEY_TIME_LAST_TRIED = "revocationKeyTimeLastTried"; + public static final String REVOCATION_KEY_DNF_COUNT = "revocationKeyDNFCount"; + public static final String REVOCATION_KEY_FILE_LENGTH = "revocationKeyFileLength"; + public static final String CORE_PACKAGE_FILE_LENGTH = "mainJarFileLength"; + public static final String EXTRA_JAR_FILE_LENGTH = "extraJarFileLength"; + public static final String PING_TIME = "pingTime"; + public static final String BWLIMIT_DELAY_TIME = "bwlimitDelayTime"; + public static final String TIME = "time"; + public static final String FORK_COUNT = "forkCount"; + public static final String TIME_LEFT = "timeLeft"; + public static final String PREV_UID = "prevUID"; + public static final String OPENNET_NODEREF = "opennetNoderef"; + public static final String REMOVE = "remove"; + public static final String PURGE = "purge"; + public static final String TRANSFER_UID = "transferUID"; + public static final String NODEREF_LENGTH = "noderefLength"; + public static final String PADDED_LENGTH = "paddedLength"; + public static final String TIME_DELTAS = "timeDeltas"; + public static final String HASHES = "hashes"; + public static final String REJECT_CODE = "rejectCode"; + public static final String ROUTING_ENABLED = "routingEnabled"; + public static final String OFFER_AUTHENTICATOR = "offerAuthenticator"; + public static final String DAWN_HTL = "dawnHtl"; + public static final String SECRET = "secret"; + public static final String NODE_IDENTITY = "nodeIdentity"; + public static final String UPTIME_PERCENT_48H = "uptimePercent48H"; + public static final String FRIEND_VISIBILITY = "friendVisibility"; + public static final String ENABLE_INSERT_FORK_WHEN_CACHEABLE = "enableInsertForkWhenCacheable"; + public static final String PREFER_INSERT = "preferInsert"; + public static final String IGNORE_LOW_BACKOFF = "ignoreLowBackoff"; + public static final String LIST_OF_UIDS = "listOfUIDs"; + public static final String UID_STILL_RUNNING_FLAGS = "UIDStillRunningFlags"; + public static final String PROBE_IDENTIFIER = "probeIdentifier"; + public static final String STORE_SIZE = "storeSize"; + public static final String LINK_LENGTHS = "linkLengths"; + public static final String UPTIME_PERCENT = "uptimePercent"; + public static final String EXPECTED_HASH = "expectedHash"; + public static final String REJECT_STATS = "rejectStats"; + public static final String OUTPUT_BANDWIDTH_CLASS = "outputBandwidthClass"; + public static final String CAPACITY_USAGE = "capacityUsage"; + + // Load management constants + public static final String AVERAGE_TRANSFERS_OUT_PER_INSERT = "averageTransfersOutPerInsert"; + public static final String OTHER_TRANSFERS_OUT_CHK = "otherTransfersOutCHK"; + public static final String OTHER_TRANSFERS_IN_CHK = "otherTransfersInCHK"; + public static final String OTHER_TRANSFERS_OUT_SSK = "otherTransfersOutSSK"; + public static final String OTHER_TRANSFERS_IN_SSK = "otherTransfersInSSK"; + + /** + * Maximum transfers out, hard limit based on congestion control; we will be rejected if our usage + * is over this. + */ + public static final String MAX_TRANSFERS_OUT = "maxTransfersOut"; + + /** + * Maximum transfers out, peer limit. If the total is over the lower limit and our usage is over + * the peer limit, we will be rejected. + */ + public static final String MAX_TRANSFERS_OUT_PEER_LIMIT = "maxTransfersOutPeerLimit"; + + /** + * Maximum transfers out, lower limit. If the total is over the lower limit and our usage is over + * the peer limit, we will be rejected. + */ + public static final String MAX_TRANSFERS_OUT_LOWER_LIMIT = "maxTransfersOutLowerLimit"; + + /** + * Maximum transfers out, upper limit. If the total is over the upper limit, everything is + * rejected. + */ + public static final String MAX_TRANSFERS_OUT_UPPER_LIMIT = "maxTransfersOutUpperLimit"; + + public static final String OUTPUT_BANDWIDTH_LOWER_LIMIT = "outputBandwidthLowerLimit"; + public static final String OUTPUT_BANDWIDTH_UPPER_LIMIT = "outputBandwidthUpperLimit"; + public static final String OUTPUT_BANDWIDTH_PEER_LIMIT = "outputBandwidthPeerLimit"; + public static final String INPUT_BANDWIDTH_LOWER_LIMIT = "inputBandwidthLowerLimit"; + public static final String INPUT_BANDWIDTH_UPPER_LIMIT = "inputBandwidthUpperLimit"; + public static final String INPUT_BANDWIDTH_PEER_LIMIT = "inputBandwidthPeerLimit"; + + public static final String REAL_TIME_FLAG = "realTimeFlag"; + + /** Very urgent control messages. */ + public static final short PRIORITY_NOW = 0; + + /** Short timeout or otherwise urgent (for example, accepts and loop rejections). */ + public static final short PRIORITY_HIGH = 1; // + + /** Routine control messages and completions. */ + public static final short PRIORITY_UNSPECIFIED = 2; + + /** Request initiators; may tolerate longer timeouts. */ + public static final short PRIORITY_LOW = 3; // long timeout, or moderately urgent + + /** + * Bulk data for realtime requests. Not strictly inferior to {@link #PRIORITY_BULK_DATA}; + * scheduling enforces fairness so realtime data does not starve bulk data. + */ + public static final short PRIORITY_REALTIME_DATA = 4; + + /** + * Bulk data transfer (lowest priority). Higher-level admission control must ensure feasibility; + * persistent starvation raises {@code bwlimitDelayTime} and causes request rejection. + */ + public static final short PRIORITY_BULK_DATA = 5; + + public static final short NUM_PRIORITIES = 6; + + // Segmented bulk transfer primitives + + /** Message carrying one data packet in a segmented bulk transfer. */ + public static final MessageType packetTransmit = + new MessageType("packetTransmit", PRIORITY_BULK_DATA); + + static { + packetTransmit.addField(UID, Long.class); + packetTransmit.addField(PACKET_NO, Integer.class); + packetTransmit.addField(SENT, BitArray.class); + packetTransmit.addField(DATA, Buffer.class); + } + + /** + * Creates a {@code packetTransmit} for a segmented bulk transfer. + * + * @param uid Transfer identifier shared by the bulk session. + * @param packetNo Zero-based packet index. + * @param sent Bitset of packets known to be sent; helps coalesce ACK/resend state. + * @param data Payload for this packet. + * @param realTime When true, boosts priority to {@link #PRIORITY_REALTIME_DATA}. + * @return Initialized message. + */ + public static Message createPacketTransmit( + long uid, int packetNo, BitArray sent, Buffer data, boolean realTime) { + Message msg = new Message(packetTransmit); + msg.set(UID, uid); + msg.set(PACKET_NO, packetNo); + msg.set(SENT, sent); + msg.set(DATA, data); + if (realTime) { + msg.boostPriority(); + } + return msg; + } + + /** + * Computes approximate on-wire size in bytes for a {@code packetTransmit} message. + * + * @param size Payload size in bytes. + * @param packets Number of packets in the transfer (for the {@link BitArray}). + * @return Payload size plus header/metadata overhead. + */ + public static int packetTransmitSize(int size, int packets) { + return size + + 8 /* uid */ + + 4 /* packet# */ + + BitArray.serializedLength(packets) + + 4 /* Message header */; + } + + /** + * Computes approximate on-wire size for a bulk packet message that does not carry a BitArray. + * + * @param size Payload size in bytes. + * @return Payload size plus header/metadata overhead. + */ + public static int bulkPacketTransmitSize(int size) { + return size + 8 /* uid */ + 4 /* packet# */ + 4 /* Message header */; + } + + // Use BULK_DATA to reduce spurious resend requests; queued after the packets it represents. + /** Signals that all packets in the current transfer have been enqueued by the sender. */ + public static final MessageType allSent = new MessageType("allSent", PRIORITY_BULK_DATA); + + static { + allSent.addField(UID, Long.class); + } + + /** + * Creates an {@code allSent} notification for the given transfer. + * + * @param uid Transfer identifier. + * @return Initialized message. + */ + public static Message createAllSent(long uid) { + Message msg = new Message(allSent); + msg.set(UID, uid); + return msg; + } + + /** Signals that all packets in the current transfer have been received. */ + public static final MessageType allReceived = + new MessageType("allReceived", PRIORITY_UNSPECIFIED); + + static { + allReceived.addField(UID, Long.class); + } + + /** + * Creates an {@code allReceived} notification for the given transfer. + * + * @param uid Transfer identifier. + * @return Initialized message. + */ + public static Message createAllReceived(long uid) { + Message msg = new Message(allReceived); + msg.set(UID, uid); + return msg; + } + + /** Indicates that a transfer was aborted and will not complete. */ + public static final MessageType sendAborted = + new MessageType("sendAborted", PRIORITY_UNSPECIFIED); + + static { + sendAborted.addField(UID, Long.class); + sendAborted.addField(DESCRIPTION, String.class); + sendAborted.addField(REASON, Integer.class); + } + + /** + * Creates a {@code sendAborted} message. + * + * @param uid Transfer identifier. + * @param reason Implementation-defined error code. + * @param description Human-readable reason. + * @return Initialized message. + */ + public static Message createSendAborted(long uid, int reason, String description) { + Message msg = new Message(sendAborted); + msg.set(UID, uid); + msg.set(REASON, reason); + msg.set(DESCRIPTION, description); + return msg; + } + + /** + * Bulk transfer packet using {@link ShortBuffer} for payload. Used by the bulk transmitter when + * BitArray acknowledgments are not included inline. + */ + public static final MessageType FNPBulkPacketSend = + new MessageType("FNPBulkPacketSend", PRIORITY_BULK_DATA); + + static { + FNPBulkPacketSend.addField(UID, Long.class); + FNPBulkPacketSend.addField(PACKET_NO, Integer.class); + FNPBulkPacketSend.addField(DATA, ShortBuffer.class); + } + + /** + * Creates an {@code FNPBulkPacketSend} for a segmented bulk transfer. + * + * @param uid Transfer identifier. + * @param packetNo Zero-based packet index. + * @param data Packet payload as a {@link ShortBuffer}. + * @return Initialized message. + */ + public static Message createFNPBulkPacketSend(long uid, int packetNo, ShortBuffer data) { + Message msg = new Message(FNPBulkPacketSend); + msg.set(UID, uid); + msg.set(PACKET_NO, packetNo); + msg.set(DATA, data); + return msg; + } + + /** Convenience overload that wraps a byte array into {@link ShortBuffer}. */ + public static Message createFNPBulkPacketSend(long uid, int packetNo, byte[] data) { + return createFNPBulkPacketSend(uid, packetNo, new ShortBuffer(data)); + } + + /** Sender aborted a bulk transfer; no further packets will be sent. */ + public static final MessageType FNPBulkSendAborted = + new MessageType("FNPBulkSendAborted", PRIORITY_UNSPECIFIED); + + static { + FNPBulkSendAborted.addField(UID, Long.class); + } + + /** Creates an {@code FNPBulkSendAborted} for the given transfer. */ + public static Message createFNPBulkSendAborted(long uid) { + Message msg = new Message(FNPBulkSendAborted); + msg.set(UID, uid); + return msg; + } + + /** Receiver aborted a bulk transfer; later packets should be discarded. */ + public static final MessageType FNPBulkReceiveAborted = + new MessageType("FNPBulkReceiveAborted", PRIORITY_UNSPECIFIED); + + static { + FNPBulkReceiveAborted.addField(UID, Long.class); + } + + /** Creates an {@code FNPBulkReceiveAborted} for the given transfer. */ + public static Message createFNPBulkReceiveAborted(long uid) { + Message msg = new Message(FNPBulkReceiveAborted); + msg.set(UID, uid); + return msg; + } + + /** Receiver acknowledges that all expected packets were received. */ + public static final MessageType FNPBulkReceivedAll = + new MessageType("FNPBulkReceivedAll", PRIORITY_UNSPECIFIED); + + static { + FNPBulkReceivedAll.addField(UID, Long.class); + } + + /** Creates an {@code FNPBulkReceivedAll} acknowledgment. */ + public static Message createFNPBulkReceivedAll(long uid) { + Message msg = new Message(FNPBulkReceivedAll); + msg.set(UID, uid); + return msg; + } + + /** Test harness: request that the peer start a trivial transfer. */ + public static final MessageType testTransferSend = + new MessageType("testTransferSend", PRIORITY_UNSPECIFIED); + + static { + testTransferSend.addField(UID, Long.class); + } + + /** Creates a {@code testTransferSend} request. */ + public static Message createTestTransferSend(long uid) { + Message msg = new Message(testTransferSend); + msg.set(UID, uid); + return msg; + } + + /** Test harness: acknowledgment of {@code testTransferSend}. */ + public static final MessageType testTransferSendAck = + new MessageType("testTransferSendAck", PRIORITY_UNSPECIFIED); + + static { + testTransferSendAck.addField(UID, Long.class); + } + + /** Creates a {@code testTransferSendAck}. */ + public static Message createTestTransferSendAck(long uid) { + Message msg = new Message(testTransferSendAck); + msg.set(UID, uid); + return msg; + } + + /** Test harness: send a CHK URI and header for validation. */ + public static final MessageType testSendCHK = + new MessageType("testSendCHK", PRIORITY_UNSPECIFIED); + + static { + testSendCHK.addField(UID, Long.class); + testSendCHK.addField(FREENET_URI, String.class); + testSendCHK.addField(CHK_HEADER, Buffer.class); + } + + /** + * Creates a {@code testSendCHK} with a URI string and header buffer. + * + * @param uid Test identifier. + * @param uri CHK URI as a string. + * @param header Serialized header. + * @return Initialized message. + */ + public static Message createTestSendCHK(long uid, String uri, Buffer header) { + Message msg = new Message(testSendCHK); + msg.set(UID, uid); + msg.set(FREENET_URI, uri); + msg.set(CHK_HEADER, header); + return msg; + } + + /** Test harness: request data by routing key with an explicit HTL. */ + public static final MessageType testRequest = + new MessageType("testRequest", PRIORITY_UNSPECIFIED); + + static { + testRequest.addField(UID, Long.class); + testRequest.addField(FREENET_ROUTING_KEY, Key.class); + testRequest.addField(HTL, Integer.class); + } + + /** + * Creates a {@code testRequest} for a routing key. + * + * @param key Routing key. + * @param id Test identifier. + * @param htl Hops-to-live. + * @return Initialized message. + */ + public static Message createTestRequest(Key key, long id, int htl) { + Message msg = new Message(testRequest); + msg.set(UID, id); + msg.set(FREENET_ROUTING_KEY, key); + msg.set(HTL, htl); + return msg; + } + + /** Test harness: negative lookup result. */ + public static final MessageType testDataNotFound = + new MessageType("testDataNotFound", PRIORITY_UNSPECIFIED); + + static { + testDataNotFound.addField(UID, Long.class); + } + + /** Creates a {@code testDataNotFound}. */ + public static Message createTestDataNotFound(long uid) { + Message msg = new Message(testDataNotFound); + msg.set(UID, uid); + return msg; + } + + /** Test harness: successful reply carrying serialized headers. */ + public static final MessageType testDataReply = + new MessageType("testDataReply", PRIORITY_UNSPECIFIED); + + static { + testDataReply.addField(UID, Long.class); + testDataReply.addField(TEST_CHK_HEADERS, Buffer.class); + } + + /** + * Creates a {@code testDataReply} with serialized headers. + * + * @param uid Test identifier. + * @param headers Serialized headers. + * @return Initialized message. + */ + public static Message createTestDataReply(long uid, byte[] headers) { + Message msg = new Message(testDataReply); + msg.set(UID, uid); + msg.set(TEST_CHK_HEADERS, new Buffer(headers)); + return msg; + } + + /** Test harness: acknowledgment to {@code testSendCHK}. */ + public static final MessageType testSendCHKAck = + new MessageType("testSendCHKAck", PRIORITY_UNSPECIFIED); + + static { + testSendCHKAck.addField(UID, Long.class); + testSendCHKAck.addField(FREENET_URI, String.class); + } + + /** + * Creates a {@code testSendCHKAck} echoing the provided key. + * + * @param uid Test identifier. + * @param key Echo of the CHK key string. + * @return Initialized message. + */ + public static Message createTestSendCHKAck(long uid, String key) { + Message msg = new Message(testSendCHKAck); + msg.set(UID, uid); + msg.set(FREENET_URI, key); + return msg; + } + + /** Test harness: acknowledgment of {@code testDataReply}. */ + public static final MessageType testDataReplyAck = + new MessageType("testDataReplyAck", PRIORITY_UNSPECIFIED); + + static { + testDataReplyAck.addField(UID, Long.class); + } + + /** Creates a {@code testDataReplyAck}. */ + public static Message createTestDataReplyAck(long id) { + Message msg = new Message(testDataReplyAck); + msg.set(UID, id); + return msg; + } + + /** Test harness: acknowledgment of {@code testDataNotFound}. */ + public static final MessageType testDataNotFoundAck = + new MessageType("testDataNotFoundAck", PRIORITY_UNSPECIFIED); + + static { + testDataNotFoundAck.addField(UID, Long.class); + } + + /** Creates a {@code testDataNotFoundAck}. */ + public static Message createTestDataNotFoundAck(long id) { + Message msg = new Message(testDataNotFoundAck); + msg.set(UID, id); + return msg; + } + + // Internal only messages + + /** Internal: indicates completion of a test receiving, with a success flag and reason. */ + public static final MessageType testReceiveCompleted = + new MessageType("testReceiveCompleted", PRIORITY_UNSPECIFIED, true, false); + + static { + testReceiveCompleted.addField(UID, Long.class); + testReceiveCompleted.addField(SUCCESS, Boolean.class); + testReceiveCompleted.addField(REASON, String.class); + } + + /** + * Creates a {@code testReceiveCompleted}. + * + * @param id Test identifier. + * @param success Whether the reception completed successfully. + * @param reason Optional reason text. + * @return Initialized message. + */ + public static Message createTestReceiveCompleted(long id, boolean success, String reason) { + Message msg = new Message(testReceiveCompleted); + msg.set(UID, id); + msg.set(SUCCESS, success); + msg.set(REASON, reason); + return msg; + } + + /** Internal: indicates completion of a test sending, with a success flag and reason. */ + public static final MessageType testSendCompleted = + new MessageType("testSendCompleted", PRIORITY_UNSPECIFIED, true, false); + + static { + testSendCompleted.addField(UID, Long.class); + testSendCompleted.addField(SUCCESS, Boolean.class); + testSendCompleted.addField(REASON, String.class); + } + + /** + * Creates a {@code testSendCompleted}. + * + * @param id Test identifier. + * @param success Whether the sending completed successfully. + * @param reason Optional reason text. + * @return Initialized message. + */ + public static Message createTestSendCompleted(long id, boolean success, String reason) { + Message msg = new Message(testSendCompleted); + msg.set(UID, id); + msg.set(SUCCESS, success); + msg.set(REASON, reason); + return msg; + } + + /** Generic node-to-node message carrying a small typed payload. */ + public static final MessageType nodeToNodeMessage = + new MessageType("nodeToNodeMessage", PRIORITY_LOW, false, false); + + static { + nodeToNodeMessage.addField(NODE_TO_NODE_MESSAGE_TYPE, Integer.class); + nodeToNodeMessage.addField(NODE_TO_NODE_MESSAGE_DATA, ShortBuffer.class); + } + + /** + * Creates a generic node-to-node message with a caller-defined {@code type} and opaque payload. + * + * @param type Application-specific subtype. + * @param data Opaque payload; interpreted by the receiver based on {@code type}. + * @return Initialized message. + */ + public static Message createNodeToNodeMessage(int type, byte[] data) { + Message msg = new Message(nodeToNodeMessage); + msg.set(NODE_TO_NODE_MESSAGE_TYPE, type); + msg.set(NODE_TO_NODE_MESSAGE_DATA, new ShortBuffer(data)); + return msg; + } + + // FNP messages + /** Request for CHK data (content-hash key) routed by location. */ + public static final MessageType FNPCHKDataRequest = + new MessageType("FNPCHKDataRequest", PRIORITY_LOW); + + static { + FNPCHKDataRequest.addField(UID, Long.class); + FNPCHKDataRequest.addField(HTL, Short.class); + FNPCHKDataRequest.addField(NEAREST_LOCATION, Double.class); + FNPCHKDataRequest.addField(FREENET_ROUTING_KEY, NodeCHK.class); + } + + /** + * Creates an {@code FNPCHKDataRequest}. + * + * @param id Request identifier. + * @param htl Hops-to-live. + * @param key Routing key (CHK). + * @return Initialized message. + */ + public static Message createFNPCHKDataRequest(long id, short htl, NodeCHK key) { + Message msg = new Message(FNPCHKDataRequest); + msg.set(UID, id); + msg.set(HTL, htl); + msg.set(FREENET_ROUTING_KEY, key); + msg.set(NEAREST_LOCATION, 0.0); + return msg; + } + + /** Request for SSK data (signed-subspace key) routed by location. */ + public static final MessageType FNPSSKDataRequest = + new MessageType("FNPSSKDataRequest", PRIORITY_LOW); + + static { + FNPSSKDataRequest.addField(UID, Long.class); + FNPSSKDataRequest.addField(HTL, Short.class); + FNPSSKDataRequest.addField(NEAREST_LOCATION, Double.class); + FNPSSKDataRequest.addField(FREENET_ROUTING_KEY, NodeSSK.class); + FNPSSKDataRequest.addField(NEED_PUB_KEY, Boolean.class); + } + + /** + * Creates an {@code FNPSSKDataRequest}. + * + * @param id Request identifier. + * @param htl Hops-to-live. + * @param key Routing key (SSK). + * @param needPubKey Whether the caller requires the public key to be returned. + * @return Initialized message. + */ + public static Message createFNPSSKDataRequest( + long id, short htl, NodeSSK key, boolean needPubKey) { + Message msg = new Message(FNPSSKDataRequest); + msg.set(UID, id); + msg.set(HTL, htl); + msg.set(FREENET_ROUTING_KEY, key); + msg.set(NEAREST_LOCATION, 0.0); + msg.set(NEED_PUB_KEY, needPubKey); + return msg; + } + + // Loop detected: choose a different peer. + /** Rejection indicating a routing loop was encountered. */ + public static final MessageType FNPRejectedLoop = new MessageType("FNPRejectLoop", PRIORITY_HIGH); + + static { + FNPRejectedLoop.addField(UID, Long.class); + } + + /** + * Creates an {@code FNPRejectedLoop} for the given request. + * + * @param id Request identifier. + * @return Initialized message. + */ + public static Message createFNPRejectedLoop(long id) { + Message msg = new Message(FNPRejectedLoop); + msg.set(UID, id); + return msg; + } + + // Too many concurrent requests for current capacity. + /** Rejection due to overload; caller may reduce the rate or choose another peer. */ + public static final MessageType FNPRejectedOverload = + new MessageType("FNPRejectOverload", PRIORITY_HIGH); + + static { + FNPRejectedOverload.addField(UID, Long.class); + FNPRejectedOverload.addField(IS_LOCAL, Boolean.class); + } + + /** + * Creates an {@code FNPRejectedOverload}. + * + * @param id Request identifier. + * @param isLocal Whether the rejecting peer is the local node. + * @return Initialized message. + */ + public static Message createFNPRejectedOverload(long id, boolean isLocal) { + Message msg = new Message(FNPRejectedOverload); + msg.set(UID, id); + msg.set(IS_LOCAL, isLocal); + return msg; + } + + /** Acknowledge that a request was accepted and will proceed. */ + public static final MessageType FNPAccepted = new MessageType("FNPAccepted", PRIORITY_HIGH); + + static { + FNPAccepted.addField(UID, Long.class); + } + + /** + * Creates an {@code FNPAccepted} acknowledgment. + * + * @param id Request identifier. + * @return Initialized message. + */ + public static Message createFNPAccepted(long id) { + Message msg = new Message(FNPAccepted); + msg.set(UID, id); + return msg; + } + + /** Negative result indicating the requested data was not found. */ + public static final MessageType FNPDataNotFound = + new MessageType("FNPDataNotFound", PRIORITY_UNSPECIFIED); + + static { + FNPDataNotFound.addField(UID, Long.class); + } + + /** + * Creates an {@code FNPDataNotFound} for the given request. + * + * @param id Request identifier. + * @return Initialized message. + */ + public static Message createFNPDataNotFound(long id) { + Message msg = new Message(FNPDataNotFound); + msg.set(UID, id); + return msg; + } + + /** Indicates a recent failure; used to guide backoff and avoid repeated attempts. */ + public static final MessageType FNPRecentlyFailed = + new MessageType("FNPRecentlyFailed", PRIORITY_HIGH); + + static { + FNPRecentlyFailed.addField(UID, Long.class); + FNPRecentlyFailed.addField(TIME_LEFT, Integer.class); + } + + /** + * Creates an {@code FNPRecentlyFailed} with a remaining-wait hint. + * + * @param id Request identifier. + * @param timeLeft Remaining time to wait before retrying (unit-implementation-defined). + * @return Initialized message. + */ + public static Message createFNPRecentlyFailed(long id, int timeLeft) { + Message msg = new Message(FNPRecentlyFailed); + msg.set(UID, id); + msg.set(TIME_LEFT, timeLeft); + return msg; + } + + /** Positive result for CHK requests carrying block headers. */ + public static final MessageType FNPCHKDataFound = + new MessageType("FNPCHKDataFound", PRIORITY_UNSPECIFIED); + + static { + FNPCHKDataFound.addField(UID, Long.class); + FNPCHKDataFound.addField(BLOCK_HEADERS, ShortBuffer.class); + } + + /** + * Creates an {@code FNPCHKDataFound} with serialized block headers. + * + * @param id Request identifier. + * @param buf Serialized headers. + * @return Initialized message. + */ + public static Message createFNPCHKDataFound(long id, byte[] buf) { + Message msg = new Message(FNPCHKDataFound); + msg.set(UID, id); + msg.set(BLOCK_HEADERS, new ShortBuffer(buf)); + return msg; + } + + /** No route found for the request; includes remaining HTL. */ + public static final MessageType FNPRouteNotFound = + new MessageType("FNPRouteNotFound", PRIORITY_UNSPECIFIED); + + static { + FNPRouteNotFound.addField(UID, Long.class); + FNPRouteNotFound.addField(HTL, Short.class); + } + + /** + * Creates an {@code FNPRouteNotFound}. + * + * @param id Request identifier. + * @param htl Remaining hops-to-live. + * @return Initialized message. + */ + public static Message createFNPRouteNotFound(long id, short htl) { + Message msg = new Message(FNPRouteNotFound); + msg.set(UID, id); + msg.set(HTL, htl); + return msg; + } + + /** Insert request routed by location (CHK/SSK-agnostic key interface). */ + public static final MessageType FNPInsertRequest = + new MessageType("FNPInsertRequest", PRIORITY_LOW); + + static { + FNPInsertRequest.addField(UID, Long.class); + FNPInsertRequest.addField(HTL, Short.class); + FNPInsertRequest.addField(NEAREST_LOCATION, Double.class); + FNPInsertRequest.addField(FREENET_ROUTING_KEY, Key.class); + } + + /** + * Creates an {@code FNPInsertRequest}. + * + * @param id Request identifier. + * @param htl Hops-to-live. + * @param key Routing key. + * @return Initialized message. + */ + public static Message createFNPInsertRequest(long id, short htl, Key key) { + Message msg = new Message(FNPInsertRequest); + msg.set(UID, id); + msg.set(HTL, htl); + msg.set(FREENET_ROUTING_KEY, key); + msg.set(NEAREST_LOCATION, 0.0); + return msg; + } + + /** Acknowledgment that an insert request was accepted downstream. */ + public static final MessageType FNPInsertReply = + new MessageType("FNPInsertReply", PRIORITY_UNSPECIFIED); + + static { + FNPInsertReply.addField(UID, Long.class); + } + + /** + * Creates an {@code FNPInsertReply} acknowledgment. + * + * @param id Request identifier. + * @return Initialized message. + */ + public static Message createFNPInsertReply(long id) { + Message msg = new Message(FNPInsertReply); + msg.set(UID, id); + return msg; + } + + /** Carries serialized block headers for a data insert. */ + public static final MessageType FNPDataInsert = new MessageType("FNPDataInsert", PRIORITY_HIGH); + + static { + FNPDataInsert.addField(UID, Long.class); + FNPDataInsert.addField(BLOCK_HEADERS, ShortBuffer.class); + } + + /** + * Creates an {@code FNPDataInsert} message with serialized block headers. + * + * @param uid Insert identifier. + * @param headers Serialized headers. + * @return Initialized message. + */ + public static Message createFNPDataInsert(long uid, byte[] headers) { + Message msg = new Message(FNPDataInsert); + msg.set(UID, uid); + msg.set(BLOCK_HEADERS, new ShortBuffer(headers)); + return msg; + } + + /** Indicates that all transfer segments for an insert completed (possibly with timeouts). */ + public static final MessageType FNPInsertTransfersCompleted = + new MessageType("FNPInsertTransfersCompleted", PRIORITY_UNSPECIFIED); + + static { + FNPInsertTransfersCompleted.addField(UID, Long.class); + FNPInsertTransfersCompleted.addField(ANY_TIMED_OUT, Boolean.class); + } + + /** + * Creates an {@code FNPInsertTransfersCompleted}. + * + * @param uid Insert identifier. + * @param anyTimedOut Whether any segment timed out. + * @return Initialized message. + */ + public static Message createFNPInsertTransfersCompleted(long uid, boolean anyTimedOut) { + Message msg = new Message(FNPInsertTransfersCompleted); + msg.set(UID, uid); + msg.set(ANY_TIMED_OUT, anyTimedOut); + return msg; + } + + // This is used by CHK inserts when the DataInsert isn't received. + // SSK inserts just use DataInsertRejected with a timeout reason. + // Consider using a single message for both. One complication is that + // CHKs have a single DataInsert (followed by a transfer), whereas SSKs have 2-3 messages, + // any of which can time out independently. We could send one message now that we have the new + // packet format; see the discussion on FNPSSKInsertRequestNew vs. the old version. + /** Timeout for CHK inserts when {@code FNPDataInsert} was not received in time. */ + public static final MessageType FNPRejectedTimeout = + new MessageType("FNPTooSlow", PRIORITY_UNSPECIFIED); + + static { + FNPRejectedTimeout.addField(UID, Long.class); + } + + /** + * Creates an {@code FNPRejectedTimeout} notification. + * + * @param uid Insert identifier. + * @return Initialized message. + */ + public static Message createFNPRejectedTimeout(long uid) { + Message msg = new Message(FNPRejectedTimeout); + msg.set(UID, uid); + return msg; + } + + /** + * Insert rejected with a reason code (see constants and {@link #getDataInsertRejectedReason}). + */ + public static final MessageType FNPDataInsertRejected = + new MessageType("FNPDataInsertRejected", PRIORITY_UNSPECIFIED); + + static { + FNPDataInsertRejected.addField(UID, Long.class); + FNPDataInsertRejected.addField(DATA_INSERT_REJECTED_REASON, Short.class); + } + + /** + * Creates an {@code FNPDataInsertRejected} with a reason code. + * + * @param uid Insert identifier. + * @param reason Reason code constant. + * @return Initialized message. + */ + public static Message createFNPDataInsertRejected(long uid, short reason) { + Message msg = new Message(FNPDataInsertRejected); + msg.set(UID, uid); + msg.set(DATA_INSERT_REJECTED_REASON, reason); + return msg; + } + + public static final short DATA_INSERT_REJECTED_VERIFY_FAILED = 1; + public static final short DATA_INSERT_REJECTED_RECEIVE_FAILED = 2; + public static final short DATA_INSERT_REJECTED_SSK_ERROR = 3; + public static final short DATA_INSERT_REJECTED_TIMEOUT_WAITING_FOR_ACCEPTED = 4; + + /** Returns a human-readable description for an insert rejection {@code reason} code. */ + public static String getDataInsertRejectedReason(short reason) { + if (reason == DATA_INSERT_REJECTED_VERIFY_FAILED) { + return "Verify failed"; + } else if (reason == DATA_INSERT_REJECTED_RECEIVE_FAILED) { + return "Receive failed"; + } else if (reason == DATA_INSERT_REJECTED_SSK_ERROR) { + return "SSK error"; + } else if (reason == DATA_INSERT_REJECTED_TIMEOUT_WAITING_FOR_ACCEPTED) { + return "Timeout waiting for Accepted (moved on)"; + } + return "Unknown reason code: " + reason; + } + + // Consider using this again now we have a packet format that can handle big messages on any + // connection. + // If re-enabled, ensure priority handling for realtime and be mindful of timeouts associated + // with sending a request at BULK. + + /** Legacy SSK insert request carrying headers, data, and pubkey hash in one message. */ + public static final MessageType FNPSSKInsertRequest = + new MessageType("FNPSSKInsertRequest", PRIORITY_BULK_DATA); + + static { + FNPSSKInsertRequest.addField(UID, Long.class); + FNPSSKInsertRequest.addField(HTL, Short.class); + FNPSSKInsertRequest.addField(FREENET_ROUTING_KEY, NodeSSK.class); + FNPSSKInsertRequest.addField(NEAREST_LOCATION, Double.class); + FNPSSKInsertRequest.addField(BLOCK_HEADERS, ShortBuffer.class); + FNPSSKInsertRequest.addField(PUBKEY_HASH, ShortBuffer.class); + FNPSSKInsertRequest.addField(DATA, ShortBuffer.class); + } + + /** + * Creates a legacy {@code FNPSSKInsertRequest} bundling metadata and data. + * + * @param uid Insert identifier. + * @param htl Hops-to-live. + * @param myKey Routing key (SSK). + * @param headers Serialized block headers. + * @param data Serialized content. + * @param pubKeyHash Hash of the public key. + * @param realTime Boost to realtime if {@code true}. + * @return Initialized message. + */ + public static Message createFNPSSKInsertRequest( + long uid, + short htl, + NodeSSK myKey, + byte[] headers, + byte[] data, + byte[] pubKeyHash, + boolean realTime) { + Message msg = new Message(FNPSSKInsertRequest); + msg.set(UID, uid); + msg.set(HTL, htl); + msg.set(FREENET_ROUTING_KEY, myKey); + msg.set(NEAREST_LOCATION, 0.0); + msg.set(BLOCK_HEADERS, new ShortBuffer(headers)); + msg.set(PUBKEY_HASH, new ShortBuffer(pubKeyHash)); + msg.set(DATA, new ShortBuffer(data)); + if (realTime) { + msg.boostPriority(); + } + return msg; + } + + /** Modern SSK insert request; headers and data are sent in follow-up bulk messages. */ + public static final MessageType FNPSSKInsertRequestNew = + new MessageType("FNPSSKInsertRequestNew", PRIORITY_LOW); + + static { + FNPSSKInsertRequestNew.addField(UID, Long.class); + FNPSSKInsertRequestNew.addField(HTL, Short.class); + FNPSSKInsertRequestNew.addField(FREENET_ROUTING_KEY, NodeSSK.class); + } + + /** + * Creates the initial {@code FNPSSKInsertRequestNew} handshake. + * + * @param uid Insert identifier. + * @param htl Hops-to-live. + * @param myKey Routing key (SSK). + * @return Initialized message. + */ + public static Message createFNPSSKInsertRequestNew(long uid, short htl, NodeSSK myKey) { + Message msg = new Message(FNPSSKInsertRequestNew); + msg.set(UID, uid); + msg.set(HTL, htl); + msg.set(FREENET_ROUTING_KEY, myKey); + return msg; + } + + // SSK inserts data and headers. These are BULK_DATA or REALTIME. + + /** Follow-up message carrying SSK insert block headers. */ + public static final MessageType FNPSSKInsertRequestHeaders = + new MessageType("FNPSSKInsertRequestHeaders", PRIORITY_BULK_DATA); + + static { + FNPSSKInsertRequestHeaders.addField(UID, Long.class); + FNPSSKInsertRequestHeaders.addField(BLOCK_HEADERS, ShortBuffer.class); + } + + /** + * Creates {@code FNPSSKInsertRequestHeaders} with serialized block headers. + * + * @param uid Insert identifier. + * @param headers Serialized headers. + * @param realTime Boost to realtime if {@code true}. + * @return Initialized message. + */ + public static Message createFNPSSKInsertRequestHeaders( + long uid, byte[] headers, boolean realTime) { + Message msg = new Message(FNPSSKInsertRequestHeaders); + msg.set(UID, uid); + msg.set(BLOCK_HEADERS, new ShortBuffer(headers)); + if (realTime) { + msg.boostPriority(); + } + return msg; + } + + /** Follow-up message carrying SSK insert payload data. */ + public static final MessageType FNPSSKInsertRequestData = + new MessageType("FNPSSKInsertRequestData", PRIORITY_BULK_DATA); + + static { + FNPSSKInsertRequestData.addField(UID, Long.class); + FNPSSKInsertRequestData.addField(DATA, ShortBuffer.class); + } + + /** + * Creates {@code FNPSSKInsertRequestData} with serialized payload. + * + * @param uid Insert identifier. + * @param data Serialized content bytes. + * @param realTime Boost to realtime if {@code true}. + * @return Initialized message. + */ + public static Message createFNPSSKInsertRequestData(long uid, byte[] data, boolean realTime) { + Message msg = new Message(FNPSSKInsertRequestData); + msg.set(UID, uid); + msg.set(DATA, new ShortBuffer(data)); + if (realTime) { + msg.boostPriority(); + } + return msg; + } + + // SSK pubkeys, data and headers are all BULK_DATA or REALTIME. + // Requests wait for them all equally, so there is no reason for them to be different, + // plus everything is throttled now. + + /** SSK positive result containing block headers. */ + public static final MessageType FNPSSKDataFoundHeaders = + new MessageType("FNPSSKDataFoundHeaders", PRIORITY_BULK_DATA); + + static { + FNPSSKDataFoundHeaders.addField(UID, Long.class); + FNPSSKDataFoundHeaders.addField(BLOCK_HEADERS, ShortBuffer.class); + } + + /** + * Creates {@code FNPSSKDataFoundHeaders}. + * + * @param uid Request identifier. + * @param headers Serialized headers. + * @param realTime Boost to realtime if {@code true}. + * @return Initialized message. + */ + public static Message createFNPSSKDataFoundHeaders(long uid, byte[] headers, boolean realTime) { + Message msg = new Message(FNPSSKDataFoundHeaders); + msg.set(UID, uid); + msg.set(BLOCK_HEADERS, new ShortBuffer(headers)); + if (realTime) { + msg.boostPriority(); + } + return msg; + } + + /** SSK positive result containing payload data. */ + public static final MessageType FNPSSKDataFoundData = + new MessageType("FNPSSKDataFoundData", PRIORITY_BULK_DATA); + + static { + FNPSSKDataFoundData.addField(UID, Long.class); + FNPSSKDataFoundData.addField(DATA, ShortBuffer.class); + } + + /** + * Creates {@code FNPSSKDataFoundData}. + * + * @param uid Request identifier. + * @param data Serialized content bytes. + * @param realTime Boost to realtime if {@code true}. + * @return Initialized message. + */ + public static Message createFNPSSKDataFoundData(long uid, byte[] data, boolean realTime) { + Message msg = new Message(FNPSSKDataFoundData); + msg.set(UID, uid); + msg.set(DATA, new ShortBuffer(data)); + if (realTime) { + msg.boostPriority(); + } + return msg; + } + + /** Acknowledges an SSK request and indicates whether a public key is needed. */ + public static final MessageType FNPSSKAccepted = new MessageType("FNPSSKAccepted", PRIORITY_HIGH); + + static { + FNPSSKAccepted.addField(UID, Long.class); + FNPSSKAccepted.addField(NEED_PUB_KEY, Boolean.class); + } + + /** + * Creates an {@code FNPSSKAccepted}. + * + * @param uid Request identifier. + * @param needPubKey Whether the receiver requires the public key. + * @return Initialized message. + */ + public static Message createFNPSSKAccepted(long uid, boolean needPubKey) { + Message msg = new Message(FNPSSKAccepted); + msg.set(UID, uid); + msg.set(NEED_PUB_KEY, needPubKey); + return msg; + } + + /** SSK public key payload. */ + public static final MessageType FNPSSKPubKey = + new MessageType("FNPSSKPubKey", PRIORITY_BULK_DATA); + + static { + FNPSSKPubKey.addField(UID, Long.class); + FNPSSKPubKey.addField(PUBKEY_AS_BYTES, ShortBuffer.class); + } + + /** + * Creates an {@code FNPSSKPubKey} containing a padded public key. + * + * @param uid Request identifier. + * @param pubkey Public key to send. + * @param realTime Boost to realtime if {@code true}. + * @return Initialized message. + */ + public static Message createFNPSSKPubKey(long uid, DSAPublicKey pubkey, boolean realTime) { + Message msg = new Message(FNPSSKPubKey); + msg.set(UID, uid); + msg.set(PUBKEY_AS_BYTES, new ShortBuffer(pubkey.asPaddedBytes())); + if (realTime) { + msg.boostPriority(); + } + return msg; + } + + /** Acknowledges receipt of an SSK public key. */ + public static final MessageType FNPSSKPubKeyAccepted = + new MessageType("FNPSSKPubKeyAccepted", PRIORITY_HIGH); + + static { + FNPSSKPubKeyAccepted.addField(UID, Long.class); + } + + /** + * Creates an {@code FNPSSKPubKeyAccepted} acknowledgment. + * + * @param uid Request identifier. + * @return Initialized message. + */ + public static Message createFNPSSKPubKeyAccepted(long uid) { + Message msg = new Message(FNPSSKPubKeyAccepted); + msg.set(UID, uid); + return msg; + } + + // Opennet completions (not sent to darknet nodes) + + /** + * Sent when a request to an opennet node completes and the data source does not path-fold. Also + * used on darknet. + */ + public static final MessageType FNPOpennetCompletedAck = + new MessageType("FNPOpennetCompletedAck", PRIORITY_HIGH); + + static { + FNPOpennetCompletedAck.addField(UID, Long.class); + } + + /** + * Creates an {@code FNPOpennetCompletedAck} for the given request. + * + * @param uid Original request identifier. + * @return Initialized message. + */ + public static Message createFNPOpennetCompletedAck(long uid) { + Message msg = new Message(FNPOpennetCompletedAck); + msg.set(UID, uid); + return msg; + } + + /** Sent when we wait for an FNP transfer or a completion from upstream, and it never comes. */ + public static final MessageType FNPOpennetCompletedTimeout = + new MessageType("FNPOpennetCompletedTimeout", PRIORITY_HIGH); + + static { + FNPOpennetCompletedTimeout.addField(UID, Long.class); + } + + /** + * Creates an {@code FNPOpennetCompletedTimeout} when the expected transfer or completion never + * arrived. + * + * @param uid Original request identifier. + * @return Initialized message. + */ + public static Message createFNPOpennetCompletedTimeout(long uid) { + Message msg = new Message(FNPOpennetCompletedTimeout); + msg.set(UID, uid); + return msg; + } + + /** + * Sent when a request completes and the data source wants to path fold. Starts a bulk data + * transfer including the (padded) noderef. + */ + public static final MessageType FNPOpennetConnectDestinationNew = + new MessageType("FNPConnectDestinationNew", PRIORITY_UNSPECIFIED); + + static { + FNPOpennetConnectDestinationNew.addField(UID, Long.class); // UID of the original message chain + FNPOpennetConnectDestinationNew.addField(TRANSFER_UID, Long.class); // UID of data transfer + FNPOpennetConnectDestinationNew.addField(NODEREF_LENGTH, Integer.class); // Size of noderef + FNPOpennetConnectDestinationNew.addField( + PADDED_LENGTH, Integer.class); // Size of actual transfer i.e., padded length + } + + /** + * Creates an {@code FNPOpennetConnectDestinationNew} to initiate path folding with a noderef + * transfer. + * + * @param uid Original request identifier. + * @param transferUID Transfer identifier for the noderef payload. + * @param noderefLength Unpadded noderef length in bytes. + * @param paddedLength Padded transfer length in bytes. + * @return Initialized message. + */ + public static Message createFNPOpennetConnectDestinationNew( + long uid, long transferUID, int noderefLength, int paddedLength) { + Message msg = new Message(FNPOpennetConnectDestinationNew); + msg.set(UID, uid); + msg.set(TRANSFER_UID, transferUID); + msg.set(NODEREF_LENGTH, noderefLength); + msg.set(PADDED_LENGTH, paddedLength); + return msg; + } + + /** + * Path folding response. Sent when the requestor wants to path fold and has received a noderef + * from the data source. Starts a bulk data transfer including the (padded) noderef. + */ + public static final MessageType FNPOpennetConnectReplyNew = + new MessageType("FNPConnectReplyNew", PRIORITY_UNSPECIFIED); + + static { + FNPOpennetConnectReplyNew.addField(UID, Long.class); // UID of the original message chain + FNPOpennetConnectReplyNew.addField(TRANSFER_UID, Long.class); // UID of data transfer + FNPOpennetConnectReplyNew.addField(NODEREF_LENGTH, Integer.class); // Size of noderef + FNPOpennetConnectReplyNew.addField( + PADDED_LENGTH, Integer.class); // Size of actual transfer i.e., padded length + } + + /** + * Creates an {@code FNPOpennetConnectReplyNew} to accept path folding and start the transfer. + * + * @param uid Original request identifier. + * @param transferUID Transfer identifier for the noderef payload. + * @param noderefLength Unpadded noderef length in bytes. + * @param paddedLength Padded transfer length in bytes. + * @return Initialized message. + */ + public static Message createFNPOpennetConnectReplyNew( + long uid, long transferUID, int noderefLength, int paddedLength) { + Message msg = new Message(FNPOpennetConnectReplyNew); + msg.set(UID, uid); + msg.set(TRANSFER_UID, transferUID); + msg.set(NODEREF_LENGTH, noderefLength); + msg.set(PADDED_LENGTH, paddedLength); + return msg; + } + + // Opennet announcement + + /** + * Announcement request. Noderef is attached, will be transferred before anything else is done. + */ + public static final MessageType FNPOpennetAnnounceRequest = + new MessageType("FNPOpennetAnnounceRequest", PRIORITY_HIGH); + + static { + FNPOpennetAnnounceRequest.addField(UID, Long.class); + FNPOpennetAnnounceRequest.addField(TRANSFER_UID, Long.class); + FNPOpennetAnnounceRequest.addField(NODEREF_LENGTH, Integer.class); + FNPOpennetAnnounceRequest.addField(PADDED_LENGTH, Integer.class); + FNPOpennetAnnounceRequest.addField(HTL, Short.class); + FNPOpennetAnnounceRequest.addField(NEAREST_LOCATION, Double.class); + FNPOpennetAnnounceRequest.addField(TARGET_LOCATION, Double.class); + } + + /** + * Creates an {@code FNPOpennetAnnounceRequest} with a padded noderef to be transferred first. + * + * @param request announce request metadata + * @return Initialized message. + */ + public static Message createFNPOpennetAnnounceRequest(OpennetAnnounceRequest request) { + Message msg = new Message(FNPOpennetAnnounceRequest); + msg.set(UID, request.uid()); + msg.set(TRANSFER_UID, request.transferUID()); + msg.set(NODEREF_LENGTH, request.noderefLength()); + msg.set(PADDED_LENGTH, request.paddedLength()); + msg.set(HTL, request.htl()); + msg.set(NEAREST_LOCATION, 0.0); + msg.set(TARGET_LOCATION, request.target()); + return msg; + } + + /** + * Announcement reply. Noderef is attached and transferred; both nodes add the other. A single + * request may result in many replies. When the announcement is done, we return a DataNotFound; if + * we run into a dead-end, we return a RejectedLoop; if we can't accept it, RejectedOverload. + */ + public static final MessageType FNPOpennetAnnounceReply = + new MessageType("FNPOpennetAnnounceReply", PRIORITY_UNSPECIFIED); + + static { + FNPOpennetAnnounceReply.addField(UID, Long.class); + FNPOpennetAnnounceReply.addField(TRANSFER_UID, Long.class); + FNPOpennetAnnounceReply.addField(NODEREF_LENGTH, Integer.class); + FNPOpennetAnnounceReply.addField(PADDED_LENGTH, Integer.class); + } + + /** + * Creates an {@code FNPOpennetAnnounceReply} referencing an attached noderef transfer. + * + * @param uid Original request identifier. + * @param transferUID Transfer identifier for the noderef payload. + * @param noderefLength Unpadded noderef length in bytes. + * @param paddedLength Padded transfer length in bytes. + * @return Initialized message. + */ + public static Message createFNPOpennetAnnounceReply( + long uid, long transferUID, int noderefLength, int paddedLength) { + Message msg = new Message(FNPOpennetAnnounceReply); + msg.set(UID, uid); + msg.set(TRANSFER_UID, transferUID); + msg.set(NODEREF_LENGTH, noderefLength); + msg.set(PADDED_LENGTH, paddedLength); + return msg; + } + + public static final MessageType FNPOpennetAnnounceCompleted = + new MessageType("FNPOpennetAnnounceCompleted", PRIORITY_UNSPECIFIED); + + static { + FNPOpennetAnnounceCompleted.addField(UID, Long.class); + } + + /** Creates an {@code FNPOpennetAnnounceCompleted} notification. */ + public static Message createFNPOpennetAnnounceCompleted(long uid) { + Message msg = new Message(FNPOpennetAnnounceCompleted); + msg.set(UID, uid); + return msg; + } + + /** Indicates that opennet functionality is disabled on the peer. */ + public static final MessageType FNPOpennetDisabled = + new MessageType("FNPOpennetDisabled", PRIORITY_HIGH); + + static { + FNPOpennetDisabled.addField(UID, Long.class); + } + + /** Creates an {@code FNPOpennetDisabled} notification. */ + public static Message createFNPOpennetDisabled(long uid) { + Message msg = new Message(FNPOpennetDisabled); + msg.set(UID, uid); + return msg; + } + + /** Noderef was rejected during path folding; includes a rejection code. */ + public static final MessageType FNPOpennetNoderefRejected = + new MessageType("FNPOpennetNoderefRejected", PRIORITY_HIGH); + + static { + FNPOpennetNoderefRejected.addField(UID, Long.class); + FNPOpennetNoderefRejected.addField(REJECT_CODE, Integer.class); + } + + /** + * Creates an {@code FNPOpennetNoderefRejected} with a reason code. + * + * @param uid Original request identifier. + * @param rejectCode One of the {@code NODEREF_REJECTED_*} constants. + * @return Initialized message. + */ + public static Message createFNPOpennetNoderefRejected(long uid, int rejectCode) { + Message msg = new Message(FNPOpennetNoderefRejected); + msg.set(UID, uid); + msg.set(REJECT_CODE, rejectCode); + return msg; + } + + /** Returns a human-readable description for an opennet noderef rejection code. */ + public static String getOpennetRejectedCode(int x) { + return switch (x) { + case NODEREF_REJECTED_TOO_BIG -> "Too big"; + case NODEREF_REJECTED_REAL_BIGGER_THAN_PADDED -> "Real length bigger than padded length"; + case NODEREF_REJECTED_TRANSFER_FAILED -> "Transfer failed"; + case NODEREF_REJECTED_INVALID -> "Invalid noderef"; + default -> "Unknown rejection code " + x; + }; + } + + public static final int NODEREF_REJECTED_TOO_BIG = 1; + public static final int NODEREF_REJECTED_REAL_BIGGER_THAN_PADDED = 2; + public static final int NODEREF_REJECTED_TRANSFER_FAILED = 3; + public static final int NODEREF_REJECTED_INVALID = 4; + + // Legacy message retained; evaluate removal in a future cleanup. + + /** Announcement declined: peer is not accepting new opennet links. */ + public static final MessageType FNPOpennetAnnounceNodeNotWanted = + new MessageType("FNPOpennetAnnounceNodeNotWanted", PRIORITY_LOW); + + static { + FNPOpennetAnnounceNodeNotWanted.addField(UID, Long.class); + } + + public static Message createFNPOpennetAnnounceNodeNotWanted(long uid) { + Message msg = new Message(FNPOpennetAnnounceNodeNotWanted); + msg.set(UID, uid); + return msg; + } + + // Key offers (ULPRs) + + /** Offer a key (ULPR) out-of-band prior to a get. */ + public static final MessageType FNPOfferKey = new MessageType("FNPOfferKey", PRIORITY_LOW); + + static { + FNPOfferKey.addField(KEY, Key.class); + FNPOfferKey.addField(OFFER_AUTHENTICATOR, ShortBuffer.class); + } + + /** + * Creates an {@code FNPOfferKey} with a keyed authenticator. + * + * @param key Offered key. + * @param authenticator Authenticator bytes. + * @return Initialized message. + */ + public static Message createFNPOfferKey(Key key, byte[] authenticator) { + Message msg = new Message(FNPOfferKey); + msg.set(KEY, key); + msg.set(OFFER_AUTHENTICATOR, new ShortBuffer(authenticator)); + return msg; + } + + // Short timeout; priority choice is explicit below. + public static final MessageType FNPGetOfferedKey = + new MessageType("FNPGetOfferedKey", PRIORITY_LOW); + + static { + FNPGetOfferedKey.addField(KEY, Key.class); + FNPGetOfferedKey.addField(OFFER_AUTHENTICATOR, ShortBuffer.class); + FNPGetOfferedKey.addField(NEED_PUB_KEY, Boolean.class); + FNPGetOfferedKey.addField(UID, Long.class); + } + + /** + * Requests data for a previously offered key. + * + * @param key Key to fetch. + * @param authenticator Authenticator matching the offer. + * @param needPubkey Whether a public key should be returned when applicable. + * @param uid Request identifier. + * @return Initialized message. + */ + public static Message createFNPGetOfferedKey( + Key key, byte[] authenticator, boolean needPubkey, long uid) { + Message msg = new Message(FNPGetOfferedKey); + msg.set(KEY, key); + msg.set(OFFER_AUTHENTICATOR, new ShortBuffer(authenticator)); + msg.set(NEED_PUB_KEY, needPubkey); + msg.set(UID, uid); + return msg; + } + + // Permanently rejected. {@code FNPRejectedOverload} is temporary. + public static final MessageType FNPGetOfferedKeyInvalid = + new MessageType("FNPGetOfferedKeyInvalid", PRIORITY_HIGH); + + static { // short timeout + FNPGetOfferedKeyInvalid.addField(UID, Long.class); + FNPGetOfferedKeyInvalid.addField(REASON, Short.class); + } + + /** + * Creates an {@code FNPGetOfferedKeyInvalid} with a reason code. + * + * @param uid Request identifier. + * @param reason One of the {@code GET_OFFERED_KEY_REJECTED_*} constants. + * @return Initialized message. + */ + public static Message createFNPGetOfferedKeyInvalid(long uid, short reason) { + Message msg = new Message(FNPGetOfferedKeyInvalid); + msg.set(UID, uid); + msg.set(REASON, reason); + return msg; + } + + public static final short GET_OFFERED_KEY_REJECTED_BAD_AUTHENTICATOR = 1; + public static final short GET_OFFERED_KEY_REJECTED_NO_KEY = 2; + + /** Link-level ping for reachability/latency checks. */ + public static final MessageType FNPPing = new MessageType("FNPPing", PRIORITY_HIGH); + + static { + FNPPing.addField(PING_SEQNO, Integer.class); + } + + /** + * Creates a ping with a sequence number echoed by {@code FNPPong}. + * + * @param seqNo Sequence number. + * @return Initialized message. + */ + public static Message createFNPPing(int seqNo) { + Message msg = new Message(FNPPing); + msg.set(PING_SEQNO, seqNo); + return msg; + } + + /** Ping response echoing the sequence number. */ + public static final MessageType FNPPong = new MessageType("FNPPong", PRIORITY_HIGH); + + static { + FNPPong.addField(PING_SEQNO, Integer.class); + } + + /** + * Creates a pong echoing the sequence number from a ping. + * + * @param seqNo Sequence number. + * @return Initialized message. + */ + public static Message createFNPPong(int seqNo) { + Message msg = new Message(FNPPong); + msg.set(PING_SEQNO, seqNo); + return msg; + } + + /** Reply containing routing-hints metrics gathered during a probe. */ + public static final MessageType FNPRHProbeReply = + new MessageType("FNPRHProbeReply", PRIORITY_UNSPECIFIED); + + static { + FNPRHProbeReply.addField(UID, Long.class); + FNPRHProbeReply.addField(NEAREST_LOCATION, Double.class); + FNPRHProbeReply.addField(BEST_LOCATION, Double.class); + FNPRHProbeReply.addField(COUNTER, Short.class); + FNPRHProbeReply.addField(UNIQUE_COUNTER, Short.class); + FNPRHProbeReply.addField(LINEAR_COUNTER, Short.class); + } + + /** + * Creates an {@code FNPRHProbeReply}. + * + * @param uid Probe identifier. + * @param nearest Nearest known location. + * @param best Best known location. + * @param counter General counter value. + * @param uniqueCounter Distinct path count (semantics implementation-defined). + * @param linearCounter Linearized counter (semantics implementation-defined). + * @return Initialized message. + */ + public static Message createFNPRHProbeReply( + long uid, + double nearest, + double best, + short counter, + short uniqueCounter, + short linearCounter) { + Message msg = new Message(FNPRHProbeReply); + msg.set(UID, uid); + msg.set(NEAREST_LOCATION, nearest); + msg.set(BEST_LOCATION, best); + msg.set(COUNTER, counter); + msg.set(UNIQUE_COUNTER, uniqueCounter); + msg.set(LINEAR_COUNTER, linearCounter); + return msg; + } + + public static final MessageType ProbeRequest = new MessageType("ProbeRequest", PRIORITY_HIGH); + + static { + ProbeRequest.addField(HTL, Byte.class); + ProbeRequest.addField(UID, Long.class); + ProbeRequest.addField(TYPE, Byte.class); + } + + /** + * Constructs a probe request. + * + * @param htl hopsToLive: hops until a result is requested. + * @param uid Probe identifier: should be unique. + * @return Message with requested attributes. + */ + public static Message createProbeRequest(byte htl, long uid, Type type) { + Message msg = new Message(ProbeRequest); + msg.set(HTL, htl); + msg.set(UID, uid); + msg.set(TYPE, type.code); + return msg; + } + + public static final MessageType ProbeError = new MessageType("ProbeError", PRIORITY_HIGH); + + static { + ProbeError.addField(UID, Long.class); + ProbeError.addField(TYPE, Byte.class); + } + + /** + * Creates a probe response which indicates there was an error. + * + * @param uid Probe identifier. + * @param error The type of error that occurred. Can be one of Probe.ProbeError. + * @return Message with the requested attributes. + */ + public static Message createProbeError(long uid, Error error) { + Message msg = new Message(ProbeError); + msg.set(UID, uid); + msg.set(TYPE, error.code); + return msg; + } + + public static final MessageType ProbeRefused = new MessageType("ProbeRefused", PRIORITY_HIGH); + + static { + ProbeRefused.addField(DMT.UID, Long.class); + } + + /** + * Creates a probe response which indicates that the endpoint opted not to respond with the + * requested result. + * + * @param uid Probe identifier. + * @return Message with the requested attribute. + */ + public static Message createProbeRefused(long uid) { + Message msg = new Message(ProbeRefused); + msg.set(UID, uid); + return msg; + } + + public static final MessageType ProbeBandwidth = new MessageType("ProbeBandwidth", PRIORITY_HIGH); + + static { + ProbeBandwidth.addField(UID, Long.class); + ProbeBandwidth.addField(OUTPUT_BANDWIDTH_UPPER_LIMIT, Float.class); + } + + /** + * Creates a probe response to a query for bandwidth limits. + * + * @param uid Probe identifier. + * @param limit Endpoint output bandwidth limit in KiB per second. + * @return Message with requested attributes. + */ + public static Message createProbeBandwidth(long uid, float limit) { + Message msg = new Message(ProbeBandwidth); + msg.set(UID, uid); + msg.set(OUTPUT_BANDWIDTH_UPPER_LIMIT, limit); + return msg; + } + + public static final MessageType ProbeBuild = new MessageType("ProbeBuild", PRIORITY_HIGH); + + static { + ProbeBuild.addField(UID, Long.class); + ProbeBuild.addField(BUILD, Integer.class); + } + + /** + * Creates a probe response to a query for build. + * + * @param uid Probe identifier. + * @param build Endpoint build of Freenet. + * @return Message with requested attributes. + */ + public static Message createProbeBuild(long uid, int build) { + Message msg = new Message(ProbeBuild); + msg.set(UID, uid); + msg.set(BUILD, build); + return msg; + } + + public static final MessageType ProbeIdentifier = + new MessageType("ProbeIdentifier", PRIORITY_HIGH); + + static { + ProbeIdentifier.addField(UID, Long.class); + ProbeIdentifier.addField(PROBE_IDENTIFIER, Long.class); + ProbeIdentifier.addField(UPTIME_PERCENT, Byte.class); + } + + /** + * Creates a probe response to a query for identifier. + * + * @param uid Probe UID. + * @param probeId Endpoint identifier. + * @param uptimePercentage 7-day uptime percentage. + * @return Message with requested attributes. + */ + public static Message createProbeIdentifier(long uid, long probeId, byte uptimePercentage) { + Message msg = new Message(ProbeIdentifier); + msg.set(UID, uid); + msg.set(PROBE_IDENTIFIER, probeId); + msg.set(UPTIME_PERCENT, uptimePercentage); + return msg; + } + + public static final MessageType ProbeLinkLengths = + new MessageType("ProbeLinkLengths", PRIORITY_HIGH); + + static { + ProbeLinkLengths.addField(UID, Long.class); + ProbeLinkLengths.addField(LINK_LENGTHS, float[].class); + } + + /** + * Creates a probe response to a query for link lengths. + * + * @param uid Probe identifier. + * @param linkLengths Endpoint link lengths. + * @return Message with requested attributes. + */ + public static Message createProbeLinkLengths(long uid, float[] linkLengths) { + Message msg = new Message(ProbeLinkLengths); + msg.set(UID, uid); + msg.set(LINK_LENGTHS, linkLengths); + return msg; + } + + public static final MessageType ProbeLocation = new MessageType("ProbeLocation", PRIORITY_HIGH); + + static { + ProbeLocation.addField(UID, Long.class); + ProbeLocation.addField(LOCATION, Float.class); + } + + /** + * Creates a probe response to a query for location. + * + * @param uid Probe identifier. + * @param location Endpoint location. + * @return Message with the requested attributes. + */ + public static Message createProbeLocation(long uid, float location) { + Message msg = new Message(ProbeLocation); + msg.set(UID, uid); + msg.set(LOCATION, location); + return msg; + } + + public static final MessageType ProbeStoreSize = new MessageType("ProbeStoreSize", PRIORITY_HIGH); + + static { + ProbeStoreSize.addField(UID, Long.class); + ProbeStoreSize.addField(STORE_SIZE, Float.class); + } + + /** + * Creates a probe response to a query for store size. + * + * @param uid Probe identifier. + * @param storeSize Endpoint store size in GiB multiplied by Gaussian noise. + * @return Message with requested attributes. + */ + public static Message createProbeStoreSize(long uid, float storeSize) { + Message msg = new Message(ProbeStoreSize); + msg.set(UID, uid); + msg.set(STORE_SIZE, storeSize); + return msg; + } + + public static final MessageType ProbeUptime = new MessageType("ProbeUptime", PRIORITY_HIGH); + + static { + ProbeUptime.addField(UID, Long.class); + ProbeUptime.addField(UPTIME_PERCENT, Float.class); + } + + /** + * Creates a probe response to a query for uptime. + * + * @param uid Probe identifier. + * @param uptimePercent Percentage of the requested period (48 hours or 7 days) which the endpoint + * was online. + * @return Message with requested attributes. + */ + public static Message createProbeUptime(long uid, float uptimePercent) { + Message msg = new Message(ProbeUptime); + msg.set(UID, uid); + msg.set(UPTIME_PERCENT, uptimePercent); + return msg; + } + + public static final MessageType ProbeRejectStats = + new MessageType("ProbeRejectStats", PRIORITY_HIGH); + + static { + ProbeRejectStats.addField(UID, Long.class); + ProbeRejectStats.addField(REJECT_STATS, ShortBuffer.class); + } + + public static Message createProbeRejectStats(long uid, byte[] rejectStats) { + Message msg = new Message(ProbeRejectStats); + msg.set(UID, uid); + msg.set(REJECT_STATS, new ShortBuffer(rejectStats)); + return msg; + } + + /** Divide the output bandwidth limit by this to get bandwidth class. */ + static final int CAPACITY_USAGE_MULTIPLIER = 10 * 1024; + + /** Maximum value of bandwidth class */ + static final byte CAPACITY_USAGE_MAX = 10; + + /** Minimum value of bandwidth class */ + static final byte CAPACITY_USAGE_MIN = 1; + + public static final MessageType ProbeOverallBulkOutputCapacityUsage = + new MessageType("ProbeOverallBulkOutputCapacityUsage", PRIORITY_HIGH); + + static { + ProbeOverallBulkOutputCapacityUsage.addField(UID, Long.class); // UID for the probe + ProbeOverallBulkOutputCapacityUsage.addField( + OUTPUT_BANDWIDTH_CLASS, Byte.class); // Approximate bandwidth, severely truncated. + ProbeOverallBulkOutputCapacityUsage.addField( + CAPACITY_USAGE, Float.class); // Noisy capacity usage. + } + + /** + * Coarsens a bandwidth limit to a small ordinal class for probes. + * + * @param bandwidthLimit Output bandwidth limit in bytes per second. + * @return A class in the range [{@link #CAPACITY_USAGE_MIN}, {@link #CAPACITY_USAGE_MAX}]. + */ + public static byte bandwidthClassForCapacityUsage(int bandwidthLimit) { + bandwidthLimit /= CAPACITY_USAGE_MULTIPLIER; + bandwidthLimit = Math.min(bandwidthLimit, CAPACITY_USAGE_MAX); + return (byte) Math.max(bandwidthLimit, CAPACITY_USAGE_MIN); + } + + /** + * Creates a probe message with an approximate output bandwidth class and a noisy capacity usage + * estimate. + * + * @param uid Probe identifier. + * @param outputBandwidthClass Ordinal class from {@link #bandwidthClassForCapacityUsage(int)}. + * @param capacityUsage Noisy capacity usage (unit-less fraction). + * @return Initialized message. + */ + public static Message createProbeOverallBulkOutputCapacityUsage( + long uid, byte outputBandwidthClass, float capacityUsage) { + Message msg = new Message(ProbeOverallBulkOutputCapacityUsage); + msg.set(UID, uid); + msg.set(OUTPUT_BANDWIDTH_CLASS, outputBandwidthClass); + msg.set(CAPACITY_USAGE, capacityUsage); + return msg; + } + + /** Start of a four-phase location swap handshake. */ + public static final MessageType FNPSwapRequest = new MessageType("FNPSwapRequest", PRIORITY_HIGH); + + static { + FNPSwapRequest.addField(UID, Long.class); + FNPSwapRequest.addField(HASH, ShortBuffer.class); + FNPSwapRequest.addField(HTL, Integer.class); + } + + /** + * Creates an {@code FNPSwapRequest}. + * + * @param uid Swap identifier. + * @param buf Hash or token bytes. + * @param htl Hops-to-live. + * @return Initialized message. + */ + public static Message createFNPSwapRequest(long uid, byte[] buf, int htl) { + Message msg = new Message(FNPSwapRequest); + msg.set(UID, uid); + msg.set(HASH, new ShortBuffer(buf)); + msg.set(HTL, htl); + return msg; + } + + /** Swap rejected; aborts the handshake. */ + public static final MessageType FNPSwapRejected = + new MessageType("FNPSwapRejected", PRIORITY_HIGH); + + static { + FNPSwapRejected.addField(UID, Long.class); + } + + /** + * Creates an {@code FNPSwapRejected}. + * + * @param uid Swap identifier. + * @return Initialized message. + */ + public static Message createFNPSwapRejected(long uid) { + Message msg = new Message(FNPSwapRejected); + msg.set(UID, uid); + return msg; + } + + /** Swap reply carrying a hash/token. */ + public static final MessageType FNPSwapReply = new MessageType("FNPSwapReply", PRIORITY_HIGH); + + static { + FNPSwapReply.addField(UID, Long.class); + FNPSwapReply.addField(HASH, ShortBuffer.class); + } + + /** + * Creates an {@code FNPSwapReply}. + * + * @param uid Swap identifier. + * @param buf Hash or token bytes. + * @return Initialized message. + */ + public static Message createFNPSwapReply(long uid, byte[] buf) { + Message msg = new Message(FNPSwapReply); + msg.set(UID, uid); + msg.set(HASH, new ShortBuffer(buf)); + return msg; + } + + /** Commit step of the swap handshake, carrying final parameters. */ + public static final MessageType FNPSwapCommit = new MessageType("FNPSwapCommit", PRIORITY_HIGH); + + static { + FNPSwapCommit.addField(UID, Long.class); + FNPSwapCommit.addField(DATA, ShortBuffer.class); + } + + /** + * Creates an {@code FNPSwapCommit}. + * + * @param uid Swap identifier. + * @param buf Commit data bytes. + * @return Initialized message. + */ + public static Message createFNPSwapCommit(long uid, byte[] buf) { + Message msg = new Message(FNPSwapCommit); + msg.set(UID, uid); + msg.set(DATA, new ShortBuffer(buf)); + return msg; + } + + /** Final state of the swap; both sides complete. */ + public static final MessageType FNPSwapComplete = + new MessageType("FNPSwapComplete", PRIORITY_HIGH); + + static { + FNPSwapComplete.addField(UID, Long.class); + FNPSwapComplete.addField(DATA, ShortBuffer.class); + } + + /** + * Creates an {@code FNPSwapComplete}. + * + * @param uid Swap identifier. + * @param buf Completion data bytes. + * @return Initialized message. + */ + public static Message createFNPSwapComplete(long uid, byte[] buf) { + Message msg = new Message(FNPSwapComplete); + msg.set(UID, uid); + msg.set(DATA, new ShortBuffer(buf)); + return msg; + } + + /** Notifies peers of a location change and provides peer location hints. */ + public static final MessageType FNPLocChangeNotificationNew = + new MessageType("FNPLocationChangeNotification2", PRIORITY_LOW); + + static { + FNPLocChangeNotificationNew.addField(LOCATION, Double.class); + FNPLocChangeNotificationNew.addField(PEER_LOCATIONS, ShortBuffer.class); + } + + /** + * Creates an {@code FNPLocChangeNotificationNew}. + * + * @param myLocation This the node's new location. + * @param locations Neighbor location hints. + * @return Initialized message. + */ + public static Message createFNPLocChangeNotificationNew(double myLocation, double[] locations) { + Message msg = new Message(FNPLocChangeNotificationNew); + ShortBuffer dst = new ShortBuffer(Fields.doublesToBytes(locations)); + msg.set(LOCATION, myLocation); + msg.set(PEER_LOCATIONS, dst); + + return msg; + } + + /** Routed ping targeting a location (used beyond direct neighbors). */ + public static final MessageType FNPRoutedPing = new MessageType("FNPRoutedPing", PRIORITY_LOW); + + static { + FNPRoutedPing.addRoutedToNodeMessageFields(); + FNPRoutedPing.addField(COUNTER, Integer.class); + } + + /** + * Creates an {@code FNPRoutedPing} targeting a location. + * + * @param uid Request identifier. + * @param targetLocation Target location in keyspace. + * @param htl Hops-to-live. + * @param counter Counter for correlation by caller. + * @param nodeIdentity Identity bytes of the destination node. + * @return Initialized message. + */ + public static Message createFNPRoutedPing( + long uid, double targetLocation, short htl, int counter, byte[] nodeIdentity) { + Message msg = new Message(FNPRoutedPing); + msg.setRoutedToNodeFields(uid, targetLocation, htl, nodeIdentity); + msg.set(COUNTER, counter); + return msg; + } + + /** Reply to a routed ping. */ + public static final MessageType FNPRoutedPong = new MessageType("FNPRoutedPong", PRIORITY_LOW); + + static { + FNPRoutedPong.addField(UID, Long.class); + FNPRoutedPong.addField(COUNTER, Integer.class); + } + + /** + * Creates an {@code FNPRoutedPong} reply. + * + * @param uid Request identifier. + * @param counter Counter echoed from the routed ping. + * @return Initialized message. + */ + public static Message createFNPRoutedPong(long uid, int counter) { + Message msg = new Message(FNPRoutedPong); + msg.set(UID, uid); + msg.set(COUNTER, counter); + return msg; + } + + /** Routed request was rejected; includes the remaining HTL. */ + public static final MessageType FNPRoutedRejected = + new MessageType("FNPRoutedRejected", PRIORITY_UNSPECIFIED); + + static { + FNPRoutedRejected.addField(UID, Long.class); + FNPRoutedRejected.addField(HTL, Short.class); + } + + /** + * Creates an {@code FNPRoutedRejected}. + * + * @param uid Request identifier. + * @param htl Remaining hops-to-live. + * @return Initialized message. + */ + public static Message createFNPRoutedRejected(long uid, short htl) { + Message msg = new Message(FNPRoutedRejected); + msg.set(UID, uid); + msg.set(HTL, htl); + return msg; + } + + /** Announces a detected external address for the remote peer. */ + public static final MessageType FNPDetectedIPAddress = + new MessageType("FNPDetectedIPAddress", PRIORITY_HIGH); + + static { + FNPDetectedIPAddress.addField(EXTERNAL_ADDRESS, Peer.class); + } + + /** + * Creates an {@code FNPDetectedIPAddress} containing a detected external address. + * + * @param peer Peering information with external address. + * @return Initialized message. + */ + public static Message createFNPDetectedIPAddress(Peer peer) { + Message msg = new Message(FNPDetectedIPAddress); + msg.set(EXTERNAL_ADDRESS, peer); + return msg; + } + + /** Wall-clock time synchronization hint. */ + public static final MessageType FNPTime = new MessageType("FNPTime", PRIORITY_HIGH); + + static { + FNPTime.addField(TIME, Long.class); + } + + /** + * Creates an {@code FNPTime} message. + * + * @param time Time in milliseconds since epoch. + * @return Initialized message. + */ + public static Message createFNPTime(long time) { + Message msg = new Message(FNPTime); + msg.set(TIME, time); + return msg; + } + + /** Uptime percentage over a short window for visibility sharing. */ + public static final MessageType FNPUptime = new MessageType("FNPUptime", PRIORITY_LOW); + + static { + FNPUptime.addField(UPTIME_PERCENT_48H, Byte.class); + } + + /** + * Creates an {@code FNPUptime} message. + * + * @param uptimePercent Percent uptime over the recent window. + * @return Initialized message. + */ + public static Message createFNPUptime(byte uptimePercent) { + Message msg = new Message(FNPUptime); + msg.set(UPTIME_PERCENT_48H, uptimePercent); + return msg; + } + + /** Visibility setting toward friends (link-level trust/visibility). */ + public static final MessageType FNPVisibility = new MessageType("FNPVisibility", PRIORITY_HIGH); + + static { + FNPVisibility.addField(FRIEND_VISIBILITY, Short.class); + } + + /** + * Creates an {@code FNPVisibility} message. + * + * @param visibility Visibility code (implementation-defined). + * @return Initialized message. + */ + public static Message createFNPVisibility(short visibility) { + Message msg = new Message(FNPVisibility); + msg.set(FRIEND_VISIBILITY, visibility); + return msg; + } + + // Legacy message retained for compatibility. + public static final MessageType FNPSentPackets = new MessageType("FNPSentPackets", PRIORITY_HIGH); + + static { + FNPSentPackets.addField(TIME_DELTAS, ShortBuffer.class); + FNPSentPackets.addField(HASHES, ShortBuffer.class); + FNPSentPackets.addField(TIME, Long.class); + } + + /** Empty no-op message used as a keepalive or placeholder. */ + public static final MessageType FNPVoid = new MessageType("FNPVoid", PRIORITY_LOW, false, true); + + public static Message createFNPVoid() { + return new Message(FNPVoid); + } + + /** Informs peer about disconnect intent and optional cleanup behavior. */ + public static final MessageType FNPDisconnect = new MessageType("FNPDisconnect", PRIORITY_HIGH); + + static { + // If true, remove from the active routing table, likely to be down for a while. + // Otherwise, just dump all current connection states and keep trying to connect. + FNPDisconnect.addField(REMOVE, Boolean.class); + // If true, purge all references to this node. Otherwise, we can keep the node + // around in secondary tables etc. to more easily reconnect later. + // (Mostly used on opennet) + FNPDisconnect.addField(PURGE, Boolean.class); + // Parting message. Maybe empty. A SimpleFieldSet in exactly the same format + // as an N2NTM. + FNPDisconnect.addField(NODE_TO_NODE_MESSAGE_TYPE, Integer.class); + FNPDisconnect.addField(NODE_TO_NODE_MESSAGE_DATA, ShortBuffer.class); + } + + /** + * Creates an {@code FNPDisconnect} message with optional purge instructions and a parting note. + * + * @param remove Remove from active routing table if {@code true}. + * @param purge Purge references to the node if {@code true}. + * @param messageType Parting note type (node-to-node message type). + * @param messageData Parting note payload; may be empty. + * @return Initialized message. + */ + public static Message createFNPDisconnect( + boolean remove, boolean purge, int messageType, ShortBuffer messageData) { + Message msg = new Message(FNPDisconnect); + msg.set(REMOVE, remove); + msg.set(PURGE, purge); + msg.set(NODE_TO_NODE_MESSAGE_TYPE, messageType); + msg.set(NODE_TO_NODE_MESSAGE_DATA, messageData); + return msg; + } + + // Update over mandatory. Not strictly part of FNP. Only goes between nodes at the link + // level, and will be sent and parsed, even if the node is out of date. Should be stable + // long-term. + + /** + * Announces core update metadata (keys, versions, and recent revocation fetch state) at link + * level. + */ + public static final MessageType CryptadUOMAnnouncement = + new MessageType("CryptadUOMAnnouncement", PRIORITY_LOW); + + static { + CryptadUOMAnnouncement.addField(CORE_PACKAGE_KEY, String.class); + CryptadUOMAnnouncement.addField(REVOCATION_KEY, String.class); + CryptadUOMAnnouncement.addField(HAVE_REVOCATION_KEY, Boolean.class); + CryptadUOMAnnouncement.addField(CORE_PACKAGE_VERSION, Integer.class); + // Last time (ms ago) we had 3 DNFs in a row on the revocation checker. + CryptadUOMAnnouncement.addField(REVOCATION_KEY_TIME_LAST_TRIED, Long.class); + // Number of DNFs so far this time. + CryptadUOMAnnouncement.addField(REVOCATION_KEY_DNF_COUNT, Integer.class); + // For convenience, may change + CryptadUOMAnnouncement.addField(REVOCATION_KEY_FILE_LENGTH, Long.class); + CryptadUOMAnnouncement.addField(CORE_PACKAGE_FILE_LENGTH, Long.class); + CryptadUOMAnnouncement.addField(PING_TIME, Integer.class); + CryptadUOMAnnouncement.addField(BWLIMIT_DELAY_TIME, Integer.class); + } + + /** Fluent builder for {@link #CryptadUOMAnnouncement} messages. */ + public static final class UOMAnnouncementBuilder { + private String corePackageKey; + private String revocationKey; + private boolean haveRevocation; + private int corePackageVersion; + private long timeLastTriedRevocationFetch; + private int revocationDNFCount; + private long revocationKeyLength; + private long corePackageLength; + private int pingTime; + private int bwlimitDelayTime; + + public UOMAnnouncementBuilder corePackageKey(String v) { + this.corePackageKey = v; + return this; + } + + public UOMAnnouncementBuilder revocationKey(String v) { + this.revocationKey = v; + return this; + } + + public UOMAnnouncementBuilder haveRevocation(boolean v) { + this.haveRevocation = v; + return this; + } + + public UOMAnnouncementBuilder corePackageVersion(int v) { + this.corePackageVersion = v; + return this; + } + + public UOMAnnouncementBuilder timeLastTriedRevocationFetch(long v) { + this.timeLastTriedRevocationFetch = v; + return this; + } + + public UOMAnnouncementBuilder revocationDNFCount(int v) { + this.revocationDNFCount = v; + return this; + } + + public UOMAnnouncementBuilder revocationKeyLength(long v) { + this.revocationKeyLength = v; + return this; + } + + public UOMAnnouncementBuilder corePackageLength(long v) { + this.corePackageLength = v; + return this; + } + + public UOMAnnouncementBuilder pingTime(int v) { + this.pingTime = v; + return this; + } + + public UOMAnnouncementBuilder bwlimitDelayTime(int v) { + this.bwlimitDelayTime = v; + return this; + } + + public Message build() { + Message msg = new Message(CryptadUOMAnnouncement); + msg.set(CORE_PACKAGE_KEY, corePackageKey); + msg.set(REVOCATION_KEY, revocationKey); + msg.set(HAVE_REVOCATION_KEY, haveRevocation); + msg.set(CORE_PACKAGE_VERSION, corePackageVersion); + msg.set(REVOCATION_KEY_TIME_LAST_TRIED, timeLastTriedRevocationFetch); + msg.set(REVOCATION_KEY_DNF_COUNT, revocationDNFCount); + msg.set(REVOCATION_KEY_FILE_LENGTH, revocationKeyLength); + msg.set(CORE_PACKAGE_FILE_LENGTH, corePackageLength); + msg.set(PING_TIME, pingTime); + msg.set(BWLIMIT_DELAY_TIME, bwlimitDelayTime); + return msg; + } + } + + /** Requests the revocation file referenced in the announcement. */ + public static final MessageType CryptadUOMRequestRevocation = + new MessageType("CryptadUOMRequestRevocation", PRIORITY_HIGH); + + static { + CryptadUOMRequestRevocation.addField(UID, Long.class); + } + + /** + * Creates a {@code CryptadUOMRequestRevocation} using {@code uid} as the transfer identifier. + * + * @param uid Transfer identifier. + * @return Initialized message. + */ + public static Message createUOMRequestRevocation(long uid) { + Message msg = new Message(CryptadUOMRequestRevocation); + msg.set(UID, uid); + return msg; + } + + // Used by new UOM. + /** Requests the core package referenced in the announcement. */ + public static final MessageType CryptadUOMRequestCorePackage = + new MessageType("CryptadUOMRequestMainJar", PRIORITY_LOW); + + static { + CryptadUOMRequestCorePackage.addField(UID, Long.class); + } + + /** + * Creates a {@code CryptadUOMRequestCorePackage} using {@code uid} as the transfer identifier. + * + * @param uid Transfer identifier. + * @return Initialized message. + */ + public static Message createUOMRequestCorePackage(long uid) { + Message msg = new Message(CryptadUOMRequestCorePackage); + msg.set(UID, uid); + return msg; + } + + /** Announces that a revocation file will be sent, with length and key. */ + public static final MessageType CryptadUOMSendingRevocation = + new MessageType("CryptadUOMSendingRevocation", PRIORITY_HIGH); + + static { + CryptadUOMSendingRevocation.addField(UID, Long.class); + // Probably excessive, but lengths are always long, and wasting a few bytes here + // doesn't matter in the least, as it's very rarely called. + CryptadUOMSendingRevocation.addField(FILE_LENGTH, Long.class); + CryptadUOMSendingRevocation.addField(REVOCATION_KEY, String.class); + } + + /** + * Creates a {@code CryptadUOMSendingRevocation}. + * + * @param uid Transfer identifier. + * @param length File length in bytes. + * @param key Revocation key (URI string). + * @return Initialized message. + */ + public static Message createUOMSendingRevocation(long uid, long length, String key) { + Message msg = new Message(CryptadUOMSendingRevocation); + msg.set(UID, uid); + msg.set(FILE_LENGTH, length); + msg.set(REVOCATION_KEY, key); + return msg; + } + + // Used by new UOM. We need to distinguish them in NodeDispatcher. + /** Announces that the core package will be sent, with length, key, and version. */ + public static final MessageType CryptadUOMSendingCorePackage = + new MessageType("CryptadUOMSendingMainJar", PRIORITY_LOW); + + static { + CryptadUOMSendingCorePackage.addField(UID, Long.class); + CryptadUOMSendingCorePackage.addField(FILE_LENGTH, Long.class); + CryptadUOMSendingCorePackage.addField(CORE_PACKAGE_KEY, String.class); + CryptadUOMSendingCorePackage.addField(CORE_PACKAGE_VERSION, Integer.class); + } + + /** + * Creates a {@code CryptadUOMSendingCorePackage}. + * + * @param uid Transfer identifier. + * @param length File length in bytes. + * @param key Key (URI string) for the core package. + * @param version Build version. + * @return Initialized message. + */ + public static Message createUOMSendingCorePackage( + long uid, long length, String key, int version) { + Message msg = new Message(CryptadUOMSendingCorePackage); + msg.set(UID, uid); + msg.set(FILE_LENGTH, length); + msg.set(CORE_PACKAGE_KEY, key); + msg.set(CORE_PACKAGE_VERSION, version); + return msg; + } + + /** + * Used to fetch a file required for deploying an update. The client knows both the size and hash + * of the file. If we don't want to send the data, we should send an FNPBulkReceiveAborted, + * otherwise we just send the data as a BulkTransmitter transfer. + */ + public static final MessageType CryptadUOMFetchDependency = + new MessageType("CryptadUOMFetchDependency", PRIORITY_LOW); + + static { + CryptadUOMFetchDependency.addField(UID, Long.class); // This will be used for the transfer. + CryptadUOMFetchDependency.addField(EXPECTED_HASH, ShortBuffer.class); // Fetch by hash + CryptadUOMFetchDependency.addField(FILE_LENGTH, Long.class); // Both sides know length. + } + + /** + * Creates a {@code CryptadUOMFetchDependency} request. + * + * @param uid Transfer identifier. + * @param hash Expected content hash. + * @param length Expected file length in bytes. + * @return Initialized message. + */ + public static Message createUOMFetchDependency(long uid, byte[] hash, long length) { + Message msg = new Message(CryptadUOMFetchDependency); + msg.set(UID, uid); + msg.set(EXPECTED_HASH, new ShortBuffer(hash)); + msg.set(FILE_LENGTH, length); + return msg; + } + + // Secondary messages (debug messages attached to primary messages) + + /** Secondary diagnostic message listing node UIDs relevant to a swap. */ + public static final MessageType FNPSwapNodeUIDs = + new MessageType("FNPSwapNodeUIDs", PRIORITY_UNSPECIFIED); + + static { + FNPSwapNodeUIDs.addField(NODE_UIDS, ShortBuffer.class); + } + + /** + * Creates an {@code FNPSwapNodeUIDs} diagnostic message. + * + * @param uids Node UID list. + * @return Initialized message. + */ + public static Message createFNPSwapLocations(long[] uids) { + Message msg = new Message(FNPSwapNodeUIDs); + msg.set(NODE_UIDS, new ShortBuffer(Fields.longsToBytes(uids))); + return msg; + } + + // More permanent secondary messages (should perhaps be replaced by new main messages when stable) + + /** Diagnostics: best alternative routes that were not selected. */ + public static final MessageType FNPBestRoutesNotTaken = + new MessageType("FNPBestRoutesNotTaken", PRIORITY_UNSPECIFIED); + + static { + // Maybe this should be some sort of typed array? + // It's just a bunch of doubles anyway. + FNPBestRoutesNotTaken.addField(BEST_LOCATIONS_NOT_VISITED, ShortBuffer.class); + } + + /** + * Creates {@code FNPBestRoutesNotTaken} with serialized locations. + * + * @param locs Serialized double array. + * @return Initialized message. + */ + public static Message createFNPBestRoutesNotTaken(byte[] locs) { + Message msg = new Message(FNPBestRoutesNotTaken); + msg.set(BEST_LOCATIONS_NOT_VISITED, new ShortBuffer(locs)); + return msg; + } + + /** Convenience overload that serializes {@code double[]} to bytes. */ + public static Message createFNPBestRoutesNotTaken(double[] locs) { + return createFNPBestRoutesNotTaken(Fields.doublesToBytes(locs)); + } + + /** Convenience overload for {@code Double[]} collections. */ + public static Message createFNPBestRoutesNotTaken(Double[] doubles) { + double[] locs = new double[doubles.length]; + for (int i = 0; i < locs.length; i++) { + locs[i] = doubles[i]; + } + return createFNPBestRoutesNotTaken(locs); + } + + /** Advertises whether routing of requests is currently enabled. */ + public static final MessageType FNPRoutingStatus = + new MessageType("FNPRoutingStatus", PRIORITY_HIGH); + + static { + FNPRoutingStatus.addField(ROUTING_ENABLED, Boolean.class); + } + + /** + * Creates an {@code FNPRoutingStatus} message. + * + * @param routeRequests {@code true} to enable routing; {@code false} to disable. + * @return Initialized message. + */ + public static Message createRoutingStatus(boolean routeRequests) { + Message msg = new Message(FNPRoutingStatus); + msg.set(ROUTING_ENABLED, routeRequests); + + return msg; + } + + /** Sub-insert hint: allow fork when cacheable (tunable control for insert behavior). */ + public static final MessageType FNPSubInsertForkControl = + new MessageType("FNPSubInsertForkControl", PRIORITY_HIGH); + + static { + FNPSubInsertForkControl.addField(ENABLE_INSERT_FORK_WHEN_CACHEABLE, Boolean.class); + } + + /** + * Creates a {@code FNPSubInsertForkControl} hint. + * + * @param enableInsertForkWhenCacheable Enable fork when cacheable. + * @return Initialized message. + */ + public static Message createFNPSubInsertForkControl(boolean enableInsertForkWhenCacheable) { + Message msg = new Message(FNPSubInsertForkControl); + msg.set(ENABLE_INSERT_FORK_WHEN_CACHEABLE, enableInsertForkWhenCacheable); + return msg; + } + + /** Sub-insert hint: prefer insert to other options when possible. */ + public static final MessageType FNPSubInsertPreferInsert = + new MessageType("FNPSubInsertPreferInsert", PRIORITY_HIGH); + + static { + FNPSubInsertPreferInsert.addField(PREFER_INSERT, Boolean.class); + } + + /** + * Creates a {@code FNPSubInsertPreferInsert} hint. + * + * @param preferInsert Prefer insert when {@code true}. + * @return Initialized message. + */ + public static Message createFNPSubInsertPreferInsert(boolean preferInsert) { + Message msg = new Message(FNPSubInsertPreferInsert); + msg.set(PREFER_INSERT, preferInsert); + return msg; + } + + /** Sub-insert hint: ignore the low backoff threshold. */ + public static final MessageType FNPSubInsertIgnoreLowBackoff = + new MessageType("FNPSubInsertIgnoreLowBackoff", PRIORITY_HIGH); + + static { + FNPSubInsertIgnoreLowBackoff.addField(IGNORE_LOW_BACKOFF, Boolean.class); + } + + /** + * Creates a {@code FNPSubInsertIgnoreLowBackoff} hint. + * + * @param ignoreLowBackoff Ignore low backoff when {@code true}. + * @return Initialized message. + */ + public static Message createFNPSubInsertIgnoreLowBackoff(boolean ignoreLowBackoff) { + Message msg = new Message(FNPSubInsertIgnoreLowBackoff); + msg.set(IGNORE_LOW_BACKOFF, ignoreLowBackoff); + return msg; + } + + /** Indicates that the preceding rejection is soft (retryable). */ + public static final MessageType FNPRejectIsSoft = + new MessageType("FNPRejectIsSoft", PRIORITY_HIGH); + + /** Creates an {@code FNPRejectIsSoft} marker message. */ + public static Message createFNPRejectIsSoft() { + return new Message(FNPRejectIsSoft); + } + + // New load management + + public static final MessageType FNPPeerLoadStatusByte = + new MessageType("FNPPeerLoadStatusByte", PRIORITY_HIGH, false, true); + + static { + FNPPeerLoadStatusByte.addField(OTHER_TRANSFERS_OUT_CHK, Byte.class); + FNPPeerLoadStatusByte.addField(OTHER_TRANSFERS_IN_CHK, Byte.class); + FNPPeerLoadStatusByte.addField(OTHER_TRANSFERS_OUT_SSK, Byte.class); + FNPPeerLoadStatusByte.addField(OTHER_TRANSFERS_IN_SSK, Byte.class); + FNPPeerLoadStatusByte.addField(AVERAGE_TRANSFERS_OUT_PER_INSERT, Byte.class); + FNPPeerLoadStatusByte.addField(OUTPUT_BANDWIDTH_LOWER_LIMIT, Integer.class); + FNPPeerLoadStatusByte.addField(OUTPUT_BANDWIDTH_UPPER_LIMIT, Integer.class); + FNPPeerLoadStatusByte.addField(OUTPUT_BANDWIDTH_PEER_LIMIT, Integer.class); + FNPPeerLoadStatusByte.addField(INPUT_BANDWIDTH_LOWER_LIMIT, Integer.class); + FNPPeerLoadStatusByte.addField(INPUT_BANDWIDTH_UPPER_LIMIT, Integer.class); + FNPPeerLoadStatusByte.addField(INPUT_BANDWIDTH_PEER_LIMIT, Integer.class); + FNPPeerLoadStatusByte.addField(MAX_TRANSFERS_OUT, Byte.class); + FNPPeerLoadStatusByte.addField(MAX_TRANSFERS_OUT_PEER_LIMIT, Byte.class); + FNPPeerLoadStatusByte.addField(MAX_TRANSFERS_OUT_LOWER_LIMIT, Byte.class); + FNPPeerLoadStatusByte.addField(MAX_TRANSFERS_OUT_UPPER_LIMIT, Byte.class); + FNPPeerLoadStatusByte.addField(REAL_TIME_FLAG, Boolean.class); + } + + public static final MessageType FNPPeerLoadStatusShort = + new MessageType("FNPPeerLoadStatusShort", PRIORITY_HIGH, false, true); + + static { + FNPPeerLoadStatusShort.addField(OTHER_TRANSFERS_OUT_CHK, Short.class); + FNPPeerLoadStatusShort.addField(OTHER_TRANSFERS_IN_CHK, Short.class); + FNPPeerLoadStatusShort.addField(OTHER_TRANSFERS_OUT_SSK, Short.class); + FNPPeerLoadStatusShort.addField(OTHER_TRANSFERS_IN_SSK, Short.class); + FNPPeerLoadStatusShort.addField(AVERAGE_TRANSFERS_OUT_PER_INSERT, Short.class); + FNPPeerLoadStatusShort.addField(OUTPUT_BANDWIDTH_LOWER_LIMIT, Integer.class); + FNPPeerLoadStatusShort.addField(OUTPUT_BANDWIDTH_UPPER_LIMIT, Integer.class); + FNPPeerLoadStatusShort.addField(OUTPUT_BANDWIDTH_PEER_LIMIT, Integer.class); + FNPPeerLoadStatusShort.addField(INPUT_BANDWIDTH_LOWER_LIMIT, Integer.class); + FNPPeerLoadStatusShort.addField(INPUT_BANDWIDTH_UPPER_LIMIT, Integer.class); + FNPPeerLoadStatusShort.addField(INPUT_BANDWIDTH_PEER_LIMIT, Integer.class); + FNPPeerLoadStatusShort.addField(MAX_TRANSFERS_OUT, Short.class); + FNPPeerLoadStatusShort.addField(MAX_TRANSFERS_OUT_PEER_LIMIT, Short.class); + FNPPeerLoadStatusShort.addField(MAX_TRANSFERS_OUT_LOWER_LIMIT, Short.class); + FNPPeerLoadStatusShort.addField(MAX_TRANSFERS_OUT_UPPER_LIMIT, Short.class); + FNPPeerLoadStatusShort.addField(REAL_TIME_FLAG, Boolean.class); + } + + public static final MessageType FNPPeerLoadStatusInt = + new MessageType("FNPPeerLoadStatusInt", PRIORITY_HIGH, false, true); + + static { + FNPPeerLoadStatusInt.addField(OTHER_TRANSFERS_OUT_CHK, Integer.class); + FNPPeerLoadStatusInt.addField(OTHER_TRANSFERS_IN_CHK, Integer.class); + FNPPeerLoadStatusInt.addField(OTHER_TRANSFERS_OUT_SSK, Integer.class); + FNPPeerLoadStatusInt.addField(OTHER_TRANSFERS_IN_SSK, Integer.class); + FNPPeerLoadStatusInt.addField(AVERAGE_TRANSFERS_OUT_PER_INSERT, Integer.class); + FNPPeerLoadStatusInt.addField(OUTPUT_BANDWIDTH_LOWER_LIMIT, Integer.class); + FNPPeerLoadStatusInt.addField(OUTPUT_BANDWIDTH_UPPER_LIMIT, Integer.class); + FNPPeerLoadStatusInt.addField(OUTPUT_BANDWIDTH_PEER_LIMIT, Integer.class); + FNPPeerLoadStatusInt.addField(INPUT_BANDWIDTH_LOWER_LIMIT, Integer.class); + FNPPeerLoadStatusInt.addField(INPUT_BANDWIDTH_UPPER_LIMIT, Integer.class); + FNPPeerLoadStatusInt.addField(INPUT_BANDWIDTH_PEER_LIMIT, Integer.class); + FNPPeerLoadStatusInt.addField(MAX_TRANSFERS_OUT, Integer.class); + FNPPeerLoadStatusInt.addField(MAX_TRANSFERS_OUT_PEER_LIMIT, Integer.class); + FNPPeerLoadStatusInt.addField(MAX_TRANSFERS_OUT_LOWER_LIMIT, Integer.class); + FNPPeerLoadStatusInt.addField(MAX_TRANSFERS_OUT_UPPER_LIMIT, Integer.class); + FNPPeerLoadStatusInt.addField(REAL_TIME_FLAG, Boolean.class); + } + + /** Secondary message toggling realtime/bulk handling on a per-message basis. */ + public static final MessageType FNPRealTimeFlag = + new MessageType("FNPRealTimeFlag", PRIORITY_HIGH); + + static { + FNPRealTimeFlag.addField(REAL_TIME_FLAG, Boolean.class); + } + + /** + * Creates a {@code FNPRealTimeFlag} submessage. + * + * @param isBulk Flag value; interpretation is implementation-defined. + * @return Initialized message. + */ + public static Message createFNPRealTimeFlag(boolean isBulk) { + Message msg = new Message(FNPRealTimeFlag); + msg.set(REAL_TIME_FLAG, isBulk); + return msg; + } + + /** + * Extracts the flag value from an optional {@code FNPRealTimeFlag} submessage. + * + * @param m Message that may carry the submessage. + * @return Flag value when present; otherwise {@code false}. + */ + public static boolean getRealTimeFlag(Message m) { + Message bulk = m.getSubMessage(FNPRealTimeFlag); + if (bulk == null) { + return false; + } + return bulk.getBoolean(REAL_TIME_FLAG); + } + + /** Returns {@code true} if the message is one of the peer-load status variants. */ + public static boolean isPeerLoadStatusMessage(Message m) { + MessageType spec = m.getSpec(); + return FNPPeerLoadStatusByte.equals(spec) + || FNPPeerLoadStatusShort.equals(spec) + || FNPPeerLoadStatusInt.equals(spec); + } + + /** Returns {@code true} if the message is a request type subject to load limiting. */ + public static boolean isLoadLimitedRequest(Message m) { + MessageType spec = m.getSpec(); + return FNPCHKDataRequest.equals(spec) + || FNPSSKDataRequest.equals(spec) + || FNPSSKInsertRequest.equals(spec) + || FNPInsertRequest.equals(spec) + || FNPSSKInsertRequestNew.equals(spec) + || FNPGetOfferedKey.equals(spec); + } + + // Extended fatal timeout handling. + /** + * Queries whether a set of upstream request UIDs is still running. Reply is {@link + * #FNPIsStillRunning}. + */ + public static final MessageType FNPCheckStillRunning = + new MessageType("FNPCheckStillRunning", PRIORITY_HIGH); + + static { + FNPCheckStillRunning.addField(UID, Long.class); // UID for this message, used to identify reply + FNPCheckStillRunning.addField(LIST_OF_UIDS, ShortBuffer.class); + } + + /** Reply indicating which UIDs are still running using a bitset. */ + public static final MessageType FNPIsStillRunning = + new MessageType("FNPIsStillRunning", PRIORITY_HIGH); + + static { + FNPIsStillRunning.addField(UID, Long.class); + FNPIsStillRunning.addField(UID_STILL_RUNNING_FLAGS, BitArray.class); + } + + // Friend-of-a-friend (FOAF) related messages + + /** FOAF: request the peer's full noderef. */ + public static final MessageType FNPGetYourFullNoderef = + new MessageType("FNPGetYourFullNoderef", PRIORITY_LOW); + + /** Creates an {@code FNPGetYourFullNoderef} request. */ + public static Message createFNPGetYourFullNoderef() { + return new Message(FNPGetYourFullNoderef); + } + + /** FOAF: reply conveying the sender's full noderef length (payload transferred separately). */ + public static final MessageType FNPMyFullNoderef = + new MessageType("FNPMyFullNoderef", PRIORITY_LOW); + + static { + FNPMyFullNoderef.addField(UID, Long.class); + // Not necessary to pad it since it's not propagated across the network. + // It might be relayed one hop, but there's enough padding elsewhere, don't worry about + // it. + // As opposed to opennet refs, which are relayed long distances, down request paths which + // they might reveal, so do need to be padded. + FNPMyFullNoderef.addField(NODEREF_LENGTH, Integer.class); + } + + /** + * Creates an {@code FNPMyFullNoderef} reply. + * + * @param uid Request identifier associated with the original FOAF query. + * @param length Unpadded length in bytes of the noderef to follow. + * @return Initialized message. + */ + public static Message createFNPMyFullNoderef(long uid, int length) { + Message m = new Message(FNPMyFullNoderef); + m.set(UID, uid); + m.set(NODEREF_LENGTH, length); + return m; + } +} diff --git a/interop-wire/src/main/java/network/crypta/io/comm/DisconnectedException.java b/interop-wire/src/main/java/network/crypta/io/comm/DisconnectedException.java new file mode 100644 index 00000000000..ac949f97634 --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/io/comm/DisconnectedException.java @@ -0,0 +1,26 @@ +package network.crypta.io.comm; + +import java.io.Serial; +import network.crypta.support.LightweightException; + +/** + * Signals that a previously connected peer or channel became disconnected while a blocking + * operation was in progress. + * + *

This checked exception typically originates from messaging and transfer code when a caller is + * waiting for an event (for example, {@code waitFor(...)} in the messaging layer) and the + * underlying connection drops locally or remotely. It is distinct from {@link + * NotConnectedException}, which indicates that no connection existed at the time the operation was + * attempted. + * + *

The class extends {@link LightweightException}; by default, stack traces may be omitted to + * keep the exception inexpensive for frequent control-flow paths. Callers should catch this + * exception to abort the current operation or re-resolve the peer according to the calling + * context's retry policy. + * + * @see NotConnectedException + * @see PeerRestartedException + */ +public class DisconnectedException extends LightweightException { + @Serial private static final long serialVersionUID = -1; +} diff --git a/interop-wire/src/main/java/network/crypta/io/comm/Dispatcher.java b/interop-wire/src/main/java/network/crypta/io/comm/Dispatcher.java new file mode 100644 index 00000000000..6232bbb54b2 --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/io/comm/Dispatcher.java @@ -0,0 +1,25 @@ +package network.crypta.io.comm; + +/** + * Callback for handling unmatched messages. + * + *

The messaging core invokes this interface when an incoming {@link Message} did not match any + * registered {@link MessageFilter}. Implementations may inspect the message and either consume it + * (returning {@code true}) or decline it (returning {@code false}). When {@code false} is returned, + * the message is treated as unhandled and may be retained for later matching according to the + * policy in {@link MessageCore}. + * + *

Threading: {@link MessageCore} may call this method from an I/O or worker thread. Implementers + * should avoid long blocking operations and offload heavy work to executors as appropriate. + */ +public interface Dispatcher { + + /** + * Attempts to handle a message that no filter claimed. + * + * @param m the message to process; never {@code null}. + * @return {@code true} if the message is fully handled and requires no further processing; {@code + * false} to leave it unhandled so the core can apply its unmatched-message policy. + */ + boolean handleMessage(Message m); +} diff --git a/interop-wire/src/main/java/network/crypta/io/comm/DuplicateMessageTypeException.java b/interop-wire/src/main/java/network/crypta/io/comm/DuplicateMessageTypeException.java new file mode 100644 index 00000000000..92ef65b168e --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/io/comm/DuplicateMessageTypeException.java @@ -0,0 +1,22 @@ +package network.crypta.io.comm; + +/** + * Signals an attempt to register a {@link MessageType} whose registry identifier already exists. + * + *

The identifier is derived from the type's name via {@link String#hashCode()}. If another type + * with the same key is already present in the registry, the registering code throws this unchecked + * exception to prevent ambiguous decoding and inconsistent schemas. + * + *

Typical source: constructing a new {@link MessageType} with a name that collides with an + * existing type. + */ +public class DuplicateMessageTypeException extends RuntimeException { + /** + * Creates the exception with a detail message. + * + * @param message detail text; often includes the conflicting type name + */ + public DuplicateMessageTypeException(String message) { + super(message); + } +} diff --git a/interop-wire/src/main/java/network/crypta/io/comm/FreenetInetAddress.java b/interop-wire/src/main/java/network/crypta/io/comm/FreenetInetAddress.java new file mode 100644 index 00000000000..bde5caa3792 --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/io/comm/FreenetInetAddress.java @@ -0,0 +1,559 @@ +package network.crypta.io.comm; + +import java.io.DataInput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import network.crypta.io.AddressIdentifier; +import network.crypta.support.io.InetAddressIpv6FirstComparator; +import network.crypta.support.transport.ip.HostnameSyntaxException; +import network.crypta.support.transport.ip.HostnameUtil; +import network.crypta.support.transport.ip.IPUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Immutable-or-lazy network endpoint reference that can be backed by a hostname or a resolved IP + * address. + * + *

When constructed from an {@link InetAddress}, the address is primary and immutable; when + * constructed from a hostname, the hostname is primary and DNS resolution occurs lazily on first + * access (or explicitly via handshake). This design helps nodes that use dynamic DNS. + * + *

Equality propagates the resolved IP between instances when the primary hostname matches + * (case-insensitive). Hostname never propagates. The hash code depends solely on the primary + * identity: hostname if present, otherwise the IP address. As a result, inserting instances into + * hashed collections is safe: neither {@link #equals(Object)} nor {@link #getAddress()} will change + * the hash code of an existing instance. + * + *

Important behavior: an instance that has only an IP (no hostname) is not equal to another + * instance that has a hostname, even when both resolve to the same numeric address. Call {@link + * #dropHostname()} to compare by IP only, or compare the underlying {@link InetAddress} values + * directly when appropriate. + * + *

DNS lookups are performed using the platform caches (as configured in the JVM). This class + * does not implement its own caching policy beyond storing the last resolved address when helpful + * for handshakes. + */ +public final class FreenetInetAddress { + private static final Logger LOG = LoggerFactory.getLogger(FreenetInetAddress.class); + + // no static initialization required + + // hostname - only set if we were created with a hostname + // and not an address + private final String hostname; + private InetAddress address; + + /** + * Constructs an instance by reading from a binary stream produced by {@link + * #writeToDataOutputStream(DataOutputStream)}. + * + *

Format: one type byte ({@code 0} for IPv4, {@code 255} for IPv6), followed by the raw IP + * bytes (4 or 16), then a UTF string containing the hostname or the empty string when absent. + * + * @param dis data source to read from + * @throws IOException if the input cannot be read or the type byte is unknown + */ + public FreenetInetAddress(DataInput dis) throws IOException { + int firstByte = dis.readUnsignedByte(); + byte[] ba; + switch (firstByte) { + case 255 -> { + if (LOG.isDebugEnabled()) LOG.debug("event=read_stream_ip type=ipv6 format=new"); + ba = new byte[16]; + dis.readFully(ba); + } + case 0 -> { + if (LOG.isDebugEnabled()) LOG.debug("event=read_stream_ip type=ipv4 format=new"); + ba = new byte[4]; + dis.readFully(ba); + } + default -> + throw new IOException( + "Unknown type byte (old form? corrupt stream? too short/long prev field?): " + + firstByte); + } + address = InetAddress.getByAddress(ba); + String name = null; + String s = dis.readUTF(); + if (!s.isEmpty()) name = s; + hostname = name; + } + + /** + * Constructs an instance by reading from a binary stream and optionally validating hostname + * syntax. + * + *

This constructor also accepts the legacy IPv4 format where the first byte is part of the + * address (no leading type byte). Newer encodings use a leading type byte as described for {@link + * #FreenetInetAddress(DataInput)}. + * + * @param dis data source to read from + * @param checkHostnameOrIPSyntax when {@code true}, validates the parsed hostname using {@link + * HostnameUtil#isValidHostname(String, boolean)} + * @throws HostnameSyntaxException if validation is requested and the hostname is not valid + * @throws IOException if the input cannot be read + */ + public FreenetInetAddress(DataInput dis, boolean checkHostnameOrIPSyntax) + throws HostnameSyntaxException, IOException { + int firstByte = dis.readUnsignedByte(); + byte[] ba; + switch (firstByte) { + case 255 -> { + if (LOG.isDebugEnabled()) + LOG.debug("event=read_stream_ip_with_validation type=ipv6 format=new"); + ba = new byte[16]; + dis.readFully(ba); + } + case 0 -> { + if (LOG.isDebugEnabled()) + LOG.debug("event=read_stream_ip_with_validation type=ipv4 format=new"); + ba = new byte[4]; + dis.readFully(ba); + } + default -> { + // Old format IPv4 address + ba = new byte[4]; + ba[0] = (byte) firstByte; + dis.readFully(ba, 1, 3); + } + } + address = InetAddress.getByAddress(ba); + String name = null; + String s = dis.readUTF(); + if (!s.isEmpty()) name = s; + hostname = name; + if (checkHostnameOrIPSyntax + && hostname != null + && !HostnameUtil.isValidHostname(hostname, true)) throw new HostnameSyntaxException(); + } + + /** + * Constructs an instance from a resolved IP address. + * + *

The IP address becomes the primary identity. No hostname is stored or looked up. + * + * @param address resolved IP address; must not be {@code null} + */ + public FreenetInetAddress(InetAddress address) { + this.address = address; + hostname = null; + } + + /** + * Constructs an instance from a textual host, which may be a DNS name or a literal IP address. + * + *

If {@code host} parses as an IP address, the instance is IP-primary. Otherwise, the instance + * is hostname-primary and resolution is deferred until required. + * + * @param host DNS name or literal IP; leading slashes are trimmed (e.g., {@code "/1.2.3.4"}) + * @param allowUnknown reserved for compatibility; does not alter behavior here + * @throws UnknownHostException if the literal address cannot be parsed + */ + public FreenetInetAddress(String host, boolean allowUnknown) throws UnknownHostException { + InitResult r = computeInitFromHost(host, allowUnknown); + this.address = r.addr; + this.hostname = r.host; + // we're created with a hostname so delay the lookup of the address + // until it's necessary to work better with dynamic DNS hostnames + } + + /** + * Constructs an instance from a textual host with optional hostname syntax validation. + * + * @param host DNS name or literal IP; leading slashes are trimmed + * @param allowUnknown reserved for compatibility; does not alter behavior here + * @param checkHostnameOrIPSyntax when {@code true}, validates {@code host} if it is a hostname + * @throws HostnameSyntaxException if validation is requested and the hostname is not valid + * @throws UnknownHostException if the literal address cannot be parsed + */ + public FreenetInetAddress(String host, boolean allowUnknown, boolean checkHostnameOrIPSyntax) + throws HostnameSyntaxException, UnknownHostException { + InitResult r = computeInitFromHost(host, allowUnknown); + this.address = r.addr; + this.hostname = r.host; + if (checkHostnameOrIPSyntax + && this.hostname != null + && !HostnameUtil.isValidHostname(this.hostname, true)) throw new HostnameSyntaxException(); + // we're created with a hostname so delay the lookup of the address + // until it's necessary to work better with dynamic DNS hostnames + } + + private record InitResult(InetAddress addr, String host) {} + + private InitResult computeInitFromHost(String host, boolean allowUnknown) + throws UnknownHostException { + InetAddress addr = null; + String h = host; + if (h != null) { + if (h.startsWith("/")) h = h.substring(1); + h = h.trim(); + } + AddressIdentifier.AddressType addressType = AddressIdentifier.getAddressType(h); + if (LOG.isTraceEnabled()) + LOG.trace( + "Address type of '{}' appears to be '{}' (allowUnknown={})", + h, + addressType, + allowUnknown); + if (addressType != AddressIdentifier.AddressType.OTHER) { + // Is an IP address + addr = InetAddress.getByName(h); + if (LOG.isDebugEnabled()) + LOG.debug("host is '{}' and addr.getHostAddress() is '{}'", h, addr.getHostAddress()); + if (addr != null) { + h = null; + } + } + if (addr == null && LOG.isTraceEnabled()) LOG.trace("'{}' does not look like an IP address", h); + return new InitResult(addr, h); + } + + /** + * Compares for equality using relaxed rules. + * + *

Behavior: - When this instance is hostname-primary, hostnames must match the ignoring case. + * If either side has a resolved IP, it is propagated to the other. If both have addresses, they + * must match. - When this instance is IP-primary, only the numeric addresses are compared. + * + * @param other address to compare + * @return {@code true} when considered equal under the relaxed rules + */ + public boolean laxEquals(FreenetInetAddress other) { + if (hostname != null) { + return laxEqualsWhenThisHasHostname(other); + } + return addressesEqual(address, other.address); + } + + private boolean laxEqualsWhenThisHasHostname(FreenetInetAddress other) { + if (other.hostname == null) { + return addressBasedComparisonWhenOtherHasNoHostname(other); + } + if (!hostname.equalsIgnoreCase(other.hostname)) { + return false; + } + propagateAddresses(other); + return (other.address == null) || (address == null) || other.address.equals(address); + } + + private boolean addressBasedComparisonWhenOtherHasNoHostname(FreenetInetAddress other) { + if (address == null) return false; // No basis for comparison. + if (other.address != null) { + return address.equals(other.address); + } + return false; + } + + private void propagateAddresses(FreenetInetAddress other) { + if (address != null && other.address == null) other.address = address; + if (other.address != null && address == null) address = other.address; + } + + private static boolean addressesEqual(InetAddress a, InetAddress b) { + return a != null && a.equals(b); + } + + /** + * Compares for equality consistent with the propagation semantics described in the class + * documentation. + * + *

When hostnames are present on both sides, they must match the ignoring case; the resolved IP + * may be propagated between instances. When neither side has a hostname, equality falls back to + * the numeric IP address. + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof FreenetInetAddress addr)) { + return false; + } + if (hostname != null) { + if (addr.hostname == null) return false; + if (!hostname.equalsIgnoreCase(addr.hostname)) { + return false; + } + // Now that we know we have the same hostname, we can propagate the IP. + if ((address != null) && (addr.address == null)) addr.address = address; + if ((addr.address != null) && (address == null)) address = addr.address; + // Except if we actually do have two different looked-up IPs! + return addr.address == null || addr.address.equals(address); + // Equal. + } + if (addr.hostname != null) return false; + + // No hostname, go by address. + return address.equals(addr.address); + } + + /** + * Compares for equality using strict rules useful for peer identity checks. + * + *

When hostnames are present on both sides, the behavior matches {@link #equals(Object)}. When + * neither side has a hostname, strict comparison requires the reverse hostnames derived from the + * numeric IPs to match the ignoring case (see {@link #getHostName(InetAddress)}). + * + * @param addr address to compare + * @return {@code true} if equal under strict comparison + */ + public boolean strictEquals(FreenetInetAddress addr) { + if (hostname != null) { + if (addr.hostname == null) return false; + if (!hostname.equalsIgnoreCase(addr.hostname)) { + return false; + } + // Now that we know we have the same hostname, we can propagate the IP. + if ((address != null) && (addr.address == null)) addr.address = address; + if ((addr.address != null) && (address == null)) address = addr.address; + // Except if we actually do have two different looked-up IPs! + return addr.address == null || addr.address.equals(address); + // Equal. + } else if (addr.hostname != null /* && hostname == null */) { + return false; + } + + // No hostname, go by address. + String reverseHostNameISee = getHostName(address); + String reverseHostNameTheySee = getHostName(addr.address); + return reverseHostNameISee != null + && reverseHostNameISee.equalsIgnoreCase(reverseHostNameTheySee); + } + + /** + * Returns the resolved IP address, performing a DNS lookup if needed. + * + *

If an address was resolved previously, it is returned without issuing a new lookup. + * + * @return resolved address or {@code null} if resolution fails + */ + public InetAddress getAddress() { + return getAddress(true); + } + + /** + * Returns the resolved IP address and optionally performs DNS resolution. + * + * @param doDNSRequest when {@code true}, resolve the hostname if not yet resolved; when {@code + * false}, return the cached value or {@code null} + * @return resolved address or {@code null} when not cached and lookups are disabled + */ + public InetAddress getAddress(boolean doDNSRequest) { + if (address != null) { + return address; + } else { + if (!doDNSRequest) return null; + InetAddress addr = getHandshakeAddress(); + if (addr != null) { + this.address = addr; + } + return addr; + } + } + + /** + * Returns the IP used for handshakes, forcing a fresh DNS resolution when hostname-primary. + * + *

This method bypasses a previously cached value if a hostname is present so that dynamic DNS + * updates are observed during connection attempts. + * + * @return the latest resolved address or {@code null} if the hostname cannot be resolved + */ + public InetAddress getHandshakeAddress() { + if (shouldReturnCachedAddress()) { + if (LOG.isDebugEnabled()) LOG.debug("hostname is null, returning {}", address); + return address; + } + return lookupHandshakeAddress(); + } + + private boolean shouldReturnCachedAddress() { + // During handshakes only return the cached value when IP is primary; a hostname may have + // changed due to dynamic DNS. + return (address != null) && (hostname == null); + } + + private InetAddress lookupHandshakeAddress() { + if (LOG.isDebugEnabled()) LOG.debug("Looking up '{}' in DNS", hostname); + /* + * Peers are constructed from an address once a + * handshake has been completed, so this lookup + * will only be performed during a handshake. + * This method should normally only be called + * from PeerNode.getHandshakeIPs() and once + * each connection from this.getAddress(). + * It does not mean we perform a DNS lookup + * with every packet we send. + */ + InetAddress[] addresses = resolveAllByName(hostname); + if (addresses.length == 0) return null; + if (addresses.length > 1) cacheFirstSortedAddress(addresses); + return addresses[0]; + } + + private InetAddress[] resolveAllByName(String host) { + try { + return InetAddress.getAllByName(host); + } catch (UnknownHostException _) { + if (LOG.isDebugEnabled()) + LOG.debug("DNS said hostname '{}' is an unknown host, returning null", host); + return new InetAddress[0]; + } + } + + private void cacheFirstSortedAddress(InetAddress[] addresses) { + /* Prefer IPv6 when multiple answers exist to encourage IPv6 connectivity. */ + Arrays.sort(addresses, InetAddressIpv6FirstComparator.COMPARATOR); + /* Cache the first answer to avoid immediate re-resolution on later calls. */ + try { + this.address = InetAddress.getByAddress(addresses[0].getAddress()); + if (LOG.isDebugEnabled()) LOG.debug("Setting address to {}", address); + } catch (UnknownHostException e) { + // Should not happen for valid IPv4/IPv6 byte arrays; skip caching if it does. + if (LOG.isWarnEnabled()) + LOG.warn( + "Failed to cache first sorted address for hostname '{}': {}", hostname, e.toString()); + } + } + + /** + * Computes a hash code consistent with the equality contract. + * + *

When a hostname is present, it is the sole contributor; otherwise the numeric address is + * used. + */ + @Override + public int hashCode() { + if (hostname != null) { + return hostname.hashCode(); // Was set at creation, so it can safely be used here. + } else { + return address.hashCode(); // Can be null, but if so, the hostname will be non-null. + } + } + + /** + * Returns a human-friendly representation: hostname when present, otherwise the numeric address. + */ + @Override + public String toString() { + if (hostname != null) { + return hostname; + } else { + return address.getHostAddress(); + } + } + + /** + * Returns a string favoring the numeric address when available, otherwise the hostname. + * + * @return numeric address or hostname; may be {@code null} when neither is available + */ + public String toStringPrefNumeric() { + if (address != null) return address.getHostAddress(); + else return hostname; + } + + /** + * Writes this instance to a {@link DataOutputStream} using the current binary encoding. + * + *

Format: one type byte ({@code 0} for IPv4, {@code 255} for IPv6), followed by the raw IP + * bytes, then a UTF string of the hostname or the empty string when absent. + * + * @param dos destination stream + * @throws IOException if writing fails or the address cannot be encoded + */ + public void writeToDataOutputStream(DataOutputStream dos) throws IOException { + InetAddress addr = this.getAddress(); + if (addr == null) throw new UnknownHostException(); + byte[] data = addr.getAddress(); + if (data.length == 4) dos.write(0); + else dos.write(255); + dos.write(data); + if (hostname != null) dos.writeUTF(hostname); + else dos.writeUTF(""); + } + + /** + * Returns the name component of an {@link InetAddress} without performing reverse DNS. + * + *

Parses the {@code toString()} form ({@code name/address}) and returns the left-hand side. If + * no name is present, returns {@link InetAddress#getHostAddress()}. + * + * @param primaryIPAddress address to read; may be {@code null} + * @return hostname or numeric address; {@code null} when the input is {@code null} + */ + public static String getHostName(InetAddress primaryIPAddress) { + if (primaryIPAddress == null) return null; + String s = primaryIPAddress.toString(); + String addr = s.substring(0, s.indexOf('/')).trim(); + if (addr.isEmpty()) return primaryIPAddress.getHostAddress(); + else return addr; + } + + /** + * Determines whether the address appears routable on the public Internet. + * + *

If a resolved address is available, the decision delegates to {@link + * IPUtil#isValidAddress(InetAddress, boolean)}. Otherwise, when {@code lookup} is {@code true}, a + * resolution attempt is made; when {@code false}, {@code defaultVal} is returned. + * + * @param lookup whether to resolve the hostname when no cached address is available + * @param defaultVal value to return when not resolved and lookups are disabled + * @param allowLocalAddresses whether RFC 1918/unique-local addresses are considered valid + * @return {@code true} if the address is considered publicly routable (or allowed locally) + */ + public boolean isRealInternetAddress( + boolean lookup, boolean defaultVal, boolean allowLocalAddresses) { + if (address != null) { + return IPUtil.isValidAddress(address, allowLocalAddresses); + } else { + if (lookup) { + InetAddress a = getAddress(); + if (a != null) return IPUtil.isValidAddress(a, allowLocalAddresses); + } + return defaultVal; + } + } + + /** + * Returns a new instance with the hostname removed, preserving only the resolved IP address. + * + *

If no address is currently resolved, returns {@code null}. Call {@link #getAddress(boolean)} + * with {@code true} first when the hostname is primary and resolution is desired. + * + * @return a new IP-primary instance, {@code null} when no address is known, or {@code this} when + * already IP-primary + */ + public FreenetInetAddress dropHostname() { + if (address == null) { + LOG.debug("dropHostname() called without a resolved address; hostname='{}'", hostname); + return null; + } + if (hostname != null) { + return new FreenetInetAddress(address); + } else return this; + } + + /** Returns whether a non-empty hostname is present. */ + public boolean hasHostname() { + return hostname != null && !hostname.isEmpty(); + } + + /** Returns whether a hostname is present but no address has been resolved yet. */ + public boolean hasHostnameNoIP() { + return hasHostname() && address == null; + } + + /** + * Returns whether the resolved address is IPv6. + * + * @param defaultValue value to return when the address has not been resolved yet + * @return {@code true} for IPv6, {@code false} for IPv4, or {@code defaultValue} when unknown + */ + public boolean isIPv6(boolean defaultValue) { + if (address == null) return defaultValue; + else return (address instanceof Inet6Address); + } +} diff --git a/interop-wire/src/main/java/network/crypta/io/comm/IncorrectTypeException.java b/interop-wire/src/main/java/network/crypta/io/comm/IncorrectTypeException.java new file mode 100644 index 00000000000..6825448cbe2 --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/io/comm/IncorrectTypeException.java @@ -0,0 +1,20 @@ +package network.crypta.io.comm; + +import java.io.Serial; + +/** + * Thrown if trying to set a field to a value of the wrong type + * + * @author ian + */ +public class IncorrectTypeException extends RuntimeException { + + public static final String VERSION = + "$Id: IncorrectTypeException.java,v 1.1 2005/01/29 19:12:10 amphibian Exp $"; + + @Serial private static final long serialVersionUID = 1L; + + public IncorrectTypeException(String s) { + super(s); + } +} diff --git a/interop-wire/src/main/java/network/crypta/io/comm/Message.java b/interop-wire/src/main/java/network/crypta/io/comm/Message.java new file mode 100644 index 00000000000..9694f061696 --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/io/comm/Message.java @@ -0,0 +1,762 @@ +package network.crypta.io.comm; + +import java.io.ByteArrayOutputStream; +import java.io.DataInput; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.Serial; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import network.crypta.support.ByteBufferInputStream; +import network.crypta.support.Fields; +import network.crypta.support.Serializer; +import network.crypta.support.ShortBuffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Message container for the Crypta wire format. + * + *

Instances encode to and decode from a compact binary representation used in UDP datagrams. A + * message carries a {@link MessageType} and a typed payload addressed by field name. Messages may + * also contain nested “sub-messages” to bundle related information without introducing new + * top‑level types. + * + *

Security note: Prefer constructing a fresh {@code Message} when forwarding rather than passing + * an incoming instance verbatim. Forwarding as‑is preserves any embedded submessages and can + * inadvertently leak metadata (e.g., labeling, collusion signals along a route) and waste + * bandwidth. Sub‑messages exist primarily for historical compatibility and should be used + * judiciously. + * + *

Thread-safety: {@code Message} is mutable and not thread‑safe. Callers should confine each + * instance to a single thread or apply external synchronization. + */ +public class Message { + private static final Logger LOG = LoggerFactory.getLogger(Message.class); + + /** Legacy source control identifier kept for historical compatibility. */ + public static final String VERSION = + "$Id: Message.java,v 1.11 2005/09/15 18:16:04 amphibian Exp $"; + + private final MessageType spec; + private final WeakReference sourceRef; + private final boolean internal; + private final HashMap payload = HashMap.newHashMap(8); + private List subMessages; + + /** + * Time of local instantiation in milliseconds since the epoch. + * + *

For decoded messages this reflects when the instance was constructed, not when the packet + * was received on the network. + */ + public final long localInstantiationTime; + + private final int receivedByteCount; + short priority; + + /** + * Decodes a message from a datagram buffer. + * + *

On failure (invalid type, internal‑only type, truncated payload, or I/O while reading the + * buffer), this method returns {@code null} without throwing. + * + * @param buf backing array containing the datagram payload + * @param offset start offset within {@code buf} + * @param length number of bytes to read starting at {@code offset} + * @param peer decoding context for the sending peer; may be {@code null} + * @param overhead additional bytes attributed to the received packet (e.g., link/protocol + * overhead) that should be counted for statistics; incorporated into {@link + * #receivedByteCount()} + * @return a decoded {@code Message}, or {@code null} if decoding fails or the type is rejected + */ + public static Message decodeMessageFromPacket( + byte[] buf, int offset, int length, MessageSource peer, int overhead) { + ByteBufferInputStream bb = new ByteBufferInputStream(buf, offset, length); + return decodeMessage(bb, peer, length + overhead, true, false, false); + } + + /** + * Decodes a message using relaxed validation. + * + *

Compared to {@link #decodeMessageFromPacket(byte[], int, int, MessageSource, int)}, this + * variant accepts certain legacy or malformed inputs that the strict decoder would reject. + * + * @param buf array containing the entire payload + * @param peer decoding context for the sending peer; may be {@code null} + * @param overhead additional bytes attributed to the received packet for statistics + * @return a decoded {@code Message}, or {@code null} when decoding fails + */ + public static Message decodeMessageLax(byte[] buf, MessageSource peer, int overhead) { + ByteBufferInputStream bb = new ByteBufferInputStream(buf); + return decodeMessage(bb, peer, buf.length + overhead, true, false, true); + } + + private static Message decodeMessage( + ByteBufferInputStream bb, + MessageSource peer, + int recvByteCount, + boolean mayHaveSubMessages, + boolean inSubMessage, + boolean veryLax) { + MessageType mspec = readMessageType(bb, veryLax); + if (mspec == null) { + if (LOG.isDebugEnabled()) + LOG.debug("event=decode-invalid-type message type missing or corrupt"); + return null; + } + if (!isAcceptable(mspec)) return null; + Message m = new Message(mspec, peer, recvByteCount); + try { + readMessageFields(mspec, m, bb); + readAndAttachSubMessagesIfAny(m, bb, peer, veryLax, mayHaveSubMessages); + } catch (EOFException e) { + logPrematureEnd(peer, mspec, inSubMessage, e); + return null; + } catch (IOException e) { + LOG.error("event=decode-io-error unexpected I/O reading message payload: {}", e, e); + return null; + } + logReturnMessage(m); + return m; + } + + private static MessageType readMessageType(ByteBufferInputStream bb, boolean veryLax) { + try { + return MessageType.getSpec(bb.readInt(), veryLax); + } catch (IOException e1) { + if (LOG.isDebugEnabled()) + LOG.debug("event=decode-type-read-failed failed to read message type id: {}", e1, e1); + return null; + } + } + + private static boolean isAcceptable(MessageType mspec) { + if (mspec == null) { + if (LOG.isDebugEnabled()) LOG.debug("event=accept-null-type rejected null message type"); + return false; + } + if (mspec.isInternalOnly()) { + if (LOG.isDebugEnabled()) + LOG.debug("event=accept-internal-only rejected internal-only message"); + return false; // silently discard internal-only messages + } + return true; + } + + private static void readAndAttachSubMessagesIfAny( + Message m, + ByteBufferInputStream bb, + MessageSource peer, + boolean veryLax, + boolean mayHaveSubMessages) { + if (!mayHaveSubMessages) return; + readAndAttachSubMessages(m, bb, peer, veryLax); + } + + private static void logPrematureEnd( + MessageSource peer, MessageType mspec, boolean inSubMessage, EOFException e) { + String msg = + peer.getPeer() + + " sent a message packet that ends prematurely while deserialising " + + mspec.getName(); + if (inSubMessage) { + if (LOG.isDebugEnabled()) LOG.debug("event=submessage-truncated {}", msg, e); + } else { + LOG.error(msg, e); + } + } + + private static void logReturnMessage(Message m) { + if (LOG.isDebugEnabled()) + LOG.debug("event=decode-complete decoded message {} from {}", m, m.getSource()); + } + + private static void readMessageFields(MessageType mspec, Message m, ByteBufferInputStream bb) + throws IOException { + DataInput dataInput = bb.asDataInput(); + for (String name : mspec.getOrderedFields()) { + Class type = mspec.getFields().get(name); + if (type.equals(List.class)) { + m.set( + name, + Serializer.readListFromDataInputStream( + mspec.getLinkedListTypes().get(name), dataInput)); + } else { + m.set(name, Serializer.readFromDataInputStream(type, dataInput)); + } + } + } + + private static void readAndAttachSubMessages( + Message m, ByteBufferInputStream bb, MessageSource peer, boolean veryLax) { + while (bb.remaining() > 2) { // sizeof(unsigned short) == 2 + ByteBufferInputStream bb2 = readSubMessageSlice(bb, m); + if (bb2 == null) { + // Not enough data or EOF - stop processing sub-messages gracefully. + return; + } + Message subMessage = decodeSubMessage(bb2, peer, veryLax); + if (subMessage == null) return; // Stop if decoding failed or returned null + if (LOG.isDebugEnabled()) + LOG.debug("event=submessage-attach added sub-message: {}", subMessage); + m.addSubMessage(subMessage); + } + } + + private static ByteBufferInputStream readSubMessageSlice(ByteBufferInputStream bb, Message m) { + try { + int size = bb.readUnsignedShort(); + if (bb.remaining() < size) return null; + return bb.slice(size); + } catch (EOFException _) { + if (LOG.isDebugEnabled()) + LOG.debug("event=submessage-none no sub-messages to read; stop at {}", m); + return null; + } catch (IOException e) { + LOG.error("event=submessage-slice-io I/O error while reading sub-message slice: {}", e, e); + return null; + } + } + + private static Message decodeSubMessage( + ByteBufferInputStream bb2, MessageSource peer, boolean veryLax) { + try { + return decodeMessage(bb2, peer, 0, false, true, veryLax); + } catch (Exception e) { + LOG.error("event=submessage-decode-failed failed to decode sub-message: {}", e, e); + return null; + } + } + + /** + * Constructs a new, locally originated message with a default priority. + * + * @param spec message type descriptor; must not be {@code null} + */ + public Message(MessageType spec) { + this(spec, null, 0); + } + + private Message(MessageType spec, MessageSource source, int recvByteCount) { + localInstantiationTime = System.currentTimeMillis(); + this.spec = spec; + if (source == null) { + internal = true; + sourceRef = null; + } else { + internal = false; + sourceRef = source.getWeakRef(); + } + receivedByteCount = recvByteCount; + priority = spec.getDefaultPriority(); + } + + /** Drops sub-messages and marks the clone as locally originated. */ + private Message(Message m) { + spec = m.spec; + sourceRef = null; + internal = m.internal; + payload.putAll(m.payload); + subMessages = null; + localInstantiationTime = System.currentTimeMillis(); + receivedByteCount = 0; + priority = m.priority; + } + + /** + * Returns the boolean value for a payload field. + * + * @param key field name defined by the {@link MessageType} + * @return field value + * @throws NullPointerException if the field is not set + * @throws ClassCastException if the stored value is not a {@code Boolean} + */ + public boolean getBoolean(String key) { + return (Boolean) payload.get(key); + } + + /** + * Returns the byte value for a payload field. + * + * @param key field name defined by the {@link MessageType} + * @return field value + * @throws NullPointerException if the field is not set + * @throws ClassCastException if the stored value is not a {@code Byte} + */ + public byte getByte(String key) { + return (Byte) payload.get(key); + } + + /** + * Returns the short value for a payload field. + * + * @param key field name defined by the {@link MessageType} + * @return field value + * @throws NullPointerException if the field is not set + * @throws ClassCastException if the stored value is not a {@code Short} + */ + public short getShort(String key) { + return (Short) payload.get(key); + } + + /** + * Returns the int value for a payload field. + * + * @param key field name defined by the {@link MessageType} + * @return field value + * @throws NullPointerException if the field is not set + * @throws ClassCastException if the stored value is not an {@code Integer} + */ + public int getInt(String key) { + return (Integer) payload.get(key); + } + + /** + * Returns the long value for a payload field. + * + * @param key field name defined by the {@link MessageType} + * @return field value + * @throws NullPointerException if the field is not set + * @throws ClassCastException if the stored value is not a {@code Long} + */ + public long getLong(String key) { + return (Long) payload.get(key); + } + + /** + * Returns the double value for a payload field. + * + * @param key field name defined by the {@link MessageType} + * @return field value + * @throws NullPointerException if the field is not set + * @throws ClassCastException if the stored value is not a {@code Double} + */ + public double getDouble(String key) { + return (Double) payload.get(key); + } + + /** + * Returns the float value for a payload field. + * + * @param key field name defined by the {@link MessageType} + * @return field value + * @throws NullPointerException if the field is not set + * @throws ClassCastException if the stored value is not a {@code Float} + */ + public float getFloat(String key) { + return (Float) payload.get(key); + } + + /** + * Returns the {@code double[]} for a payload field. + * + * @param key field name defined by the {@link MessageType} + * @return field value + * @throws NullPointerException if the field is not set + * @throws ClassCastException if the stored value is not a {@code double[]} + */ + public double[] getDoubleArray(String key) { + return ((double[]) payload.get(key)); + } + + /** + * Returns the {@code float[]} for a payload field. + * + * @param key field name defined by the {@link MessageType} + * @return field value + * @throws NullPointerException if the field is not set + * @throws ClassCastException if the stored value is not a {@code float[]} + */ + public float[] getFloatArray(String key) { + return (float[]) payload.get(key); + } + + /** + * Returns the {@link String} for a payload field. + * + * @param key field name defined by the {@link MessageType} + * @return field value + * @throws NullPointerException if the field is not set + * @throws ClassCastException if the stored value is not a {@code String} + */ + public String getString(String key) { + return (String) payload.get(key); + } + + /** + * Returns the raw object for a payload field. + * + * @param key field name defined by the {@link MessageType} + * @return the stored value, or {@code null} if the field is not set + */ + public Object getObject(String key) { + return payload.get(key); + } + + /** + * Returns the bytes from a {@link ShortBuffer} payload field. + * + * @param key field name defined by the {@link MessageType} + * @return backing byte array of the {@code ShortBuffer} + * @throws NullPointerException if the field is not set + * @throws ClassCastException if the stored value is not a {@code ShortBuffer} + */ + public byte[] getShortBufferBytes(String key) { + ShortBuffer buffer = (ShortBuffer) getObject(key); + return buffer.getData(); + } + + /** + * Sets a boolean payload field. + * + * @param key field name defined by the {@link MessageType} + * @param b value to store + * @throws IncorrectTypeException if the {@code MessageType} does not accept a boolean for {@code + * key} + */ + public void set(String key, boolean b) { + set(key, Boolean.valueOf(b)); + } + + /** + * Sets a byte payload field. + * + * @param key field name defined by the {@link MessageType} + * @param b value to store + * @throws IncorrectTypeException if the {@code MessageType} does not accept a byte for {@code + * key} + */ + public void set(String key, byte b) { + set(key, Byte.valueOf(b)); + } + + /** + * Sets a short payload field. + * + * @param key field name defined by the {@link MessageType} + * @param s value to store + * @throws IncorrectTypeException if the {@code MessageType} does not accept a short for {@code + * key} + */ + public void set(String key, short s) { + set(key, Short.valueOf(s)); + } + + /** + * Sets an int payload field. + * + * @param key field name defined by the {@link MessageType} + * @param i value to store + * @throws IncorrectTypeException if the {@code MessageType} does not accept an int for {@code + * key} + */ + public void set(String key, int i) { + set(key, Integer.valueOf(i)); + } + + /** + * Sets a long payload field. + * + * @param key field name defined by the {@link MessageType} + * @param l value to store + * @throws IncorrectTypeException if the {@code MessageType} does not accept a long for {@code + * key} + */ + public void set(String key, long l) { + set(key, Long.valueOf(l)); + } + + /** + * Sets a double payload field. + * + * @param key field name defined by the {@link MessageType} + * @param d value to store + * @throws IncorrectTypeException if the {@code MessageType} does not accept a double for {@code + * key} + */ + public void set(String key, double d) { + set(key, Double.valueOf(d)); + } + + /** + * Sets a float payload field. + * + * @param key field name defined by the {@link MessageType} + * @param f value to store + * @throws IncorrectTypeException if the {@code MessageType} does not accept a float for {@code + * key} + */ + public void set(String key, float f) { + set(key, Float.valueOf(f)); + } + + /** + * Sets a payload field to an arbitrary object after type checking. + * + *

The value must be non‑{@code null} and accepted by {@link MessageType#checkType(String, + * Object)} for the supplied field name. + * + * @param key field name defined by the {@link MessageType} + * @param value non‑{@code null} value to store + * @throws IncorrectTypeException if {@code value} is {@code null} or not allowed for {@code key} + */ + public void set(String key, Object value) { + if (!spec.checkType(key, value)) { + if (value == null) { + throw new IncorrectTypeException("Got null for " + key); + } + throw new IncorrectTypeException( + "Got " + value.getClass() + ", expected " + spec.typeOf(key)); + } + payload.put(key, value); + } + + /** + * Encodes this message to its wire representation. + * + *

The returned array contains the message header, ordered fields, and (when present) any + * sub‑messages. Encoding does not mutate this instance. + * + * @return a new byte array containing the encoded message + * @throws IllegalStateException if an I/O error occurs while writing to the in‑memory buffer + */ + public byte[] encodeToPacket() { + return encodeToPacket(true); + } + + private byte[] encodeToPacket(boolean includeSubMessages) { + + if (LOG.isTraceEnabled()) + LOG.trace( + "event=encode-spec-id message type id hash: {} for {}", + spec.getName().hashCode(), + spec.getName()); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + try { + dos.writeInt(spec.getName().hashCode()); + for (String name : spec.getOrderedFields()) { + Serializer.writeToDataOutputStream(payload.get(name), dos); + } + dos.flush(); + } catch (IOException e) { + throw new IllegalStateException( + "Failed to encode message header/fields for " + spec.getName(), e); + } + + if (subMessages != null && includeSubMessages) { + for (Message _subMessage : subMessages) { + byte[] temp = _subMessage.encodeToPacket(false); + try { + dos.writeShort(temp.length); + dos.write(temp); + } catch (IOException e) { + throw new IllegalStateException("Failed to encode sub-message for " + spec.getName(), e); + } + } + } + + byte[] buf = baos.toByteArray(); + if (LOG.isTraceEnabled()) + LOG.trace("event=encode-complete length: {}, hash: {}", buf.length, Fields.hashCode(buf)); + return buf; + } + + @Override + public String toString() { + StringBuilder ret = new StringBuilder(1000); + String comma = ""; + ret.append(spec.getName()).append(" {"); + for (String name : spec.getFields().keySet()) { + ret.append(comma); + ret.append(name).append('=').append(payload.get(name)); + comma = ", "; + } + ret.append('}'); + return ret.toString(); + } + + /** + * Returns the originating peer context, if still available. + * + *

For locally constructed messages this is {@code null}. For decoded messages a weak reference + * is held to the peer context and may have been cleared by the GC. + * + * @return {@code null} for local messages, or a possibly {@code null} {@link MessageSource} + */ + public MessageSource getSource() { + return sourceRef == null ? null : sourceRef.get(); + } + + /** + * Indicates whether this message originated locally. + * + * @return {@code true} when constructed locally; {@code false} when decoded from the wire + */ + public boolean isInternal() { + return internal; + } + + /** + * Returns the type descriptor for this message. + * + * @return the {@link MessageType} associated with this message + */ + public MessageType getSpec() { + return spec; + } + + /** + * Returns whether a payload field has been set. + * + * @param fieldName field name defined by the {@link MessageType} + * @return {@code true} if the field exists in the payload map + */ + public boolean isSet(String fieldName) { + return payload.containsKey(fieldName); + } + + /** + * Retrieves a non‑{@code null} payload value by name. + * + * @param fieldName field name defined by the {@link MessageType} + * @return the stored value + * @throws FieldNotSetException if the field has not been set + */ + public Object getFromPayload(String fieldName) throws FieldNotSetException { + Object r = payload.get(fieldName); + if (r == null) { + throw new FieldNotSetException(fieldName + " not set"); + } + return r; + } + + public static class FieldNotSetException extends RuntimeException { + @Serial private static final long serialVersionUID = 1L; + + /** + * Creates an exception indicating that a required field was not present in the payload. + * + * @param message detail message + */ + public FieldNotSetException(String message) { + super(message); + } + } + + /** + * Sets the standard fields used when routing to a specific node. + * + *

This is a convenience for populating {@link DMT#UID}, {@link DMT#TARGET_LOCATION}, {@link + * DMT#HTL}, and {@link DMT#NODE_IDENTITY}. + * + * @param uid unique identifier for the routed request + * @param targetLocation target location value used by the routing algorithm + * @param htl routing HTL value + * @param nodeIdentity node identity bytes; copied into a {@link ShortBuffer} + */ + public void setRoutedToNodeFields( + long uid, double targetLocation, short htl, byte[] nodeIdentity) { + set(DMT.UID, uid); + set(DMT.TARGET_LOCATION, targetLocation); + set(DMT.HTL, htl); + set(DMT.NODE_IDENTITY, new ShortBuffer(nodeIdentity)); + } + + /** + * Returns the number of bytes attributed to receiving this message. + * + *

For decoded messages this includes the decoded payload length plus any extra overhead value + * supplied to the decoder. For locally created messages this is {@code 0}. + * + * @return attributed byte count for statistics and accounting + */ + public int receivedByteCount() { + return receivedByteCount; + } + + /** + * Appends a sub‑message to this message. + * + * @param subMessage message to attach; not copied + */ + public void addSubMessage(Message subMessage) { + if (subMessages == null) subMessages = new ArrayList<>(); + subMessages.add(subMessage); + } + + /** + * Returns the first attached sub‑message of the given type, if present. + * + * @param t message type to match + * @return a sub‑message, or {@code null} if none matches + */ + public Message getSubMessage(MessageType t) { + if (subMessages == null) return null; + for (Message m : subMessages) { + if (t.equals(m.getSpec())) return m; + } + return null; + } + + /** + * Removes and returns the first attached sub‑message of the given type. + * + * @param t message type to match + * @return the removed sub‑message, or {@code null} if none matches + */ + public Message grabSubMessage(MessageType t) { + if (subMessages == null) return null; + for (int i = 0; i < subMessages.size(); i++) { + Message m = subMessages.get(i); + if (t.equals(m.getSpec())) { + subMessages.remove(i); + return m; + } + } + return null; + } + + /** + * Returns the age of this message in milliseconds since local instantiation. + * + * @return elapsed time in milliseconds + */ + public long age() { + return System.currentTimeMillis() - localInstantiationTime; + } + + /** + * Returns the current scheduling priority value. + * + * @return implementation‑defined priority + */ + public short getPriority() { + return priority; + } + + /** + * Adjusts the internal priority to favor earlier handling. + * + *

The exact scale and ordering are implementation‑defined. + */ + public void boostPriority() { + priority--; + } + + /** + * Creates a shallow copy without sub‑messages and with a local origin. + * + *

Payload entries and priority are copied; sub‑messages are dropped, and the source reference + * is cleared. + * + * @return a new {@code Message} with no sub‑messages + */ + public Message cloneAndDropSubMessages() { + return new Message(this); + } +} diff --git a/interop-wire/src/main/java/network/crypta/io/comm/MessageSource.java b/interop-wire/src/main/java/network/crypta/io/comm/MessageSource.java new file mode 100644 index 00000000000..a12a0b32809 --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/io/comm/MessageSource.java @@ -0,0 +1,11 @@ +package network.crypta.io.comm; + +import java.lang.ref.WeakReference; + +public interface MessageSource { + Peer getPeer(); + + long getBootID(); + + WeakReference getWeakRef(); +} diff --git a/interop-wire/src/main/java/network/crypta/io/comm/MessageType.java b/interop-wire/src/main/java/network/crypta/io/comm/MessageType.java new file mode 100644 index 00000000000..e8dbc85d99e --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/io/comm/MessageType.java @@ -0,0 +1,297 @@ +package network.crypta.io.comm; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import network.crypta.support.Serializer; +import network.crypta.support.ShortBuffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Describes the schema of an application message and maintains a registry of known types. + * + *

A MessageType records field names with their Java types and the order in which fields are + * encoded. Instances are registered in a process-wide map keyed by {@code name.hashCode()} so they + * can be resolved when decoding by identifier. The mapping is legacy behavior and may be subject to + * hash collisions; callers should not rely on the numeric identifier being globally unique. + */ +public final class MessageType { + private static final Logger LOG = LoggerFactory.getLogger(MessageType.class); + + /** Source-control marker string retained for historical reference. Not used by runtime logic. */ + public static final String VERSION = + "$Id: MessageType.java,v 1.6 2005/08/25 17:28:19 amphibian Exp $"; + + private static final HashMap _specs = new HashMap<>(); + + private final String name; + private final List orderedFields = new ArrayList<>(); + private final HashMap> fields = new HashMap<>(); + private final HashMap> linkedListTypes = new HashMap<>(); + private final boolean internalOnly; + private final short priority; + private final boolean isLossyPacketMessage; + + /** + * Creates and registers a message type with the given name and default priority. + * + * @param name human-readable name; its {@link String#hashCode()} becomes the registry key + * @param priority default priority used when enqueuing messages of this type + * @throws DuplicateMessageTypeException if a type with the same hash identifier already exists + */ + public MessageType(String name, short priority) { + this(name, priority, false, false); + } + + /** + * Creates and registers a message type and configures optional transport flags. + * + *

The instance is inserted into the global registry keyed by {@code name.hashCode()}. + * + * @param name human-readable name; also used to derive the registry identifier + * @param priority default priority used for outbound scheduling + * @param internal whether messages of this type are internal-only (e.g., not accepted over UDP) + * @param isLossyPacketMessage whether the type is eligible for lossy packet transport + * @throws DuplicateMessageTypeException if a type with the same hash identifier already exists + */ + public MessageType(String name, short priority, boolean internal, boolean isLossyPacketMessage) { + this.name = name; + this.priority = priority; + this.isLossyPacketMessage = isLossyPacketMessage; + internalOnly = internal; + // XXX: Using name.hashCode() as an identifier risks collisions; this is legacy behavior. + Integer id = name.hashCode(); + if (_specs.containsKey(id)) { + throw new DuplicateMessageTypeException( + "A message type by the name of " + name + " already exists!"); + } + _specs.put(id, this); + } + + /** + * Removes this instance from the global registry. + * + *

After calling this method, {@link #getSpec(Integer, boolean)} with this type's identifier + * returns {@code null}. + */ + public void unregister() { + _specs.remove(name.hashCode()); + } + + /** + * Adds a field of type {@link List} and records the element type for that list field. + * + *

The field name is also appended to the ordered field list. + * + * @param name field name + * @param parameter the list element type (e.g., {@code Integer.class}) + */ + public void addLinkedListField(String name, Class parameter) { + linkedListTypes.put(name, parameter); + addField(name, List.class); + } + + /** + * Adds a field definition. + * + *

The field is stored by name with its declared Java type and its name is appended to the + * ordered field list. If the same field name is added multiple times, the most recent type wins. + * + * @param name field name + * @param type declared Java type for values assigned to this field + */ + public void addField(String name, Class type) { + fields.put(name, type); + orderedFields.add(name); + } + + /** + * Adds common routing-related fields used by node-to-node messages. + * + *

Fields are identified by constants in {@link DMT}: {@code UID} ({@link Long}), {@code + * TARGET_LOCATION} ({@link Double}), {@code HTL} ({@link Short}), and {@code NODE_IDENTITY} + * ({@link ShortBuffer}). + */ + public void addRoutedToNodeMessageFields() { + addField(DMT.UID, Long.class); + addField(DMT.TARGET_LOCATION, Double.class); + addField(DMT.HTL, Short.class); + addField(DMT.NODE_IDENTITY, ShortBuffer.class); + } + + /** + * Verifies that a value is assignment-compatible with the declared type of field. + * + * @param fieldName the field to check + * @param fieldValue the value to test; {@code null} returns {@code false} + * @return {@code true} if {@code fieldValue.getClass()} is equal to, or a subclass/implementation + * of, the declared field type; {@code false} if the value is {@code null} or incompatible + * @throws IllegalStateException if {@code fieldName} is not defined for this type + */ + public boolean checkType(String fieldName, Object fieldValue) { + if (fieldValue == null) { + return false; + } + Class defClass = fields.get(fieldName); + if (defClass == null) { + throw new IllegalStateException( + "Cannot set field \"" + + fieldName + + "\" which is not defined" + + " in the message type \"" + + getName() + + "\"."); + } + Class valueClass = fieldValue.getClass(); + if (defClass == valueClass) return true; + return defClass.isAssignableFrom(valueClass); + } + + /** + * Returns the declared Java type for a field. + * + * @param field field name + * @return the {@link Class} for the field, or {@code null} if the name is not defined + */ + public Class typeOf(String field) { + return fields.get(field); + } + + /** + * Compares by {@code name} value equality. + * + *

Message types are registered by name; value equality keeps comparisons stable even if two + * instances reference distinct but equal strings. + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof MessageType other)) { + return false; + } + return java.util.Objects.equals(other.name, name); + } + + /** + * Computes a hash code from the {@code name} string. + * + * @return {@code name.hashCode()} + */ + @Override + public int hashCode() { + return name.hashCode(); + } + + /** + * Resolves a registered message type by its hashed identifier. + * + *

When {@code dontLog} is {@code false}, unknown IDs are logged at INFO. Unknown types can + * occur due to version skew or malformed traffic and are not necessarily fatal. + * + * @param specID identifier derived from {@code name.hashCode()} + * @param dontLog if {@code true}, suppresses INFO logging when the ID is not present + * @return the matching {@code MessageType}, or {@code null} if not registered + */ + public static MessageType getSpec(Integer specID, boolean dontLog) { + MessageType id = _specs.get(specID); + if (id == null && !dontLog) LOG.info("Unrecognised message type received ({})", specID); + + return id; + } + + /** + * Returns the human-readable name for this type. + * + * @return the name used when the type was created + */ + public String getName() { + return name; + } + + /** + * Returns the mapping of field names to declared Java types. + * + *

The returned map is backed by this instance and is mutable; changes affect the message type. + * + * @return live map of field definitions + */ + public Map> getFields() { + return fields; + } + + /** + * Returns field names in their defined encoding order. + * + *

The returned list is backed by this instance and is mutable. + * + * @return live list of field names in order + */ + public List getOrderedFields() { + return orderedFields; + } + + /** + * Returns element types for fields declared as {@link List}. + * + *

The map keys are field names; values are the corresponding element {@link Class} objects. + * The returned map is backed by this instance and is mutable. + * + * @return live map of list field element types + */ + public Map> getLinkedListTypes() { + return linkedListTypes; + } + + /** + * Indicates whether this type is internal-only. + * + *

If {@code true}, incoming UDP messages with this spec are silently discarded. + * + * @return {@code true} if internal-only; {@code false} otherwise + */ + public boolean isInternalOnly() { + return internalOnly; + } + + /** + * Returns the default priority associated with this type. + * + *

Senders may raise the effective priority dynamically (e.g., for real-time traffic). + * + * @return default priority value + */ + public short getDefaultPriority() { + return priority; + } + + /** + * Estimates the maximum encoded size, in bytes, for simple messages. + * + *

This mirrors {@code Message.encodeToPacket}. It sums the fixed sizes for known field types + * and uses {@code 4 + 2*maxStringLength} for strings. This method is not suitable for complex + * structures such as nested collections beyond {@link List} placeholders. + * + * @param maxStringLength maximum number of characters assumed for string fields + * @return upper bound on the encoded size in bytes + * @throws IllegalArgumentException if any field type is unsupported by {@link Serializer#length} + */ + public int getMaxSize(int maxStringLength) { + // This method mirrors Message.encodeToPacket. + int length = 0; + length += 4; // _spec.getName().hashCode() + for (Map.Entry> entry : fields.entrySet()) { + length += Serializer.length(entry.getValue(), maxStringLength); + } + return length; + } + + /** + * Indicates whether this type is intended for lossy packet transport. + * + * @return {@code true} if lossy packet encoding is permitted + */ + public boolean isLossyPacketMessage() { + return isLossyPacketMessage; + } +} diff --git a/interop-wire/src/main/java/network/crypta/io/comm/NotConnectedException.java b/interop-wire/src/main/java/network/crypta/io/comm/NotConnectedException.java new file mode 100644 index 00000000000..6c3a12a05ad --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/io/comm/NotConnectedException.java @@ -0,0 +1,50 @@ +package network.crypta.io.comm; + +import java.io.Serial; +import network.crypta.support.LightweightException; + +/** + * Indicates that an operation requiring an active peer connection was attempted when no connection + * existed. + * + *

This exception commonly occurs when sending a message to a peer that is not currently + * connected. It is distinct from {@link DisconnectedException}, which signals that a previously + * established connection dropped during a blocking wait. As a {@link LightweightException}, stack + * traces may be omitted by default to keep the exception inexpensive in frequent control-flow + * paths. + * + * @author amphibian + * @see DisconnectedException + * @see PeerRestartedException + */ +public class NotConnectedException extends LightweightException { + @Serial private static final long serialVersionUID = -1; + + /** + * Constructs an instance with a detail message. + * + * @param string human-readable detail; may be {@code null}. + */ + public NotConnectedException(String string) { + super(string); + } + + /** Constructs an instance with no detail message. */ + public NotConnectedException() { + super(); + } + + /** + * Constructs an instance derived from a {@link DisconnectedException}. + * + *

The message is taken from {@code e.toString()}, and this exception's cause is initialized to + * {@code e}. Useful when translating a mid-wait disconnect into a "not connected" condition for + * higher layers. + * + * @param e the disconnection to wrap as the cause; must not be {@code null}. + */ + public NotConnectedException(DisconnectedException e) { + super(e.toString()); + initCause(e); + } +} diff --git a/interop-wire/src/main/java/network/crypta/io/comm/OpennetAnnounceRequest.java b/interop-wire/src/main/java/network/crypta/io/comm/OpennetAnnounceRequest.java new file mode 100644 index 00000000000..06f9a3bc472 --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/io/comm/OpennetAnnounceRequest.java @@ -0,0 +1,14 @@ +package network.crypta.io.comm; + +/** + * Immutable metadata for an opennet announce request message. + * + * @param uid request identifier used to correlate responses + * @param transferUID transfer identifier for the noderef payload + * @param noderefLength unpadded noderef length in bytes + * @param paddedLength padded transfer length in bytes + * @param target target location in {@code [0.0, 1.0)} + * @param htl hop-to-live value for routing + */ +public record OpennetAnnounceRequest( + long uid, long transferUID, int noderefLength, int paddedLength, double target, short htl) {} diff --git a/interop-wire/src/main/java/network/crypta/io/comm/Peer.java b/interop-wire/src/main/java/network/crypta/io/comm/Peer.java new file mode 100644 index 00000000000..428ee3f028c --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/io/comm/Peer.java @@ -0,0 +1,415 @@ +package network.crypta.io.comm; + +import java.io.DataInput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.Serial; +import java.io.Serializable; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Comparator; +import network.crypta.io.WritableToDataOutputStream; +import network.crypta.support.io.InetAddressIpv6FirstComparator; +import network.crypta.support.transport.ip.HostnameSyntaxException; +import network.crypta.support.transport.ip.IPUtil; + +/** + * Immutable network endpoint consisting of a host (DNS name or IP literal) and a TCP port. + * + *

The host is represented by {@link FreenetInetAddress}, which can hold either a DNS hostname or + * a resolved numeric address and defines multiple equality modes. This class mirrors those + * semantics via {@link #laxEquals(Object)}, {@link #equals(Object)}, and {@link + * #strictEquals(Object)} while also requiring the port numbers to match. + * + *

Instances created from a DNS name may re-resolve the address when explicitly requested (see + * {@link #getHandshakeAddress()}). Regular address lookups (see {@link #getAddress(boolean)}) + * prefer cached results to avoid unnecessary DNS requests. + */ +public final class Peer implements WritableToDataOutputStream { + + /** + * Thrown when a resolved address is considered local/non‑public and therefore not acceptable for + * the requested operation. + */ + public static class LocalAddressException extends Exception { + @Serial private static final long serialVersionUID = -1; + } + + /** Comparator that prefers hostname peers, then IPv6 before IPv4. See {@link PeerComparator}. */ + public static final PeerComparator PEER_COMPARATOR = new PeerComparator(); + + /** Legacy revision marker string. */ + public static final String VERSION = "$Id: Peer.java,v 1.4 2005/08/25 17:28:19 amphibian Exp $"; + + private final FreenetInetAddress addr; + private final int port; + + /** + * Constructs a peer by reading from a {@link DataInput} in the same format written by {@link + * #writeToDataOutputStream(DataOutputStream)}. + * + *

Format: serialized {@link FreenetInetAddress} followed by a 32‑bit port. The port must be in + * {@code [0, 65535]}. + * + * @param dis data input to read from + * @throws IOException if the input is malformed or the port is out of range + */ + public Peer(DataInput dis) throws IOException { + addr = new FreenetInetAddress(dis); + port = dis.readInt(); + if (port > 65535 || port < 0) throw new IOException("bogus port"); + } + + /** + * Constructs a peer from a {@link DataInput}, optionally validating hostname/IP syntax while + * reading the {@link FreenetInetAddress}. + * + * @param dis data input to read from + * @param checkHostnameOrIPSyntax when {@code true}, validate DNS hostname or IPv4 syntax during + * deserialization + * @throws HostnameSyntaxException if syntax validation is enabled and the host is not well‑formed + * @throws IOException if the input is malformed or the port is out of range + */ + public Peer(DataInput dis, boolean checkHostnameOrIPSyntax) + throws HostnameSyntaxException, IOException { + addr = new FreenetInetAddress(dis, checkHostnameOrIPSyntax); + port = dis.readInt(); + if (port > 65535 || port < 0) throw new IOException("bogus port"); + } + + /** + * Constructs a peer from a concrete {@link InetAddress} and port. The numeric address is + * considered primary and does not change with later DNS updates. + * + * @param address resolved numeric address + * @param port TCP port in {@code [0, 65535]} + * @throws IllegalArgumentException if {@code port} is outside the valid range + */ + public Peer(InetAddress address, int port) { + addr = new FreenetInetAddress(address); + this.port = port; + if (this.port > 65535 || this.port < 0) throw new IllegalArgumentException("bogus port"); + } + + /** + * Parses {@code "host:port"} where {@code host} is a DNS name or IP literal. When a DNS name is + * provided, the name is treated as primary; {@link #getHandshakeAddress()} may re‑resolve it. + * + *

The split uses the last colon to separate the port, which tolerates IPv6 literals when the + * port suffix is present. + * + * @param physical input in the form {@code :} + * @param allowUnknown when {@code true}, allow construction even if the DNS name cannot be + * resolved at this time + * @throws PeerParseException if the input is malformed, or the port is missing/invalid + * @throws UnknownHostException if {@code allowUnknown} is {@code false} and the DNS lookup fails + */ + public Peer(String physical, boolean allowUnknown) + throws PeerParseException, UnknownHostException { + int offset = physical.lastIndexOf(':'); // split on final ':' to tolerate IPv6 literals + if (offset < 0) throw new PeerParseException(); + String host = physical.substring(0, offset); + addr = new FreenetInetAddress(host, allowUnknown); + String strport = physical.substring(offset + 1); + try { + port = Integer.parseInt(strport); + if (port < 0 || port > 65535) throw new PeerParseException("Invalid port " + port); + } catch (NumberFormatException e) { + throw new PeerParseException(e); + } + } + + /** + * Parses {@code "host:port"} with optional hostname/IP syntax validation. Semantics are identical + * to {@link #Peer(String, boolean)} with the added syntax check step. + * + * @param physical input in the form {@code :} + * @param allowUnknown when {@code true}, allow construction even if the DNS name cannot be + * resolved at this time + * @param checkHostnameOrIPSyntax when {@code true}, validate the DNS hostname or IPv4 literal + * syntax + * @throws HostnameSyntaxException if syntax validation fails + * @throws PeerParseException if the input is malformed, or the port is missing/invalid + * @throws UnknownHostException if {@code allowUnknown} is {@code false} and the DNS lookup fails + */ + public Peer(String physical, boolean allowUnknown, boolean checkHostnameOrIPSyntax) + throws HostnameSyntaxException, PeerParseException, UnknownHostException { + int offset = physical.lastIndexOf(':'); // split on final ':' to tolerate IPv6 literals + if (offset < 0) throw new PeerParseException("No port number: \"" + physical + "\""); + String host = physical.substring(0, offset); + addr = new FreenetInetAddress(host, allowUnknown, checkHostnameOrIPSyntax); + String strport = physical.substring(offset + 1); + try { + port = Integer.parseInt(strport); + if (port < 0 || port > 65535) throw new PeerParseException("Invalid port " + port); + } catch (NumberFormatException e) { + throw new PeerParseException(e); + } + } + + /** + * Constructs a peer from an existing {@link FreenetInetAddress} and port. No copying is + * performed; the provided instance is kept as‑is. + * + * @param addr address holder; must be non‑null + * @param port TCP port in {@code [0, 65535]} + * @throws NullPointerException if {@code addr} is {@code null} + * @throws IllegalArgumentException if {@code port} is outside the valid range + */ + public Peer(FreenetInetAddress addr, int port) { + this.addr = addr; + if (addr == null) throw new NullPointerException(); + this.port = port; + if (this.port > 65535 || this.port < 0) throw new IllegalArgumentException("bogus port"); + } + + /** + * Returns {@code true} when the port is zero. This is used as a sentinel for an uninitialized or + * special peer in some contexts. + */ + public boolean isNull() { + return port == 0; + } + + /** + * Returns {@code true} if {@code o} is a {@code Peer} with the same port and an address that is + * equal under {@link FreenetInetAddress#laxEquals(FreenetInetAddress)}. + */ + public boolean laxEquals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Peer peer)) { + return false; + } + + if (port != peer.port) { + return false; + } + return addr.laxEquals(peer.addr); + } + + /** + * Compares by port and address using {@link FreenetInetAddress#equals(Object)}. This is the + * default, non‑lax, non‑strict comparison. + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Peer peer)) { + return false; + } + + if (port != peer.port) { + return false; + } + return addr.equals(peer.addr); + } + + /** + * Returns {@code true} if {@code o} is a {@code Peer} with the same port and an address that is + * equal under {@link FreenetInetAddress#strictEquals(FreenetInetAddress)}. + */ + public boolean strictEquals(Object o) { + if (this == o) { + return true; + } + if (o == null) return false; + if (!(o instanceof Peer peer)) { + return false; + } + + if (port != peer.port) { + return false; + } + return addr.strictEquals(peer.addr); + } + + /** + * Returns the IP address, performing a DNS lookup if needed and allowed by {@link + * #getAddress(boolean)} semantics. + */ + public InetAddress getAddress() { + return getAddress(true); + } + + /** + * Returns the IP address. When {@code doDNSRequest} is {@code true} and no cached value exists, a + * DNS lookup may be performed. If a previous lookup exists, the cached value is returned without + * forcing a refresh. + * + * @param doDNSRequest whether a DNS lookup is permitted + * @return the resolved address, or {@code null} when unknown and a lookup is not performed + */ + public InetAddress getAddress(boolean doDNSRequest) { + return addr.getAddress(doDNSRequest); + } + + /** + * Returns the IP address with optional DNS lookup and local‑address filtering. + * + * @param doDNSRequest whether a DNS lookup is permitted + * @param allowLocal when {@code false}, reject addresses considered local/non‑public + * @return the resolved address, or {@code null} when unknown and a lookup is not performed + * @throws LocalAddressException if the resolved address is not acceptable and {@code allowLocal} + * is {@code false} + */ + public InetAddress getAddress(boolean doDNSRequest, boolean allowLocal) + throws LocalAddressException { + InetAddress a = addr.getAddress(doDNSRequest); + if (a == null) return null; + if (allowLocal || IPUtil.isValidAddress(a, false)) return a; + throw new LocalAddressException(); + } + + /** + * Forces a re‑lookup when the hostname is primary, even if a previous resolution exists. This is + * typically used before reconnection attempts when a dynamic DNS address may have changed. + */ + @SuppressWarnings("UnusedReturnValue") + public InetAddress getHandshakeAddress() { + return addr.getHandshakeAddress(); + } + + /** Computes a hash code consistent with {@link #equals(Object)}. */ + @Override + public int hashCode() { + return addr.hashCode() + port; + } + + /** + * Returns the TCP port number. + * + * @return port in {@code [0, 65535]} + */ + public int getPort() { + return port; + } + + /** + * Returns {@code host:port} using the underlying {@link FreenetInetAddress#toString()} for the + * host component. + */ + @Override + public String toString() { + return addr.toString() + ':' + port; + } + + /** + * Writes this peer to a {@link DataOutputStream}. The format matches the constructor that accepts + * a {@link DataInput}: serialized {@link FreenetInetAddress} then a 32‑bit port. + * + * @param dos output stream + * @throws IOException if writing fails + */ + @Override + public void writeToDataOutputStream(DataOutputStream dos) throws IOException { + addr.writeToDataOutputStream(dos); + dos.writeInt(port); + } + + /** + * Returns the underlying address holder for advanced queries. + * + * @return the {@link FreenetInetAddress} backing this peer + */ + public FreenetInetAddress getFreenetAddress() { + return addr; + } + + /** + * Indicates whether the address is considered a real Internet address. The check may perform a + * lookup based on {@code lookup} and may treat local addresses as acceptable based on {@code + * allowLocalAddresses}. + * + * @param lookup whether to perform a lookup if needed + * @param defaultVal value to return when the address is unknown + * @param allowLocalAddresses whether to treat local/private addresses as acceptable + * @return {@code true} if the address is acceptable per the above parameters + */ + public boolean isRealInternetAddress( + boolean lookup, boolean defaultVal, boolean allowLocalAddresses) { + return addr.isRealInternetAddress(lookup, defaultVal, allowLocalAddresses); + } + + /** + * Returns {@code host:port} but prefers a numeric IP representation for the host when available, + * avoiding DNS names. + */ + public String toStringPrefNumeric() { + return addr.toStringPrefNumeric() + ':' + port; + } + + /** + * Returns a peer where the hostname (if any) is dropped in favor of a numeric address. If no + * numeric address is known, returns {@code null}. If the underlying address is unchanged, this + * instance is returned. + * + * @return a peer without a hostname, or {@code null} if not possible + */ + public Peer dropHostName() { + FreenetInetAddress newAddr = addr.dropHostname(); + if (newAddr == null) return null; + if (!addr.equals(newAddr)) { + return new Peer(newAddr, port); + } else return this; + } + + /** + * Returns {@code true} when the address is IPv6. If the address is unknown, {@code defaultValue} + * is returned. + * + * @param defaultValue value to return when the address has not been resolved + */ + public boolean isIPv6(boolean defaultValue) { + if (addr == null) return defaultValue; + return addr.isIPv6(defaultValue); + } + + /** + * Comparator that orders peers as follows: + * + *

+ * + *

This is not a strict total ordering. Callers that require a complete order should apply an + * additional tie‑breaker. + */ + public static class PeerComparator implements Comparator, Serializable { + @Serial private static final long serialVersionUID = 1L; + + /** + * Compares two peers according to {@link PeerComparator} rules. + * + * @return negative if {@code p0} should sort before {@code p1}; positive if after; zero when + * considered equivalent by this comparator + */ + @Override + public int compare(Peer p0, Peer p1) { + boolean hasHostnameP0 = p0.getFreenetAddress().hasHostname(); + boolean hasHostnameP1 = p1.getFreenetAddress().hasHostname(); + boolean isIpv6P0 = p0.isIPv6(false); // default used when the address is not yet resolved + boolean isIpv6P1 = p1.isIPv6(false); // default used when the address is not yet resolved + if (hasHostnameP0 && !hasHostnameP1) { + return -1; + } else if (!hasHostnameP0 && hasHostnameP1) { + return 1; + } else if (hasHostnameP0) { + return 0; + } + if (isIpv6P0 && !isIpv6P1) { + return -1; + } else if (!isIpv6P0 && isIpv6P1) { + return 1; + } + return InetAddressIpv6FirstComparator.COMPARATOR.compare(p0.getAddress(), p1.getAddress()); + } + } +} diff --git a/interop-wire/src/main/java/network/crypta/io/comm/PeerParseException.java b/interop-wire/src/main/java/network/crypta/io/comm/PeerParseException.java new file mode 100644 index 00000000000..d2e3a8d10a3 --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/io/comm/PeerParseException.java @@ -0,0 +1,39 @@ +package network.crypta.io.comm; + +import java.io.Serial; + +/** + * Indicates that a textual or structured peer description could not be parsed. + * + *

Thrown by code that converts external representations (for example, node references or address + * strings) into {@link Peer} instances when the input is syntactically invalid or missing required + * fields. + * + * @author amphibian + */ +public class PeerParseException extends Exception { + @Serial private static final long serialVersionUID = -1; + + /** + * Constructs an instance that wraps a lower-level cause. + * + * @param e the underlying parsing failure; may be {@code null}. + */ + public PeerParseException(Exception e) { + super(e); + } + + /** Constructs an instance with no detail message. */ + public PeerParseException() { + super(); + } + + /** + * Constructs an instance with a detail message. + * + * @param string human-readable detail; may be {@code null}. + */ + public PeerParseException(String string) { + super(string); + } +} diff --git a/interop-wire/src/main/java/network/crypta/io/comm/PeerRestartedException.java b/interop-wire/src/main/java/network/crypta/io/comm/PeerRestartedException.java new file mode 100644 index 00000000000..a7fa4c34d42 --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/io/comm/PeerRestartedException.java @@ -0,0 +1,25 @@ +package network.crypta.io.comm; + +import java.io.Serial; +import network.crypta.support.LightweightException; + +/** + * Indicates that the remote peer restarted during an in‑flight operation. + * + *

This specialized form of {@link DisconnectedException} is used when an established connection + * drops because the peer process has restarted. Typical triggers include attempting to send a + * throttled packet, waiting for an incoming reply, or otherwise interacting with a session whose + * {@link PeerContext#getBootID() boot ID} has changed. + * + *

As a descendant of {@link LightweightException} (via {@link DisconnectedException}), stack + * traces may be omitted by default to keep the exception inexpensive in frequent control‑flow + * paths. + * + * @see DisconnectedException + * @see NotConnectedException + * @see PeerContext#getBootID() + */ +public class PeerRestartedException extends DisconnectedException { + + @Serial private static final long serialVersionUID = 616182042289792833L; +} diff --git a/interop-wire/src/main/java/network/crypta/io/comm/ReferenceSignatureVerificationException.java b/interop-wire/src/main/java/network/crypta/io/comm/ReferenceSignatureVerificationException.java new file mode 100644 index 00000000000..619ff8c95b9 --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/io/comm/ReferenceSignatureVerificationException.java @@ -0,0 +1,39 @@ +package network.crypta.io.comm; + +import java.io.Serial; + +/** + * Exception indicating that verification of a reference signature failed. + * + *

This exception is thrown when a cryptographic signature associated with a reference cannot be + * validated. Callers should treat this as an authentication failure and ignore or discard the + * unverified data. + * + * @author amphibian + */ +public class ReferenceSignatureVerificationException extends Exception { + @Serial private static final long serialVersionUID = -1; + + /** + * Creates an instance that wraps the underlying cause. + * + * @param e the cause of the verification failure; may be {@code null} + */ + public ReferenceSignatureVerificationException(Exception e) { + super(e); + } + + /** Creates an instance with no detail message or cause. */ + public ReferenceSignatureVerificationException() { + super(); + } + + /** + * Creates an instance with the specified detail message. + * + * @param string the detail message to include in this exception + */ + public ReferenceSignatureVerificationException(String string) { + super(string); + } +} diff --git a/interop-wire/src/main/java/network/crypta/io/comm/RetrievalException.java b/interop-wire/src/main/java/network/crypta/io/comm/RetrievalException.java new file mode 100644 index 00000000000..ac2b20b9486 --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/io/comm/RetrievalException.java @@ -0,0 +1,151 @@ +package network.crypta.io.comm; + +import java.io.Serial; +import network.crypta.support.LightweightException; + +/** + * Exception indicating that a block or related data could not be retrieved over the I/O + * communication layer. + * + *

The reason for the failure is encoded as an integer {@linkplain #getReason() reason code}. + * Human-readable forms are provided by {@link #getErrString()} and {@link #getErrString(int)}. The + * {@link #toString()} and {@link #getMessage()} methods include both the canonical reason string + * and an optional detail message. + * + *

This class extends {@link LightweightException}; consult that type for behavior differences + * from {@link Exception} (e.g., message formatting or stack handling). + * + * @author ian + */ +public class RetrievalException extends LightweightException { + @Serial private static final long serialVersionUID = 3257565105301500723L; + + /** Unspecified failure reason. */ + public static final int UNKNOWN = 0; + + /** Premature end of input while reading a block or message. */ + public static final int PREMATURE_EOF = 2; + + /** Underlying I/O error occurred during transfer. */ + public static final int IO_ERROR = 3; + + /** Sender terminated unexpectedly during the transfer. */ + public static final int SENDER_DIED = 5; + + /** Operation exceeded the allowed time. */ + public static final int TIMED_OUT = 4; + + /** The requested block already exists locally; retrieval not performed. */ + public static final int ALREADY_CACHED = 6; + + /** Sender disconnected before completion. */ + public static final int SENDER_DISCONNECTED = 7; + + /** Data insert is unavailable or not permitted for this operation. */ + public static final int NO_DATAINSERT = 8; + + /** Transfer was canceled by the receiver. */ + public static final int CANCELLED_BY_RECEIVER = 9; + + /** Receiver terminated unexpectedly during the transfer. */ + public static final int RECEIVER_DIED = 11; + + /** Could not send a block within the configured timeout. */ + public static final int UNABLE_TO_SEND_BLOCK_WITHIN_TIMEOUT = 12; + + /** Transfer aborted because the peer switched to turtle mode. */ + public static final int GONE_TO_TURTLE_MODE = 13; + + /** Transfer aborted because the turtle job/session was terminated. */ + public static final int TURTLE_KILLED = 14; + + // Numeric reason code; one of the public constants above. + final int reason; + // Optional human-readable detail for diagnostics (may be empty). + final String cause; + + /** + * Creates an instance with the given reason code. + * + * @param reason one of the public reason code constants defined in this class + */ + public RetrievalException(int reason) { + this.reason = reason; + this.cause = getErrString(reason); + } + + /** + * Creates an instance with the given reason code and a detail message. + * + *

If {@code cause} is {@code null}, empty, or the literal {@code "null"}, the canonical reason + * string from {@link #getErrString(int)} is used instead. + * + * @param reason one of the public reason code constants defined in this class + * @param cause optional detail message for logging or diagnostics + */ + public RetrievalException(int reason, String cause) { + this.reason = reason; + this.cause = + cause == null || cause.isEmpty() || cause.equals("null") ? getErrString(reason) : cause; + } + + /** + * Returns the numeric reason code associated with this failure. + * + * @return one of the public reason code constants defined in this class + */ + public int getReason() { + return reason; + } + + /** + * Returns a concise description in the form {@code REASON:detail}. The reason token contains no + * spaces, which simplifies parsing in logs and diagnostics. + */ + @Override + public String toString() { + return getErrString(reason) + ":" + cause; + } + + /** + * Returns the canonical reason token for this instance. + * + *

The token contains no spaces to remain stable in logs and machine parsing. + * + * @return a non-null, no-space canonical token for the reason code + */ + public String getErrString() { + return getErrString(reason); + } + + /** + * Maps a reason code to its canonical token. + * + * @param reason a numeric reason code + * @return a non-null, no-space canonical token describing the reason code; {@code "UNKNOWN (n)"} + * for unrecognized values + */ + public static String getErrString(int reason) { + return switch (reason) { + case PREMATURE_EOF -> "PREMATURE_EOF"; + case IO_ERROR -> "IO_ERROR"; + case SENDER_DIED -> "SENDER_DIED"; + case TIMED_OUT -> "TIMED_OUT"; + case ALREADY_CACHED -> "ALREADY_CACHED"; + case SENDER_DISCONNECTED -> "SENDER_DISCONNECTED"; + case NO_DATAINSERT -> "NO_DATAINSERT"; + case CANCELLED_BY_RECEIVER -> "CANCELLED_BY_RECEIVER"; + case UNKNOWN -> "UNKNOWN"; + case UNABLE_TO_SEND_BLOCK_WITHIN_TIMEOUT -> "UNABLE_TO_SEND_BLOCK_WITHIN_TIMEOUT"; + case GONE_TO_TURTLE_MODE -> "GONE_TO_TURTLE_MODE"; + case TURTLE_KILLED -> "TURTLE_KILLED"; + default -> "UNKNOWN (" + reason + ")"; + }; + } + + /** Returns the detail message. Equivalent to {@link #toString()}. */ + @Override + public String getMessage() { + return toString(); + } +} diff --git a/interop-wire/src/main/java/network/crypta/io/comm/TrafficClass.java b/interop-wire/src/main/java/network/crypta/io/comm/TrafficClass.java new file mode 100644 index 00000000000..a4ca6246e5e --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/io/comm/TrafficClass.java @@ -0,0 +1,72 @@ +package network.crypta.io.comm; + +/** + * Differentiated Services Code Point (DSCP) values for use with {@link + * java.net.Socket#setTrafficClass(int)}. + * + *

Each constant encodes the 6-bit DSCP value left-shifted into the IPv4/IPv6 traffic class + * field; the two least significant bits (ECN) are {@code 0}. These values can be passed directly to + * {@code Socket#setTrafficClass(int)} on platforms that honor DSCP settings. + * + *

Naming follows common DSCP conventions: {@code CSx} (Class Selector), {@code AFxy} (Assured + * Forwarding), and a critical/expedited forwarding class. + * + * @see java.net.Socket#setTrafficClass(int) + * @see Differentiated services + */ +public enum TrafficClass { + BEST_EFFORT(0), + DSCP_CRITICAL(0xB8), + DSCP_AF11(0x28), + DSCP_AF12(0x30), + DSCP_AF13(0x38), + DSCP_AF21(0x48), + DSCP_AF22(0x50), + DSCP_AF23(0x52), + DSCP_AF31(0x58), + DSCP_AF32(0x70), + DSCP_AF33(0x78), + DSCP_AF41(0x88), + DSCP_AF42(0x90), + DSCP_AF43(0x98), + DSCP_CS0(0), + DSCP_CS1(0x20), + DSCP_CS2(0x40), + DSCP_CS3(0x60), + DSCP_CS4(0x80), + DSCP_CS5(0xA0), + DSCP_CS6(0xC0), + DSCP_CS7(0xE0), + RFC1349_IPTOS_LOWCOST(0x02), + RFC1349_IPTOS_RELIABILITY(0x04), + RFC1349_IPTOS_THROUGHPUT(0x08), + RFC1349_IPTOS_LOWDELAY(0x10); + + public final int value; + + TrafficClass(int tc) { + value = tc; + } + + public static TrafficClass getDefault() { + // Default: CS1 (often treated as lower priority but suitable for bulk throughput). + return TrafficClass.DSCP_CS1; + } + + public static TrafficClass fromNameOrValue(String tcName) { + // Accept either a symbolic enum name (case-insensitive) or a decimal integer value. + int tcParsed = -1; + try { + tcParsed = Integer.parseInt(tcName); + } catch (NumberFormatException _) { + // Not an integer; fall through and attempt name matching. + } + + for (TrafficClass t : TrafficClass.values()) { + if (t.toString().equalsIgnoreCase(tcName) || t.value == tcParsed) { + return t; + } + } + throw new IllegalArgumentException(); + } +} diff --git a/interop-wire/src/main/java/network/crypta/node/Version.java b/interop-wire/src/main/java/network/crypta/node/Version.java new file mode 100644 index 00000000000..097bc4ab3bd --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/node/Version.java @@ -0,0 +1,506 @@ +package network.crypta.node; + +import java.util.Arrays; +import network.crypta.support.Fields; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Version information and helpers for the Cryptad node. + * + *

This class is the canonical source for node identity values, wire-version descriptors, and + * compatibility decisions used during peer negotiation. It exposes both static constants and helper + * methods that parse and compare serialized version strings exchanged by peers. Callers use these + * helpers to answer whether a remote peer is acceptable, to extract build identifiers from + * user-visible version text, and to track the highest build observed in the network. + * + *

All methods are static and side effects are intentionally limited to updating a single + * in-process "highest seen build" counter. Returned version component arrays are defensive copies, + * so callers cannot mutate internal cached values. Logging is used for rejection diagnostics but + * does not alter compatibility outcomes. + */ +public final class Version { + /** Human-readable product name of the node. */ + public static final String NODE_NAME = "Cryptad"; + + /** Minimum acceptable build for stable Fred peers. */ + public static final int LAST_GOOD_FRED_STABLE_BUILD = 1; + + /** Protocol version the node speaks on the wire. */ + public static final String LAST_GOOD_FRED_PROTOCOL_VERSION = "1.0"; + + /** Git revision embedded at build time. */ + public static final String GIT_REVISION = "@git_rev@"; + + /** Minimum acceptable build number for Cryptad peers. */ + public static final int MIN_ACCEPTABLE_CRYPTAD_BUILD_NUMBER = 1; + + /** Minimum acceptable build number for Freenet peers. */ + public static final int MIN_ACCEPTABLE_FRED_BUILD_NUMBER = 1475; + + // Private constants + private static final String BUILD_NUMBER_STRING = "@build_number@"; + private static final String WIRE_NAME = "Fred"; + private static final String STABLE_FRED_NODE_VERSION = "0.7"; + private static final String[] EMPTY_VERSION_COMPONENTS = new String[0]; + + private static final int BUILD_NUMBER = computeBuildNumber(); + private static final Logger LOG = LoggerFactory.getLogger("network.crypta.node.Version"); + + private static volatile int highestSeenBuild = BUILD_NUMBER; + + private static final String[] CACHED_VERSION_COMPONENTS = { + NODE_NAME, + Integer.toString(BUILD_NUMBER), + LAST_GOOD_FRED_PROTOCOL_VERSION, + Integer.toString(BUILD_NUMBER) + }; + + private static final String[] CACHED_MIN_ACCEPTABLE_VERSION_COMPONENTS = { + WIRE_NAME, + STABLE_FRED_NODE_VERSION, + LAST_GOOD_FRED_PROTOCOL_VERSION, + Integer.toString(MIN_ACCEPTABLE_FRED_BUILD_NUMBER) + }; + + private Version() {} + + /** + * Returns the build number embedded at runtime. + * + * @return current node build number resolved during class initialization + */ + public static int currentBuildNumber() { + return BUILD_NUMBER; + } + + /** + * Returns the Git revision string embedded at build time. + * + * @return build-time Git revision token for this runtime + */ + public static String gitRevision() { + return GIT_REVISION; + } + + /** + * Returns the current node version tuple as components. + * + * @return a new array containing node name, build, protocol, and compatibility build fields + */ + public static String[] getVersionComponents() { + return Arrays.copyOf(CACHED_VERSION_COMPONENTS, CACHED_VERSION_COMPONENTS.length); + } + + /** + * Returns minimum acceptable peer-version components for Fred compatibility. + * + * @return a new array containing the minimum acceptable wire peer descriptor components + */ + public static String[] getMinAcceptableVersionComponents() { + return Arrays.copyOf( + CACHED_MIN_ACCEPTABLE_VERSION_COMPONENTS, CACHED_MIN_ACCEPTABLE_VERSION_COMPONENTS.length); + } + + /** + * Returns the current node version in serialized wire format. + * + * @return comma-separated version string advertised to remote peers + */ + public static String getVersionString() { + return Fields.commaList(getVersionComponents()); + } + + /** + * Returns the serialized minimum acceptable peer-version descriptor. + * + * @return comma-separated minimum acceptable version string + */ + public static String getMinAcceptableVersionString() { + return Fields.commaList(getMinAcceptableVersionComponents()); + } + + /** + * Checks whether a peer version string is compatible with this node. + * + * @param version peer-reported serialized version string to validate + * @return {@code true} when the version parses successfully and passes compatibility checks + */ + public static boolean isCompatibleVersion(String version) { + String[] v = parseVersionOrNull(version); + if (v.length == 0) { + return false; + } + if (rejectIfCryptadTooOld(v, version)) { + return false; + } + if (rejectIfFredTooOld(v, version)) { + return false; + } + if (LOG.isDebugEnabled()) { + LOG.debug("Accepting peer version string: {}", version); + } + return true; + } + + /** + * Checks compatibility using the peer version and its stated minimum acceptable version. + * + * @param versionStr peer-reported serialized version string + * @param lastGoodVersionStr peer-reported minimum acceptable version string + * @return {@code true} when both descriptors parse and satisfy compatibility rules + */ + public static boolean isCompatibleVersionWithLastGood( + String versionStr, String lastGoodVersionStr) { + String[] v = parseVersionOrNull(versionStr); + if (v.length == 0) { + return false; + } + String[] lgv = parseVersionOrNull(lastGoodVersionStr, "lastGoodVersion"); + if (lgv.length == 0) { + return false; + } + + if (NODE_NAME.equals(v[0]) + && NODE_NAME.equals(lgv[0]) + && !checkCryptadCompatibility(v, lgv, versionStr, lastGoodVersionStr)) { + return false; + } + + if (WIRE_NAME.equals(v[0]) && !checkFredCompatibility(v, lgv, versionStr, lastGoodVersionStr)) { + return false; + } + + if (LOG.isDebugEnabled()) { + LOG.debug("Accepting peer version with lastGood: {}", versionStr); + } + return true; + } + + /** + * Parses a version string and extracts its build number. + * + * @param version serialized peer version string to parse + * @return parsed build number extracted from the provided version descriptor + * @throws VersionParseException if the version string format is invalid + */ + public static int parseBuildNumberFromVersionStr(String version) throws VersionParseException { + if (version == null) { + LOG.error("version == null!", new Exception("error")); + throw new IllegalArgumentException("version == null"); + } + + String[] v = Fields.commaList(version); + if (v.length < 3 || hasInvalidProtocol(v[2])) { + throw new VersionParseException("not long enough or bad protocol: " + version); + } + + try { + if (NODE_NAME.equals(v[0])) { + return Integer.parseInt(v[1]); + } + if (WIRE_NAME.equals(v[0])) { + if (v.length == 3) { + throw new VersionParseException("Fred version missing build number: " + version); + } + return Integer.parseInt(v[3]); + } + throw new VersionParseException("unknown node name: " + v[0]); + } catch (NumberFormatException e) { + String bad = v.length > 3 ? v[3] : null; + VersionParseException wrapped = + new VersionParseException( + "Got NumberFormatException on " + bad + " : " + e + " for " + version); + wrapped.initCause(e); + throw wrapped; + } + } + + /** + * Parses a build number and falls back to a default when parsing fails. + * + * @param version serialized peer version string to parse + * @param defaultValue fallback value returned when parsing fails + * @return parsed build number, or {@code defaultValue} if parsing throws + */ + public static int parseBuildNumberFromVersionStr(String version, int defaultValue) { + try { + return parseBuildNumberFromVersionStr(version); + } catch (Exception _) { + return defaultValue; + } + } + + /** + * Records a peer version and updates the highest seen build when applicable. + * + * @param versionStr serialized peer version string observed from the network + */ + public static void seenVersion(String versionStr) { + String[] v = Fields.commaList(versionStr); + if (v.length < 3) { + return; + } + + int version; + try { + if (NODE_NAME.equals(v[0])) { + version = Integer.parseInt(v[1]); + } else if (WIRE_NAME.equals(v[0])) { + if (v.length == 3) { + return; + } + version = Integer.parseInt(v[3]); + } else { + return; + } + } catch (Exception _) { + return; + } + + if (version > highestSeenBuild) { + if (LOG.isDebugEnabled()) { + LOG.debug("New highest seen build: {}", version); + } + highestSeenBuild = version; + } + } + + /** + * Returns the highest peer build number observed in this process. + * + * @return highest build number accepted by {@link #seenVersion(String)} + */ + public static int getHighestSeenBuild() { + return highestSeenBuild; + } + + /** + * Returns whether parsed version components identify a Cryptad node. + * + * @param v parsed version components array to inspect + * @return {@code true} when the first component identifies {@link #NODE_NAME} + */ + public static boolean isCryptad(String[] v) { + return v.length >= 2 && NODE_NAME.equals(v[0]); + } + + /** + * Returns whether two parsed version arrays belong to compatible release series. + * + * @param v parsed peer version components + * @param lgv parsed peer "last good" version components + * @return {@code true} when both arrays represent a compatible version family + */ + @SuppressWarnings("unused") + public static boolean isCompatibleSeries(String[] v, String[] lgv) { + if (v.length < 2 || lgv.length < 2) { + return false; + } + return switch (v[0]) { + case NODE_NAME -> true; // Cryptad compatible with Fred + case WIRE_NAME -> v[1].equals(lgv[1]) && v.length >= 4 && lgv.length >= 4; + default -> false; + }; + } + + private static boolean hasInvalidProtocol(String protocol) { + return !LAST_GOOD_FRED_PROTOCOL_VERSION.equals(protocol); + } + + private static boolean checkCryptadCompatibility( + String[] v, String[] lgv, String versionStr, String lastGoodVersionStr) { + Integer version = parseIntOrNull(getOrNull(v, 1)); + Integer minVersion = parseIntOrNull(getOrNull(lgv, 1)); + return isNumberAtLeast(version, minVersion, versionStr, lastGoodVersionStr); + } + + private static boolean checkFredCompatibility( + String[] v, String[] lgv, String versionStr, String lastGoodVersionStr) { + if (lgv.length > 0 && NODE_NAME.equals(lgv[0])) { + return false; + } + if (rejectIfFredTooOld(v, versionStr)) { + return false; + } + Integer build = parseIntOrNull(getOrNull(v, 3)); + Integer minBuild = parseIntOrNull(getOrNull(lgv, 3)); + return isNumberAtLeast(build, minBuild, versionStr, lastGoodVersionStr); + } + + private static String[] parseVersionOrNull(String version) { + return parseVersionOrNull(version, "version"); + } + + private static String[] parseVersionOrNull(String version, String label) { + if (version == null) { + LOG.error("{} == null!", label, new Exception("error")); + return EMPTY_VERSION_COMPONENTS; + } + String[] v = Fields.commaList(version); + if (v.length < 3 || hasInvalidProtocol(v[2])) { + return EMPTY_VERSION_COMPONENTS; + } + return v; + } + + private static boolean isNumberAtLeast( + Integer actual, Integer min, String versionStr, String lastGoodVersionStr) { + if (actual == null || min == null) { + if (LOG.isDebugEnabled()) { + LOG.debug( + "Rejecting version due to non-numeric build (compat check): version={}" + + " lastGoodVersion={}", + versionStr, + lastGoodVersionStr); + } + return false; + } + + if (actual < min) { + if (LOG.isDebugEnabled()) { + LOG.debug( + "Rejecting version below minimum (compat check): version={} lastGoodVersion={}", + versionStr, + lastGoodVersionStr); + } + return false; + } + + return true; + } + + private static boolean rejectIfCryptadTooOld(String[] v, String original) { + if (!isCryptad(v)) { + return false; + } + Integer version = parseIntOrNull(getOrNull(v, 1)); + int req = MIN_ACCEPTABLE_CRYPTAD_BUILD_NUMBER; + + if (version == null) { + if (LOG.isDebugEnabled()) { + LOG.debug("Rejecting Cryptad version with non-numeric build: {}", original); + } + return true; + } + if (version < req) { + if (LOG.isDebugEnabled()) { + LOG.debug( + "Rejecting Cryptad version below minimum build: {}" + + " (minAcceptableCryptadBuildNumber={})", + original, + req); + } + return true; + } + return false; + } + + private static boolean rejectIfFredTooOld(String[] v, String original) { + if (!isFredStableVersion(v)) { + return false; + } + + Integer build = parseIntOrNull(getOrNull(v, 3)); + if (build == null) { + if (LOG.isDebugEnabled()) { + LOG.debug("Rejecting Fred stable version with non-numeric build: {}", original); + } + return true; + } + + if (build < LAST_GOOD_FRED_STABLE_BUILD) { + if (LOG.isDebugEnabled()) { + LOG.debug( + "Rejecting Fred stable version below last good build: {} (lastGoodStableBuild={})", + original, + LAST_GOOD_FRED_STABLE_BUILD); + } + return true; + } + return false; + } + + private static boolean isFredStableVersion(String[] v) { + return v.length >= 4 && WIRE_NAME.equals(v[0]) && STABLE_FRED_NODE_VERSION.equals(v[1]); + } + + /** + * Compares two build numbers considering node names first. + * + *

Cryptad nodes are always considered newer than Fred nodes. + * + * @param nodeName1 node-name component for the first build number + * @param buildNumber1 first build number to compare + * @param nodeName2 node-name component for the second build number + * @param buildNumber2 second build number to compare + * @return a negative value, zero, or a positive value following {@link Integer#compare(int, int)} + */ + public static int compareBuildNumbers( + String nodeName1, int buildNumber1, String nodeName2, int buildNumber2) { + if (nodeName1 == null || nodeName2 == null) { + return Integer.compare(buildNumber1, buildNumber2); + } + + if (NODE_NAME.equals(nodeName1) && WIRE_NAME.equals(nodeName2)) { + return 1; + } + if (WIRE_NAME.equals(nodeName1) && NODE_NAME.equals(nodeName2)) { + return -1; + } + return Integer.compare(buildNumber1, buildNumber2); + } + + /** + * Checks if a peer's build is at least the specified minimum build number, considering node name. + * + * @param nodeName peer node-name component + * @param buildNumber peer build number + * @param minBuildNumber minimum acceptable build number for non-Cryptad peers + * @return {@code true} when the peer satisfies minimum build requirements + */ + public static boolean isBuildAtLeast(String nodeName, int buildNumber, int minBuildNumber) { + if (NODE_NAME.equals(nodeName)) { + return true; + } + return buildNumber >= minBuildNumber; + } + + /** + * Extracts the node-name component from a serialized version string. + * + * @param version serialized version string to parse + * @return first version component, or {@code null} when missing or input is {@code null} + */ + public static String parseNodeNameFromVersionStr(String version) { + if (version == null) { + return null; + } + String[] parts = Fields.commaList(version); + return parts.length > 0 ? parts[0] : null; + } + + @SuppressWarnings("DataFlowIssue") + private static int computeBuildNumber() { + try { + return Integer.parseInt(BUILD_NUMBER_STRING); + } catch (NumberFormatException _) { + return 0; + } + } + + private static Integer parseIntOrNull(String s) { + if (s == null) { + return null; + } + try { + return Integer.parseInt(s); + } catch (NumberFormatException _) { + return null; + } + } + + private static String getOrNull(String[] arr, int index) { + return index >= 0 && index < arr.length ? arr[index] : null; + } +} diff --git a/interop-wire/src/main/java/network/crypta/node/VersionParseException.java b/interop-wire/src/main/java/network/crypta/node/VersionParseException.java new file mode 100644 index 00000000000..66f03d00b75 --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/node/VersionParseException.java @@ -0,0 +1,27 @@ +package network.crypta.node; + +import java.io.Serial; + +/** + * Exception indicating that parsing a version string failed. + * + *

Used by version-processing utilities (for example, {@link Version}) when converting a textual + * version into numeric components such as an arbitrary build number. It is a checked exception so + * callers explicitly handle malformed or unsupported version formats. The class is immutable and + * carries no additional state beyond the detail message. + * + * @author toad + */ +public class VersionParseException extends Exception { + // Fixed UID to keep serialization compatibility across releases. + @Serial private static final long serialVersionUID = -19006235321212642L; + + /** + * Creates an instance with a human-readable detail message. + * + * @param msg detail describing the parse error context; may be {@code null}. + */ + public VersionParseException(String msg) { + super(msg); + } +} diff --git a/interop-wire/src/main/java/network/crypta/node/probe/Error.java b/interop-wire/src/main/java/network/crypta/node/probe/Error.java new file mode 100644 index 00000000000..49a862a4406 --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/node/probe/Error.java @@ -0,0 +1,151 @@ +package network.crypta.node.probe; + +/** + * Enumerates well-known failure conditions that can occur while running a probe in the Crypta + * network. + * + *

Each constant represents a distinct class of failure that a probing node or a relaying node + * can report when a probe cannot be completed successfully. The enum is intentionally coupled with + * a stable {@linkplain #code numeric code} so that values can be serialized over the network + * without relying on fragile {@code name()} or ordinal-based encodings. Codes are small and + * consecutive, starting at zero. + * + *

Typical usage follows this pattern: + * + *

+ * + *

This type is immutable and thread-safe. It has no mutable state and can be freely shared + * across threads. The mapping between codes and enum constants is fixed at compile time and does + * not change at runtime. + */ +@SuppressWarnings("JavaLangClash") +public enum Error { + /** + * The target node disconnected while the probe awaited a response. + * + *

This indicates that a network connection or session broke before a definitive outcome could + * be delivered. The disconnect can originate either from the remote peer voluntarily closing the + * connection or due to transport-layer failures, timeouts at lower layers, or process shutdowns. + * Retrying may succeed if the disconnection was transient. + */ + DISCONNECTED((byte) 0), + /** + * The receiving node rejected the probe because a local DoS/overload guard is active. + * + *

Nodes enforce admission control for probes to protect resources. When limits are exceeded, + * new probes are refused and this error is emitted. Backing off and retrying after a delay is the + * recommended strategy; immediate retries are likely to be rejected again while the guard is + * engaged. + */ + OVERLOAD((byte) 1), + /** + * The probe timed out before a response or terminal failure arrived. + * + *

Timeouts can result from long network paths, slow or congested links, or nodes that are + * alive but too busy to respond within the configured time budget. Upstream policies determine + * the actual timeout thresholds. Callers typically treat this as retryable with exponential + * backoff. + */ + TIMEOUT((byte) 2), + /** + * A node reported an error code that is not recognized by the local implementation. + * + *

When received locally, the unrecognized numeric value is preserved alongside this category + * to aid diagnostics. This generally indicates a version skew between peers or an extension that + * is not understood. The failure itself is terminal for the current probe; callers should avoid + * assuming any semantics beyond “not recognized”. + */ + UNKNOWN((byte) 3), + /** + * The remote node understood the request envelope but did not recognize the probe type. + * + *

This differs from protocol-level errors: for locally started probes an unknown type would be + * rejected earlier as a protocol error. Over the network it communicates that the peer does not + * implement the requested probe, which may happen with mixed-version deployments. + */ + UNRECOGNIZED_TYPE((byte) 4), + /** + * The node accepted and understood the request but failed to forward it to the next hop. + * + *

Forwarding can fail due to routing constraints, peer selection limits, or transient + * connectivity issues. Implementations usually cap the number of send attempts to prevent + * excessive retries. + * + * @see Probe#MAX_SEND_ATTEMPTS + */ + CANNOT_FORWARD((byte) 5); + + /** + * Stable numeric value that represents this constant on the wire. + * + *

Use this field when serializing an {@code Error} over the network. Do not rely on {@link + * Enum#name()} or ordinal values; those are unstable across refactorings and re-orderings. Codes + * are consecutive and begin at zero. + */ + public final byte code; + + private static final int MAX_CODE = Error.values().length; + + Error(byte code) { + this.code = code; + } + + /** + * Reports whether a numeric code corresponds to a defined {@code Error} value. + * + *

This helper enables fast validation without exceptions. It checks that the code falls within + * the inclusive lower bound and exclusive upper bound of known codes. The mapping is stable for a + * given build. A {@code true} outcome guarantees that {@link #valueOf(byte)} will succeed. + * + *

Usage example: + * + *

{@code
+   * if (Error.isValid(code)) {
+   *   var e = Error.valueOf(code);
+   *   // handle e
+   * } else {
+   *   // handle unknown code
+   * }
+   * }
+ * + * @param code numeric value received from a peer or decoded from storage; values below zero or + * beyond the highest assigned code are invalid. + * @return {@code true} when {@code code} maps to a known constant; {@code false} when it does + * not, in which case callers should treat it as unknown. + */ + static boolean isValid(byte code) { + // Assumes codes are consecutive, start at zero, and all are valid. + return code >= 0 && code < MAX_CODE; + } + + /** + * Converts a numeric code to its corresponding {@code Error} constant. + * + *

This method performs a constant-time switch over the supported values and never returns + * {@code null}. It is idempotent for the same input and does not perform any I/O. Prefer calling + * {@link #isValid(byte)} first when handling untrusted input to avoid exceptions in hot paths. + * + * @param code numeric identifier previously obtained from {@link #code} during serialization or + * received from a peer; must be within the set of defined values. + * @return the non-null {@code Error} constant that matches {@code code}; ownership is shared and + * instances are the canonical enum singletons. + * @throws IllegalArgumentException if there is no constant with the requested {@code code}; + * callers should treat such cases as an unknown error category. + */ + static Error valueOf(byte code) throws IllegalArgumentException { + return switch (code) { + case 0 -> DISCONNECTED; + case 1 -> OVERLOAD; + case 2 -> TIMEOUT; + case 3 -> UNKNOWN; + case 4 -> UNRECOGNIZED_TYPE; + case 5 -> CANNOT_FORWARD; + default -> + throw new IllegalArgumentException("There is no ProbeError with code " + code + "."); + }; + } +} diff --git a/interop-wire/src/main/java/network/crypta/node/probe/Type.java b/interop-wire/src/main/java/network/crypta/node/probe/Type.java new file mode 100644 index 00000000000..b74e8c9bbfa --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/node/probe/Type.java @@ -0,0 +1,112 @@ +package network.crypta.node.probe; + +/** + * Enumerates the different probe result types exchanged between nodes in the Crypta network. + * + *

Each constant identifies a specific class of diagnostic or status information that can be + * requested by a peer and returned by a responding node. The enum associates every type with a + * compact {@code byte} {@linkplain #code on-the-wire code} that is stable for serialization and + * deserialization. This allows efficient transmission while keeping the higher-level semantics + * explicit and type-safe in code. + * + *

Typical usage is to read a raw {@code byte} value from an inbound message, verify it with + * {@link #isValid(byte)}, and then obtain the corresponding {@link #valueOf(byte)} to branch on the + * enum constant. When emitting messages, use the {@link #code} of a constant instead of its ordinal + * value; the mapping is explicit and not tied to declaration order. Values outside the declared + * range are considered invalid and must be rejected by callers. + * + *

+ */ +public enum Type { + /** + * Requests or reports information about a node's bandwidth, such as capacity or recent + * utilization, subject to the probe protocol details. + */ + BANDWIDTH((byte) 0), + /** Reports the build identifier or version information for the running node software. */ + BUILD((byte) 1), + /** + * Exchanges a stable node identifier value as defined by the protocol, enabling peers to + * correlate responses with a specific node across requests. + */ + IDENTIFIER((byte) 2), + /** Provides summary statistics describing link length distributions in the network topology. */ + LINK_LENGTHS((byte) 3), + /** Reports coarse location or placement information used by the routing layer. */ + LOCATION((byte) 4), + /** Conveys the size or capacity of local persistent stores managed by the node. */ + STORE_SIZE((byte) 5), + /** Returns a node uptime measurement aggregated over a recent 48-hour window. */ + UPTIME_48H((byte) 6), + /** Returns a node uptime measurement aggregated over a recent 7-day window. */ + UPTIME_7D((byte) 7), + /** Reports request rejection statistics to aid diagnosis of overload or policy throttling. */ + REJECT_STATS((byte) 8), + /** + * Reports aggregated bulk output capacity usage, suitable for understanding sustained egress load + * relative to configured limits. + */ + OVERALL_BULK_OUTPUT_CAPACITY_USAGE((byte) 9); + + /** + * Compact, stable {@code byte} value used on the wire to represent this probe type. + * + *

The value is explicitly assigned per constant and is independent of {@link #ordinal()}. It + * is immutable and designed for serialization/deserialization; callers should not infer ordering + * or density beyond the fact that valid codes occupy the range {@code 0..(values().length-1)}. + */ + public final byte code; + + private static final int MAX_CODE = Type.values().length; + + Type(byte code) { + this.code = code; + } + + /** + * Checks whether {@link #valueOf(byte)} will succeed for the given code without throwing. + * + *

This helper exists to avoid using exceptions for control flow when parsing untrusted input. + * A return value of {@code true} indicates the code maps to one of the declared enum constants at + * the time of the call. The check is constant-time with respect to the number of enum constants. + * + * @param code the raw on-the-wire value to validate; values below zero are always invalid. + * @return {@code true} when the code corresponds to a declared constant; {@code false} otherwise. + */ + static boolean isValid(byte code) { + return code >= 0 && code < MAX_CODE; + } + + /** + * Determines the enum constant that corresponds to the supplied on-the-wire code. + * + *

This method performs a direct mapping from the compact {@code byte} representation to the + * strongly typed {@code Type}. It is idempotent and side effect free. Callers should prefer + * {@link #isValid(byte)} to guard inputs when handling data from untrusted peers. + * + * @param code the compact code to resolve; must match one of the declared constants. + * @return the {@code Type} constant corresponding to {@code code}; never {@code null}. + * @throws IllegalArgumentException if {@code code} does not correspond to any declared constant. + */ + static Type valueOf(byte code) throws IllegalArgumentException { + return switch (code) { + case 0 -> BANDWIDTH; + case 1 -> BUILD; + case 2 -> IDENTIFIER; + case 3 -> LINK_LENGTHS; + case 4 -> LOCATION; + case 5 -> STORE_SIZE; + case 6 -> UPTIME_48H; + case 7 -> UPTIME_7D; + case 8 -> REJECT_STATS; + case 9 -> OVERALL_BULK_OUTPUT_CAPACITY_USAGE; + default -> + throw new IllegalArgumentException("There is no ProbeType with code " + code + "."); + }; + } +} diff --git a/interop-wire/src/main/java/network/crypta/support/Serializer.java b/interop-wire/src/main/java/network/crypta/support/Serializer.java new file mode 100644 index 00000000000..5e59971161d --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/support/Serializer.java @@ -0,0 +1,412 @@ +package network.crypta.support; + +import java.io.DataInput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import network.crypta.io.WritableToDataOutputStream; +import network.crypta.io.comm.Peer; +import network.crypta.keys.Key; +import network.crypta.keys.NodeCHK; +import network.crypta.keys.NodeSSK; + +/** + * Utility for serializing and deserializing a constrained set of value types to and from {@link + * java.io.DataInput} / {@link java.io.DataOutputStream}. + * + *

This class supports a small, explicit set of types used by the networking and messaging code: + * boxed primitives ({@link Boolean}, {@link Byte}, {@link Short}, {@link Integer}, {@link Long}, + * {@link Float}, {@link Double}), {@link String}, {@link List}, {@code double[]} and {@code + * float[]}, several support types ({@link Buffer}, {@link ShortBuffer}, {@link Peer}, {@link + * BitArray}), and selected key types ({@link NodeCHK}, {@link NodeSSK}, and {@link Key}). + * + *

Strings are encoded as a 32-bit length followed by that many 16-bit Java {@code char}s written + * via {@link java.io.DataOutputStream#writeChar(int)} (i.e., not modified UTF-8 and not {@link + * java.io.DataOutputStream#writeUTF(String)}). Booleans are encoded strictly as a single byte with + * value {@code 0} (false) or {@code 1} (true); other values are rejected at read time. Arrays use a + * compact length prefix: one unsigned byte for {@code double[]} (max 255 elements) and a 16-bit + * signed short for {@code float[]}. + * + *

The class is stateless and thread-safe. All methods are static; instantiation is prevented. + */ +public class Serializer { + private Serializer() {} + + /** Historical SCM identifier (kept for traceability). */ + public static final String VERSION = + "$Id: Serializer.java,v 1.5 2005/09/15 18:16:04 amphibian Exp $"; + + /** + * Upper bound, in bits, used when deserializing {@link BitArray} to prevent pathological + * allocations. + */ + public static final int MAX_BITARRAY_SIZE = SerializationLimits.MAX_BITARRAY_SIZE; + + /** Maximum allowed inbound variable-length payload, in bytes. */ + public static final int MAX_ARRAY_LENGTH = SerializationLimits.MAX_ARRAY_LENGTH; + + /** + * Reads a {@link List} whose elements are of {@code elementType} from the given {@link + * DataInput}. + * + *

The on-wire representation is a 32-bit element count followed by that many element values, + * each encoded using {@link #readFromDataInputStream(Class, DataInput)} with the supplied {@code + * elementType}. The method does not enforce an explicit upper bound on the element count; callers + * must ensure the producer is trusted or that inputs are reasonably bounded. + * + * @param elementType the expected element type. Must be one of the types recognized by {@link + * #readFromDataInputStream(Class, DataInput)}. + * @param dis the input to read from; not closed by this method. + * @return a new {@link List} containing the decoded elements in order. + * @throws IOException if an I/O error occurs, or an element payload is invalid for the declared + * type. + * @throws IllegalArgumentException if {@code elementType} is unsupported. + * @see #readFromDataInputStream(Class, DataInput) + * @see #writeToDataOutputStream(Object, DataOutputStream) + */ + public static List readListFromDataInputStream(Class elementType, DataInput dis) + throws IOException { + int length = dis.readInt(); + List ret = new ArrayList<>(Math.max(0, length)); + for (int x = 0; x < length; x++) { + ret.add(readFromDataInputStream(elementType, dis)); + } + return ret; + } + + /** + * Reads a single value of the requested {@code type} from a {@link DataInput}. + * + *

Supported types include boxed primitives ({@link Boolean}, {@link Byte}, {@link Short}, + * {@link Integer}, {@link Long}, {@link Float}, {@link Double}); {@link String}; support types + * ({@link Buffer}, {@link ShortBuffer}, {@link Peer}, {@link BitArray}); selected keys ({@link + * NodeCHK}, {@link NodeSSK}, {@link Key}); {@code double[]} and {@code float[]}. + * + *

Booleans are read in a strict form via a single byte: {@code 0} or {@code 1}. Strings are + * read as a 32-bit length (bounded by {@link #MAX_ARRAY_LENGTH}) plus that many 16-bit + * characters. + * + * @param type the class indicating the type to read. Must be one of the supported types listed + * above. + * @param dis the input to read from; not closed by this method. + * @return the decoded value; never {@code null}. + * @throws IOException if an I/O error occurs, or the next value is malformed for the requested + * type (for example, a boolean byte not equal to {@code 0} or {@code 1}, an invalid string + * length, or an oversized array). + * @throws IllegalArgumentException if {@code type} is unsupported. + */ + public static Object readFromDataInputStream(Class type, DataInput dis) throws IOException { + if (type.equals(Boolean.class)) { + return readStrictBoolean(dis); + } + + if (isNumericBoxed(type)) { + return readNumeric(type, dis); + } + + if (type.equals(String.class)) { + return readStringWithBounds(dis); + } + + if (isSpecializedSupportType(type)) { + return readSpecializedSupportType(type, dis); + } + + if (isKeyType(type)) { + // Use Key.read(...) rather than type-specific methods because write(...) writes the TYPE + // field. + return Key.read(dis); + } + + if (type.equals(double[].class)) { + return readDoubleArray(dis); + } + + if (type.equals(float[].class)) { + return readFloatArray(dis); + } + + throw new IllegalArgumentException("Unrecognised field type: " + type); + } + + private static Object readStrictBoolean(DataInput dis) throws IOException { + final byte bool = dis.readByte(); + // Read a single byte, not {@code readBoolean()}, to reject non-0/1 values. + return switch (bool) { + case 1 -> Boolean.TRUE; + case 0 -> Boolean.FALSE; + default -> throw new IOException("Boolean is non boolean value: " + bool); + }; + } + + private static boolean isNumericBoxed(Class type) { + return type.equals(Byte.class) + || type.equals(Short.class) + || type.equals(Integer.class) + || type.equals(Long.class) + || type.equals(Double.class) + || type.equals(Float.class); + } + + private static Object readNumeric(Class type, DataInput dis) throws IOException { + if (type.equals(Byte.class)) { + return dis.readByte(); + } + if (type.equals(Short.class)) { + return dis.readShort(); + } + if (type.equals(Integer.class)) { + return dis.readInt(); + } + if (type.equals(Long.class)) { + return dis.readLong(); + } + if (type.equals(Double.class)) { + return dis.readDouble(); + } + // Float.class + return dis.readFloat(); + } + + private static String readStringWithBounds(DataInput dis) throws IOException { + final int length = dis.readInt(); + // Limit length to MAX_ARRAY_LENGTH to avoid unreasonable or malicious sizes. Total byte + // accounting is left to callers because structures around the string are fixed-size. + if (length < 0 || length > MAX_ARRAY_LENGTH) { + throw new IOException("Invalid string length: " + length); + } + StringBuilder sb = new StringBuilder(length); + for (int x = 0; x < length; x++) { + sb.append(dis.readChar()); + } + return sb.toString(); + } + + private static boolean isSpecializedSupportType(Class type) { + return type.equals(Buffer.class) + || type.equals(ShortBuffer.class) + || type.equals(Peer.class) + || type.equals(BitArray.class); + } + + private static Object readSpecializedSupportType(Class type, DataInput dis) + throws IOException { + if (type.equals(Buffer.class)) { + return new Buffer(dis); + } + if (type.equals(ShortBuffer.class)) { + return new ShortBuffer(dis); + } + if (type.equals(Peer.class)) { + return new Peer(dis); + } + // BitArray.class + return new BitArray(dis, MAX_BITARRAY_SIZE); + } + + private static boolean isKeyType(Class type) { + return type.equals(NodeCHK.class) || type.equals(NodeSSK.class) || type.equals(Key.class); + } + + private static double[] readDoubleArray(DataInput dis) throws IOException { + // Length is stored in one unsigned byte; mask to avoid sign extension. + double[] array = new double[dis.readByte() & 0xFF]; + for (int i = 0; i < array.length; i++) array[i] = dis.readDouble(); + return array; + } + + private static float[] readFloatArray(DataInput dis) throws IOException { + final short length = dis.readShort(); + // Bound by max allowed bytes; each float is 4 bytes. + if (length < 0 || length > MAX_ARRAY_LENGTH / 4) { + throw new IOException("Invalid flat array length: " + length); + } + float[] array = new float[length]; + for (int i = 0; i < array.length; i++) array[i] = dis.readFloat(); + return array; + } + + /** + * Writes a single supported value to a {@link DataOutputStream} using the format recognized by + * {@link #readFromDataInputStream(Class, DataInput)}. + * + *

The {@code object} must be non-{@code null} and of a supported type. For lists, see {@link + * #writeList(List, DataOutputStream)} for details. Strings are written as a 32-bit length + * followed by UTF-16 {@code char}s via {@link DataOutputStream#writeChar(int)}. + * + * @param object the value to serialize; must be non-{@code null}. + * @param dos the destination stream; not closed by this method. + * @throws IOException if an I/O error occurs during writing. + * @throws IllegalArgumentException if {@code object} has an unsupported type or violates a format + * constraint (for example, a {@code double[]} longer than 255 elements). + */ + public static void writeToDataOutputStream(Object object, DataOutputStream dos) + throws IOException { + Class type = object.getClass(); + + if (isScalarBoxed(type)) { + writeScalar(object, dos); + return; + } + + if (WritableToDataOutputStream.class.isAssignableFrom(type)) { + ((WritableToDataOutputStream) object).writeToDataOutputStream(dos); + return; + } + + if (type.equals(String.class)) { + writeString((String) object, dos); + return; + } + + if (object instanceof List list) { + writeList(list, dos); + return; + } + + if (type.equals(double[].class)) { + writeDoubleArray((double[]) object, dos); + return; + } + + if (type.equals(float[].class)) { + writeFloatArray((float[]) object, dos); + return; + } + + throw new IllegalArgumentException("Unrecognised field type: " + type); + } + + private static boolean isScalarBoxed(Class type) { + return type.equals(Long.class) + || type.equals(Boolean.class) + || type.equals(Integer.class) + || type.equals(Short.class) + || type.equals(Double.class) + || type.equals(Float.class) + || type.equals(Byte.class); + } + + private static void writeScalar(Object object, DataOutputStream dos) throws IOException { + Class type = object.getClass(); + if (type.equals(Long.class)) { + dos.writeLong((Long) object); + return; + } + if (type.equals(Boolean.class)) { + dos.writeBoolean((Boolean) object); + return; + } + if (type.equals(Integer.class)) { + dos.writeInt((Integer) object); + return; + } + if (type.equals(Short.class)) { + dos.writeShort((Short) object); + return; + } + if (type.equals(Double.class)) { + dos.writeDouble((Double) object); + return; + } + if (type.equals(Float.class)) { + dos.writeFloat((Float) object); + return; + } + // Byte.class + dos.write((Byte) object); + } + + private static void writeString(String s, DataOutputStream dos) throws IOException { + dos.writeInt(s.length()); + for (int x = 0; x < s.length(); x++) { + dos.writeChar(s.charAt(x)); + } + } + + /** + * Serializes a {@link List} using a snapshot-and-write strategy to avoid concurrent modification + * races. + * + *

First, the method takes a stable snapshot of the list under synchronization on the list + * instance itself; then it releases the lock and writes the snapshot to the stream. Synchronizing + * on the list coordinates with callers that already use {@code synchronized(list)} during + * mutation and avoids {@link java.util.ConcurrentModificationException} and length/content + * mismatches while iterating. + * + * @param list the list to serialize; elements must be of a type supported by {@link + * #writeToDataOutputStream(Object, DataOutputStream)}. + * @param dos the destination stream; not closed by this method. + * @throws IOException if an I/O error occurs while writing an element. + */ + @SuppressWarnings({"java:S2445", "SynchronizationOnLocalVariableOrMethodParameter"}) + private static void writeList(final List list, DataOutputStream dos) throws IOException { + // Intentionally lock on the list instance, so external synchronization on the same + // object also applies. Snapshot under lock; perform I/O after releasing it to + // minimize contention. + final Object[] snapshot; + synchronized (list) { + snapshot = list.toArray(); + } + dos.writeInt(snapshot.length); + for (Object o : snapshot) { + writeToDataOutputStream(o, dos); + } + } + + private static void writeDoubleArray(double[] array, DataOutputStream dos) throws IOException { + // {@code writeByte} keeps the lower 8 bits; cap length to 255 elements. + if (array.length > 255) { + throw new IllegalArgumentException( + "Cannot serialize an array of more than 255 doubles; attempted to " + + "serialize " + + array.length + + "."); + } + dos.writeByte(array.length); + for (double element : array) dos.writeDouble(element); + } + + private static void writeFloatArray(float[] array, DataOutputStream dos) throws IOException { + dos.writeShort(array.length); + for (float element : array) dos.writeFloat(element); + } + + /** + * Returns the serialized size, in bytes, for fixed-size simple values. + * + *

For {@link String}, the result is an upper bound computed as {@code 4 + 2 * maxStringLength} + * (a 32-bit length plus two bytes per character). For types implementing {@link + * WritableToDataOutputStream} and for {@link List}, the size is unknown and this method throws + * {@link IllegalArgumentException}. + * + * @param type the type to measure. + * @param maxStringLength maximum characters to assume for strings when computing an upper bound. + * @return the exact byte size for fixed-size types or an upper bound for strings. + * @throws IllegalArgumentException if the type is unsupported or variable-length. + */ + public static int length(Class type, int maxStringLength) { + if (type.equals(Long.class)) { + return 8; + } else if (type.equals(Boolean.class)) { + return 1; + } else if (type.equals(Integer.class)) { + return 4; + } else if (type.equals(Short.class)) { + return 2; + } else if (type.equals(Double.class)) { + return 8; + } else if (WritableToDataOutputStream.class.isAssignableFrom(type)) { + throw new IllegalArgumentException("Unknown length for " + type); + } else if (type.equals(String.class)) { + return 4 + maxStringLength * 2; // Written as chars + } else if (List.class.isAssignableFrom(type)) { + throw new IllegalArgumentException("Unknown length for List"); + } else if (type.equals(Byte.class)) { + return 1; + } else { + throw new IllegalArgumentException("Unrecognised field type: " + type); + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index c7dd834c58b..c9fcf4385ca 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,6 +28,7 @@ include( ":foundation-store", ":foundation-store-contracts", ":foundation-crypto-keys", + ":interop-wire", ":foundation-config", ":foundation-fs", ":foundation-compat", diff --git a/src/main/java/network/crypta/io/comm/PeerContext.java b/src/main/java/network/crypta/io/comm/PeerContext.java index e297abf539e..ec39f8df162 100644 --- a/src/main/java/network/crypta/io/comm/PeerContext.java +++ b/src/main/java/network/crypta/io/comm/PeerContext.java @@ -19,25 +19,14 @@ * * @author amphibian */ -public interface PeerContext { +public interface PeerContext extends MessageSource { // Largely opaque interface for now. - /** - * Returns the transport identity for this peer. - * - *

The value may be {@code null} if the peer identity is unknown, transient, or has been - * cleared (for example, after disconnect or GC of backing state). Callers should not retain or - * mutate the returned object. - * - * @return the peer descriptor, or {@code null} when unavailable. - */ - Peer getPeer(); - /** * Requests that the underlying connection be closed. * - *

Pending operations may fail or be cancelled; subsequent calls to {@link #isConnected()} - * generally return {@code false}. + *

Pending operations may fail or be canceled; later calls to {@link #isConnected()} generally + * return {@code false}. */ void forceDisconnect(); @@ -82,9 +71,9 @@ public interface PeerContext { * cancel the sending. If provided, {@link AsyncMessageCallback} is invoked as delivery * progresses. Implementations should account bytes via {@link ByteCounter} where appropriate. * - * @param msg the message to send; must be constructed for this send path (not reused from a + * @param msg the message to send; must be constructed for this sending path (not reused from a * different source). - * @param cb optional callback invoked on send events; may be {@code null}. + * @param cb optional callback invoked on sending events; may be {@code null}. * @param ctr byte counter to update for statistics and throttling; may be {@code null} if the * caller does not wish to record counts. * @return a handle representing the queued message. @@ -95,14 +84,6 @@ default MessageItem sendAsync(Message msg, AsyncMessageCallback cb, ByteCounter return transport().sendAsync(msg, cb, ctr); } - /** - * Returns the current boot identifier for the remote peer. - * - *

The value changes on each restart of the remote node and can be used to detect restarts - * while in-flight operations are pending. - */ - long getBootID(); - /** * Returns the packet-level throttle for the peer's current address. * @@ -116,6 +97,7 @@ default PacketThrottle getThrottle() { } /** Returns the transport handler that receives packets from this peer. */ + @SuppressWarnings("unused") default SocketHandler getSocketHandler() { return transport().getSocketHandler(); } @@ -129,6 +111,7 @@ default SocketHandler getSocketHandler() { *

Implementations are encouraged to reuse a single weak reference per instance to avoid * allocation overhead. */ + @Override WeakReference getWeakRef(); /** Returns a compact, log-friendly description of this peer. */ @@ -151,7 +134,7 @@ default SocketHandler getSocketHandler() { boolean unqueueMessage(MessageItem item); /** - * Reports the time spent waiting for a send slot due to throttling. + * Reports the time spent waiting for a sending slot due to throttling. * * @param time elapsed time in milliseconds. * @param realTime {@code true} for the real-time path; {@code false} for bulk. diff --git a/src/main/java/network/crypta/node/NodeDataRequestHandler.java b/src/main/java/network/crypta/node/NodeDataRequestHandler.java index 61a2e859486..b0865eb8c84 100644 --- a/src/main/java/network/crypta/node/NodeDataRequestHandler.java +++ b/src/main/java/network/crypta/node/NodeDataRequestHandler.java @@ -6,6 +6,7 @@ import network.crypta.io.comm.Message; import network.crypta.io.comm.MessageType; import network.crypta.io.comm.NotConnectedException; +import network.crypta.io.comm.PeerContext; import network.crypta.keys.Key; import network.crypta.keys.KeyBlock; import network.crypta.keys.NodeSSK; @@ -326,8 +327,9 @@ private void handleDataRequest(Message m, boolean isSSK) { private void rejectRequest(Message m, ByteCounter ctr) { long uid = m.getLong(DMT.UID); Message msg = DMT.createFNPRejectedOverload(uid, true); + PeerContext source = (PeerContext) m.getSource(); try { - m.getSource().transport().sendAsync(msg, null, ctr); + source.transport().sendAsync(msg, null, ctr); } catch (NotConnectedException _) { // Ignore } diff --git a/src/main/java/network/crypta/node/NodeNotRoutableHandler.java b/src/main/java/network/crypta/node/NodeNotRoutableHandler.java index ec0fdab6fd5..1340a964229 100644 --- a/src/main/java/network/crypta/node/NodeNotRoutableHandler.java +++ b/src/main/java/network/crypta/node/NodeNotRoutableHandler.java @@ -6,6 +6,7 @@ import network.crypta.io.comm.Message; import network.crypta.io.comm.MessageType; import network.crypta.io.comm.NotConnectedException; +import network.crypta.io.comm.PeerContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,8 +20,8 @@ * rejected quickly and consistently. * *

The handler is intentionally stateless aside from a reference to the owning {@link Node}. It - * does not maintain per-peer state, cache results, or schedule asynchronous work beyond the - * transport send itself. Callers should treat it as a pure decision step: if it returns {@code + * does not maintain a per-peer state, cache results, or schedule asynchronous work beyond the + * transport sending itself. Callers should treat it as a pure decision step: if it returns {@code * true}, a response has been queued; if it returns {@code false}, the caller should continue with * normal processing. No internal synchronization is performed, so callers are expected to invoke it * on threads that already coordinate node message handling. @@ -38,7 +39,7 @@ final class NodeNotRoutableHandler { /** Logger used for trace-level diagnostics during early rejection decisions. */ private static final Logger LOG = LoggerFactory.getLogger(NodeNotRoutableHandler.class); - /** Owning node instance used to access routing state and network statistics. */ + /** The owning node instance used to access routing state and network statistics. */ private final Node node; /** @@ -59,8 +60,8 @@ final class NodeNotRoutableHandler { * *

This method inspects the message type and, for known request types that cannot be routed * from the source, sends a rejection response that mirrors the request UID. The response is - * queued through the transport layer and accounted against the appropriate byte counter. When the - * message type is not handled by this class, the method returns {@code false} and performs no + * queued through the transport layer and accounted against the appropriate byte counter. When + * this class does not handle the message type, the method returns {@code false} and performs no * side effects. * *

The method is idempotent with respect to its return value but not with respect to network @@ -96,8 +97,8 @@ boolean handle(Message m) { * Sends a rejection response for the given message using the supplied counter. * *

This method extracts the request UID, builds a rejection response, and queues it on the - * transport. If the peer disconnects before the send is queued, the exception is ignored so that - * upstream dispatch logic can continue without additional error handling. + * transport. If the peer disconnects before the sending is queued, the exception is ignored so + * that upstream dispatch logic can continue without additional error handling. * * @param m message whose UID is mirrored in the rejection response; must be non-null. * @param ctr byte counter used for accounting the reject response; must be non-null. @@ -105,8 +106,9 @@ boolean handle(Message m) { private void rejectRequest(Message m, ByteCounter ctr) { long uid = m.getLong(DMT.UID); Message msg = DMT.createFNPRejectedOverload(uid, true); + PeerContext source = (PeerContext) m.getSource(); try { - m.getSource().transport().sendAsync(msg, null, ctr); + source.transport().sendAsync(msg, null, ctr); } catch (NotConnectedException _) { // Ignore } From a5f0251a06bdfa1493eec6e53a414e9fd586eec5 Mon Sep 17 00:00:00 2001 From: Leumor <116955025+leumor@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:31:05 +0000 Subject: [PATCH 2/3] docs(modules): Refresh extracted leaf guidance Update README and the relevant repository skills to describe the current multi-project layout after the support, store, crypto, and wire extractions. This refresh covers module ownership, root-vs-leaf boundaries, compile guidance, packaging expectations, and the moved AEAD source paths so future agent runs and contributor docs point at the right locations. --- .agents/skills/cryptad-architecture/SKILL.md | 73 ++++++++++++++---- .agents/skills/cryptad-build-test/SKILL.md | 28 ++++++- .agents/skills/cryptad-crypto-aead/SKILL.md | 11 ++- .agents/skills/cryptad-packaging/SKILL.md | 7 +- README.md | 79 ++++++++++++++------ 5 files changed, 156 insertions(+), 42 deletions(-) diff --git a/.agents/skills/cryptad-architecture/SKILL.md b/.agents/skills/cryptad-architecture/SKILL.md index eaffa1c2626..220310934d7 100644 --- a/.agents/skills/cryptad-architecture/SKILL.md +++ b/.agents/skills/cryptad-architecture/SKILL.md @@ -13,7 +13,7 @@ Use this skill when you need to: - Understand request routing, updates, plugins, or storage. - Make changes that could affect wire compatibility or on-disk formats. -## Build/module layout (PR-1) +## Build/module layout (current) - Cryptad now uses a partial multi-project Gradle build. - The root project `:cryptad` remains the daemon/application project. - The root project still owns `buildJar`, `run`, `runLauncher`, distribution/jpackage tasks, the @@ -21,10 +21,22 @@ Use this skill when you need to: - Leaf subprojects: - `:foundation-support` → the current stable generic subset of `network.crypta.support`, `network.crypta.support.api`, `network.crypta.support.io`, - `network.crypta.support.compress`, `network.crypta.support.math`, plus - `network.crypta.node.FSParseException` + `network.crypta.support.compress`, `network.crypta.support.math`, + `network.crypta.support.transport.ip`, plus `network.crypta.io.AddressIdentifier`, + `network.crypta.io.WritableToDataOutputStream`, `network.crypta.node.FSParseException`, + `network.crypta.node.FastRunnable`, and `network.crypta.node.SemiOrderedShutdownHook` - `:foundation-store-contracts` → neutral `network.crypta.store` contracts - `BlockMetadata`, `GetPubkey`, `StorableBlock` + `BlockMetadata`, `GetPubkey`, `StorableBlock`, plus the `network.crypta.store.alerts` seam + (`StoreAlertSink`, `StoreMaintenanceAlertKind`, `StoreMaintenanceAlertSource`) + - `:foundation-crypto-keys` → `network.crypta.crypt`, `network.crypta.keys`, plus + `network.crypta.support.io.BucketTools` and + `network.crypta.support.io.PrependLengthOutputStream` + - `:foundation-store` → reusable `network.crypta.store` implementations plus + `network.crypta.store.caching` and `network.crypta.store.saltedhash` + - `:interop-wire` → the leaf-safe wire/message/schema/version/probe nucleus: + selected `network.crypta.io.comm` types, `network.crypta.node.Version`, + `network.crypta.node.probe.Error`, `network.crypta.node.probe.Type`, and + `network.crypta.support.Serializer` - `:foundation-config` → `network.crypta.config`, `network.crypta.l10n`, and the main l10n resources; public config APIs re-export `:foundation-support` and `:foundation-fs` where they expose shared types @@ -47,10 +59,16 @@ Use this skill when you need to: `LegacyFirstTimeWizardPort`, `LegacyToadletSymlinkPort`, `LegacyWelcomePagePort`, and `LegacyWelcomeActionPort`. - The large cyclic daemon core still lives in the root project: - `network.crypta.node` (except the transitional `FSParseException` move), - `network.crypta.io`, `network.crypta.client`, `network.crypta.clients`, - the remaining daemon-coupled support code, `network.crypta.crypt`, `network.crypta.keys`, - most store implementations under `network.crypta.store`, and `network.crypta.tools`. + most of `network.crypta.node`, the daemon-coupled transport/socket/filter side of + `network.crypta.io.comm`, `network.crypta.client`, `network.crypta.clients`, + `network.crypta.node.runtime`, the remaining daemon-coupled support code, and + `network.crypta.tools`. +- The wire split is intentionally narrow: + `:interop-wire` owns the message/schema nucleus, while root keeps `MessageCore`, + `MessageFilter`, `AsyncMessageFilterCallback`, `SlowAsyncMessageFilterCallback`, + `PeerContext`, incoming-packet filters, socket handlers, and statistics collection. + `network.crypta.io.comm.Message` now depends on the minimal `MessageSource` seam instead of + directly on `PeerContext`. - `:foundation-config` is the current home for all main `network.crypta.config` and `network.crypta.l10n` sources. Their unit tests still live in the root test tree and are run by the root project. @@ -74,19 +92,35 @@ Use this skill when you need to: - Storage abstractions: `FreenetStore` - CHK/SSK stores: `CHKStore`, `SSKStore` - Caching: `SlashdotStore` -- Neutral contracts `BlockMetadata`, `GetPubkey`, and `StorableBlock` now live in - `:foundation-store-contracts`; store implementations still remain in the root project. +- `:foundation-store` now owns the reusable store implementations, cache layer, and salted-hash + store code. +- Neutral contracts `BlockMetadata`, `GetPubkey`, and `StorableBlock` live in + `:foundation-store-contracts`, along with the store-maintenance alert seam in + `network.crypta.store.alerts`. +- Root-owned runtime/UI integration remains in `network.crypta.node.runtime`, for example + `UserAlertManagerStoreAlertSink`. ### Cryptography (`network.crypta.crypt`) - Encryption: block cipher / AES streams - Signatures: DSA/ECDSA - Hashing: SHA-256 and others - RNG: `RandomSource` / Yarrow +- This package now lives in `:foundation-crypto-keys`. ### Key management (`network.crypta.keys`) - Client keys: `ClientCHK`, `ClientSSK` - URIs: `FreenetURI` - Updatable keys: USK +- This package now lives in `:foundation-crypto-keys`. + +### Wire/message nucleus (`network.crypta.io.comm`, `network.crypta.node.Version`) +- `:interop-wire` owns the leaf-safe message/schema/address subset such as `Message`, + `MessageType`, `Peer`, `FreenetInetAddress`, and related exceptions. +- `:interop-wire` also owns `network.crypta.node.Version`, + `network.crypta.node.VersionParseException`, `network.crypta.node.probe.Error`, + `network.crypta.node.probe.Type`, and `network.crypta.support.Serializer`. +- Root keeps transport-facing code such as `PeerContext`, `MessageCore`, filters, packet/socket + handlers, and runtime helpers like `network.crypta.node.runtime.SSL`. ### Client APIs - High-level client: `network.crypta.client` @@ -194,7 +228,11 @@ Use this skill when you need to: - Logging, data structures, threading, helpers - `:foundation-support` now owns the stable generic support subset across `network.crypta.support`, `network.crypta.support.api`, `network.crypta.support.io`, - `network.crypta.support.compress`, and `network.crypta.support.math`. + `network.crypta.support.compress`, `network.crypta.support.math`, and + `network.crypta.support.transport.ip`. +- `:foundation-support` also owns `network.crypta.io.AddressIdentifier`, + `network.crypta.io.WritableToDataOutputStream`, `network.crypta.node.FastRunnable`, + `network.crypta.node.SemiOrderedShutdownHook`, and `network.crypta.support.SerializationLimits`. - The root project still owns daemon-coupled support code and higher-level wiring that is not yet stable enough to extract cleanly. @@ -205,8 +243,14 @@ Use this skill when you need to: ### Foundation leaf modules - `:foundation-support`: stable generic `network.crypta.support*` subset plus - `network.crypta.node.FSParseException` -- `:foundation-store-contracts`: neutral `network.crypta.store` contracts + `network.crypta.io.AddressIdentifier`, `network.crypta.io.WritableToDataOutputStream`, + `network.crypta.node.FSParseException`, `network.crypta.node.FastRunnable`, and + `network.crypta.node.SemiOrderedShutdownHook` +- `:foundation-store-contracts`: neutral `network.crypta.store` contracts plus + `network.crypta.store.alerts` +- `:foundation-crypto-keys`: `network.crypta.crypt`, `network.crypta.keys` +- `:foundation-store`: reusable `network.crypta.store` implementations +- `:interop-wire`: wire/message/schema/version/probe nucleus - `:foundation-config`: `network.crypta.config`, `network.crypta.l10n` - `:foundation-fs`: `network.crypta.fs` - `:foundation-compat`: `network.crypta.compat` @@ -250,7 +294,8 @@ Use this skill when you need to: ## Versioning system - A single integer build number is set in `build.gradle.kts` (`version = ""`). -- Version tokens are replaced into `network/crypta/node/Version.java` during build (`@build_number@`, `@git_rev@`). +- Version tokens are replaced into the `:interop-wire` `network/crypta/node/Version.java` + template during build (`@build_number@`, `@git_rev@`). - Version strings support both Cryptad and Fred formats; compatibility enforces protocol match and minimum builds. - Freenet interop uses historical identifiers (e.g., `"Fred,0.7"`) for wire compatibility where applicable. - Core update descriptors (`core-info.json`) must publish `version` as an integer string; this value diff --git a/.agents/skills/cryptad-build-test/SKILL.md b/.agents/skills/cryptad-build-test/SKILL.md index b7bf06739b5..5e10bc41463 100644 --- a/.agents/skills/cryptad-build-test/SKILL.md +++ b/.agents/skills/cryptad-build-test/SKILL.md @@ -18,15 +18,27 @@ Use this skill when you need to: ## Build layout - Cryptad now uses a partial multi-project Gradle build. - Use root-project tasks by default; the root project remains the daemon/application target. -- Current leaf projects are `:foundation-support`, `:foundation-store-contracts`, +- Current leaf projects are `:foundation-support`, `:foundation-store`, + `:foundation-store-contracts`, `:foundation-crypto-keys`, `:interop-wire`, `:foundation-config`, `:foundation-fs`, `:foundation-compat`, `:runtime-spi`, `:thirdparty-onion`, `:thirdparty-legacy`, and `:launcher-desktop`. - The extracted leaf projects compile separately, but `buildJar`, `run`, `runLauncher`, `assembleCryptadDist`, and jpackage tasks are still rooted at `:cryptad`. - `:foundation-support` owns the current stable generic support subset under - `network.crypta.support*` plus `network.crypta.node.FSParseException`. + `network.crypta.support*`, `network.crypta.support.transport.ip`, + `network.crypta.io.AddressIdentifier`, `network.crypta.io.WritableToDataOutputStream`, + `network.crypta.node.FSParseException`, `network.crypta.node.FastRunnable`, and + `network.crypta.node.SemiOrderedShutdownHook`. - `:foundation-store-contracts` owns the neutral `network.crypta.store` contracts - `BlockMetadata`, `GetPubkey`, and `StorableBlock`. + `BlockMetadata`, `GetPubkey`, and `StorableBlock`, plus the `network.crypta.store.alerts` + seam. +- `:foundation-crypto-keys` owns `network.crypta.crypt`, `network.crypta.keys`, and the adjacent + `BucketTools` / `PrependLengthOutputStream` helpers. +- `:foundation-store` owns reusable `network.crypta.store` implementations plus + `network.crypta.store.caching` and `network.crypta.store.saltedhash`. +- `:interop-wire` owns the narrow wire/message/schema/version/probe nucleus: + leaf-safe `network.crypta.io.comm` message/schema classes, `network.crypta.node.Version`, + `network.crypta.node.probe.Error` and `Type`, and `network.crypta.support.Serializer`. - `:foundation-config` owns the main `network.crypta.config` and `network.crypta.l10n` sources. Its public APIs now re-export `:foundation-support` and `:foundation-fs` where needed. - Every extracted internal leaf must keep leaf-owned aggregated-output metadata in sync at @@ -72,9 +84,17 @@ When running ./gradlew test via OpenCode bash, set timeout ≥ 15 minutes (≥ 9 - `./gradlew compileJava` - Compile the support leaf when you touched extracted generic support classes: - `./gradlew :foundation-support:classes` +- Compile the crypto/keys leaf when you touched `network.crypta.crypt`, `network.crypta.keys`, + or the moved bucket/length helpers: + - `./gradlew :foundation-crypto-keys:classes` +- Compile the reusable store leaf when you touched extracted `network.crypta.store`, + `network.crypta.store.caching`, or `network.crypta.store.saltedhash` code: + - `./gradlew :foundation-store:compileJava` - Compile the neutral store-contracts leaf when you touched `BlockMetadata`, `GetPubkey`, or - `StorableBlock`: + `StorableBlock`, or the store-maintenance alert seam: - `./gradlew :foundation-store-contracts:compileJava` +- Compile the wire/version leaf when you touched moved message/schema/address/version/probe code: + - `./gradlew :interop-wire:compileJava` - Compile the config/l10n leaf when you touched extracted config or l10n sources: - `./gradlew :foundation-config:classes` - Compile only the runtime SPI leaf when you touched just that JDK-only API surface: diff --git a/.agents/skills/cryptad-crypto-aead/SKILL.md b/.agents/skills/cryptad-crypto-aead/SKILL.md index 402227cd82c..86554095dde 100644 --- a/.agents/skills/cryptad-crypto-aead/SKILL.md +++ b/.agents/skills/cryptad-crypto-aead/SKILL.md @@ -13,6 +13,13 @@ Use this skill when you touch: - On-disk encryption formats for persistent files or plugin stores - Any migration/compatibility path involving OCB or AES-GCM +## Source ownership +- `network.crypta.crypt` and `network.crypta.keys` now live in the `:foundation-crypto-keys` + subproject. +- Canonical source root: `foundation-crypto-keys/src/main/java/` +- The root project and other leaf projects depend on `:foundation-crypto-keys`; do not recreate + duplicate crypto helpers in root-owned code when a reusable leaf-owned type already exists. + ## Current state (breaking change) - AEAD has migrated from OCB to AES-GCM (breaking). - On-disk prefix remains 16 bytes: @@ -26,8 +33,8 @@ Use this skill when you touch: - Plugin stores (`*.data.crypt`) cannot be read; plugins start with empty/default store data. ### Primary files -- `src/main/java/network/crypta/crypt/AEADInputStream.java` -- `src/main/java/network/crypta/crypt/AEADOutputStream.java` +- `foundation-crypto-keys/src/main/java/network/crypta/crypt/AEADInputStream.java` +- `foundation-crypto-keys/src/main/java/network/crypta/crypt/AEADOutputStream.java` ## Legacy note: OCB nonce compatibility (do not regress) Historically: diff --git a/.agents/skills/cryptad-packaging/SKILL.md b/.agents/skills/cryptad-packaging/SKILL.md index a65bacba908..14f427c8791 100644 --- a/.agents/skills/cryptad-packaging/SKILL.md +++ b/.agents/skills/cryptad-packaging/SKILL.md @@ -20,12 +20,17 @@ Use this skill when working on: - Packaging remains root-owned. - The root project `:cryptad` still owns `buildJar`, `assembleCryptadDist`, `dist*`, `run`, `runLauncher`, and jpackage tasks. -- Current contributing leaf modules are `:foundation-support`, `:foundation-store-contracts`, +- Current contributing leaf modules are `:foundation-support`, `:foundation-store`, + `:foundation-store-contracts`, `:foundation-crypto-keys`, `:interop-wire`, `:foundation-config`, `:foundation-fs`, `:foundation-compat`, `:runtime-spi`, `:thirdparty-onion`, `:thirdparty-legacy`, and `:launcher-desktop`. - Extracted leaf modules contribute jars and resources through the root runtime classpath. - `:foundation-support` and `:foundation-store-contracts` contribute shared runtime classes via their leaf JARs like the other extracted modules. +- `:foundation-crypto-keys` and `:foundation-store` contribute the extracted crypto/key/store + runtime classes through their leaf JARs. +- `:interop-wire` contributes the extracted message/schema/version/probe nucleus and serializer + classes through its leaf JAR. - `:foundation-config` contributes the config/l10n code and main l10n resources via its leaf JAR and re-exports `:foundation-support` and `:foundation-fs` where public APIs expose those types. - The `:runtime-spi` JAR is packaged like the other leaf artifacts; packaging still produces one diff --git a/README.md b/README.md index 2c3b4f3ab82..da71b979918 100644 --- a/README.md +++ b/README.md @@ -169,10 +169,21 @@ Cryptad now uses a partial multi-project Gradle build. `run`, `runLauncher`, `assembleCryptadDist`, and jpackage task graph. - `:foundation-support` owns the current stable generic support subset under `network.crypta.support`, `network.crypta.support.api`, `network.crypta.support.io`, - `network.crypta.support.compress`, `network.crypta.support.math`, plus - `network.crypta.node.FSParseException`. + `network.crypta.support.compress`, `network.crypta.support.math`, + `network.crypta.support.transport.ip`, plus `network.crypta.io.AddressIdentifier`, + `network.crypta.io.WritableToDataOutputStream`, `network.crypta.node.FSParseException`, + `network.crypta.node.FastRunnable`, and `network.crypta.node.SemiOrderedShutdownHook`. - `:foundation-store-contracts` owns the neutral `network.crypta.store` contracts - `BlockMetadata`, `GetPubkey`, and `StorableBlock`. + `BlockMetadata`, `GetPubkey`, and `StorableBlock`, plus the store-maintenance alert seam under + `network.crypta.store.alerts`. +- `:foundation-crypto-keys` owns `network.crypta.crypt`, `network.crypta.keys`, and the + crypto-adjacent `network.crypta.support.io.BucketTools` and + `network.crypta.support.io.PrependLengthOutputStream` helpers. +- `:foundation-store` owns the reusable `network.crypta.store` implementations plus + `network.crypta.store.caching` and `network.crypta.store.saltedhash`. +- `:interop-wire` owns the narrow wire/message/schema/version/probe nucleus: leaf-safe + `network.crypta.io.comm` message/schema classes, `network.crypta.node.Version`, + `network.crypta.node.probe.Error` and `Type`, and `network.crypta.support.Serializer`. - `:foundation-config` owns `network.crypta.config`, `network.crypta.l10n`, and the main `network/crypta/l10n/crypta.l10n.en.properties` resource. Its public APIs now export `:foundation-support` and `:foundation-fs` where config types expose `SimpleFieldSet` or @@ -188,11 +199,13 @@ Cryptad now uses a partial multi-project Gradle build. - `:launcher-desktop` owns `network.crypta.launcher`, `com.jthemedetecor`, `oshi`, and launcher resources. - The large cyclic daemon core remains in the root project for now, and all tests still live - there. Most store implementations and daemon-coupled support code remain root-owned. + there. The root still owns daemon-coupled transport/socket code in `network.crypta.io.comm`, + runtime adapters and helpers under `network.crypta.node.runtime`, and the remaining + daemon-coupled support/UI wiring. - Higher-level infrastructure now crosses a narrower boundary through - `network.crypta.runtime.spi.RuntimePorts`, implemented in the root project by - `network.crypta.node.runtime.LegacyRuntimePorts`, plus HTTP-local wiring records and - package-local seams such as + `network.crypta.runtime.spi.RuntimePorts`, the minimal wire-side `MessageSource` seam used by + leaf-owned messages, and package-local seams such as + `network.crypta.node.runtime.LegacyRuntimePorts`, `network.crypta.clients.http.BookmarkEditorToadletRuntimePorts`, `network.crypta.clients.http.ConfigToadletRuntimePorts`, `network.crypta.clients.http.ConnectionsToadletRuntimePorts`, @@ -468,10 +481,18 @@ cd build/jpackage/Crypta.app/Contents artifacts; use the commands in “Spotless + Dependency Verification” below. Root build also includes: -- `:foundation-support`: extracted stable support/api/io/compress/math subset plus - `network.crypta.node.FSParseException`. -- `:foundation-store-contracts`: neutral store contracts used by `crypt`, `keys`, and `store` - code during extraction work. +- `:foundation-support`: extracted stable support/api/io/compress/math/transport subset plus + `network.crypta.io.AddressIdentifier`, `network.crypta.io.WritableToDataOutputStream`, + `network.crypta.node.FSParseException`, `network.crypta.node.FastRunnable`, and + `network.crypta.node.SemiOrderedShutdownHook`. +- `:foundation-store-contracts`: neutral store contracts plus the store-maintenance alert seam + shared by store code and root runtime/UI adapters. +- `:foundation-crypto-keys`: extracted `network.crypta.crypt`, `network.crypta.keys`, and the + adjacent `BucketTools` / `PrependLengthOutputStream` helpers. +- `:foundation-store`: extracted reusable `network.crypta.store` implementations, caching, and + salted-hash storage code. +- `:interop-wire`: extracted wire/message/schema/address/version/probe nucleus plus + `network.crypta.support.Serializer`. - `:foundation-config`: extracted config/l10n code and main l10n resources. Its public APIs re-export `:foundation-support` and `:foundation-fs` where required. - `:launcher-desktop`: Swing launcher code and desktop/theme detection dependencies. @@ -526,15 +547,26 @@ Tip: Keep the Spotless formatter at the intended version (currently `googleJavaF - Root project `:cryptad` remains the daemon/application build and still owns the strongly coupled core packages, all tests, packaging/runtime tasks, and the current `LegacyRuntimePorts` bridge into the runtime SPI. - - Leaf subprojects are `:foundation-support`, `:foundation-store-contracts`, + - Leaf subprojects are `:foundation-support`, `:foundation-store`, + `:foundation-store-contracts`, `:foundation-crypto-keys`, `:interop-wire`, `:foundation-config`, `:foundation-fs`, `:foundation-compat`, `:runtime-spi`, `:thirdparty-onion`, `:thirdparty-legacy`, and `:launcher-desktop`. - Core network (`network.crypta.node`): `Node`, `PeerNode`, `PeerManager`, `PacketSender`, `RequestStarter`, `RequestScheduler`, `NodeUpdateManager`. - Storage (`network.crypta.store`): `FreenetStore`, `CHKStore`, `SSKStore`, `SlashdotStore`. - Neutral contracts such as `BlockMetadata`, `GetPubkey`, and `StorableBlock` now live in - `:foundation-store-contracts`; store implementations still remain in the root project. -- Crypto (`network.crypta.crypt`): AES, DSA/ECDSA, SHA‑256, `RandomSource`/Yarrow. -- Keys (`network.crypta.keys`): `ClientCHK`, `ClientSSK`, `FreenetURI`, USK. + `:foundation-store` now owns the reusable store implementations, cache layer, and salted-hash + storage code. `:foundation-store-contracts` owns the neutral contracts plus the + `network.crypta.store.alerts` seam used by root runtime/UI adapters such as + `UserAlertManagerStoreAlertSink`. +- Crypto (`network.crypta.crypt`): AES, DSA/ECDSA, SHA‑256, `RandomSource`/Yarrow. This package + now lives in `:foundation-crypto-keys`. +- Keys (`network.crypta.keys`): `ClientCHK`, `ClientSSK`, `FreenetURI`, USK. This package now + lives in `:foundation-crypto-keys`. +- Wire/message nucleus (`network.crypta.io.comm`, `network.crypta.node.Version`, + `network.crypta.node.probe`, `network.crypta.support.Serializer`): `:interop-wire` owns the + leaf-safe message/schema/address/version/probe subset, including `Message`, `MessageType`, + `Peer`, `FreenetInetAddress`, `Version`, and the probe enums. The root project still owns the + transport/socket/filter side of `network.crypta.io.comm`, and `Message` now depends on the + minimal `MessageSource` seam rather than directly on `PeerContext`. - Clients: `network.crypta.client`, FCP (`network.crypta.clients.fcp`), HTTP (`network.crypta.clients.http`). FCP now consumes execution, randomness, transfer policy, lifecycle, config access, and detached peer mutations through `RuntimePorts` and FCP-local @@ -567,17 +599,22 @@ Tip: Keep the Spotless formatter at the intended version (currently `googleJavaF filesystem-facing types. Higher layers should still prefer `RuntimePorts#config()` and the root `LegacyConfigPort` bridge instead of reaching through daemon internals. - Support foundation leaf (`:foundation-support`): stable generic support, support-api, - support-io, support-compress, and support-math classes plus - `network.crypta.node.FSParseException`. + support-io, support-compress, support-math, and transport-IP classes plus + `network.crypta.io.AddressIdentifier`, `network.crypta.io.WritableToDataOutputStream`, + `network.crypta.node.FSParseException`, `network.crypta.node.FastRunnable`, and + `network.crypta.node.SemiOrderedShutdownHook`. - Support (`network.crypta.support`): logging, data structures, threading, and helpers are now split between `:foundation-support` and the root project. Keep generic reusable utilities in the foundation leaf; daemon-coupled support code still remains in root. - Launcher/Desktop: `:launcher-desktop` provides `network.crypta.launcher`, `com.jthemedetecor`, launcher resources, and desktop-theme integration. - Extracted foundations: `:foundation-support` provides the stable generic support subset, - `:foundation-store-contracts` provides neutral `network.crypta.store` contracts, - `:foundation-config` provides config/l10n, `:foundation-fs` provides `network.crypta.fs`, and - `:foundation-compat` provides `network.crypta.compat`. + `:foundation-store-contracts` provides neutral store contracts and alert seams, + `:foundation-crypto-keys` provides `network.crypta.crypt` and `network.crypta.keys`, + `:foundation-store` provides reusable store implementations, `:interop-wire` provides the + wire/version/probe nucleus, `:foundation-config` provides config/l10n, + `:foundation-fs` provides `network.crypta.fs`, and `:foundation-compat` provides + `network.crypta.compat`. - Runtime boundary leaf: `:runtime-spi` provides `network.crypta.runtime.spi`. - Vendored libraries: `:thirdparty-onion` provides `com.onionnetworks`, `:thirdparty-legacy` provides `org.bitpedia`, `org.sevenzip`, and `org.spaceroots`. From aa2100bfb5db422b6862df9b62bd7acd62a80f71 Mon Sep 17 00:00:00 2001 From: Leumor <116955025+leumor@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:31:45 +0000 Subject: [PATCH 3/3] refactor(interop-wire): Remove relocated root sources Delete the root copies of the message/schema/version/probe files that now belong to :interop-wire. This completes the extraction by leaving the root project with only the transport-facing comm classes and the small handler adaptations that still depend on PeerContext transport methods. --- .../crypta/io/comm/AsyncMessageCallback.java | 38 - .../network/crypta/io/comm/ByteCounter.java | 47 - src/main/java/network/crypta/io/comm/DMT.java | 2924 ----------------- .../crypta/io/comm/DisconnectedException.java | 26 - .../network/crypta/io/comm/Dispatcher.java | 25 - .../comm/DuplicateMessageTypeException.java | 22 - .../crypta/io/comm/FreenetInetAddress.java | 559 ---- .../io/comm/IncorrectTypeException.java | 20 - .../java/network/crypta/io/comm/Message.java | 762 ----- .../network/crypta/io/comm/MessageType.java | 297 -- .../crypta/io/comm/NotConnectedException.java | 50 - .../io/comm/OpennetAnnounceRequest.java | 14 - .../java/network/crypta/io/comm/Peer.java | 415 --- .../crypta/io/comm/PeerParseException.java | 39 - .../io/comm/PeerRestartedException.java | 25 - ...ferenceSignatureVerificationException.java | 39 - .../crypta/io/comm/RetrievalException.java | 151 - .../network/crypta/io/comm/TrafficClass.java | 72 - .../java/network/crypta/node/Version.java | 506 --- .../crypta/node/VersionParseException.java | 27 - .../java/network/crypta/node/probe/Error.java | 151 - .../java/network/crypta/node/probe/Type.java | 112 - .../network/crypta/support/Serializer.java | 412 --- 23 files changed, 6733 deletions(-) delete mode 100644 src/main/java/network/crypta/io/comm/AsyncMessageCallback.java delete mode 100644 src/main/java/network/crypta/io/comm/ByteCounter.java delete mode 100644 src/main/java/network/crypta/io/comm/DMT.java delete mode 100644 src/main/java/network/crypta/io/comm/DisconnectedException.java delete mode 100644 src/main/java/network/crypta/io/comm/Dispatcher.java delete mode 100644 src/main/java/network/crypta/io/comm/DuplicateMessageTypeException.java delete mode 100644 src/main/java/network/crypta/io/comm/FreenetInetAddress.java delete mode 100644 src/main/java/network/crypta/io/comm/IncorrectTypeException.java delete mode 100644 src/main/java/network/crypta/io/comm/Message.java delete mode 100644 src/main/java/network/crypta/io/comm/MessageType.java delete mode 100644 src/main/java/network/crypta/io/comm/NotConnectedException.java delete mode 100644 src/main/java/network/crypta/io/comm/OpennetAnnounceRequest.java delete mode 100644 src/main/java/network/crypta/io/comm/Peer.java delete mode 100644 src/main/java/network/crypta/io/comm/PeerParseException.java delete mode 100644 src/main/java/network/crypta/io/comm/PeerRestartedException.java delete mode 100644 src/main/java/network/crypta/io/comm/ReferenceSignatureVerificationException.java delete mode 100644 src/main/java/network/crypta/io/comm/RetrievalException.java delete mode 100644 src/main/java/network/crypta/io/comm/TrafficClass.java delete mode 100644 src/main/java/network/crypta/node/Version.java delete mode 100644 src/main/java/network/crypta/node/VersionParseException.java delete mode 100644 src/main/java/network/crypta/node/probe/Error.java delete mode 100644 src/main/java/network/crypta/node/probe/Type.java delete mode 100644 src/main/java/network/crypta/support/Serializer.java diff --git a/src/main/java/network/crypta/io/comm/AsyncMessageCallback.java b/src/main/java/network/crypta/io/comm/AsyncMessageCallback.java deleted file mode 100644 index 2a1388cd4d8..00000000000 --- a/src/main/java/network/crypta/io/comm/AsyncMessageCallback.java +++ /dev/null @@ -1,38 +0,0 @@ -package network.crypta.io.comm; - -/** - * Receives progress and completion notifications for an asynchronously sent packet/message. - * - *

The sender uses this callback to report when a packet has been handed off to the transport and - * when the transmission completes or fails. On a reliable (non-lossy) transport, the peer's - * acknowledgment may follow immediately after {@link #sent()}; on a lossy transport, only {@link - * #acknowledged()} confirms reception by the remote node. - * - *

Implementations should return quickly and avoid long blocking operations; callback methods may - * be invoked from internal networking or scheduler threads. - */ -public interface AsyncMessageCallback { - - /** - * Invoked when the packet actually leaves the local node (i.e., handed to the transport or - * written to the socket/output queue). This does not imply the remote node has received it on a - * lossy transport. - */ - void sent(); - - /** - * Invoked when the remote node acknowledges receipt of the packet. This marks the end of the - * transmission for this packet. On a reliable (non-lossy) transport this may be called - * immediately after {@link #sent()}. - */ - void acknowledged(); - - /** - * Invoked if the connection is lost while the packet is queued or after it has been sent. - * Terminal. - */ - void disconnected(); - - /** Invoked if the packet is lost due to an unrecoverable internal error. Terminal. */ - void fatalError(); -} diff --git a/src/main/java/network/crypta/io/comm/ByteCounter.java b/src/main/java/network/crypta/io/comm/ByteCounter.java deleted file mode 100644 index d7544389d63..00000000000 --- a/src/main/java/network/crypta/io/comm/ByteCounter.java +++ /dev/null @@ -1,47 +0,0 @@ -package network.crypta.io.comm; - -/** - * Reports byte counts for network I/O. - * - *

This minimal callback interface is used by messaging and transfer layers to report the - * quantity of data that has been sent or received. Counts are expressed in bytes. Implementations - * typically update statistics, throttling, or diagnostic views; the specific side effects are - * implementation-defined. - * - *

Threading: Callers may invoke these methods from arbitrary threads (for example, I/O or worker - * threads). Implementations should document their own thread-safety guarantees if they are shared - * across threads. - */ -public interface ByteCounter { - - /** - * Records raw bytes transmitted on the wire. - * - *

The count includes all protocol overhead (headers, framing, etc.) and may also include bytes - * that a higher-level throttle has already accounted for. - * - * @param x number of bytes sent (bytes) - */ - void sentBytes(int x); - - /** - * Records raw bytes received from the wire. - * - *

The count includes all protocol overhead (headers, framing, etc.). - * - * @param x number of bytes received (bytes) - */ - void receivedBytes(int x); - - /** - * Records application payload bytes transmitted. - * - *

Only report the size of user-visible data and exclude protocol overhead. For the same - * transfer the caller is expected to also report the raw size via {@link #sentBytes(int)}; do not - * sum the totals from {@code sentBytes(...)} and {@code sentPayload(...)} externally, as they - * represent overlapping measurements and will double-count. - * - * @param x number of payload bytes sent (bytes) - */ - void sentPayload(int x); -} diff --git a/src/main/java/network/crypta/io/comm/DMT.java b/src/main/java/network/crypta/io/comm/DMT.java deleted file mode 100644 index 5f356999437..00000000000 --- a/src/main/java/network/crypta/io/comm/DMT.java +++ /dev/null @@ -1,2924 +0,0 @@ -package network.crypta.io.comm; - -import network.crypta.crypt.DSAPublicKey; -import network.crypta.keys.Key; -import network.crypta.keys.NodeCHK; -import network.crypta.keys.NodeSSK; -import network.crypta.node.probe.Error; -import network.crypta.node.probe.Type; -import network.crypta.support.BitArray; -import network.crypta.support.Buffer; -import network.crypta.support.Fields; -import network.crypta.support.ShortBuffer; - -/** - * Registry of data message types (DMT) and field keys used by the Crypta node-to-node communication - * layer. - * - *

This utility exposes: - * - *

    - *
  • String constants for field names used when declaring {@link MessageType} schemas. - *
  • Static {@link MessageType} instances describing request and response payloads. - *
  • Factory helpers that create correctly typed {@link Message} instances. - *
- * - *

All members are static; the class is non-instantiable. Priorities indicate relative scheduling - * on the link; lower numeric values are more urgent. See the {@code PRIORITY_*} constants for - * semantics. - * - *

Thread-safety: message type definitions are initialized once and then immutable. Factory - * methods allocate new {@link Message} objects and are thread-safe. - */ -@SuppressWarnings("unused") -public class DMT { - - private DMT() {} - - public static final String UID = "uid"; - /* - * Field name constants used by {@link MessageType#addField(String, Class)} and by callers when - * reading/writing {@link Message} values. Names are part of the on-wire schema; avoid renaming. - */ - public static final String SEND_TIME = "sendTime"; - public static final String EXTERNAL_ADDRESS = "externalAddress"; - public static final String BUILD = "build"; - public static final String FIRST_GOOD_BUILD = "firstGoodBuild"; - public static final String JOINER = "joiner"; - public static final String REASON = "reason"; - public static final String DESCRIPTION = "description"; - public static final String TTL = "ttl"; - public static final String PEERS = "peers"; - public static final String URL = "url"; - public static final String FORWARDERS = "forwarders"; - public static final String FILE_LENGTH = "fileLength"; - public static final String LAST_MODIFIED = "lastModified"; - public static final String CHUNK_NO = "chunkNo"; - public static final String DATA_SOURCE = "dataSource"; - public static final String CACHED = "cached"; - public static final String PACKET_NO = "packetNo"; - public static final String DATA = "data"; - public static final String IS_HASH = "isHash"; - public static final String HASH = "hash"; - public static final String SENT = "sent"; - public static final String MISSING = "missing"; - public static final String KEY = "key"; - public static final String CHK_HEADER = "chkHeader"; - public static final String FREENET_URI = "freenetURI"; - public static final String FREENET_ROUTING_KEY = "freenetRoutingKey"; - public static final String TEST_CHK_HEADERS = "testCHKHeaders"; - public static final String HTL = "hopsToLive"; - public static final String SUCCESS = "success"; - public static final String FNP_SOURCE_PEERNODE = "sourcePeerNode"; - public static final String PING_SEQNO = "pingSequenceNumber"; - public static final String LOCATION = "location"; - public static final String NEAREST_LOCATION = "nearestLocation"; - public static final String BEST_LOCATION = "bestLocation"; - public static final String TARGET_LOCATION = "targetLocation"; - public static final String TYPE = "type"; - public static final String PAYLOAD = "payload"; - public static final String COUNTER = "counter"; - public static final String UNIQUE_COUNTER = "uniqueCounter"; - public static final String LINEAR_COUNTER = "linearCounter"; - public static final String RETURN_LOCATION = "returnLocation"; - public static final String BLOCK_HEADERS = "blockHeaders"; - public static final String DATA_INSERT_REJECTED_REASON = "dataInsertRejectedReason"; - public static final String STREAM_SEQNO = "streamSequenceNumber"; - public static final String IS_LOCAL = "isLocal"; - public static final String ANY_TIMED_OUT = "anyTimedOut"; - public static final String PUBKEY_HASH = "pubkeyHash"; - public static final String NEED_PUB_KEY = "needPubKey"; - public static final String PUBKEY_AS_BYTES = "pubkeyAsBytes"; - public static final String SOURCE_NODENAME = "sourceNodename"; - public static final String TARGET_NODENAME = "targetNodename"; - public static final String NODE_TO_NODE_MESSAGE_TYPE = "nodeToNodeMessageType"; - public static final String NODE_TO_NODE_MESSAGE_TEXT = "nodeToNodeMessageText"; - public static final String NODE_TO_NODE_MESSAGE_DATA = "nodeToNodeMessageData"; - public static final String NODE_UIDS = "nodeUIDs"; - public static final String MY_UID = "myUID"; - public static final String PEER_LOCATIONS = "peerLocations"; - public static final String PEER_UIDS = "peerUIDs"; - public static final String BEST_LOCATIONS_NOT_VISITED = "bestLocationsNotVisited"; - public static final String CORE_PACKAGE_KEY = "mainJarKey"; - public static final String EXTRA_JAR_KEY = "extraJarKey"; - public static final String REVOCATION_KEY = "revocationKey"; - public static final String HAVE_REVOCATION_KEY = "haveRevocationKey"; - // Wire field name intentionally preserved for protocol compatibility. - public static final String CORE_PACKAGE_VERSION = "mainJarVersion"; - public static final String EXTRA_JAR_VERSION = "extJarVersion"; - public static final String REVOCATION_KEY_TIME_LAST_TRIED = "revocationKeyTimeLastTried"; - public static final String REVOCATION_KEY_DNF_COUNT = "revocationKeyDNFCount"; - public static final String REVOCATION_KEY_FILE_LENGTH = "revocationKeyFileLength"; - public static final String CORE_PACKAGE_FILE_LENGTH = "mainJarFileLength"; - public static final String EXTRA_JAR_FILE_LENGTH = "extraJarFileLength"; - public static final String PING_TIME = "pingTime"; - public static final String BWLIMIT_DELAY_TIME = "bwlimitDelayTime"; - public static final String TIME = "time"; - public static final String FORK_COUNT = "forkCount"; - public static final String TIME_LEFT = "timeLeft"; - public static final String PREV_UID = "prevUID"; - public static final String OPENNET_NODEREF = "opennetNoderef"; - public static final String REMOVE = "remove"; - public static final String PURGE = "purge"; - public static final String TRANSFER_UID = "transferUID"; - public static final String NODEREF_LENGTH = "noderefLength"; - public static final String PADDED_LENGTH = "paddedLength"; - public static final String TIME_DELTAS = "timeDeltas"; - public static final String HASHES = "hashes"; - public static final String REJECT_CODE = "rejectCode"; - public static final String ROUTING_ENABLED = "routingEnabled"; - public static final String OFFER_AUTHENTICATOR = "offerAuthenticator"; - public static final String DAWN_HTL = "dawnHtl"; - public static final String SECRET = "secret"; - public static final String NODE_IDENTITY = "nodeIdentity"; - public static final String UPTIME_PERCENT_48H = "uptimePercent48H"; - public static final String FRIEND_VISIBILITY = "friendVisibility"; - public static final String ENABLE_INSERT_FORK_WHEN_CACHEABLE = "enableInsertForkWhenCacheable"; - public static final String PREFER_INSERT = "preferInsert"; - public static final String IGNORE_LOW_BACKOFF = "ignoreLowBackoff"; - public static final String LIST_OF_UIDS = "listOfUIDs"; - public static final String UID_STILL_RUNNING_FLAGS = "UIDStillRunningFlags"; - public static final String PROBE_IDENTIFIER = "probeIdentifier"; - public static final String STORE_SIZE = "storeSize"; - public static final String LINK_LENGTHS = "linkLengths"; - public static final String UPTIME_PERCENT = "uptimePercent"; - public static final String EXPECTED_HASH = "expectedHash"; - public static final String REJECT_STATS = "rejectStats"; - public static final String OUTPUT_BANDWIDTH_CLASS = "outputBandwidthClass"; - public static final String CAPACITY_USAGE = "capacityUsage"; - - // Load management constants - public static final String AVERAGE_TRANSFERS_OUT_PER_INSERT = "averageTransfersOutPerInsert"; - public static final String OTHER_TRANSFERS_OUT_CHK = "otherTransfersOutCHK"; - public static final String OTHER_TRANSFERS_IN_CHK = "otherTransfersInCHK"; - public static final String OTHER_TRANSFERS_OUT_SSK = "otherTransfersOutSSK"; - public static final String OTHER_TRANSFERS_IN_SSK = "otherTransfersInSSK"; - - /** - * Maximum transfers out, hard limit based on congestion control; we will be rejected if our usage - * is over this. - */ - public static final String MAX_TRANSFERS_OUT = "maxTransfersOut"; - - /** - * Maximum transfers out, peer limit. If the total is over the lower limit and our usage is over - * the peer limit, we will be rejected. - */ - public static final String MAX_TRANSFERS_OUT_PEER_LIMIT = "maxTransfersOutPeerLimit"; - - /** - * Maximum transfers out, lower limit. If the total is over the lower limit and our usage is over - * the peer limit, we will be rejected. - */ - public static final String MAX_TRANSFERS_OUT_LOWER_LIMIT = "maxTransfersOutLowerLimit"; - - /** - * Maximum transfers out, upper limit. If the total is over the upper limit, everything is - * rejected. - */ - public static final String MAX_TRANSFERS_OUT_UPPER_LIMIT = "maxTransfersOutUpperLimit"; - - public static final String OUTPUT_BANDWIDTH_LOWER_LIMIT = "outputBandwidthLowerLimit"; - public static final String OUTPUT_BANDWIDTH_UPPER_LIMIT = "outputBandwidthUpperLimit"; - public static final String OUTPUT_BANDWIDTH_PEER_LIMIT = "outputBandwidthPeerLimit"; - public static final String INPUT_BANDWIDTH_LOWER_LIMIT = "inputBandwidthLowerLimit"; - public static final String INPUT_BANDWIDTH_UPPER_LIMIT = "inputBandwidthUpperLimit"; - public static final String INPUT_BANDWIDTH_PEER_LIMIT = "inputBandwidthPeerLimit"; - - public static final String REAL_TIME_FLAG = "realTimeFlag"; - - /** Very urgent control messages. */ - public static final short PRIORITY_NOW = 0; - - /** Short timeout or otherwise urgent (for example, accepts and loop rejections). */ - public static final short PRIORITY_HIGH = 1; // - - /** Routine control messages and completions. */ - public static final short PRIORITY_UNSPECIFIED = 2; - - /** Request initiators; may tolerate longer timeouts. */ - public static final short PRIORITY_LOW = 3; // long timeout, or moderately urgent - - /** - * Bulk data for realtime requests. Not strictly inferior to {@link #PRIORITY_BULK_DATA}; - * scheduling enforces fairness so realtime data does not starve bulk data. - */ - public static final short PRIORITY_REALTIME_DATA = 4; - - /** - * Bulk data transfer (lowest priority). Higher-level admission control must ensure feasibility; - * persistent starvation raises {@code bwlimitDelayTime} and causes request rejection. - */ - public static final short PRIORITY_BULK_DATA = 5; - - public static final short NUM_PRIORITIES = 6; - - // Segmented bulk transfer primitives - - /** Message carrying one data packet in a segmented bulk transfer. */ - public static final MessageType packetTransmit = - new MessageType("packetTransmit", PRIORITY_BULK_DATA); - - static { - packetTransmit.addField(UID, Long.class); - packetTransmit.addField(PACKET_NO, Integer.class); - packetTransmit.addField(SENT, BitArray.class); - packetTransmit.addField(DATA, Buffer.class); - } - - /** - * Creates a {@code packetTransmit} for a segmented bulk transfer. - * - * @param uid Transfer identifier shared by the bulk session. - * @param packetNo Zero-based packet index. - * @param sent Bitset of packets known to be sent; helps coalesce ACK/resend state. - * @param data Payload for this packet. - * @param realTime When true, boosts priority to {@link #PRIORITY_REALTIME_DATA}. - * @return Initialized message. - */ - public static Message createPacketTransmit( - long uid, int packetNo, BitArray sent, Buffer data, boolean realTime) { - Message msg = new Message(packetTransmit); - msg.set(UID, uid); - msg.set(PACKET_NO, packetNo); - msg.set(SENT, sent); - msg.set(DATA, data); - if (realTime) { - msg.boostPriority(); - } - return msg; - } - - /** - * Computes approximate on-wire size in bytes for a {@code packetTransmit} message. - * - * @param size Payload size in bytes. - * @param packets Number of packets in the transfer (for the {@link BitArray}). - * @return Payload size plus header/metadata overhead. - */ - public static int packetTransmitSize(int size, int packets) { - return size - + 8 /* uid */ - + 4 /* packet# */ - + BitArray.serializedLength(packets) - + 4 /* Message header */; - } - - /** - * Computes approximate on-wire size for a bulk packet message that does not carry a BitArray. - * - * @param size Payload size in bytes. - * @return Payload size plus header/metadata overhead. - */ - public static int bulkPacketTransmitSize(int size) { - return size + 8 /* uid */ + 4 /* packet# */ + 4 /* Message header */; - } - - // Use BULK_DATA to reduce spurious resend requests; queued after the packets it represents. - /** Signals that all packets in the current transfer have been enqueued by the sender. */ - public static final MessageType allSent = new MessageType("allSent", PRIORITY_BULK_DATA); - - static { - allSent.addField(UID, Long.class); - } - - /** - * Creates an {@code allSent} notification for the given transfer. - * - * @param uid Transfer identifier. - * @return Initialized message. - */ - public static Message createAllSent(long uid) { - Message msg = new Message(allSent); - msg.set(UID, uid); - return msg; - } - - /** Signals that all packets in the current transfer have been received. */ - public static final MessageType allReceived = - new MessageType("allReceived", PRIORITY_UNSPECIFIED); - - static { - allReceived.addField(UID, Long.class); - } - - /** - * Creates an {@code allReceived} notification for the given transfer. - * - * @param uid Transfer identifier. - * @return Initialized message. - */ - public static Message createAllReceived(long uid) { - Message msg = new Message(allReceived); - msg.set(UID, uid); - return msg; - } - - /** Indicates that a transfer was aborted and will not complete. */ - public static final MessageType sendAborted = - new MessageType("sendAborted", PRIORITY_UNSPECIFIED); - - static { - sendAborted.addField(UID, Long.class); - sendAborted.addField(DESCRIPTION, String.class); - sendAborted.addField(REASON, Integer.class); - } - - /** - * Creates a {@code sendAborted} message. - * - * @param uid Transfer identifier. - * @param reason Implementation-defined error code. - * @param description Human-readable reason. - * @return Initialized message. - */ - public static Message createSendAborted(long uid, int reason, String description) { - Message msg = new Message(sendAborted); - msg.set(UID, uid); - msg.set(REASON, reason); - msg.set(DESCRIPTION, description); - return msg; - } - - /** - * Bulk transfer packet using {@link ShortBuffer} for payload. Used by the bulk transmitter when - * BitArray acknowledgments are not included inline. - */ - public static final MessageType FNPBulkPacketSend = - new MessageType("FNPBulkPacketSend", PRIORITY_BULK_DATA); - - static { - FNPBulkPacketSend.addField(UID, Long.class); - FNPBulkPacketSend.addField(PACKET_NO, Integer.class); - FNPBulkPacketSend.addField(DATA, ShortBuffer.class); - } - - /** - * Creates an {@code FNPBulkPacketSend} for a segmented bulk transfer. - * - * @param uid Transfer identifier. - * @param packetNo Zero-based packet index. - * @param data Packet payload as a {@link ShortBuffer}. - * @return Initialized message. - */ - public static Message createFNPBulkPacketSend(long uid, int packetNo, ShortBuffer data) { - Message msg = new Message(FNPBulkPacketSend); - msg.set(UID, uid); - msg.set(PACKET_NO, packetNo); - msg.set(DATA, data); - return msg; - } - - /** Convenience overload that wraps a byte array into {@link ShortBuffer}. */ - public static Message createFNPBulkPacketSend(long uid, int packetNo, byte[] data) { - return createFNPBulkPacketSend(uid, packetNo, new ShortBuffer(data)); - } - - /** Sender aborted a bulk transfer; no further packets will be sent. */ - public static final MessageType FNPBulkSendAborted = - new MessageType("FNPBulkSendAborted", PRIORITY_UNSPECIFIED); - - static { - FNPBulkSendAborted.addField(UID, Long.class); - } - - /** Creates an {@code FNPBulkSendAborted} for the given transfer. */ - public static Message createFNPBulkSendAborted(long uid) { - Message msg = new Message(FNPBulkSendAborted); - msg.set(UID, uid); - return msg; - } - - /** Receiver aborted a bulk transfer; later packets should be discarded. */ - public static final MessageType FNPBulkReceiveAborted = - new MessageType("FNPBulkReceiveAborted", PRIORITY_UNSPECIFIED); - - static { - FNPBulkReceiveAborted.addField(UID, Long.class); - } - - /** Creates an {@code FNPBulkReceiveAborted} for the given transfer. */ - public static Message createFNPBulkReceiveAborted(long uid) { - Message msg = new Message(FNPBulkReceiveAborted); - msg.set(UID, uid); - return msg; - } - - /** Receiver acknowledges that all expected packets were received. */ - public static final MessageType FNPBulkReceivedAll = - new MessageType("FNPBulkReceivedAll", PRIORITY_UNSPECIFIED); - - static { - FNPBulkReceivedAll.addField(UID, Long.class); - } - - /** Creates an {@code FNPBulkReceivedAll} acknowledgment. */ - public static Message createFNPBulkReceivedAll(long uid) { - Message msg = new Message(FNPBulkReceivedAll); - msg.set(UID, uid); - return msg; - } - - /** Test harness: request that the peer start a trivial transfer. */ - public static final MessageType testTransferSend = - new MessageType("testTransferSend", PRIORITY_UNSPECIFIED); - - static { - testTransferSend.addField(UID, Long.class); - } - - /** Creates a {@code testTransferSend} request. */ - public static Message createTestTransferSend(long uid) { - Message msg = new Message(testTransferSend); - msg.set(UID, uid); - return msg; - } - - /** Test harness: acknowledgment of {@code testTransferSend}. */ - public static final MessageType testTransferSendAck = - new MessageType("testTransferSendAck", PRIORITY_UNSPECIFIED); - - static { - testTransferSendAck.addField(UID, Long.class); - } - - /** Creates a {@code testTransferSendAck}. */ - public static Message createTestTransferSendAck(long uid) { - Message msg = new Message(testTransferSendAck); - msg.set(UID, uid); - return msg; - } - - /** Test harness: send a CHK URI and header for validation. */ - public static final MessageType testSendCHK = - new MessageType("testSendCHK", PRIORITY_UNSPECIFIED); - - static { - testSendCHK.addField(UID, Long.class); - testSendCHK.addField(FREENET_URI, String.class); - testSendCHK.addField(CHK_HEADER, Buffer.class); - } - - /** - * Creates a {@code testSendCHK} with a URI string and header buffer. - * - * @param uid Test identifier. - * @param uri CHK URI as a string. - * @param header Serialized header. - * @return Initialized message. - */ - public static Message createTestSendCHK(long uid, String uri, Buffer header) { - Message msg = new Message(testSendCHK); - msg.set(UID, uid); - msg.set(FREENET_URI, uri); - msg.set(CHK_HEADER, header); - return msg; - } - - /** Test harness: request data by routing key with an explicit HTL. */ - public static final MessageType testRequest = - new MessageType("testRequest", PRIORITY_UNSPECIFIED); - - static { - testRequest.addField(UID, Long.class); - testRequest.addField(FREENET_ROUTING_KEY, Key.class); - testRequest.addField(HTL, Integer.class); - } - - /** - * Creates a {@code testRequest} for a routing key. - * - * @param key Routing key. - * @param id Test identifier. - * @param htl Hops-to-live. - * @return Initialized message. - */ - public static Message createTestRequest(Key key, long id, int htl) { - Message msg = new Message(testRequest); - msg.set(UID, id); - msg.set(FREENET_ROUTING_KEY, key); - msg.set(HTL, htl); - return msg; - } - - /** Test harness: negative lookup result. */ - public static final MessageType testDataNotFound = - new MessageType("testDataNotFound", PRIORITY_UNSPECIFIED); - - static { - testDataNotFound.addField(UID, Long.class); - } - - /** Creates a {@code testDataNotFound}. */ - public static Message createTestDataNotFound(long uid) { - Message msg = new Message(testDataNotFound); - msg.set(UID, uid); - return msg; - } - - /** Test harness: successful reply carrying serialized headers. */ - public static final MessageType testDataReply = - new MessageType("testDataReply", PRIORITY_UNSPECIFIED); - - static { - testDataReply.addField(UID, Long.class); - testDataReply.addField(TEST_CHK_HEADERS, Buffer.class); - } - - /** - * Creates a {@code testDataReply} with serialized headers. - * - * @param uid Test identifier. - * @param headers Serialized headers. - * @return Initialized message. - */ - public static Message createTestDataReply(long uid, byte[] headers) { - Message msg = new Message(testDataReply); - msg.set(UID, uid); - msg.set(TEST_CHK_HEADERS, new Buffer(headers)); - return msg; - } - - /** Test harness: acknowledgment to {@code testSendCHK}. */ - public static final MessageType testSendCHKAck = - new MessageType("testSendCHKAck", PRIORITY_UNSPECIFIED); - - static { - testSendCHKAck.addField(UID, Long.class); - testSendCHKAck.addField(FREENET_URI, String.class); - } - - /** - * Creates a {@code testSendCHKAck} echoing the provided key. - * - * @param uid Test identifier. - * @param key Echo of the CHK key string. - * @return Initialized message. - */ - public static Message createTestSendCHKAck(long uid, String key) { - Message msg = new Message(testSendCHKAck); - msg.set(UID, uid); - msg.set(FREENET_URI, key); - return msg; - } - - /** Test harness: acknowledgment of {@code testDataReply}. */ - public static final MessageType testDataReplyAck = - new MessageType("testDataReplyAck", PRIORITY_UNSPECIFIED); - - static { - testDataReplyAck.addField(UID, Long.class); - } - - /** Creates a {@code testDataReplyAck}. */ - public static Message createTestDataReplyAck(long id) { - Message msg = new Message(testDataReplyAck); - msg.set(UID, id); - return msg; - } - - /** Test harness: acknowledgment of {@code testDataNotFound}. */ - public static final MessageType testDataNotFoundAck = - new MessageType("testDataNotFoundAck", PRIORITY_UNSPECIFIED); - - static { - testDataNotFoundAck.addField(UID, Long.class); - } - - /** Creates a {@code testDataNotFoundAck}. */ - public static Message createTestDataNotFoundAck(long id) { - Message msg = new Message(testDataNotFoundAck); - msg.set(UID, id); - return msg; - } - - // Internal only messages - - /** Internal: indicates completion of a test receiving, with a success flag and reason. */ - public static final MessageType testReceiveCompleted = - new MessageType("testReceiveCompleted", PRIORITY_UNSPECIFIED, true, false); - - static { - testReceiveCompleted.addField(UID, Long.class); - testReceiveCompleted.addField(SUCCESS, Boolean.class); - testReceiveCompleted.addField(REASON, String.class); - } - - /** - * Creates a {@code testReceiveCompleted}. - * - * @param id Test identifier. - * @param success Whether the reception completed successfully. - * @param reason Optional reason text. - * @return Initialized message. - */ - public static Message createTestReceiveCompleted(long id, boolean success, String reason) { - Message msg = new Message(testReceiveCompleted); - msg.set(UID, id); - msg.set(SUCCESS, success); - msg.set(REASON, reason); - return msg; - } - - /** Internal: indicates completion of a test sending, with a success flag and reason. */ - public static final MessageType testSendCompleted = - new MessageType("testSendCompleted", PRIORITY_UNSPECIFIED, true, false); - - static { - testSendCompleted.addField(UID, Long.class); - testSendCompleted.addField(SUCCESS, Boolean.class); - testSendCompleted.addField(REASON, String.class); - } - - /** - * Creates a {@code testSendCompleted}. - * - * @param id Test identifier. - * @param success Whether the sending completed successfully. - * @param reason Optional reason text. - * @return Initialized message. - */ - public static Message createTestSendCompleted(long id, boolean success, String reason) { - Message msg = new Message(testSendCompleted); - msg.set(UID, id); - msg.set(SUCCESS, success); - msg.set(REASON, reason); - return msg; - } - - /** Generic node-to-node message carrying a small typed payload. */ - public static final MessageType nodeToNodeMessage = - new MessageType("nodeToNodeMessage", PRIORITY_LOW, false, false); - - static { - nodeToNodeMessage.addField(NODE_TO_NODE_MESSAGE_TYPE, Integer.class); - nodeToNodeMessage.addField(NODE_TO_NODE_MESSAGE_DATA, ShortBuffer.class); - } - - /** - * Creates a generic node-to-node message with a caller-defined {@code type} and opaque payload. - * - * @param type Application-specific subtype. - * @param data Opaque payload; interpreted by the receiver based on {@code type}. - * @return Initialized message. - */ - public static Message createNodeToNodeMessage(int type, byte[] data) { - Message msg = new Message(nodeToNodeMessage); - msg.set(NODE_TO_NODE_MESSAGE_TYPE, type); - msg.set(NODE_TO_NODE_MESSAGE_DATA, new ShortBuffer(data)); - return msg; - } - - // FNP messages - /** Request for CHK data (content-hash key) routed by location. */ - public static final MessageType FNPCHKDataRequest = - new MessageType("FNPCHKDataRequest", PRIORITY_LOW); - - static { - FNPCHKDataRequest.addField(UID, Long.class); - FNPCHKDataRequest.addField(HTL, Short.class); - FNPCHKDataRequest.addField(NEAREST_LOCATION, Double.class); - FNPCHKDataRequest.addField(FREENET_ROUTING_KEY, NodeCHK.class); - } - - /** - * Creates an {@code FNPCHKDataRequest}. - * - * @param id Request identifier. - * @param htl Hops-to-live. - * @param key Routing key (CHK). - * @return Initialized message. - */ - public static Message createFNPCHKDataRequest(long id, short htl, NodeCHK key) { - Message msg = new Message(FNPCHKDataRequest); - msg.set(UID, id); - msg.set(HTL, htl); - msg.set(FREENET_ROUTING_KEY, key); - msg.set(NEAREST_LOCATION, 0.0); - return msg; - } - - /** Request for SSK data (signed-subspace key) routed by location. */ - public static final MessageType FNPSSKDataRequest = - new MessageType("FNPSSKDataRequest", PRIORITY_LOW); - - static { - FNPSSKDataRequest.addField(UID, Long.class); - FNPSSKDataRequest.addField(HTL, Short.class); - FNPSSKDataRequest.addField(NEAREST_LOCATION, Double.class); - FNPSSKDataRequest.addField(FREENET_ROUTING_KEY, NodeSSK.class); - FNPSSKDataRequest.addField(NEED_PUB_KEY, Boolean.class); - } - - /** - * Creates an {@code FNPSSKDataRequest}. - * - * @param id Request identifier. - * @param htl Hops-to-live. - * @param key Routing key (SSK). - * @param needPubKey Whether the caller requires the public key to be returned. - * @return Initialized message. - */ - public static Message createFNPSSKDataRequest( - long id, short htl, NodeSSK key, boolean needPubKey) { - Message msg = new Message(FNPSSKDataRequest); - msg.set(UID, id); - msg.set(HTL, htl); - msg.set(FREENET_ROUTING_KEY, key); - msg.set(NEAREST_LOCATION, 0.0); - msg.set(NEED_PUB_KEY, needPubKey); - return msg; - } - - // Loop detected: choose a different peer. - /** Rejection indicating a routing loop was encountered. */ - public static final MessageType FNPRejectedLoop = new MessageType("FNPRejectLoop", PRIORITY_HIGH); - - static { - FNPRejectedLoop.addField(UID, Long.class); - } - - /** - * Creates an {@code FNPRejectedLoop} for the given request. - * - * @param id Request identifier. - * @return Initialized message. - */ - public static Message createFNPRejectedLoop(long id) { - Message msg = new Message(FNPRejectedLoop); - msg.set(UID, id); - return msg; - } - - // Too many concurrent requests for current capacity. - /** Rejection due to overload; caller may reduce the rate or choose another peer. */ - public static final MessageType FNPRejectedOverload = - new MessageType("FNPRejectOverload", PRIORITY_HIGH); - - static { - FNPRejectedOverload.addField(UID, Long.class); - FNPRejectedOverload.addField(IS_LOCAL, Boolean.class); - } - - /** - * Creates an {@code FNPRejectedOverload}. - * - * @param id Request identifier. - * @param isLocal Whether the rejecting peer is the local node. - * @return Initialized message. - */ - public static Message createFNPRejectedOverload(long id, boolean isLocal) { - Message msg = new Message(FNPRejectedOverload); - msg.set(UID, id); - msg.set(IS_LOCAL, isLocal); - return msg; - } - - /** Acknowledge that a request was accepted and will proceed. */ - public static final MessageType FNPAccepted = new MessageType("FNPAccepted", PRIORITY_HIGH); - - static { - FNPAccepted.addField(UID, Long.class); - } - - /** - * Creates an {@code FNPAccepted} acknowledgment. - * - * @param id Request identifier. - * @return Initialized message. - */ - public static Message createFNPAccepted(long id) { - Message msg = new Message(FNPAccepted); - msg.set(UID, id); - return msg; - } - - /** Negative result indicating the requested data was not found. */ - public static final MessageType FNPDataNotFound = - new MessageType("FNPDataNotFound", PRIORITY_UNSPECIFIED); - - static { - FNPDataNotFound.addField(UID, Long.class); - } - - /** - * Creates an {@code FNPDataNotFound} for the given request. - * - * @param id Request identifier. - * @return Initialized message. - */ - public static Message createFNPDataNotFound(long id) { - Message msg = new Message(FNPDataNotFound); - msg.set(UID, id); - return msg; - } - - /** Indicates a recent failure; used to guide backoff and avoid repeated attempts. */ - public static final MessageType FNPRecentlyFailed = - new MessageType("FNPRecentlyFailed", PRIORITY_HIGH); - - static { - FNPRecentlyFailed.addField(UID, Long.class); - FNPRecentlyFailed.addField(TIME_LEFT, Integer.class); - } - - /** - * Creates an {@code FNPRecentlyFailed} with a remaining-wait hint. - * - * @param id Request identifier. - * @param timeLeft Remaining time to wait before retrying (unit-implementation-defined). - * @return Initialized message. - */ - public static Message createFNPRecentlyFailed(long id, int timeLeft) { - Message msg = new Message(FNPRecentlyFailed); - msg.set(UID, id); - msg.set(TIME_LEFT, timeLeft); - return msg; - } - - /** Positive result for CHK requests carrying block headers. */ - public static final MessageType FNPCHKDataFound = - new MessageType("FNPCHKDataFound", PRIORITY_UNSPECIFIED); - - static { - FNPCHKDataFound.addField(UID, Long.class); - FNPCHKDataFound.addField(BLOCK_HEADERS, ShortBuffer.class); - } - - /** - * Creates an {@code FNPCHKDataFound} with serialized block headers. - * - * @param id Request identifier. - * @param buf Serialized headers. - * @return Initialized message. - */ - public static Message createFNPCHKDataFound(long id, byte[] buf) { - Message msg = new Message(FNPCHKDataFound); - msg.set(UID, id); - msg.set(BLOCK_HEADERS, new ShortBuffer(buf)); - return msg; - } - - /** No route found for the request; includes remaining HTL. */ - public static final MessageType FNPRouteNotFound = - new MessageType("FNPRouteNotFound", PRIORITY_UNSPECIFIED); - - static { - FNPRouteNotFound.addField(UID, Long.class); - FNPRouteNotFound.addField(HTL, Short.class); - } - - /** - * Creates an {@code FNPRouteNotFound}. - * - * @param id Request identifier. - * @param htl Remaining hops-to-live. - * @return Initialized message. - */ - public static Message createFNPRouteNotFound(long id, short htl) { - Message msg = new Message(FNPRouteNotFound); - msg.set(UID, id); - msg.set(HTL, htl); - return msg; - } - - /** Insert request routed by location (CHK/SSK-agnostic key interface). */ - public static final MessageType FNPInsertRequest = - new MessageType("FNPInsertRequest", PRIORITY_LOW); - - static { - FNPInsertRequest.addField(UID, Long.class); - FNPInsertRequest.addField(HTL, Short.class); - FNPInsertRequest.addField(NEAREST_LOCATION, Double.class); - FNPInsertRequest.addField(FREENET_ROUTING_KEY, Key.class); - } - - /** - * Creates an {@code FNPInsertRequest}. - * - * @param id Request identifier. - * @param htl Hops-to-live. - * @param key Routing key. - * @return Initialized message. - */ - public static Message createFNPInsertRequest(long id, short htl, Key key) { - Message msg = new Message(FNPInsertRequest); - msg.set(UID, id); - msg.set(HTL, htl); - msg.set(FREENET_ROUTING_KEY, key); - msg.set(NEAREST_LOCATION, 0.0); - return msg; - } - - /** Acknowledgment that an insert request was accepted downstream. */ - public static final MessageType FNPInsertReply = - new MessageType("FNPInsertReply", PRIORITY_UNSPECIFIED); - - static { - FNPInsertReply.addField(UID, Long.class); - } - - /** - * Creates an {@code FNPInsertReply} acknowledgment. - * - * @param id Request identifier. - * @return Initialized message. - */ - public static Message createFNPInsertReply(long id) { - Message msg = new Message(FNPInsertReply); - msg.set(UID, id); - return msg; - } - - /** Carries serialized block headers for a data insert. */ - public static final MessageType FNPDataInsert = new MessageType("FNPDataInsert", PRIORITY_HIGH); - - static { - FNPDataInsert.addField(UID, Long.class); - FNPDataInsert.addField(BLOCK_HEADERS, ShortBuffer.class); - } - - /** - * Creates an {@code FNPDataInsert} message with serialized block headers. - * - * @param uid Insert identifier. - * @param headers Serialized headers. - * @return Initialized message. - */ - public static Message createFNPDataInsert(long uid, byte[] headers) { - Message msg = new Message(FNPDataInsert); - msg.set(UID, uid); - msg.set(BLOCK_HEADERS, new ShortBuffer(headers)); - return msg; - } - - /** Indicates that all transfer segments for an insert completed (possibly with timeouts). */ - public static final MessageType FNPInsertTransfersCompleted = - new MessageType("FNPInsertTransfersCompleted", PRIORITY_UNSPECIFIED); - - static { - FNPInsertTransfersCompleted.addField(UID, Long.class); - FNPInsertTransfersCompleted.addField(ANY_TIMED_OUT, Boolean.class); - } - - /** - * Creates an {@code FNPInsertTransfersCompleted}. - * - * @param uid Insert identifier. - * @param anyTimedOut Whether any segment timed out. - * @return Initialized message. - */ - public static Message createFNPInsertTransfersCompleted(long uid, boolean anyTimedOut) { - Message msg = new Message(FNPInsertTransfersCompleted); - msg.set(UID, uid); - msg.set(ANY_TIMED_OUT, anyTimedOut); - return msg; - } - - // This is used by CHK inserts when the DataInsert isn't received. - // SSK inserts just use DataInsertRejected with a timeout reason. - // Consider using a single message for both. One complication is that - // CHKs have a single DataInsert (followed by a transfer), whereas SSKs have 2-3 messages, - // any of which can time out independently. We could send one message now that we have the new - // packet format; see the discussion on FNPSSKInsertRequestNew vs. the old version. - /** Timeout for CHK inserts when {@code FNPDataInsert} was not received in time. */ - public static final MessageType FNPRejectedTimeout = - new MessageType("FNPTooSlow", PRIORITY_UNSPECIFIED); - - static { - FNPRejectedTimeout.addField(UID, Long.class); - } - - /** - * Creates an {@code FNPRejectedTimeout} notification. - * - * @param uid Insert identifier. - * @return Initialized message. - */ - public static Message createFNPRejectedTimeout(long uid) { - Message msg = new Message(FNPRejectedTimeout); - msg.set(UID, uid); - return msg; - } - - /** - * Insert rejected with a reason code (see constants and {@link #getDataInsertRejectedReason}). - */ - public static final MessageType FNPDataInsertRejected = - new MessageType("FNPDataInsertRejected", PRIORITY_UNSPECIFIED); - - static { - FNPDataInsertRejected.addField(UID, Long.class); - FNPDataInsertRejected.addField(DATA_INSERT_REJECTED_REASON, Short.class); - } - - /** - * Creates an {@code FNPDataInsertRejected} with a reason code. - * - * @param uid Insert identifier. - * @param reason Reason code constant. - * @return Initialized message. - */ - public static Message createFNPDataInsertRejected(long uid, short reason) { - Message msg = new Message(FNPDataInsertRejected); - msg.set(UID, uid); - msg.set(DATA_INSERT_REJECTED_REASON, reason); - return msg; - } - - public static final short DATA_INSERT_REJECTED_VERIFY_FAILED = 1; - public static final short DATA_INSERT_REJECTED_RECEIVE_FAILED = 2; - public static final short DATA_INSERT_REJECTED_SSK_ERROR = 3; - public static final short DATA_INSERT_REJECTED_TIMEOUT_WAITING_FOR_ACCEPTED = 4; - - /** Returns a human-readable description for an insert rejection {@code reason} code. */ - public static String getDataInsertRejectedReason(short reason) { - if (reason == DATA_INSERT_REJECTED_VERIFY_FAILED) { - return "Verify failed"; - } else if (reason == DATA_INSERT_REJECTED_RECEIVE_FAILED) { - return "Receive failed"; - } else if (reason == DATA_INSERT_REJECTED_SSK_ERROR) { - return "SSK error"; - } else if (reason == DATA_INSERT_REJECTED_TIMEOUT_WAITING_FOR_ACCEPTED) { - return "Timeout waiting for Accepted (moved on)"; - } - return "Unknown reason code: " + reason; - } - - // Consider using this again now we have a packet format that can handle big messages on any - // connection. - // If re-enabled, ensure priority handling for realtime and be mindful of timeouts associated - // with sending a request at BULK. - - /** Legacy SSK insert request carrying headers, data, and pubkey hash in one message. */ - public static final MessageType FNPSSKInsertRequest = - new MessageType("FNPSSKInsertRequest", PRIORITY_BULK_DATA); - - static { - FNPSSKInsertRequest.addField(UID, Long.class); - FNPSSKInsertRequest.addField(HTL, Short.class); - FNPSSKInsertRequest.addField(FREENET_ROUTING_KEY, NodeSSK.class); - FNPSSKInsertRequest.addField(NEAREST_LOCATION, Double.class); - FNPSSKInsertRequest.addField(BLOCK_HEADERS, ShortBuffer.class); - FNPSSKInsertRequest.addField(PUBKEY_HASH, ShortBuffer.class); - FNPSSKInsertRequest.addField(DATA, ShortBuffer.class); - } - - /** - * Creates a legacy {@code FNPSSKInsertRequest} bundling metadata and data. - * - * @param uid Insert identifier. - * @param htl Hops-to-live. - * @param myKey Routing key (SSK). - * @param headers Serialized block headers. - * @param data Serialized content. - * @param pubKeyHash Hash of the public key. - * @param realTime Boost to realtime if {@code true}. - * @return Initialized message. - */ - public static Message createFNPSSKInsertRequest( - long uid, - short htl, - NodeSSK myKey, - byte[] headers, - byte[] data, - byte[] pubKeyHash, - boolean realTime) { - Message msg = new Message(FNPSSKInsertRequest); - msg.set(UID, uid); - msg.set(HTL, htl); - msg.set(FREENET_ROUTING_KEY, myKey); - msg.set(NEAREST_LOCATION, 0.0); - msg.set(BLOCK_HEADERS, new ShortBuffer(headers)); - msg.set(PUBKEY_HASH, new ShortBuffer(pubKeyHash)); - msg.set(DATA, new ShortBuffer(data)); - if (realTime) { - msg.boostPriority(); - } - return msg; - } - - /** Modern SSK insert request; headers and data are sent in follow-up bulk messages. */ - public static final MessageType FNPSSKInsertRequestNew = - new MessageType("FNPSSKInsertRequestNew", PRIORITY_LOW); - - static { - FNPSSKInsertRequestNew.addField(UID, Long.class); - FNPSSKInsertRequestNew.addField(HTL, Short.class); - FNPSSKInsertRequestNew.addField(FREENET_ROUTING_KEY, NodeSSK.class); - } - - /** - * Creates the initial {@code FNPSSKInsertRequestNew} handshake. - * - * @param uid Insert identifier. - * @param htl Hops-to-live. - * @param myKey Routing key (SSK). - * @return Initialized message. - */ - public static Message createFNPSSKInsertRequestNew(long uid, short htl, NodeSSK myKey) { - Message msg = new Message(FNPSSKInsertRequestNew); - msg.set(UID, uid); - msg.set(HTL, htl); - msg.set(FREENET_ROUTING_KEY, myKey); - return msg; - } - - // SSK inserts data and headers. These are BULK_DATA or REALTIME. - - /** Follow-up message carrying SSK insert block headers. */ - public static final MessageType FNPSSKInsertRequestHeaders = - new MessageType("FNPSSKInsertRequestHeaders", PRIORITY_BULK_DATA); - - static { - FNPSSKInsertRequestHeaders.addField(UID, Long.class); - FNPSSKInsertRequestHeaders.addField(BLOCK_HEADERS, ShortBuffer.class); - } - - /** - * Creates {@code FNPSSKInsertRequestHeaders} with serialized block headers. - * - * @param uid Insert identifier. - * @param headers Serialized headers. - * @param realTime Boost to realtime if {@code true}. - * @return Initialized message. - */ - public static Message createFNPSSKInsertRequestHeaders( - long uid, byte[] headers, boolean realTime) { - Message msg = new Message(FNPSSKInsertRequestHeaders); - msg.set(UID, uid); - msg.set(BLOCK_HEADERS, new ShortBuffer(headers)); - if (realTime) { - msg.boostPriority(); - } - return msg; - } - - /** Follow-up message carrying SSK insert payload data. */ - public static final MessageType FNPSSKInsertRequestData = - new MessageType("FNPSSKInsertRequestData", PRIORITY_BULK_DATA); - - static { - FNPSSKInsertRequestData.addField(UID, Long.class); - FNPSSKInsertRequestData.addField(DATA, ShortBuffer.class); - } - - /** - * Creates {@code FNPSSKInsertRequestData} with serialized payload. - * - * @param uid Insert identifier. - * @param data Serialized content bytes. - * @param realTime Boost to realtime if {@code true}. - * @return Initialized message. - */ - public static Message createFNPSSKInsertRequestData(long uid, byte[] data, boolean realTime) { - Message msg = new Message(FNPSSKInsertRequestData); - msg.set(UID, uid); - msg.set(DATA, new ShortBuffer(data)); - if (realTime) { - msg.boostPriority(); - } - return msg; - } - - // SSK pubkeys, data and headers are all BULK_DATA or REALTIME. - // Requests wait for them all equally, so there is no reason for them to be different, - // plus everything is throttled now. - - /** SSK positive result containing block headers. */ - public static final MessageType FNPSSKDataFoundHeaders = - new MessageType("FNPSSKDataFoundHeaders", PRIORITY_BULK_DATA); - - static { - FNPSSKDataFoundHeaders.addField(UID, Long.class); - FNPSSKDataFoundHeaders.addField(BLOCK_HEADERS, ShortBuffer.class); - } - - /** - * Creates {@code FNPSSKDataFoundHeaders}. - * - * @param uid Request identifier. - * @param headers Serialized headers. - * @param realTime Boost to realtime if {@code true}. - * @return Initialized message. - */ - public static Message createFNPSSKDataFoundHeaders(long uid, byte[] headers, boolean realTime) { - Message msg = new Message(FNPSSKDataFoundHeaders); - msg.set(UID, uid); - msg.set(BLOCK_HEADERS, new ShortBuffer(headers)); - if (realTime) { - msg.boostPriority(); - } - return msg; - } - - /** SSK positive result containing payload data. */ - public static final MessageType FNPSSKDataFoundData = - new MessageType("FNPSSKDataFoundData", PRIORITY_BULK_DATA); - - static { - FNPSSKDataFoundData.addField(UID, Long.class); - FNPSSKDataFoundData.addField(DATA, ShortBuffer.class); - } - - /** - * Creates {@code FNPSSKDataFoundData}. - * - * @param uid Request identifier. - * @param data Serialized content bytes. - * @param realTime Boost to realtime if {@code true}. - * @return Initialized message. - */ - public static Message createFNPSSKDataFoundData(long uid, byte[] data, boolean realTime) { - Message msg = new Message(FNPSSKDataFoundData); - msg.set(UID, uid); - msg.set(DATA, new ShortBuffer(data)); - if (realTime) { - msg.boostPriority(); - } - return msg; - } - - /** Acknowledges an SSK request and indicates whether a public key is needed. */ - public static final MessageType FNPSSKAccepted = new MessageType("FNPSSKAccepted", PRIORITY_HIGH); - - static { - FNPSSKAccepted.addField(UID, Long.class); - FNPSSKAccepted.addField(NEED_PUB_KEY, Boolean.class); - } - - /** - * Creates an {@code FNPSSKAccepted}. - * - * @param uid Request identifier. - * @param needPubKey Whether the receiver requires the public key. - * @return Initialized message. - */ - public static Message createFNPSSKAccepted(long uid, boolean needPubKey) { - Message msg = new Message(FNPSSKAccepted); - msg.set(UID, uid); - msg.set(NEED_PUB_KEY, needPubKey); - return msg; - } - - /** SSK public key payload. */ - public static final MessageType FNPSSKPubKey = - new MessageType("FNPSSKPubKey", PRIORITY_BULK_DATA); - - static { - FNPSSKPubKey.addField(UID, Long.class); - FNPSSKPubKey.addField(PUBKEY_AS_BYTES, ShortBuffer.class); - } - - /** - * Creates an {@code FNPSSKPubKey} containing a padded public key. - * - * @param uid Request identifier. - * @param pubkey Public key to send. - * @param realTime Boost to realtime if {@code true}. - * @return Initialized message. - */ - public static Message createFNPSSKPubKey(long uid, DSAPublicKey pubkey, boolean realTime) { - Message msg = new Message(FNPSSKPubKey); - msg.set(UID, uid); - msg.set(PUBKEY_AS_BYTES, new ShortBuffer(pubkey.asPaddedBytes())); - if (realTime) { - msg.boostPriority(); - } - return msg; - } - - /** Acknowledges receipt of an SSK public key. */ - public static final MessageType FNPSSKPubKeyAccepted = - new MessageType("FNPSSKPubKeyAccepted", PRIORITY_HIGH); - - static { - FNPSSKPubKeyAccepted.addField(UID, Long.class); - } - - /** - * Creates an {@code FNPSSKPubKeyAccepted} acknowledgment. - * - * @param uid Request identifier. - * @return Initialized message. - */ - public static Message createFNPSSKPubKeyAccepted(long uid) { - Message msg = new Message(FNPSSKPubKeyAccepted); - msg.set(UID, uid); - return msg; - } - - // Opennet completions (not sent to darknet nodes) - - /** - * Sent when a request to an opennet node completes and the data source does not path-fold. Also - * used on darknet. - */ - public static final MessageType FNPOpennetCompletedAck = - new MessageType("FNPOpennetCompletedAck", PRIORITY_HIGH); - - static { - FNPOpennetCompletedAck.addField(UID, Long.class); - } - - /** - * Creates an {@code FNPOpennetCompletedAck} for the given request. - * - * @param uid Original request identifier. - * @return Initialized message. - */ - public static Message createFNPOpennetCompletedAck(long uid) { - Message msg = new Message(FNPOpennetCompletedAck); - msg.set(UID, uid); - return msg; - } - - /** Sent when we wait for an FNP transfer or a completion from upstream, and it never comes. */ - public static final MessageType FNPOpennetCompletedTimeout = - new MessageType("FNPOpennetCompletedTimeout", PRIORITY_HIGH); - - static { - FNPOpennetCompletedTimeout.addField(UID, Long.class); - } - - /** - * Creates an {@code FNPOpennetCompletedTimeout} when the expected transfer or completion never - * arrived. - * - * @param uid Original request identifier. - * @return Initialized message. - */ - public static Message createFNPOpennetCompletedTimeout(long uid) { - Message msg = new Message(FNPOpennetCompletedTimeout); - msg.set(UID, uid); - return msg; - } - - /** - * Sent when a request completes and the data source wants to path fold. Starts a bulk data - * transfer including the (padded) noderef. - */ - public static final MessageType FNPOpennetConnectDestinationNew = - new MessageType("FNPConnectDestinationNew", PRIORITY_UNSPECIFIED); - - static { - FNPOpennetConnectDestinationNew.addField(UID, Long.class); // UID of the original message chain - FNPOpennetConnectDestinationNew.addField(TRANSFER_UID, Long.class); // UID of data transfer - FNPOpennetConnectDestinationNew.addField(NODEREF_LENGTH, Integer.class); // Size of noderef - FNPOpennetConnectDestinationNew.addField( - PADDED_LENGTH, Integer.class); // Size of actual transfer i.e., padded length - } - - /** - * Creates an {@code FNPOpennetConnectDestinationNew} to initiate path folding with a noderef - * transfer. - * - * @param uid Original request identifier. - * @param transferUID Transfer identifier for the noderef payload. - * @param noderefLength Unpadded noderef length in bytes. - * @param paddedLength Padded transfer length in bytes. - * @return Initialized message. - */ - public static Message createFNPOpennetConnectDestinationNew( - long uid, long transferUID, int noderefLength, int paddedLength) { - Message msg = new Message(FNPOpennetConnectDestinationNew); - msg.set(UID, uid); - msg.set(TRANSFER_UID, transferUID); - msg.set(NODEREF_LENGTH, noderefLength); - msg.set(PADDED_LENGTH, paddedLength); - return msg; - } - - /** - * Path folding response. Sent when the requestor wants to path fold and has received a noderef - * from the data source. Starts a bulk data transfer including the (padded) noderef. - */ - public static final MessageType FNPOpennetConnectReplyNew = - new MessageType("FNPConnectReplyNew", PRIORITY_UNSPECIFIED); - - static { - FNPOpennetConnectReplyNew.addField(UID, Long.class); // UID of the original message chain - FNPOpennetConnectReplyNew.addField(TRANSFER_UID, Long.class); // UID of data transfer - FNPOpennetConnectReplyNew.addField(NODEREF_LENGTH, Integer.class); // Size of noderef - FNPOpennetConnectReplyNew.addField( - PADDED_LENGTH, Integer.class); // Size of actual transfer i.e., padded length - } - - /** - * Creates an {@code FNPOpennetConnectReplyNew} to accept path folding and start the transfer. - * - * @param uid Original request identifier. - * @param transferUID Transfer identifier for the noderef payload. - * @param noderefLength Unpadded noderef length in bytes. - * @param paddedLength Padded transfer length in bytes. - * @return Initialized message. - */ - public static Message createFNPOpennetConnectReplyNew( - long uid, long transferUID, int noderefLength, int paddedLength) { - Message msg = new Message(FNPOpennetConnectReplyNew); - msg.set(UID, uid); - msg.set(TRANSFER_UID, transferUID); - msg.set(NODEREF_LENGTH, noderefLength); - msg.set(PADDED_LENGTH, paddedLength); - return msg; - } - - // Opennet announcement - - /** - * Announcement request. Noderef is attached, will be transferred before anything else is done. - */ - public static final MessageType FNPOpennetAnnounceRequest = - new MessageType("FNPOpennetAnnounceRequest", PRIORITY_HIGH); - - static { - FNPOpennetAnnounceRequest.addField(UID, Long.class); - FNPOpennetAnnounceRequest.addField(TRANSFER_UID, Long.class); - FNPOpennetAnnounceRequest.addField(NODEREF_LENGTH, Integer.class); - FNPOpennetAnnounceRequest.addField(PADDED_LENGTH, Integer.class); - FNPOpennetAnnounceRequest.addField(HTL, Short.class); - FNPOpennetAnnounceRequest.addField(NEAREST_LOCATION, Double.class); - FNPOpennetAnnounceRequest.addField(TARGET_LOCATION, Double.class); - } - - /** - * Creates an {@code FNPOpennetAnnounceRequest} with a padded noderef to be transferred first. - * - * @param request announce request metadata - * @return Initialized message. - */ - public static Message createFNPOpennetAnnounceRequest(OpennetAnnounceRequest request) { - Message msg = new Message(FNPOpennetAnnounceRequest); - msg.set(UID, request.uid()); - msg.set(TRANSFER_UID, request.transferUID()); - msg.set(NODEREF_LENGTH, request.noderefLength()); - msg.set(PADDED_LENGTH, request.paddedLength()); - msg.set(HTL, request.htl()); - msg.set(NEAREST_LOCATION, 0.0); - msg.set(TARGET_LOCATION, request.target()); - return msg; - } - - /** - * Announcement reply. Noderef is attached and transferred; both nodes add the other. A single - * request may result in many replies. When the announcement is done, we return a DataNotFound; if - * we run into a dead-end, we return a RejectedLoop; if we can't accept it, RejectedOverload. - */ - public static final MessageType FNPOpennetAnnounceReply = - new MessageType("FNPOpennetAnnounceReply", PRIORITY_UNSPECIFIED); - - static { - FNPOpennetAnnounceReply.addField(UID, Long.class); - FNPOpennetAnnounceReply.addField(TRANSFER_UID, Long.class); - FNPOpennetAnnounceReply.addField(NODEREF_LENGTH, Integer.class); - FNPOpennetAnnounceReply.addField(PADDED_LENGTH, Integer.class); - } - - /** - * Creates an {@code FNPOpennetAnnounceReply} referencing an attached noderef transfer. - * - * @param uid Original request identifier. - * @param transferUID Transfer identifier for the noderef payload. - * @param noderefLength Unpadded noderef length in bytes. - * @param paddedLength Padded transfer length in bytes. - * @return Initialized message. - */ - public static Message createFNPOpennetAnnounceReply( - long uid, long transferUID, int noderefLength, int paddedLength) { - Message msg = new Message(FNPOpennetAnnounceReply); - msg.set(UID, uid); - msg.set(TRANSFER_UID, transferUID); - msg.set(NODEREF_LENGTH, noderefLength); - msg.set(PADDED_LENGTH, paddedLength); - return msg; - } - - public static final MessageType FNPOpennetAnnounceCompleted = - new MessageType("FNPOpennetAnnounceCompleted", PRIORITY_UNSPECIFIED); - - static { - FNPOpennetAnnounceCompleted.addField(UID, Long.class); - } - - /** Creates an {@code FNPOpennetAnnounceCompleted} notification. */ - public static Message createFNPOpennetAnnounceCompleted(long uid) { - Message msg = new Message(FNPOpennetAnnounceCompleted); - msg.set(UID, uid); - return msg; - } - - /** Indicates that opennet functionality is disabled on the peer. */ - public static final MessageType FNPOpennetDisabled = - new MessageType("FNPOpennetDisabled", PRIORITY_HIGH); - - static { - FNPOpennetDisabled.addField(UID, Long.class); - } - - /** Creates an {@code FNPOpennetDisabled} notification. */ - public static Message createFNPOpennetDisabled(long uid) { - Message msg = new Message(FNPOpennetDisabled); - msg.set(UID, uid); - return msg; - } - - /** Noderef was rejected during path folding; includes a rejection code. */ - public static final MessageType FNPOpennetNoderefRejected = - new MessageType("FNPOpennetNoderefRejected", PRIORITY_HIGH); - - static { - FNPOpennetNoderefRejected.addField(UID, Long.class); - FNPOpennetNoderefRejected.addField(REJECT_CODE, Integer.class); - } - - /** - * Creates an {@code FNPOpennetNoderefRejected} with a reason code. - * - * @param uid Original request identifier. - * @param rejectCode One of the {@code NODEREF_REJECTED_*} constants. - * @return Initialized message. - */ - public static Message createFNPOpennetNoderefRejected(long uid, int rejectCode) { - Message msg = new Message(FNPOpennetNoderefRejected); - msg.set(UID, uid); - msg.set(REJECT_CODE, rejectCode); - return msg; - } - - /** Returns a human-readable description for an opennet noderef rejection code. */ - public static String getOpennetRejectedCode(int x) { - return switch (x) { - case NODEREF_REJECTED_TOO_BIG -> "Too big"; - case NODEREF_REJECTED_REAL_BIGGER_THAN_PADDED -> "Real length bigger than padded length"; - case NODEREF_REJECTED_TRANSFER_FAILED -> "Transfer failed"; - case NODEREF_REJECTED_INVALID -> "Invalid noderef"; - default -> "Unknown rejection code " + x; - }; - } - - public static final int NODEREF_REJECTED_TOO_BIG = 1; - public static final int NODEREF_REJECTED_REAL_BIGGER_THAN_PADDED = 2; - public static final int NODEREF_REJECTED_TRANSFER_FAILED = 3; - public static final int NODEREF_REJECTED_INVALID = 4; - - // Legacy message retained; evaluate removal in a future cleanup. - - /** Announcement declined: peer is not accepting new opennet links. */ - public static final MessageType FNPOpennetAnnounceNodeNotWanted = - new MessageType("FNPOpennetAnnounceNodeNotWanted", PRIORITY_LOW); - - static { - FNPOpennetAnnounceNodeNotWanted.addField(UID, Long.class); - } - - public static Message createFNPOpennetAnnounceNodeNotWanted(long uid) { - Message msg = new Message(FNPOpennetAnnounceNodeNotWanted); - msg.set(UID, uid); - return msg; - } - - // Key offers (ULPRs) - - /** Offer a key (ULPR) out-of-band prior to a get. */ - public static final MessageType FNPOfferKey = new MessageType("FNPOfferKey", PRIORITY_LOW); - - static { - FNPOfferKey.addField(KEY, Key.class); - FNPOfferKey.addField(OFFER_AUTHENTICATOR, ShortBuffer.class); - } - - /** - * Creates an {@code FNPOfferKey} with a keyed authenticator. - * - * @param key Offered key. - * @param authenticator Authenticator bytes. - * @return Initialized message. - */ - public static Message createFNPOfferKey(Key key, byte[] authenticator) { - Message msg = new Message(FNPOfferKey); - msg.set(KEY, key); - msg.set(OFFER_AUTHENTICATOR, new ShortBuffer(authenticator)); - return msg; - } - - // Short timeout; priority choice is explicit below. - public static final MessageType FNPGetOfferedKey = - new MessageType("FNPGetOfferedKey", PRIORITY_LOW); - - static { - FNPGetOfferedKey.addField(KEY, Key.class); - FNPGetOfferedKey.addField(OFFER_AUTHENTICATOR, ShortBuffer.class); - FNPGetOfferedKey.addField(NEED_PUB_KEY, Boolean.class); - FNPGetOfferedKey.addField(UID, Long.class); - } - - /** - * Requests data for a previously offered key. - * - * @param key Key to fetch. - * @param authenticator Authenticator matching the offer. - * @param needPubkey Whether a public key should be returned when applicable. - * @param uid Request identifier. - * @return Initialized message. - */ - public static Message createFNPGetOfferedKey( - Key key, byte[] authenticator, boolean needPubkey, long uid) { - Message msg = new Message(FNPGetOfferedKey); - msg.set(KEY, key); - msg.set(OFFER_AUTHENTICATOR, new ShortBuffer(authenticator)); - msg.set(NEED_PUB_KEY, needPubkey); - msg.set(UID, uid); - return msg; - } - - // Permanently rejected. {@code FNPRejectedOverload} is temporary. - public static final MessageType FNPGetOfferedKeyInvalid = - new MessageType("FNPGetOfferedKeyInvalid", PRIORITY_HIGH); - - static { // short timeout - FNPGetOfferedKeyInvalid.addField(UID, Long.class); - FNPGetOfferedKeyInvalid.addField(REASON, Short.class); - } - - /** - * Creates an {@code FNPGetOfferedKeyInvalid} with a reason code. - * - * @param uid Request identifier. - * @param reason One of the {@code GET_OFFERED_KEY_REJECTED_*} constants. - * @return Initialized message. - */ - public static Message createFNPGetOfferedKeyInvalid(long uid, short reason) { - Message msg = new Message(FNPGetOfferedKeyInvalid); - msg.set(UID, uid); - msg.set(REASON, reason); - return msg; - } - - public static final short GET_OFFERED_KEY_REJECTED_BAD_AUTHENTICATOR = 1; - public static final short GET_OFFERED_KEY_REJECTED_NO_KEY = 2; - - /** Link-level ping for reachability/latency checks. */ - public static final MessageType FNPPing = new MessageType("FNPPing", PRIORITY_HIGH); - - static { - FNPPing.addField(PING_SEQNO, Integer.class); - } - - /** - * Creates a ping with a sequence number echoed by {@code FNPPong}. - * - * @param seqNo Sequence number. - * @return Initialized message. - */ - public static Message createFNPPing(int seqNo) { - Message msg = new Message(FNPPing); - msg.set(PING_SEQNO, seqNo); - return msg; - } - - /** Ping response echoing the sequence number. */ - public static final MessageType FNPPong = new MessageType("FNPPong", PRIORITY_HIGH); - - static { - FNPPong.addField(PING_SEQNO, Integer.class); - } - - /** - * Creates a pong echoing the sequence number from a ping. - * - * @param seqNo Sequence number. - * @return Initialized message. - */ - public static Message createFNPPong(int seqNo) { - Message msg = new Message(FNPPong); - msg.set(PING_SEQNO, seqNo); - return msg; - } - - /** Reply containing routing-hints metrics gathered during a probe. */ - public static final MessageType FNPRHProbeReply = - new MessageType("FNPRHProbeReply", PRIORITY_UNSPECIFIED); - - static { - FNPRHProbeReply.addField(UID, Long.class); - FNPRHProbeReply.addField(NEAREST_LOCATION, Double.class); - FNPRHProbeReply.addField(BEST_LOCATION, Double.class); - FNPRHProbeReply.addField(COUNTER, Short.class); - FNPRHProbeReply.addField(UNIQUE_COUNTER, Short.class); - FNPRHProbeReply.addField(LINEAR_COUNTER, Short.class); - } - - /** - * Creates an {@code FNPRHProbeReply}. - * - * @param uid Probe identifier. - * @param nearest Nearest known location. - * @param best Best known location. - * @param counter General counter value. - * @param uniqueCounter Distinct path count (semantics implementation-defined). - * @param linearCounter Linearized counter (semantics implementation-defined). - * @return Initialized message. - */ - public static Message createFNPRHProbeReply( - long uid, - double nearest, - double best, - short counter, - short uniqueCounter, - short linearCounter) { - Message msg = new Message(FNPRHProbeReply); - msg.set(UID, uid); - msg.set(NEAREST_LOCATION, nearest); - msg.set(BEST_LOCATION, best); - msg.set(COUNTER, counter); - msg.set(UNIQUE_COUNTER, uniqueCounter); - msg.set(LINEAR_COUNTER, linearCounter); - return msg; - } - - public static final MessageType ProbeRequest = new MessageType("ProbeRequest", PRIORITY_HIGH); - - static { - ProbeRequest.addField(HTL, Byte.class); - ProbeRequest.addField(UID, Long.class); - ProbeRequest.addField(TYPE, Byte.class); - } - - /** - * Constructs a probe request. - * - * @param htl hopsToLive: hops until a result is requested. - * @param uid Probe identifier: should be unique. - * @return Message with requested attributes. - */ - public static Message createProbeRequest(byte htl, long uid, Type type) { - Message msg = new Message(ProbeRequest); - msg.set(HTL, htl); - msg.set(UID, uid); - msg.set(TYPE, type.code); - return msg; - } - - public static final MessageType ProbeError = new MessageType("ProbeError", PRIORITY_HIGH); - - static { - ProbeError.addField(UID, Long.class); - ProbeError.addField(TYPE, Byte.class); - } - - /** - * Creates a probe response which indicates there was an error. - * - * @param uid Probe identifier. - * @param error The type of error that occurred. Can be one of Probe.ProbeError. - * @return Message with the requested attributes. - */ - public static Message createProbeError(long uid, Error error) { - Message msg = new Message(ProbeError); - msg.set(UID, uid); - msg.set(TYPE, error.code); - return msg; - } - - public static final MessageType ProbeRefused = new MessageType("ProbeRefused", PRIORITY_HIGH); - - static { - ProbeRefused.addField(DMT.UID, Long.class); - } - - /** - * Creates a probe response which indicates that the endpoint opted not to respond with the - * requested result. - * - * @param uid Probe identifier. - * @return Message with the requested attribute. - */ - public static Message createProbeRefused(long uid) { - Message msg = new Message(ProbeRefused); - msg.set(UID, uid); - return msg; - } - - public static final MessageType ProbeBandwidth = new MessageType("ProbeBandwidth", PRIORITY_HIGH); - - static { - ProbeBandwidth.addField(UID, Long.class); - ProbeBandwidth.addField(OUTPUT_BANDWIDTH_UPPER_LIMIT, Float.class); - } - - /** - * Creates a probe response to a query for bandwidth limits. - * - * @param uid Probe identifier. - * @param limit Endpoint output bandwidth limit in KiB per second. - * @return Message with requested attributes. - */ - public static Message createProbeBandwidth(long uid, float limit) { - Message msg = new Message(ProbeBandwidth); - msg.set(UID, uid); - msg.set(OUTPUT_BANDWIDTH_UPPER_LIMIT, limit); - return msg; - } - - public static final MessageType ProbeBuild = new MessageType("ProbeBuild", PRIORITY_HIGH); - - static { - ProbeBuild.addField(UID, Long.class); - ProbeBuild.addField(BUILD, Integer.class); - } - - /** - * Creates a probe response to a query for build. - * - * @param uid Probe identifier. - * @param build Endpoint build of Freenet. - * @return Message with requested attributes. - */ - public static Message createProbeBuild(long uid, int build) { - Message msg = new Message(ProbeBuild); - msg.set(UID, uid); - msg.set(BUILD, build); - return msg; - } - - public static final MessageType ProbeIdentifier = - new MessageType("ProbeIdentifier", PRIORITY_HIGH); - - static { - ProbeIdentifier.addField(UID, Long.class); - ProbeIdentifier.addField(PROBE_IDENTIFIER, Long.class); - ProbeIdentifier.addField(UPTIME_PERCENT, Byte.class); - } - - /** - * Creates a probe response to a query for identifier. - * - * @param uid Probe UID. - * @param probeId Endpoint identifier. - * @param uptimePercentage 7-day uptime percentage. - * @return Message with requested attributes. - */ - public static Message createProbeIdentifier(long uid, long probeId, byte uptimePercentage) { - Message msg = new Message(ProbeIdentifier); - msg.set(UID, uid); - msg.set(PROBE_IDENTIFIER, probeId); - msg.set(UPTIME_PERCENT, uptimePercentage); - return msg; - } - - public static final MessageType ProbeLinkLengths = - new MessageType("ProbeLinkLengths", PRIORITY_HIGH); - - static { - ProbeLinkLengths.addField(UID, Long.class); - ProbeLinkLengths.addField(LINK_LENGTHS, float[].class); - } - - /** - * Creates a probe response to a query for link lengths. - * - * @param uid Probe identifier. - * @param linkLengths Endpoint link lengths. - * @return Message with requested attributes. - */ - public static Message createProbeLinkLengths(long uid, float[] linkLengths) { - Message msg = new Message(ProbeLinkLengths); - msg.set(UID, uid); - msg.set(LINK_LENGTHS, linkLengths); - return msg; - } - - public static final MessageType ProbeLocation = new MessageType("ProbeLocation", PRIORITY_HIGH); - - static { - ProbeLocation.addField(UID, Long.class); - ProbeLocation.addField(LOCATION, Float.class); - } - - /** - * Creates a probe response to a query for location. - * - * @param uid Probe identifier. - * @param location Endpoint location. - * @return Message with the requested attributes. - */ - public static Message createProbeLocation(long uid, float location) { - Message msg = new Message(ProbeLocation); - msg.set(UID, uid); - msg.set(LOCATION, location); - return msg; - } - - public static final MessageType ProbeStoreSize = new MessageType("ProbeStoreSize", PRIORITY_HIGH); - - static { - ProbeStoreSize.addField(UID, Long.class); - ProbeStoreSize.addField(STORE_SIZE, Float.class); - } - - /** - * Creates a probe response to a query for store size. - * - * @param uid Probe identifier. - * @param storeSize Endpoint store size in GiB multiplied by Gaussian noise. - * @return Message with requested attributes. - */ - public static Message createProbeStoreSize(long uid, float storeSize) { - Message msg = new Message(ProbeStoreSize); - msg.set(UID, uid); - msg.set(STORE_SIZE, storeSize); - return msg; - } - - public static final MessageType ProbeUptime = new MessageType("ProbeUptime", PRIORITY_HIGH); - - static { - ProbeUptime.addField(UID, Long.class); - ProbeUptime.addField(UPTIME_PERCENT, Float.class); - } - - /** - * Creates a probe response to a query for uptime. - * - * @param uid Probe identifier. - * @param uptimePercent Percentage of the requested period (48 hours or 7 days) which the endpoint - * was online. - * @return Message with requested attributes. - */ - public static Message createProbeUptime(long uid, float uptimePercent) { - Message msg = new Message(ProbeUptime); - msg.set(UID, uid); - msg.set(UPTIME_PERCENT, uptimePercent); - return msg; - } - - public static final MessageType ProbeRejectStats = - new MessageType("ProbeRejectStats", PRIORITY_HIGH); - - static { - ProbeRejectStats.addField(UID, Long.class); - ProbeRejectStats.addField(REJECT_STATS, ShortBuffer.class); - } - - public static Message createProbeRejectStats(long uid, byte[] rejectStats) { - Message msg = new Message(ProbeRejectStats); - msg.set(UID, uid); - msg.set(REJECT_STATS, new ShortBuffer(rejectStats)); - return msg; - } - - /** Divide the output bandwidth limit by this to get bandwidth class. */ - static final int CAPACITY_USAGE_MULTIPLIER = 10 * 1024; - - /** Maximum value of bandwidth class */ - static final byte CAPACITY_USAGE_MAX = 10; - - /** Minimum value of bandwidth class */ - static final byte CAPACITY_USAGE_MIN = 1; - - public static final MessageType ProbeOverallBulkOutputCapacityUsage = - new MessageType("ProbeOverallBulkOutputCapacityUsage", PRIORITY_HIGH); - - static { - ProbeOverallBulkOutputCapacityUsage.addField(UID, Long.class); // UID for the probe - ProbeOverallBulkOutputCapacityUsage.addField( - OUTPUT_BANDWIDTH_CLASS, Byte.class); // Approximate bandwidth, severely truncated. - ProbeOverallBulkOutputCapacityUsage.addField( - CAPACITY_USAGE, Float.class); // Noisy capacity usage. - } - - /** - * Coarsens a bandwidth limit to a small ordinal class for probes. - * - * @param bandwidthLimit Output bandwidth limit in bytes per second. - * @return A class in the range [{@link #CAPACITY_USAGE_MIN}, {@link #CAPACITY_USAGE_MAX}]. - */ - public static byte bandwidthClassForCapacityUsage(int bandwidthLimit) { - bandwidthLimit /= CAPACITY_USAGE_MULTIPLIER; - bandwidthLimit = Math.min(bandwidthLimit, CAPACITY_USAGE_MAX); - return (byte) Math.max(bandwidthLimit, CAPACITY_USAGE_MIN); - } - - /** - * Creates a probe message with an approximate output bandwidth class and a noisy capacity usage - * estimate. - * - * @param uid Probe identifier. - * @param outputBandwidthClass Ordinal class from {@link #bandwidthClassForCapacityUsage(int)}. - * @param capacityUsage Noisy capacity usage (unit-less fraction). - * @return Initialized message. - */ - public static Message createProbeOverallBulkOutputCapacityUsage( - long uid, byte outputBandwidthClass, float capacityUsage) { - Message msg = new Message(ProbeOverallBulkOutputCapacityUsage); - msg.set(UID, uid); - msg.set(OUTPUT_BANDWIDTH_CLASS, outputBandwidthClass); - msg.set(CAPACITY_USAGE, capacityUsage); - return msg; - } - - /** Start of a four-phase location swap handshake. */ - public static final MessageType FNPSwapRequest = new MessageType("FNPSwapRequest", PRIORITY_HIGH); - - static { - FNPSwapRequest.addField(UID, Long.class); - FNPSwapRequest.addField(HASH, ShortBuffer.class); - FNPSwapRequest.addField(HTL, Integer.class); - } - - /** - * Creates an {@code FNPSwapRequest}. - * - * @param uid Swap identifier. - * @param buf Hash or token bytes. - * @param htl Hops-to-live. - * @return Initialized message. - */ - public static Message createFNPSwapRequest(long uid, byte[] buf, int htl) { - Message msg = new Message(FNPSwapRequest); - msg.set(UID, uid); - msg.set(HASH, new ShortBuffer(buf)); - msg.set(HTL, htl); - return msg; - } - - /** Swap rejected; aborts the handshake. */ - public static final MessageType FNPSwapRejected = - new MessageType("FNPSwapRejected", PRIORITY_HIGH); - - static { - FNPSwapRejected.addField(UID, Long.class); - } - - /** - * Creates an {@code FNPSwapRejected}. - * - * @param uid Swap identifier. - * @return Initialized message. - */ - public static Message createFNPSwapRejected(long uid) { - Message msg = new Message(FNPSwapRejected); - msg.set(UID, uid); - return msg; - } - - /** Swap reply carrying a hash/token. */ - public static final MessageType FNPSwapReply = new MessageType("FNPSwapReply", PRIORITY_HIGH); - - static { - FNPSwapReply.addField(UID, Long.class); - FNPSwapReply.addField(HASH, ShortBuffer.class); - } - - /** - * Creates an {@code FNPSwapReply}. - * - * @param uid Swap identifier. - * @param buf Hash or token bytes. - * @return Initialized message. - */ - public static Message createFNPSwapReply(long uid, byte[] buf) { - Message msg = new Message(FNPSwapReply); - msg.set(UID, uid); - msg.set(HASH, new ShortBuffer(buf)); - return msg; - } - - /** Commit step of the swap handshake, carrying final parameters. */ - public static final MessageType FNPSwapCommit = new MessageType("FNPSwapCommit", PRIORITY_HIGH); - - static { - FNPSwapCommit.addField(UID, Long.class); - FNPSwapCommit.addField(DATA, ShortBuffer.class); - } - - /** - * Creates an {@code FNPSwapCommit}. - * - * @param uid Swap identifier. - * @param buf Commit data bytes. - * @return Initialized message. - */ - public static Message createFNPSwapCommit(long uid, byte[] buf) { - Message msg = new Message(FNPSwapCommit); - msg.set(UID, uid); - msg.set(DATA, new ShortBuffer(buf)); - return msg; - } - - /** Final state of the swap; both sides complete. */ - public static final MessageType FNPSwapComplete = - new MessageType("FNPSwapComplete", PRIORITY_HIGH); - - static { - FNPSwapComplete.addField(UID, Long.class); - FNPSwapComplete.addField(DATA, ShortBuffer.class); - } - - /** - * Creates an {@code FNPSwapComplete}. - * - * @param uid Swap identifier. - * @param buf Completion data bytes. - * @return Initialized message. - */ - public static Message createFNPSwapComplete(long uid, byte[] buf) { - Message msg = new Message(FNPSwapComplete); - msg.set(UID, uid); - msg.set(DATA, new ShortBuffer(buf)); - return msg; - } - - /** Notifies peers of a location change and provides peer location hints. */ - public static final MessageType FNPLocChangeNotificationNew = - new MessageType("FNPLocationChangeNotification2", PRIORITY_LOW); - - static { - FNPLocChangeNotificationNew.addField(LOCATION, Double.class); - FNPLocChangeNotificationNew.addField(PEER_LOCATIONS, ShortBuffer.class); - } - - /** - * Creates an {@code FNPLocChangeNotificationNew}. - * - * @param myLocation This the node's new location. - * @param locations Neighbor location hints. - * @return Initialized message. - */ - public static Message createFNPLocChangeNotificationNew(double myLocation, double[] locations) { - Message msg = new Message(FNPLocChangeNotificationNew); - ShortBuffer dst = new ShortBuffer(Fields.doublesToBytes(locations)); - msg.set(LOCATION, myLocation); - msg.set(PEER_LOCATIONS, dst); - - return msg; - } - - /** Routed ping targeting a location (used beyond direct neighbors). */ - public static final MessageType FNPRoutedPing = new MessageType("FNPRoutedPing", PRIORITY_LOW); - - static { - FNPRoutedPing.addRoutedToNodeMessageFields(); - FNPRoutedPing.addField(COUNTER, Integer.class); - } - - /** - * Creates an {@code FNPRoutedPing} targeting a location. - * - * @param uid Request identifier. - * @param targetLocation Target location in keyspace. - * @param htl Hops-to-live. - * @param counter Counter for correlation by caller. - * @param nodeIdentity Identity bytes of the destination node. - * @return Initialized message. - */ - public static Message createFNPRoutedPing( - long uid, double targetLocation, short htl, int counter, byte[] nodeIdentity) { - Message msg = new Message(FNPRoutedPing); - msg.setRoutedToNodeFields(uid, targetLocation, htl, nodeIdentity); - msg.set(COUNTER, counter); - return msg; - } - - /** Reply to a routed ping. */ - public static final MessageType FNPRoutedPong = new MessageType("FNPRoutedPong", PRIORITY_LOW); - - static { - FNPRoutedPong.addField(UID, Long.class); - FNPRoutedPong.addField(COUNTER, Integer.class); - } - - /** - * Creates an {@code FNPRoutedPong} reply. - * - * @param uid Request identifier. - * @param counter Counter echoed from the routed ping. - * @return Initialized message. - */ - public static Message createFNPRoutedPong(long uid, int counter) { - Message msg = new Message(FNPRoutedPong); - msg.set(UID, uid); - msg.set(COUNTER, counter); - return msg; - } - - /** Routed request was rejected; includes the remaining HTL. */ - public static final MessageType FNPRoutedRejected = - new MessageType("FNPRoutedRejected", PRIORITY_UNSPECIFIED); - - static { - FNPRoutedRejected.addField(UID, Long.class); - FNPRoutedRejected.addField(HTL, Short.class); - } - - /** - * Creates an {@code FNPRoutedRejected}. - * - * @param uid Request identifier. - * @param htl Remaining hops-to-live. - * @return Initialized message. - */ - public static Message createFNPRoutedRejected(long uid, short htl) { - Message msg = new Message(FNPRoutedRejected); - msg.set(UID, uid); - msg.set(HTL, htl); - return msg; - } - - /** Announces a detected external address for the remote peer. */ - public static final MessageType FNPDetectedIPAddress = - new MessageType("FNPDetectedIPAddress", PRIORITY_HIGH); - - static { - FNPDetectedIPAddress.addField(EXTERNAL_ADDRESS, Peer.class); - } - - /** - * Creates an {@code FNPDetectedIPAddress} containing a detected external address. - * - * @param peer Peering information with external address. - * @return Initialized message. - */ - public static Message createFNPDetectedIPAddress(Peer peer) { - Message msg = new Message(FNPDetectedIPAddress); - msg.set(EXTERNAL_ADDRESS, peer); - return msg; - } - - /** Wall-clock time synchronization hint. */ - public static final MessageType FNPTime = new MessageType("FNPTime", PRIORITY_HIGH); - - static { - FNPTime.addField(TIME, Long.class); - } - - /** - * Creates an {@code FNPTime} message. - * - * @param time Time in milliseconds since epoch. - * @return Initialized message. - */ - public static Message createFNPTime(long time) { - Message msg = new Message(FNPTime); - msg.set(TIME, time); - return msg; - } - - /** Uptime percentage over a short window for visibility sharing. */ - public static final MessageType FNPUptime = new MessageType("FNPUptime", PRIORITY_LOW); - - static { - FNPUptime.addField(UPTIME_PERCENT_48H, Byte.class); - } - - /** - * Creates an {@code FNPUptime} message. - * - * @param uptimePercent Percent uptime over the recent window. - * @return Initialized message. - */ - public static Message createFNPUptime(byte uptimePercent) { - Message msg = new Message(FNPUptime); - msg.set(UPTIME_PERCENT_48H, uptimePercent); - return msg; - } - - /** Visibility setting toward friends (link-level trust/visibility). */ - public static final MessageType FNPVisibility = new MessageType("FNPVisibility", PRIORITY_HIGH); - - static { - FNPVisibility.addField(FRIEND_VISIBILITY, Short.class); - } - - /** - * Creates an {@code FNPVisibility} message. - * - * @param visibility Visibility code (implementation-defined). - * @return Initialized message. - */ - public static Message createFNPVisibility(short visibility) { - Message msg = new Message(FNPVisibility); - msg.set(FRIEND_VISIBILITY, visibility); - return msg; - } - - // Legacy message retained for compatibility. - public static final MessageType FNPSentPackets = new MessageType("FNPSentPackets", PRIORITY_HIGH); - - static { - FNPSentPackets.addField(TIME_DELTAS, ShortBuffer.class); - FNPSentPackets.addField(HASHES, ShortBuffer.class); - FNPSentPackets.addField(TIME, Long.class); - } - - /** Empty no-op message used as a keepalive or placeholder. */ - public static final MessageType FNPVoid = new MessageType("FNPVoid", PRIORITY_LOW, false, true); - - public static Message createFNPVoid() { - return new Message(FNPVoid); - } - - /** Informs peer about disconnect intent and optional cleanup behavior. */ - public static final MessageType FNPDisconnect = new MessageType("FNPDisconnect", PRIORITY_HIGH); - - static { - // If true, remove from the active routing table, likely to be down for a while. - // Otherwise, just dump all current connection states and keep trying to connect. - FNPDisconnect.addField(REMOVE, Boolean.class); - // If true, purge all references to this node. Otherwise, we can keep the node - // around in secondary tables etc. to more easily reconnect later. - // (Mostly used on opennet) - FNPDisconnect.addField(PURGE, Boolean.class); - // Parting message. Maybe empty. A SimpleFieldSet in exactly the same format - // as an N2NTM. - FNPDisconnect.addField(NODE_TO_NODE_MESSAGE_TYPE, Integer.class); - FNPDisconnect.addField(NODE_TO_NODE_MESSAGE_DATA, ShortBuffer.class); - } - - /** - * Creates an {@code FNPDisconnect} message with optional purge instructions and a parting note. - * - * @param remove Remove from active routing table if {@code true}. - * @param purge Purge references to the node if {@code true}. - * @param messageType Parting note type (node-to-node message type). - * @param messageData Parting note payload; may be empty. - * @return Initialized message. - */ - public static Message createFNPDisconnect( - boolean remove, boolean purge, int messageType, ShortBuffer messageData) { - Message msg = new Message(FNPDisconnect); - msg.set(REMOVE, remove); - msg.set(PURGE, purge); - msg.set(NODE_TO_NODE_MESSAGE_TYPE, messageType); - msg.set(NODE_TO_NODE_MESSAGE_DATA, messageData); - return msg; - } - - // Update over mandatory. Not strictly part of FNP. Only goes between nodes at the link - // level, and will be sent and parsed, even if the node is out of date. Should be stable - // long-term. - - /** - * Announces core update metadata (keys, versions, and recent revocation fetch state) at link - * level. - */ - public static final MessageType CryptadUOMAnnouncement = - new MessageType("CryptadUOMAnnouncement", PRIORITY_LOW); - - static { - CryptadUOMAnnouncement.addField(CORE_PACKAGE_KEY, String.class); - CryptadUOMAnnouncement.addField(REVOCATION_KEY, String.class); - CryptadUOMAnnouncement.addField(HAVE_REVOCATION_KEY, Boolean.class); - CryptadUOMAnnouncement.addField(CORE_PACKAGE_VERSION, Integer.class); - // Last time (ms ago) we had 3 DNFs in a row on the revocation checker. - CryptadUOMAnnouncement.addField(REVOCATION_KEY_TIME_LAST_TRIED, Long.class); - // Number of DNFs so far this time. - CryptadUOMAnnouncement.addField(REVOCATION_KEY_DNF_COUNT, Integer.class); - // For convenience, may change - CryptadUOMAnnouncement.addField(REVOCATION_KEY_FILE_LENGTH, Long.class); - CryptadUOMAnnouncement.addField(CORE_PACKAGE_FILE_LENGTH, Long.class); - CryptadUOMAnnouncement.addField(PING_TIME, Integer.class); - CryptadUOMAnnouncement.addField(BWLIMIT_DELAY_TIME, Integer.class); - } - - /** Fluent builder for {@link #CryptadUOMAnnouncement} messages. */ - public static final class UOMAnnouncementBuilder { - private String corePackageKey; - private String revocationKey; - private boolean haveRevocation; - private int corePackageVersion; - private long timeLastTriedRevocationFetch; - private int revocationDNFCount; - private long revocationKeyLength; - private long corePackageLength; - private int pingTime; - private int bwlimitDelayTime; - - public UOMAnnouncementBuilder corePackageKey(String v) { - this.corePackageKey = v; - return this; - } - - public UOMAnnouncementBuilder revocationKey(String v) { - this.revocationKey = v; - return this; - } - - public UOMAnnouncementBuilder haveRevocation(boolean v) { - this.haveRevocation = v; - return this; - } - - public UOMAnnouncementBuilder corePackageVersion(int v) { - this.corePackageVersion = v; - return this; - } - - public UOMAnnouncementBuilder timeLastTriedRevocationFetch(long v) { - this.timeLastTriedRevocationFetch = v; - return this; - } - - public UOMAnnouncementBuilder revocationDNFCount(int v) { - this.revocationDNFCount = v; - return this; - } - - public UOMAnnouncementBuilder revocationKeyLength(long v) { - this.revocationKeyLength = v; - return this; - } - - public UOMAnnouncementBuilder corePackageLength(long v) { - this.corePackageLength = v; - return this; - } - - public UOMAnnouncementBuilder pingTime(int v) { - this.pingTime = v; - return this; - } - - public UOMAnnouncementBuilder bwlimitDelayTime(int v) { - this.bwlimitDelayTime = v; - return this; - } - - public Message build() { - Message msg = new Message(CryptadUOMAnnouncement); - msg.set(CORE_PACKAGE_KEY, corePackageKey); - msg.set(REVOCATION_KEY, revocationKey); - msg.set(HAVE_REVOCATION_KEY, haveRevocation); - msg.set(CORE_PACKAGE_VERSION, corePackageVersion); - msg.set(REVOCATION_KEY_TIME_LAST_TRIED, timeLastTriedRevocationFetch); - msg.set(REVOCATION_KEY_DNF_COUNT, revocationDNFCount); - msg.set(REVOCATION_KEY_FILE_LENGTH, revocationKeyLength); - msg.set(CORE_PACKAGE_FILE_LENGTH, corePackageLength); - msg.set(PING_TIME, pingTime); - msg.set(BWLIMIT_DELAY_TIME, bwlimitDelayTime); - return msg; - } - } - - /** Requests the revocation file referenced in the announcement. */ - public static final MessageType CryptadUOMRequestRevocation = - new MessageType("CryptadUOMRequestRevocation", PRIORITY_HIGH); - - static { - CryptadUOMRequestRevocation.addField(UID, Long.class); - } - - /** - * Creates a {@code CryptadUOMRequestRevocation} using {@code uid} as the transfer identifier. - * - * @param uid Transfer identifier. - * @return Initialized message. - */ - public static Message createUOMRequestRevocation(long uid) { - Message msg = new Message(CryptadUOMRequestRevocation); - msg.set(UID, uid); - return msg; - } - - // Used by new UOM. - /** Requests the core package referenced in the announcement. */ - public static final MessageType CryptadUOMRequestCorePackage = - new MessageType("CryptadUOMRequestMainJar", PRIORITY_LOW); - - static { - CryptadUOMRequestCorePackage.addField(UID, Long.class); - } - - /** - * Creates a {@code CryptadUOMRequestCorePackage} using {@code uid} as the transfer identifier. - * - * @param uid Transfer identifier. - * @return Initialized message. - */ - public static Message createUOMRequestCorePackage(long uid) { - Message msg = new Message(CryptadUOMRequestCorePackage); - msg.set(UID, uid); - return msg; - } - - /** Announces that a revocation file will be sent, with length and key. */ - public static final MessageType CryptadUOMSendingRevocation = - new MessageType("CryptadUOMSendingRevocation", PRIORITY_HIGH); - - static { - CryptadUOMSendingRevocation.addField(UID, Long.class); - // Probably excessive, but lengths are always long, and wasting a few bytes here - // doesn't matter in the least, as it's very rarely called. - CryptadUOMSendingRevocation.addField(FILE_LENGTH, Long.class); - CryptadUOMSendingRevocation.addField(REVOCATION_KEY, String.class); - } - - /** - * Creates a {@code CryptadUOMSendingRevocation}. - * - * @param uid Transfer identifier. - * @param length File length in bytes. - * @param key Revocation key (URI string). - * @return Initialized message. - */ - public static Message createUOMSendingRevocation(long uid, long length, String key) { - Message msg = new Message(CryptadUOMSendingRevocation); - msg.set(UID, uid); - msg.set(FILE_LENGTH, length); - msg.set(REVOCATION_KEY, key); - return msg; - } - - // Used by new UOM. We need to distinguish them in NodeDispatcher. - /** Announces that the core package will be sent, with length, key, and version. */ - public static final MessageType CryptadUOMSendingCorePackage = - new MessageType("CryptadUOMSendingMainJar", PRIORITY_LOW); - - static { - CryptadUOMSendingCorePackage.addField(UID, Long.class); - CryptadUOMSendingCorePackage.addField(FILE_LENGTH, Long.class); - CryptadUOMSendingCorePackage.addField(CORE_PACKAGE_KEY, String.class); - CryptadUOMSendingCorePackage.addField(CORE_PACKAGE_VERSION, Integer.class); - } - - /** - * Creates a {@code CryptadUOMSendingCorePackage}. - * - * @param uid Transfer identifier. - * @param length File length in bytes. - * @param key Key (URI string) for the core package. - * @param version Build version. - * @return Initialized message. - */ - public static Message createUOMSendingCorePackage( - long uid, long length, String key, int version) { - Message msg = new Message(CryptadUOMSendingCorePackage); - msg.set(UID, uid); - msg.set(FILE_LENGTH, length); - msg.set(CORE_PACKAGE_KEY, key); - msg.set(CORE_PACKAGE_VERSION, version); - return msg; - } - - /** - * Used to fetch a file required for deploying an update. The client knows both the size and hash - * of the file. If we don't want to send the data, we should send an FNPBulkReceiveAborted, - * otherwise we just send the data as a BulkTransmitter transfer. - */ - public static final MessageType CryptadUOMFetchDependency = - new MessageType("CryptadUOMFetchDependency", PRIORITY_LOW); - - static { - CryptadUOMFetchDependency.addField(UID, Long.class); // This will be used for the transfer. - CryptadUOMFetchDependency.addField(EXPECTED_HASH, ShortBuffer.class); // Fetch by hash - CryptadUOMFetchDependency.addField(FILE_LENGTH, Long.class); // Both sides know length. - } - - /** - * Creates a {@code CryptadUOMFetchDependency} request. - * - * @param uid Transfer identifier. - * @param hash Expected content hash. - * @param length Expected file length in bytes. - * @return Initialized message. - */ - public static Message createUOMFetchDependency(long uid, byte[] hash, long length) { - Message msg = new Message(CryptadUOMFetchDependency); - msg.set(UID, uid); - msg.set(EXPECTED_HASH, new ShortBuffer(hash)); - msg.set(FILE_LENGTH, length); - return msg; - } - - // Secondary messages (debug messages attached to primary messages) - - /** Secondary diagnostic message listing node UIDs relevant to a swap. */ - public static final MessageType FNPSwapNodeUIDs = - new MessageType("FNPSwapNodeUIDs", PRIORITY_UNSPECIFIED); - - static { - FNPSwapNodeUIDs.addField(NODE_UIDS, ShortBuffer.class); - } - - /** - * Creates an {@code FNPSwapNodeUIDs} diagnostic message. - * - * @param uids Node UID list. - * @return Initialized message. - */ - public static Message createFNPSwapLocations(long[] uids) { - Message msg = new Message(FNPSwapNodeUIDs); - msg.set(NODE_UIDS, new ShortBuffer(Fields.longsToBytes(uids))); - return msg; - } - - // More permanent secondary messages (should perhaps be replaced by new main messages when stable) - - /** Diagnostics: best alternative routes that were not selected. */ - public static final MessageType FNPBestRoutesNotTaken = - new MessageType("FNPBestRoutesNotTaken", PRIORITY_UNSPECIFIED); - - static { - // Maybe this should be some sort of typed array? - // It's just a bunch of doubles anyway. - FNPBestRoutesNotTaken.addField(BEST_LOCATIONS_NOT_VISITED, ShortBuffer.class); - } - - /** - * Creates {@code FNPBestRoutesNotTaken} with serialized locations. - * - * @param locs Serialized double array. - * @return Initialized message. - */ - public static Message createFNPBestRoutesNotTaken(byte[] locs) { - Message msg = new Message(FNPBestRoutesNotTaken); - msg.set(BEST_LOCATIONS_NOT_VISITED, new ShortBuffer(locs)); - return msg; - } - - /** Convenience overload that serializes {@code double[]} to bytes. */ - public static Message createFNPBestRoutesNotTaken(double[] locs) { - return createFNPBestRoutesNotTaken(Fields.doublesToBytes(locs)); - } - - /** Convenience overload for {@code Double[]} collections. */ - public static Message createFNPBestRoutesNotTaken(Double[] doubles) { - double[] locs = new double[doubles.length]; - for (int i = 0; i < locs.length; i++) { - locs[i] = doubles[i]; - } - return createFNPBestRoutesNotTaken(locs); - } - - /** Advertises whether routing of requests is currently enabled. */ - public static final MessageType FNPRoutingStatus = - new MessageType("FNPRoutingStatus", PRIORITY_HIGH); - - static { - FNPRoutingStatus.addField(ROUTING_ENABLED, Boolean.class); - } - - /** - * Creates an {@code FNPRoutingStatus} message. - * - * @param routeRequests {@code true} to enable routing; {@code false} to disable. - * @return Initialized message. - */ - public static Message createRoutingStatus(boolean routeRequests) { - Message msg = new Message(FNPRoutingStatus); - msg.set(ROUTING_ENABLED, routeRequests); - - return msg; - } - - /** Sub-insert hint: allow fork when cacheable (tunable control for insert behavior). */ - public static final MessageType FNPSubInsertForkControl = - new MessageType("FNPSubInsertForkControl", PRIORITY_HIGH); - - static { - FNPSubInsertForkControl.addField(ENABLE_INSERT_FORK_WHEN_CACHEABLE, Boolean.class); - } - - /** - * Creates a {@code FNPSubInsertForkControl} hint. - * - * @param enableInsertForkWhenCacheable Enable fork when cacheable. - * @return Initialized message. - */ - public static Message createFNPSubInsertForkControl(boolean enableInsertForkWhenCacheable) { - Message msg = new Message(FNPSubInsertForkControl); - msg.set(ENABLE_INSERT_FORK_WHEN_CACHEABLE, enableInsertForkWhenCacheable); - return msg; - } - - /** Sub-insert hint: prefer insert to other options when possible. */ - public static final MessageType FNPSubInsertPreferInsert = - new MessageType("FNPSubInsertPreferInsert", PRIORITY_HIGH); - - static { - FNPSubInsertPreferInsert.addField(PREFER_INSERT, Boolean.class); - } - - /** - * Creates a {@code FNPSubInsertPreferInsert} hint. - * - * @param preferInsert Prefer insert when {@code true}. - * @return Initialized message. - */ - public static Message createFNPSubInsertPreferInsert(boolean preferInsert) { - Message msg = new Message(FNPSubInsertPreferInsert); - msg.set(PREFER_INSERT, preferInsert); - return msg; - } - - /** Sub-insert hint: ignore the low backoff threshold. */ - public static final MessageType FNPSubInsertIgnoreLowBackoff = - new MessageType("FNPSubInsertIgnoreLowBackoff", PRIORITY_HIGH); - - static { - FNPSubInsertIgnoreLowBackoff.addField(IGNORE_LOW_BACKOFF, Boolean.class); - } - - /** - * Creates a {@code FNPSubInsertIgnoreLowBackoff} hint. - * - * @param ignoreLowBackoff Ignore low backoff when {@code true}. - * @return Initialized message. - */ - public static Message createFNPSubInsertIgnoreLowBackoff(boolean ignoreLowBackoff) { - Message msg = new Message(FNPSubInsertIgnoreLowBackoff); - msg.set(IGNORE_LOW_BACKOFF, ignoreLowBackoff); - return msg; - } - - /** Indicates that the preceding rejection is soft (retryable). */ - public static final MessageType FNPRejectIsSoft = - new MessageType("FNPRejectIsSoft", PRIORITY_HIGH); - - /** Creates an {@code FNPRejectIsSoft} marker message. */ - public static Message createFNPRejectIsSoft() { - return new Message(FNPRejectIsSoft); - } - - // New load management - - public static final MessageType FNPPeerLoadStatusByte = - new MessageType("FNPPeerLoadStatusByte", PRIORITY_HIGH, false, true); - - static { - FNPPeerLoadStatusByte.addField(OTHER_TRANSFERS_OUT_CHK, Byte.class); - FNPPeerLoadStatusByte.addField(OTHER_TRANSFERS_IN_CHK, Byte.class); - FNPPeerLoadStatusByte.addField(OTHER_TRANSFERS_OUT_SSK, Byte.class); - FNPPeerLoadStatusByte.addField(OTHER_TRANSFERS_IN_SSK, Byte.class); - FNPPeerLoadStatusByte.addField(AVERAGE_TRANSFERS_OUT_PER_INSERT, Byte.class); - FNPPeerLoadStatusByte.addField(OUTPUT_BANDWIDTH_LOWER_LIMIT, Integer.class); - FNPPeerLoadStatusByte.addField(OUTPUT_BANDWIDTH_UPPER_LIMIT, Integer.class); - FNPPeerLoadStatusByte.addField(OUTPUT_BANDWIDTH_PEER_LIMIT, Integer.class); - FNPPeerLoadStatusByte.addField(INPUT_BANDWIDTH_LOWER_LIMIT, Integer.class); - FNPPeerLoadStatusByte.addField(INPUT_BANDWIDTH_UPPER_LIMIT, Integer.class); - FNPPeerLoadStatusByte.addField(INPUT_BANDWIDTH_PEER_LIMIT, Integer.class); - FNPPeerLoadStatusByte.addField(MAX_TRANSFERS_OUT, Byte.class); - FNPPeerLoadStatusByte.addField(MAX_TRANSFERS_OUT_PEER_LIMIT, Byte.class); - FNPPeerLoadStatusByte.addField(MAX_TRANSFERS_OUT_LOWER_LIMIT, Byte.class); - FNPPeerLoadStatusByte.addField(MAX_TRANSFERS_OUT_UPPER_LIMIT, Byte.class); - FNPPeerLoadStatusByte.addField(REAL_TIME_FLAG, Boolean.class); - } - - public static final MessageType FNPPeerLoadStatusShort = - new MessageType("FNPPeerLoadStatusShort", PRIORITY_HIGH, false, true); - - static { - FNPPeerLoadStatusShort.addField(OTHER_TRANSFERS_OUT_CHK, Short.class); - FNPPeerLoadStatusShort.addField(OTHER_TRANSFERS_IN_CHK, Short.class); - FNPPeerLoadStatusShort.addField(OTHER_TRANSFERS_OUT_SSK, Short.class); - FNPPeerLoadStatusShort.addField(OTHER_TRANSFERS_IN_SSK, Short.class); - FNPPeerLoadStatusShort.addField(AVERAGE_TRANSFERS_OUT_PER_INSERT, Short.class); - FNPPeerLoadStatusShort.addField(OUTPUT_BANDWIDTH_LOWER_LIMIT, Integer.class); - FNPPeerLoadStatusShort.addField(OUTPUT_BANDWIDTH_UPPER_LIMIT, Integer.class); - FNPPeerLoadStatusShort.addField(OUTPUT_BANDWIDTH_PEER_LIMIT, Integer.class); - FNPPeerLoadStatusShort.addField(INPUT_BANDWIDTH_LOWER_LIMIT, Integer.class); - FNPPeerLoadStatusShort.addField(INPUT_BANDWIDTH_UPPER_LIMIT, Integer.class); - FNPPeerLoadStatusShort.addField(INPUT_BANDWIDTH_PEER_LIMIT, Integer.class); - FNPPeerLoadStatusShort.addField(MAX_TRANSFERS_OUT, Short.class); - FNPPeerLoadStatusShort.addField(MAX_TRANSFERS_OUT_PEER_LIMIT, Short.class); - FNPPeerLoadStatusShort.addField(MAX_TRANSFERS_OUT_LOWER_LIMIT, Short.class); - FNPPeerLoadStatusShort.addField(MAX_TRANSFERS_OUT_UPPER_LIMIT, Short.class); - FNPPeerLoadStatusShort.addField(REAL_TIME_FLAG, Boolean.class); - } - - public static final MessageType FNPPeerLoadStatusInt = - new MessageType("FNPPeerLoadStatusInt", PRIORITY_HIGH, false, true); - - static { - FNPPeerLoadStatusInt.addField(OTHER_TRANSFERS_OUT_CHK, Integer.class); - FNPPeerLoadStatusInt.addField(OTHER_TRANSFERS_IN_CHK, Integer.class); - FNPPeerLoadStatusInt.addField(OTHER_TRANSFERS_OUT_SSK, Integer.class); - FNPPeerLoadStatusInt.addField(OTHER_TRANSFERS_IN_SSK, Integer.class); - FNPPeerLoadStatusInt.addField(AVERAGE_TRANSFERS_OUT_PER_INSERT, Integer.class); - FNPPeerLoadStatusInt.addField(OUTPUT_BANDWIDTH_LOWER_LIMIT, Integer.class); - FNPPeerLoadStatusInt.addField(OUTPUT_BANDWIDTH_UPPER_LIMIT, Integer.class); - FNPPeerLoadStatusInt.addField(OUTPUT_BANDWIDTH_PEER_LIMIT, Integer.class); - FNPPeerLoadStatusInt.addField(INPUT_BANDWIDTH_LOWER_LIMIT, Integer.class); - FNPPeerLoadStatusInt.addField(INPUT_BANDWIDTH_UPPER_LIMIT, Integer.class); - FNPPeerLoadStatusInt.addField(INPUT_BANDWIDTH_PEER_LIMIT, Integer.class); - FNPPeerLoadStatusInt.addField(MAX_TRANSFERS_OUT, Integer.class); - FNPPeerLoadStatusInt.addField(MAX_TRANSFERS_OUT_PEER_LIMIT, Integer.class); - FNPPeerLoadStatusInt.addField(MAX_TRANSFERS_OUT_LOWER_LIMIT, Integer.class); - FNPPeerLoadStatusInt.addField(MAX_TRANSFERS_OUT_UPPER_LIMIT, Integer.class); - FNPPeerLoadStatusInt.addField(REAL_TIME_FLAG, Boolean.class); - } - - /** Secondary message toggling realtime/bulk handling on a per-message basis. */ - public static final MessageType FNPRealTimeFlag = - new MessageType("FNPRealTimeFlag", PRIORITY_HIGH); - - static { - FNPRealTimeFlag.addField(REAL_TIME_FLAG, Boolean.class); - } - - /** - * Creates a {@code FNPRealTimeFlag} submessage. - * - * @param isBulk Flag value; interpretation is implementation-defined. - * @return Initialized message. - */ - public static Message createFNPRealTimeFlag(boolean isBulk) { - Message msg = new Message(FNPRealTimeFlag); - msg.set(REAL_TIME_FLAG, isBulk); - return msg; - } - - /** - * Extracts the flag value from an optional {@code FNPRealTimeFlag} submessage. - * - * @param m Message that may carry the submessage. - * @return Flag value when present; otherwise {@code false}. - */ - public static boolean getRealTimeFlag(Message m) { - Message bulk = m.getSubMessage(FNPRealTimeFlag); - if (bulk == null) { - return false; - } - return bulk.getBoolean(REAL_TIME_FLAG); - } - - /** Returns {@code true} if the message is one of the peer-load status variants. */ - public static boolean isPeerLoadStatusMessage(Message m) { - MessageType spec = m.getSpec(); - return FNPPeerLoadStatusByte.equals(spec) - || FNPPeerLoadStatusShort.equals(spec) - || FNPPeerLoadStatusInt.equals(spec); - } - - /** Returns {@code true} if the message is a request type subject to load limiting. */ - public static boolean isLoadLimitedRequest(Message m) { - MessageType spec = m.getSpec(); - return FNPCHKDataRequest.equals(spec) - || FNPSSKDataRequest.equals(spec) - || FNPSSKInsertRequest.equals(spec) - || FNPInsertRequest.equals(spec) - || FNPSSKInsertRequestNew.equals(spec) - || FNPGetOfferedKey.equals(spec); - } - - // Extended fatal timeout handling. - /** - * Queries whether a set of upstream request UIDs is still running. Reply is {@link - * #FNPIsStillRunning}. - */ - public static final MessageType FNPCheckStillRunning = - new MessageType("FNPCheckStillRunning", PRIORITY_HIGH); - - static { - FNPCheckStillRunning.addField(UID, Long.class); // UID for this message, used to identify reply - FNPCheckStillRunning.addField(LIST_OF_UIDS, ShortBuffer.class); - } - - /** Reply indicating which UIDs are still running using a bitset. */ - public static final MessageType FNPIsStillRunning = - new MessageType("FNPIsStillRunning", PRIORITY_HIGH); - - static { - FNPIsStillRunning.addField(UID, Long.class); - FNPIsStillRunning.addField(UID_STILL_RUNNING_FLAGS, BitArray.class); - } - - // Friend-of-a-friend (FOAF) related messages - - /** FOAF: request the peer's full noderef. */ - public static final MessageType FNPGetYourFullNoderef = - new MessageType("FNPGetYourFullNoderef", PRIORITY_LOW); - - /** Creates an {@code FNPGetYourFullNoderef} request. */ - public static Message createFNPGetYourFullNoderef() { - return new Message(FNPGetYourFullNoderef); - } - - /** FOAF: reply conveying the sender's full noderef length (payload transferred separately). */ - public static final MessageType FNPMyFullNoderef = - new MessageType("FNPMyFullNoderef", PRIORITY_LOW); - - static { - FNPMyFullNoderef.addField(UID, Long.class); - // Not necessary to pad it since it's not propagated across the network. - // It might be relayed one hop, but there's enough padding elsewhere, don't worry about - // it. - // As opposed to opennet refs, which are relayed long distances, down request paths which - // they might reveal, so do need to be padded. - FNPMyFullNoderef.addField(NODEREF_LENGTH, Integer.class); - } - - /** - * Creates an {@code FNPMyFullNoderef} reply. - * - * @param uid Request identifier associated with the original FOAF query. - * @param length Unpadded length in bytes of the noderef to follow. - * @return Initialized message. - */ - public static Message createFNPMyFullNoderef(long uid, int length) { - Message m = new Message(FNPMyFullNoderef); - m.set(UID, uid); - m.set(NODEREF_LENGTH, length); - return m; - } -} diff --git a/src/main/java/network/crypta/io/comm/DisconnectedException.java b/src/main/java/network/crypta/io/comm/DisconnectedException.java deleted file mode 100644 index ac949f97634..00000000000 --- a/src/main/java/network/crypta/io/comm/DisconnectedException.java +++ /dev/null @@ -1,26 +0,0 @@ -package network.crypta.io.comm; - -import java.io.Serial; -import network.crypta.support.LightweightException; - -/** - * Signals that a previously connected peer or channel became disconnected while a blocking - * operation was in progress. - * - *

This checked exception typically originates from messaging and transfer code when a caller is - * waiting for an event (for example, {@code waitFor(...)} in the messaging layer) and the - * underlying connection drops locally or remotely. It is distinct from {@link - * NotConnectedException}, which indicates that no connection existed at the time the operation was - * attempted. - * - *

The class extends {@link LightweightException}; by default, stack traces may be omitted to - * keep the exception inexpensive for frequent control-flow paths. Callers should catch this - * exception to abort the current operation or re-resolve the peer according to the calling - * context's retry policy. - * - * @see NotConnectedException - * @see PeerRestartedException - */ -public class DisconnectedException extends LightweightException { - @Serial private static final long serialVersionUID = -1; -} diff --git a/src/main/java/network/crypta/io/comm/Dispatcher.java b/src/main/java/network/crypta/io/comm/Dispatcher.java deleted file mode 100644 index 6232bbb54b2..00000000000 --- a/src/main/java/network/crypta/io/comm/Dispatcher.java +++ /dev/null @@ -1,25 +0,0 @@ -package network.crypta.io.comm; - -/** - * Callback for handling unmatched messages. - * - *

The messaging core invokes this interface when an incoming {@link Message} did not match any - * registered {@link MessageFilter}. Implementations may inspect the message and either consume it - * (returning {@code true}) or decline it (returning {@code false}). When {@code false} is returned, - * the message is treated as unhandled and may be retained for later matching according to the - * policy in {@link MessageCore}. - * - *

Threading: {@link MessageCore} may call this method from an I/O or worker thread. Implementers - * should avoid long blocking operations and offload heavy work to executors as appropriate. - */ -public interface Dispatcher { - - /** - * Attempts to handle a message that no filter claimed. - * - * @param m the message to process; never {@code null}. - * @return {@code true} if the message is fully handled and requires no further processing; {@code - * false} to leave it unhandled so the core can apply its unmatched-message policy. - */ - boolean handleMessage(Message m); -} diff --git a/src/main/java/network/crypta/io/comm/DuplicateMessageTypeException.java b/src/main/java/network/crypta/io/comm/DuplicateMessageTypeException.java deleted file mode 100644 index 92ef65b168e..00000000000 --- a/src/main/java/network/crypta/io/comm/DuplicateMessageTypeException.java +++ /dev/null @@ -1,22 +0,0 @@ -package network.crypta.io.comm; - -/** - * Signals an attempt to register a {@link MessageType} whose registry identifier already exists. - * - *

The identifier is derived from the type's name via {@link String#hashCode()}. If another type - * with the same key is already present in the registry, the registering code throws this unchecked - * exception to prevent ambiguous decoding and inconsistent schemas. - * - *

Typical source: constructing a new {@link MessageType} with a name that collides with an - * existing type. - */ -public class DuplicateMessageTypeException extends RuntimeException { - /** - * Creates the exception with a detail message. - * - * @param message detail text; often includes the conflicting type name - */ - public DuplicateMessageTypeException(String message) { - super(message); - } -} diff --git a/src/main/java/network/crypta/io/comm/FreenetInetAddress.java b/src/main/java/network/crypta/io/comm/FreenetInetAddress.java deleted file mode 100644 index bde5caa3792..00000000000 --- a/src/main/java/network/crypta/io/comm/FreenetInetAddress.java +++ /dev/null @@ -1,559 +0,0 @@ -package network.crypta.io.comm; - -import java.io.DataInput; -import java.io.DataOutputStream; -import java.io.IOException; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.Arrays; -import network.crypta.io.AddressIdentifier; -import network.crypta.support.io.InetAddressIpv6FirstComparator; -import network.crypta.support.transport.ip.HostnameSyntaxException; -import network.crypta.support.transport.ip.HostnameUtil; -import network.crypta.support.transport.ip.IPUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Immutable-or-lazy network endpoint reference that can be backed by a hostname or a resolved IP - * address. - * - *

When constructed from an {@link InetAddress}, the address is primary and immutable; when - * constructed from a hostname, the hostname is primary and DNS resolution occurs lazily on first - * access (or explicitly via handshake). This design helps nodes that use dynamic DNS. - * - *

Equality propagates the resolved IP between instances when the primary hostname matches - * (case-insensitive). Hostname never propagates. The hash code depends solely on the primary - * identity: hostname if present, otherwise the IP address. As a result, inserting instances into - * hashed collections is safe: neither {@link #equals(Object)} nor {@link #getAddress()} will change - * the hash code of an existing instance. - * - *

Important behavior: an instance that has only an IP (no hostname) is not equal to another - * instance that has a hostname, even when both resolve to the same numeric address. Call {@link - * #dropHostname()} to compare by IP only, or compare the underlying {@link InetAddress} values - * directly when appropriate. - * - *

DNS lookups are performed using the platform caches (as configured in the JVM). This class - * does not implement its own caching policy beyond storing the last resolved address when helpful - * for handshakes. - */ -public final class FreenetInetAddress { - private static final Logger LOG = LoggerFactory.getLogger(FreenetInetAddress.class); - - // no static initialization required - - // hostname - only set if we were created with a hostname - // and not an address - private final String hostname; - private InetAddress address; - - /** - * Constructs an instance by reading from a binary stream produced by {@link - * #writeToDataOutputStream(DataOutputStream)}. - * - *

Format: one type byte ({@code 0} for IPv4, {@code 255} for IPv6), followed by the raw IP - * bytes (4 or 16), then a UTF string containing the hostname or the empty string when absent. - * - * @param dis data source to read from - * @throws IOException if the input cannot be read or the type byte is unknown - */ - public FreenetInetAddress(DataInput dis) throws IOException { - int firstByte = dis.readUnsignedByte(); - byte[] ba; - switch (firstByte) { - case 255 -> { - if (LOG.isDebugEnabled()) LOG.debug("event=read_stream_ip type=ipv6 format=new"); - ba = new byte[16]; - dis.readFully(ba); - } - case 0 -> { - if (LOG.isDebugEnabled()) LOG.debug("event=read_stream_ip type=ipv4 format=new"); - ba = new byte[4]; - dis.readFully(ba); - } - default -> - throw new IOException( - "Unknown type byte (old form? corrupt stream? too short/long prev field?): " - + firstByte); - } - address = InetAddress.getByAddress(ba); - String name = null; - String s = dis.readUTF(); - if (!s.isEmpty()) name = s; - hostname = name; - } - - /** - * Constructs an instance by reading from a binary stream and optionally validating hostname - * syntax. - * - *

This constructor also accepts the legacy IPv4 format where the first byte is part of the - * address (no leading type byte). Newer encodings use a leading type byte as described for {@link - * #FreenetInetAddress(DataInput)}. - * - * @param dis data source to read from - * @param checkHostnameOrIPSyntax when {@code true}, validates the parsed hostname using {@link - * HostnameUtil#isValidHostname(String, boolean)} - * @throws HostnameSyntaxException if validation is requested and the hostname is not valid - * @throws IOException if the input cannot be read - */ - public FreenetInetAddress(DataInput dis, boolean checkHostnameOrIPSyntax) - throws HostnameSyntaxException, IOException { - int firstByte = dis.readUnsignedByte(); - byte[] ba; - switch (firstByte) { - case 255 -> { - if (LOG.isDebugEnabled()) - LOG.debug("event=read_stream_ip_with_validation type=ipv6 format=new"); - ba = new byte[16]; - dis.readFully(ba); - } - case 0 -> { - if (LOG.isDebugEnabled()) - LOG.debug("event=read_stream_ip_with_validation type=ipv4 format=new"); - ba = new byte[4]; - dis.readFully(ba); - } - default -> { - // Old format IPv4 address - ba = new byte[4]; - ba[0] = (byte) firstByte; - dis.readFully(ba, 1, 3); - } - } - address = InetAddress.getByAddress(ba); - String name = null; - String s = dis.readUTF(); - if (!s.isEmpty()) name = s; - hostname = name; - if (checkHostnameOrIPSyntax - && hostname != null - && !HostnameUtil.isValidHostname(hostname, true)) throw new HostnameSyntaxException(); - } - - /** - * Constructs an instance from a resolved IP address. - * - *

The IP address becomes the primary identity. No hostname is stored or looked up. - * - * @param address resolved IP address; must not be {@code null} - */ - public FreenetInetAddress(InetAddress address) { - this.address = address; - hostname = null; - } - - /** - * Constructs an instance from a textual host, which may be a DNS name or a literal IP address. - * - *

If {@code host} parses as an IP address, the instance is IP-primary. Otherwise, the instance - * is hostname-primary and resolution is deferred until required. - * - * @param host DNS name or literal IP; leading slashes are trimmed (e.g., {@code "/1.2.3.4"}) - * @param allowUnknown reserved for compatibility; does not alter behavior here - * @throws UnknownHostException if the literal address cannot be parsed - */ - public FreenetInetAddress(String host, boolean allowUnknown) throws UnknownHostException { - InitResult r = computeInitFromHost(host, allowUnknown); - this.address = r.addr; - this.hostname = r.host; - // we're created with a hostname so delay the lookup of the address - // until it's necessary to work better with dynamic DNS hostnames - } - - /** - * Constructs an instance from a textual host with optional hostname syntax validation. - * - * @param host DNS name or literal IP; leading slashes are trimmed - * @param allowUnknown reserved for compatibility; does not alter behavior here - * @param checkHostnameOrIPSyntax when {@code true}, validates {@code host} if it is a hostname - * @throws HostnameSyntaxException if validation is requested and the hostname is not valid - * @throws UnknownHostException if the literal address cannot be parsed - */ - public FreenetInetAddress(String host, boolean allowUnknown, boolean checkHostnameOrIPSyntax) - throws HostnameSyntaxException, UnknownHostException { - InitResult r = computeInitFromHost(host, allowUnknown); - this.address = r.addr; - this.hostname = r.host; - if (checkHostnameOrIPSyntax - && this.hostname != null - && !HostnameUtil.isValidHostname(this.hostname, true)) throw new HostnameSyntaxException(); - // we're created with a hostname so delay the lookup of the address - // until it's necessary to work better with dynamic DNS hostnames - } - - private record InitResult(InetAddress addr, String host) {} - - private InitResult computeInitFromHost(String host, boolean allowUnknown) - throws UnknownHostException { - InetAddress addr = null; - String h = host; - if (h != null) { - if (h.startsWith("/")) h = h.substring(1); - h = h.trim(); - } - AddressIdentifier.AddressType addressType = AddressIdentifier.getAddressType(h); - if (LOG.isTraceEnabled()) - LOG.trace( - "Address type of '{}' appears to be '{}' (allowUnknown={})", - h, - addressType, - allowUnknown); - if (addressType != AddressIdentifier.AddressType.OTHER) { - // Is an IP address - addr = InetAddress.getByName(h); - if (LOG.isDebugEnabled()) - LOG.debug("host is '{}' and addr.getHostAddress() is '{}'", h, addr.getHostAddress()); - if (addr != null) { - h = null; - } - } - if (addr == null && LOG.isTraceEnabled()) LOG.trace("'{}' does not look like an IP address", h); - return new InitResult(addr, h); - } - - /** - * Compares for equality using relaxed rules. - * - *

Behavior: - When this instance is hostname-primary, hostnames must match the ignoring case. - * If either side has a resolved IP, it is propagated to the other. If both have addresses, they - * must match. - When this instance is IP-primary, only the numeric addresses are compared. - * - * @param other address to compare - * @return {@code true} when considered equal under the relaxed rules - */ - public boolean laxEquals(FreenetInetAddress other) { - if (hostname != null) { - return laxEqualsWhenThisHasHostname(other); - } - return addressesEqual(address, other.address); - } - - private boolean laxEqualsWhenThisHasHostname(FreenetInetAddress other) { - if (other.hostname == null) { - return addressBasedComparisonWhenOtherHasNoHostname(other); - } - if (!hostname.equalsIgnoreCase(other.hostname)) { - return false; - } - propagateAddresses(other); - return (other.address == null) || (address == null) || other.address.equals(address); - } - - private boolean addressBasedComparisonWhenOtherHasNoHostname(FreenetInetAddress other) { - if (address == null) return false; // No basis for comparison. - if (other.address != null) { - return address.equals(other.address); - } - return false; - } - - private void propagateAddresses(FreenetInetAddress other) { - if (address != null && other.address == null) other.address = address; - if (other.address != null && address == null) address = other.address; - } - - private static boolean addressesEqual(InetAddress a, InetAddress b) { - return a != null && a.equals(b); - } - - /** - * Compares for equality consistent with the propagation semantics described in the class - * documentation. - * - *

When hostnames are present on both sides, they must match the ignoring case; the resolved IP - * may be propagated between instances. When neither side has a hostname, equality falls back to - * the numeric IP address. - */ - @Override - public boolean equals(Object o) { - if (!(o instanceof FreenetInetAddress addr)) { - return false; - } - if (hostname != null) { - if (addr.hostname == null) return false; - if (!hostname.equalsIgnoreCase(addr.hostname)) { - return false; - } - // Now that we know we have the same hostname, we can propagate the IP. - if ((address != null) && (addr.address == null)) addr.address = address; - if ((addr.address != null) && (address == null)) address = addr.address; - // Except if we actually do have two different looked-up IPs! - return addr.address == null || addr.address.equals(address); - // Equal. - } - if (addr.hostname != null) return false; - - // No hostname, go by address. - return address.equals(addr.address); - } - - /** - * Compares for equality using strict rules useful for peer identity checks. - * - *

When hostnames are present on both sides, the behavior matches {@link #equals(Object)}. When - * neither side has a hostname, strict comparison requires the reverse hostnames derived from the - * numeric IPs to match the ignoring case (see {@link #getHostName(InetAddress)}). - * - * @param addr address to compare - * @return {@code true} if equal under strict comparison - */ - public boolean strictEquals(FreenetInetAddress addr) { - if (hostname != null) { - if (addr.hostname == null) return false; - if (!hostname.equalsIgnoreCase(addr.hostname)) { - return false; - } - // Now that we know we have the same hostname, we can propagate the IP. - if ((address != null) && (addr.address == null)) addr.address = address; - if ((addr.address != null) && (address == null)) address = addr.address; - // Except if we actually do have two different looked-up IPs! - return addr.address == null || addr.address.equals(address); - // Equal. - } else if (addr.hostname != null /* && hostname == null */) { - return false; - } - - // No hostname, go by address. - String reverseHostNameISee = getHostName(address); - String reverseHostNameTheySee = getHostName(addr.address); - return reverseHostNameISee != null - && reverseHostNameISee.equalsIgnoreCase(reverseHostNameTheySee); - } - - /** - * Returns the resolved IP address, performing a DNS lookup if needed. - * - *

If an address was resolved previously, it is returned without issuing a new lookup. - * - * @return resolved address or {@code null} if resolution fails - */ - public InetAddress getAddress() { - return getAddress(true); - } - - /** - * Returns the resolved IP address and optionally performs DNS resolution. - * - * @param doDNSRequest when {@code true}, resolve the hostname if not yet resolved; when {@code - * false}, return the cached value or {@code null} - * @return resolved address or {@code null} when not cached and lookups are disabled - */ - public InetAddress getAddress(boolean doDNSRequest) { - if (address != null) { - return address; - } else { - if (!doDNSRequest) return null; - InetAddress addr = getHandshakeAddress(); - if (addr != null) { - this.address = addr; - } - return addr; - } - } - - /** - * Returns the IP used for handshakes, forcing a fresh DNS resolution when hostname-primary. - * - *

This method bypasses a previously cached value if a hostname is present so that dynamic DNS - * updates are observed during connection attempts. - * - * @return the latest resolved address or {@code null} if the hostname cannot be resolved - */ - public InetAddress getHandshakeAddress() { - if (shouldReturnCachedAddress()) { - if (LOG.isDebugEnabled()) LOG.debug("hostname is null, returning {}", address); - return address; - } - return lookupHandshakeAddress(); - } - - private boolean shouldReturnCachedAddress() { - // During handshakes only return the cached value when IP is primary; a hostname may have - // changed due to dynamic DNS. - return (address != null) && (hostname == null); - } - - private InetAddress lookupHandshakeAddress() { - if (LOG.isDebugEnabled()) LOG.debug("Looking up '{}' in DNS", hostname); - /* - * Peers are constructed from an address once a - * handshake has been completed, so this lookup - * will only be performed during a handshake. - * This method should normally only be called - * from PeerNode.getHandshakeIPs() and once - * each connection from this.getAddress(). - * It does not mean we perform a DNS lookup - * with every packet we send. - */ - InetAddress[] addresses = resolveAllByName(hostname); - if (addresses.length == 0) return null; - if (addresses.length > 1) cacheFirstSortedAddress(addresses); - return addresses[0]; - } - - private InetAddress[] resolveAllByName(String host) { - try { - return InetAddress.getAllByName(host); - } catch (UnknownHostException _) { - if (LOG.isDebugEnabled()) - LOG.debug("DNS said hostname '{}' is an unknown host, returning null", host); - return new InetAddress[0]; - } - } - - private void cacheFirstSortedAddress(InetAddress[] addresses) { - /* Prefer IPv6 when multiple answers exist to encourage IPv6 connectivity. */ - Arrays.sort(addresses, InetAddressIpv6FirstComparator.COMPARATOR); - /* Cache the first answer to avoid immediate re-resolution on later calls. */ - try { - this.address = InetAddress.getByAddress(addresses[0].getAddress()); - if (LOG.isDebugEnabled()) LOG.debug("Setting address to {}", address); - } catch (UnknownHostException e) { - // Should not happen for valid IPv4/IPv6 byte arrays; skip caching if it does. - if (LOG.isWarnEnabled()) - LOG.warn( - "Failed to cache first sorted address for hostname '{}': {}", hostname, e.toString()); - } - } - - /** - * Computes a hash code consistent with the equality contract. - * - *

When a hostname is present, it is the sole contributor; otherwise the numeric address is - * used. - */ - @Override - public int hashCode() { - if (hostname != null) { - return hostname.hashCode(); // Was set at creation, so it can safely be used here. - } else { - return address.hashCode(); // Can be null, but if so, the hostname will be non-null. - } - } - - /** - * Returns a human-friendly representation: hostname when present, otherwise the numeric address. - */ - @Override - public String toString() { - if (hostname != null) { - return hostname; - } else { - return address.getHostAddress(); - } - } - - /** - * Returns a string favoring the numeric address when available, otherwise the hostname. - * - * @return numeric address or hostname; may be {@code null} when neither is available - */ - public String toStringPrefNumeric() { - if (address != null) return address.getHostAddress(); - else return hostname; - } - - /** - * Writes this instance to a {@link DataOutputStream} using the current binary encoding. - * - *

Format: one type byte ({@code 0} for IPv4, {@code 255} for IPv6), followed by the raw IP - * bytes, then a UTF string of the hostname or the empty string when absent. - * - * @param dos destination stream - * @throws IOException if writing fails or the address cannot be encoded - */ - public void writeToDataOutputStream(DataOutputStream dos) throws IOException { - InetAddress addr = this.getAddress(); - if (addr == null) throw new UnknownHostException(); - byte[] data = addr.getAddress(); - if (data.length == 4) dos.write(0); - else dos.write(255); - dos.write(data); - if (hostname != null) dos.writeUTF(hostname); - else dos.writeUTF(""); - } - - /** - * Returns the name component of an {@link InetAddress} without performing reverse DNS. - * - *

Parses the {@code toString()} form ({@code name/address}) and returns the left-hand side. If - * no name is present, returns {@link InetAddress#getHostAddress()}. - * - * @param primaryIPAddress address to read; may be {@code null} - * @return hostname or numeric address; {@code null} when the input is {@code null} - */ - public static String getHostName(InetAddress primaryIPAddress) { - if (primaryIPAddress == null) return null; - String s = primaryIPAddress.toString(); - String addr = s.substring(0, s.indexOf('/')).trim(); - if (addr.isEmpty()) return primaryIPAddress.getHostAddress(); - else return addr; - } - - /** - * Determines whether the address appears routable on the public Internet. - * - *

If a resolved address is available, the decision delegates to {@link - * IPUtil#isValidAddress(InetAddress, boolean)}. Otherwise, when {@code lookup} is {@code true}, a - * resolution attempt is made; when {@code false}, {@code defaultVal} is returned. - * - * @param lookup whether to resolve the hostname when no cached address is available - * @param defaultVal value to return when not resolved and lookups are disabled - * @param allowLocalAddresses whether RFC 1918/unique-local addresses are considered valid - * @return {@code true} if the address is considered publicly routable (or allowed locally) - */ - public boolean isRealInternetAddress( - boolean lookup, boolean defaultVal, boolean allowLocalAddresses) { - if (address != null) { - return IPUtil.isValidAddress(address, allowLocalAddresses); - } else { - if (lookup) { - InetAddress a = getAddress(); - if (a != null) return IPUtil.isValidAddress(a, allowLocalAddresses); - } - return defaultVal; - } - } - - /** - * Returns a new instance with the hostname removed, preserving only the resolved IP address. - * - *

If no address is currently resolved, returns {@code null}. Call {@link #getAddress(boolean)} - * with {@code true} first when the hostname is primary and resolution is desired. - * - * @return a new IP-primary instance, {@code null} when no address is known, or {@code this} when - * already IP-primary - */ - public FreenetInetAddress dropHostname() { - if (address == null) { - LOG.debug("dropHostname() called without a resolved address; hostname='{}'", hostname); - return null; - } - if (hostname != null) { - return new FreenetInetAddress(address); - } else return this; - } - - /** Returns whether a non-empty hostname is present. */ - public boolean hasHostname() { - return hostname != null && !hostname.isEmpty(); - } - - /** Returns whether a hostname is present but no address has been resolved yet. */ - public boolean hasHostnameNoIP() { - return hasHostname() && address == null; - } - - /** - * Returns whether the resolved address is IPv6. - * - * @param defaultValue value to return when the address has not been resolved yet - * @return {@code true} for IPv6, {@code false} for IPv4, or {@code defaultValue} when unknown - */ - public boolean isIPv6(boolean defaultValue) { - if (address == null) return defaultValue; - else return (address instanceof Inet6Address); - } -} diff --git a/src/main/java/network/crypta/io/comm/IncorrectTypeException.java b/src/main/java/network/crypta/io/comm/IncorrectTypeException.java deleted file mode 100644 index 6825448cbe2..00000000000 --- a/src/main/java/network/crypta/io/comm/IncorrectTypeException.java +++ /dev/null @@ -1,20 +0,0 @@ -package network.crypta.io.comm; - -import java.io.Serial; - -/** - * Thrown if trying to set a field to a value of the wrong type - * - * @author ian - */ -public class IncorrectTypeException extends RuntimeException { - - public static final String VERSION = - "$Id: IncorrectTypeException.java,v 1.1 2005/01/29 19:12:10 amphibian Exp $"; - - @Serial private static final long serialVersionUID = 1L; - - public IncorrectTypeException(String s) { - super(s); - } -} diff --git a/src/main/java/network/crypta/io/comm/Message.java b/src/main/java/network/crypta/io/comm/Message.java deleted file mode 100644 index 7fe4045e1a6..00000000000 --- a/src/main/java/network/crypta/io/comm/Message.java +++ /dev/null @@ -1,762 +0,0 @@ -package network.crypta.io.comm; - -import java.io.ByteArrayOutputStream; -import java.io.DataInput; -import java.io.DataOutputStream; -import java.io.EOFException; -import java.io.IOException; -import java.io.Serial; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import network.crypta.support.ByteBufferInputStream; -import network.crypta.support.Fields; -import network.crypta.support.Serializer; -import network.crypta.support.ShortBuffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Message container for the Crypta wire format. - * - *

Instances encode to and decode from a compact binary representation used in UDP datagrams. A - * message carries a {@link MessageType} and a typed payload addressed by field name. Messages may - * also contain nested “sub-messages” to bundle related information without introducing new - * top‑level types. - * - *

Security note: Prefer constructing a fresh {@code Message} when forwarding rather than passing - * an incoming instance verbatim. Forwarding as‑is preserves any embedded submessages and can - * inadvertently leak metadata (e.g., labeling, collusion signals along a route) and waste - * bandwidth. Sub‑messages exist primarily for historical compatibility and should be used - * judiciously. - * - *

Thread-safety: {@code Message} is mutable and not thread‑safe. Callers should confine each - * instance to a single thread or apply external synchronization. - */ -public class Message { - private static final Logger LOG = LoggerFactory.getLogger(Message.class); - - /** Legacy source control identifier kept for historical compatibility. */ - public static final String VERSION = - "$Id: Message.java,v 1.11 2005/09/15 18:16:04 amphibian Exp $"; - - private final MessageType spec; - private final WeakReference sourceRef; - private final boolean internal; - private final HashMap payload = HashMap.newHashMap(8); - private List subMessages; - - /** - * Time of local instantiation in milliseconds since the epoch. - * - *

For decoded messages this reflects when the instance was constructed, not when the packet - * was received on the network. - */ - public final long localInstantiationTime; - - private final int receivedByteCount; - short priority; - - /** - * Decodes a message from a datagram buffer. - * - *

On failure (invalid type, internal‑only type, truncated payload, or I/O while reading the - * buffer), this method returns {@code null} without throwing. - * - * @param buf backing array containing the datagram payload - * @param offset start offset within {@code buf} - * @param length number of bytes to read starting at {@code offset} - * @param peer decoding context for the sending peer; may be {@code null} - * @param overhead additional bytes attributed to the received packet (e.g., link/protocol - * overhead) that should be counted for statistics; incorporated into {@link - * #receivedByteCount()} - * @return a decoded {@code Message}, or {@code null} if decoding fails or the type is rejected - */ - public static Message decodeMessageFromPacket( - byte[] buf, int offset, int length, PeerContext peer, int overhead) { - ByteBufferInputStream bb = new ByteBufferInputStream(buf, offset, length); - return decodeMessage(bb, peer, length + overhead, true, false, false); - } - - /** - * Decodes a message using relaxed validation. - * - *

Compared to {@link #decodeMessageFromPacket(byte[], int, int, PeerContext, int)}, this - * variant accepts certain legacy or malformed inputs that the strict decoder would reject. - * - * @param buf array containing the entire payload - * @param peer decoding context for the sending peer; may be {@code null} - * @param overhead additional bytes attributed to the received packet for statistics - * @return a decoded {@code Message}, or {@code null} when decoding fails - */ - public static Message decodeMessageLax(byte[] buf, PeerContext peer, int overhead) { - ByteBufferInputStream bb = new ByteBufferInputStream(buf); - return decodeMessage(bb, peer, buf.length + overhead, true, false, true); - } - - private static Message decodeMessage( - ByteBufferInputStream bb, - PeerContext peer, - int recvByteCount, - boolean mayHaveSubMessages, - boolean inSubMessage, - boolean veryLax) { - MessageType mspec = readMessageType(bb, veryLax); - if (mspec == null) { - if (LOG.isDebugEnabled()) - LOG.debug("event=decode-invalid-type message type missing or corrupt"); - return null; - } - if (!isAcceptable(mspec)) return null; - Message m = new Message(mspec, peer, recvByteCount); - try { - readMessageFields(mspec, m, bb); - readAndAttachSubMessagesIfAny(m, bb, peer, veryLax, mayHaveSubMessages); - } catch (EOFException e) { - logPrematureEnd(peer, mspec, inSubMessage, e); - return null; - } catch (IOException e) { - LOG.error("event=decode-io-error unexpected I/O reading message payload: {}", e, e); - return null; - } - logReturnMessage(m); - return m; - } - - private static MessageType readMessageType(ByteBufferInputStream bb, boolean veryLax) { - try { - return MessageType.getSpec(bb.readInt(), veryLax); - } catch (IOException e1) { - if (LOG.isDebugEnabled()) - LOG.debug("event=decode-type-read-failed failed to read message type id: {}", e1, e1); - return null; - } - } - - private static boolean isAcceptable(MessageType mspec) { - if (mspec == null) { - if (LOG.isDebugEnabled()) LOG.debug("event=accept-null-type rejected null message type"); - return false; - } - if (mspec.isInternalOnly()) { - if (LOG.isDebugEnabled()) - LOG.debug("event=accept-internal-only rejected internal-only message"); - return false; // silently discard internal-only messages - } - return true; - } - - private static void readAndAttachSubMessagesIfAny( - Message m, - ByteBufferInputStream bb, - PeerContext peer, - boolean veryLax, - boolean mayHaveSubMessages) { - if (!mayHaveSubMessages) return; - readAndAttachSubMessages(m, bb, peer, veryLax); - } - - private static void logPrematureEnd( - PeerContext peer, MessageType mspec, boolean inSubMessage, EOFException e) { - String msg = - peer.getPeer() - + " sent a message packet that ends prematurely while deserialising " - + mspec.getName(); - if (inSubMessage) { - if (LOG.isDebugEnabled()) LOG.debug("event=submessage-truncated {}", msg, e); - } else { - LOG.error(msg, e); - } - } - - private static void logReturnMessage(Message m) { - if (LOG.isDebugEnabled()) - LOG.debug("event=decode-complete decoded message {} from {}", m, m.getSource()); - } - - private static void readMessageFields(MessageType mspec, Message m, ByteBufferInputStream bb) - throws IOException { - DataInput dataInput = bb.asDataInput(); - for (String name : mspec.getOrderedFields()) { - Class type = mspec.getFields().get(name); - if (type.equals(List.class)) { - m.set( - name, - Serializer.readListFromDataInputStream( - mspec.getLinkedListTypes().get(name), dataInput)); - } else { - m.set(name, Serializer.readFromDataInputStream(type, dataInput)); - } - } - } - - private static void readAndAttachSubMessages( - Message m, ByteBufferInputStream bb, PeerContext peer, boolean veryLax) { - while (bb.remaining() > 2) { // sizeof(unsigned short) == 2 - ByteBufferInputStream bb2 = readSubMessageSlice(bb, m); - if (bb2 == null) { - // Not enough data or EOF - stop processing sub-messages gracefully. - return; - } - Message subMessage = decodeSubMessage(bb2, peer, veryLax); - if (subMessage == null) return; // Stop if decoding failed or returned null - if (LOG.isDebugEnabled()) - LOG.debug("event=submessage-attach added sub-message: {}", subMessage); - m.addSubMessage(subMessage); - } - } - - private static ByteBufferInputStream readSubMessageSlice(ByteBufferInputStream bb, Message m) { - try { - int size = bb.readUnsignedShort(); - if (bb.remaining() < size) return null; - return bb.slice(size); - } catch (EOFException _) { - if (LOG.isDebugEnabled()) - LOG.debug("event=submessage-none no sub-messages to read; stop at {}", m); - return null; - } catch (IOException e) { - LOG.error("event=submessage-slice-io I/O error while reading sub-message slice: {}", e, e); - return null; - } - } - - private static Message decodeSubMessage( - ByteBufferInputStream bb2, PeerContext peer, boolean veryLax) { - try { - return decodeMessage(bb2, peer, 0, false, true, veryLax); - } catch (Exception e) { - LOG.error("event=submessage-decode-failed failed to decode sub-message: {}", e, e); - return null; - } - } - - /** - * Constructs a new, locally originated message with a default priority. - * - * @param spec message type descriptor; must not be {@code null} - */ - public Message(MessageType spec) { - this(spec, null, 0); - } - - private Message(MessageType spec, PeerContext source, int recvByteCount) { - localInstantiationTime = System.currentTimeMillis(); - this.spec = spec; - if (source == null) { - internal = true; - sourceRef = null; - } else { - internal = false; - sourceRef = source.getWeakRef(); - } - receivedByteCount = recvByteCount; - priority = spec.getDefaultPriority(); - } - - /** Drops sub-messages and marks the clone as locally originated. */ - private Message(Message m) { - spec = m.spec; - sourceRef = null; - internal = m.internal; - payload.putAll(m.payload); - subMessages = null; - localInstantiationTime = System.currentTimeMillis(); - receivedByteCount = 0; - priority = m.priority; - } - - /** - * Returns the boolean value for a payload field. - * - * @param key field name defined by the {@link MessageType} - * @return field value - * @throws NullPointerException if the field is not set - * @throws ClassCastException if the stored value is not a {@code Boolean} - */ - public boolean getBoolean(String key) { - return (Boolean) payload.get(key); - } - - /** - * Returns the byte value for a payload field. - * - * @param key field name defined by the {@link MessageType} - * @return field value - * @throws NullPointerException if the field is not set - * @throws ClassCastException if the stored value is not a {@code Byte} - */ - public byte getByte(String key) { - return (Byte) payload.get(key); - } - - /** - * Returns the short value for a payload field. - * - * @param key field name defined by the {@link MessageType} - * @return field value - * @throws NullPointerException if the field is not set - * @throws ClassCastException if the stored value is not a {@code Short} - */ - public short getShort(String key) { - return (Short) payload.get(key); - } - - /** - * Returns the int value for a payload field. - * - * @param key field name defined by the {@link MessageType} - * @return field value - * @throws NullPointerException if the field is not set - * @throws ClassCastException if the stored value is not an {@code Integer} - */ - public int getInt(String key) { - return (Integer) payload.get(key); - } - - /** - * Returns the long value for a payload field. - * - * @param key field name defined by the {@link MessageType} - * @return field value - * @throws NullPointerException if the field is not set - * @throws ClassCastException if the stored value is not a {@code Long} - */ - public long getLong(String key) { - return (Long) payload.get(key); - } - - /** - * Returns the double value for a payload field. - * - * @param key field name defined by the {@link MessageType} - * @return field value - * @throws NullPointerException if the field is not set - * @throws ClassCastException if the stored value is not a {@code Double} - */ - public double getDouble(String key) { - return (Double) payload.get(key); - } - - /** - * Returns the float value for a payload field. - * - * @param key field name defined by the {@link MessageType} - * @return field value - * @throws NullPointerException if the field is not set - * @throws ClassCastException if the stored value is not a {@code Float} - */ - public float getFloat(String key) { - return (Float) payload.get(key); - } - - /** - * Returns the {@code double[]} for a payload field. - * - * @param key field name defined by the {@link MessageType} - * @return field value - * @throws NullPointerException if the field is not set - * @throws ClassCastException if the stored value is not a {@code double[]} - */ - public double[] getDoubleArray(String key) { - return ((double[]) payload.get(key)); - } - - /** - * Returns the {@code float[]} for a payload field. - * - * @param key field name defined by the {@link MessageType} - * @return field value - * @throws NullPointerException if the field is not set - * @throws ClassCastException if the stored value is not a {@code float[]} - */ - public float[] getFloatArray(String key) { - return (float[]) payload.get(key); - } - - /** - * Returns the {@link String} for a payload field. - * - * @param key field name defined by the {@link MessageType} - * @return field value - * @throws NullPointerException if the field is not set - * @throws ClassCastException if the stored value is not a {@code String} - */ - public String getString(String key) { - return (String) payload.get(key); - } - - /** - * Returns the raw object for a payload field. - * - * @param key field name defined by the {@link MessageType} - * @return the stored value, or {@code null} if the field is not set - */ - public Object getObject(String key) { - return payload.get(key); - } - - /** - * Returns the bytes from a {@link ShortBuffer} payload field. - * - * @param key field name defined by the {@link MessageType} - * @return backing byte array of the {@code ShortBuffer} - * @throws NullPointerException if the field is not set - * @throws ClassCastException if the stored value is not a {@code ShortBuffer} - */ - public byte[] getShortBufferBytes(String key) { - ShortBuffer buffer = (ShortBuffer) getObject(key); - return buffer.getData(); - } - - /** - * Sets a boolean payload field. - * - * @param key field name defined by the {@link MessageType} - * @param b value to store - * @throws IncorrectTypeException if the {@code MessageType} does not accept a boolean for {@code - * key} - */ - public void set(String key, boolean b) { - set(key, Boolean.valueOf(b)); - } - - /** - * Sets a byte payload field. - * - * @param key field name defined by the {@link MessageType} - * @param b value to store - * @throws IncorrectTypeException if the {@code MessageType} does not accept a byte for {@code - * key} - */ - public void set(String key, byte b) { - set(key, Byte.valueOf(b)); - } - - /** - * Sets a short payload field. - * - * @param key field name defined by the {@link MessageType} - * @param s value to store - * @throws IncorrectTypeException if the {@code MessageType} does not accept a short for {@code - * key} - */ - public void set(String key, short s) { - set(key, Short.valueOf(s)); - } - - /** - * Sets an int payload field. - * - * @param key field name defined by the {@link MessageType} - * @param i value to store - * @throws IncorrectTypeException if the {@code MessageType} does not accept an int for {@code - * key} - */ - public void set(String key, int i) { - set(key, Integer.valueOf(i)); - } - - /** - * Sets a long payload field. - * - * @param key field name defined by the {@link MessageType} - * @param l value to store - * @throws IncorrectTypeException if the {@code MessageType} does not accept a long for {@code - * key} - */ - public void set(String key, long l) { - set(key, Long.valueOf(l)); - } - - /** - * Sets a double payload field. - * - * @param key field name defined by the {@link MessageType} - * @param d value to store - * @throws IncorrectTypeException if the {@code MessageType} does not accept a double for {@code - * key} - */ - public void set(String key, double d) { - set(key, Double.valueOf(d)); - } - - /** - * Sets a float payload field. - * - * @param key field name defined by the {@link MessageType} - * @param f value to store - * @throws IncorrectTypeException if the {@code MessageType} does not accept a float for {@code - * key} - */ - public void set(String key, float f) { - set(key, Float.valueOf(f)); - } - - /** - * Sets a payload field to an arbitrary object after type checking. - * - *

The value must be non‑{@code null} and accepted by {@link MessageType#checkType(String, - * Object)} for the supplied field name. - * - * @param key field name defined by the {@link MessageType} - * @param value non‑{@code null} value to store - * @throws IncorrectTypeException if {@code value} is {@code null} or not allowed for {@code key} - */ - public void set(String key, Object value) { - if (!spec.checkType(key, value)) { - if (value == null) { - throw new IncorrectTypeException("Got null for " + key); - } - throw new IncorrectTypeException( - "Got " + value.getClass() + ", expected " + spec.typeOf(key)); - } - payload.put(key, value); - } - - /** - * Encodes this message to its wire representation. - * - *

The returned array contains the message header, ordered fields, and (when present) any - * sub‑messages. Encoding does not mutate this instance. - * - * @return a new byte array containing the encoded message - * @throws IllegalStateException if an I/O error occurs while writing to the in‑memory buffer - */ - public byte[] encodeToPacket() { - return encodeToPacket(true); - } - - private byte[] encodeToPacket(boolean includeSubMessages) { - - if (LOG.isTraceEnabled()) - LOG.trace( - "event=encode-spec-id message type id hash: {} for {}", - spec.getName().hashCode(), - spec.getName()); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(baos); - try { - dos.writeInt(spec.getName().hashCode()); - for (String name : spec.getOrderedFields()) { - Serializer.writeToDataOutputStream(payload.get(name), dos); - } - dos.flush(); - } catch (IOException e) { - throw new IllegalStateException( - "Failed to encode message header/fields for " + spec.getName(), e); - } - - if (subMessages != null && includeSubMessages) { - for (Message _subMessage : subMessages) { - byte[] temp = _subMessage.encodeToPacket(false); - try { - dos.writeShort(temp.length); - dos.write(temp); - } catch (IOException e) { - throw new IllegalStateException("Failed to encode sub-message for " + spec.getName(), e); - } - } - } - - byte[] buf = baos.toByteArray(); - if (LOG.isTraceEnabled()) - LOG.trace("event=encode-complete length: {}, hash: {}", buf.length, Fields.hashCode(buf)); - return buf; - } - - @Override - public String toString() { - StringBuilder ret = new StringBuilder(1000); - String comma = ""; - ret.append(spec.getName()).append(" {"); - for (String name : spec.getFields().keySet()) { - ret.append(comma); - ret.append(name).append('=').append(payload.get(name)); - comma = ", "; - } - ret.append('}'); - return ret.toString(); - } - - /** - * Returns the originating peer context, if still available. - * - *

For locally constructed messages this is {@code null}. For decoded messages a weak reference - * is held to the peer context and may have been cleared by the GC. - * - * @return {@code null} for local messages, or a possibly {@code null} {@link PeerContext} - */ - public PeerContext getSource() { - return sourceRef == null ? null : sourceRef.get(); - } - - /** - * Indicates whether this message originated locally. - * - * @return {@code true} when constructed locally; {@code false} when decoded from the wire - */ - public boolean isInternal() { - return internal; - } - - /** - * Returns the type descriptor for this message. - * - * @return the {@link MessageType} associated with this message - */ - public MessageType getSpec() { - return spec; - } - - /** - * Returns whether a payload field has been set. - * - * @param fieldName field name defined by the {@link MessageType} - * @return {@code true} if the field exists in the payload map - */ - public boolean isSet(String fieldName) { - return payload.containsKey(fieldName); - } - - /** - * Retrieves a non‑{@code null} payload value by name. - * - * @param fieldName field name defined by the {@link MessageType} - * @return the stored value - * @throws FieldNotSetException if the field has not been set - */ - public Object getFromPayload(String fieldName) throws FieldNotSetException { - Object r = payload.get(fieldName); - if (r == null) { - throw new FieldNotSetException(fieldName + " not set"); - } - return r; - } - - public static class FieldNotSetException extends RuntimeException { - @Serial private static final long serialVersionUID = 1L; - - /** - * Creates an exception indicating that a required field was not present in the payload. - * - * @param message detail message - */ - public FieldNotSetException(String message) { - super(message); - } - } - - /** - * Sets the standard fields used when routing to a specific node. - * - *

This is a convenience for populating {@link DMT#UID}, {@link DMT#TARGET_LOCATION}, {@link - * DMT#HTL}, and {@link DMT#NODE_IDENTITY}. - * - * @param uid unique identifier for the routed request - * @param targetLocation target location value used by the routing algorithm - * @param htl routing HTL value - * @param nodeIdentity node identity bytes; copied into a {@link ShortBuffer} - */ - public void setRoutedToNodeFields( - long uid, double targetLocation, short htl, byte[] nodeIdentity) { - set(DMT.UID, uid); - set(DMT.TARGET_LOCATION, targetLocation); - set(DMT.HTL, htl); - set(DMT.NODE_IDENTITY, new ShortBuffer(nodeIdentity)); - } - - /** - * Returns the number of bytes attributed to receiving this message. - * - *

For decoded messages this includes the decoded payload length plus any extra overhead value - * supplied to the decoder. For locally created messages this is {@code 0}. - * - * @return attributed byte count for statistics and accounting - */ - public int receivedByteCount() { - return receivedByteCount; - } - - /** - * Appends a sub‑message to this message. - * - * @param subMessage message to attach; not copied - */ - public void addSubMessage(Message subMessage) { - if (subMessages == null) subMessages = new ArrayList<>(); - subMessages.add(subMessage); - } - - /** - * Returns the first attached sub‑message of the given type, if present. - * - * @param t message type to match - * @return a sub‑message, or {@code null} if none matches - */ - public Message getSubMessage(MessageType t) { - if (subMessages == null) return null; - for (Message m : subMessages) { - if (t.equals(m.getSpec())) return m; - } - return null; - } - - /** - * Removes and returns the first attached sub‑message of the given type. - * - * @param t message type to match - * @return the removed sub‑message, or {@code null} if none matches - */ - public Message grabSubMessage(MessageType t) { - if (subMessages == null) return null; - for (int i = 0; i < subMessages.size(); i++) { - Message m = subMessages.get(i); - if (t.equals(m.getSpec())) { - subMessages.remove(i); - return m; - } - } - return null; - } - - /** - * Returns the age of this message in milliseconds since local instantiation. - * - * @return elapsed time in milliseconds - */ - public long age() { - return System.currentTimeMillis() - localInstantiationTime; - } - - /** - * Returns the current scheduling priority value. - * - * @return implementation‑defined priority - */ - public short getPriority() { - return priority; - } - - /** - * Adjusts the internal priority to favor earlier handling. - * - *

The exact scale and ordering are implementation‑defined. - */ - public void boostPriority() { - priority--; - } - - /** - * Creates a shallow copy without sub‑messages and with a local origin. - * - *

Payload entries and priority are copied; sub‑messages are dropped, and the source reference - * is cleared. - * - * @return a new {@code Message} with no sub‑messages - */ - public Message cloneAndDropSubMessages() { - return new Message(this); - } -} diff --git a/src/main/java/network/crypta/io/comm/MessageType.java b/src/main/java/network/crypta/io/comm/MessageType.java deleted file mode 100644 index e8dbc85d99e..00000000000 --- a/src/main/java/network/crypta/io/comm/MessageType.java +++ /dev/null @@ -1,297 +0,0 @@ -package network.crypta.io.comm; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import network.crypta.support.Serializer; -import network.crypta.support.ShortBuffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Describes the schema of an application message and maintains a registry of known types. - * - *

A MessageType records field names with their Java types and the order in which fields are - * encoded. Instances are registered in a process-wide map keyed by {@code name.hashCode()} so they - * can be resolved when decoding by identifier. The mapping is legacy behavior and may be subject to - * hash collisions; callers should not rely on the numeric identifier being globally unique. - */ -public final class MessageType { - private static final Logger LOG = LoggerFactory.getLogger(MessageType.class); - - /** Source-control marker string retained for historical reference. Not used by runtime logic. */ - public static final String VERSION = - "$Id: MessageType.java,v 1.6 2005/08/25 17:28:19 amphibian Exp $"; - - private static final HashMap _specs = new HashMap<>(); - - private final String name; - private final List orderedFields = new ArrayList<>(); - private final HashMap> fields = new HashMap<>(); - private final HashMap> linkedListTypes = new HashMap<>(); - private final boolean internalOnly; - private final short priority; - private final boolean isLossyPacketMessage; - - /** - * Creates and registers a message type with the given name and default priority. - * - * @param name human-readable name; its {@link String#hashCode()} becomes the registry key - * @param priority default priority used when enqueuing messages of this type - * @throws DuplicateMessageTypeException if a type with the same hash identifier already exists - */ - public MessageType(String name, short priority) { - this(name, priority, false, false); - } - - /** - * Creates and registers a message type and configures optional transport flags. - * - *

The instance is inserted into the global registry keyed by {@code name.hashCode()}. - * - * @param name human-readable name; also used to derive the registry identifier - * @param priority default priority used for outbound scheduling - * @param internal whether messages of this type are internal-only (e.g., not accepted over UDP) - * @param isLossyPacketMessage whether the type is eligible for lossy packet transport - * @throws DuplicateMessageTypeException if a type with the same hash identifier already exists - */ - public MessageType(String name, short priority, boolean internal, boolean isLossyPacketMessage) { - this.name = name; - this.priority = priority; - this.isLossyPacketMessage = isLossyPacketMessage; - internalOnly = internal; - // XXX: Using name.hashCode() as an identifier risks collisions; this is legacy behavior. - Integer id = name.hashCode(); - if (_specs.containsKey(id)) { - throw new DuplicateMessageTypeException( - "A message type by the name of " + name + " already exists!"); - } - _specs.put(id, this); - } - - /** - * Removes this instance from the global registry. - * - *

After calling this method, {@link #getSpec(Integer, boolean)} with this type's identifier - * returns {@code null}. - */ - public void unregister() { - _specs.remove(name.hashCode()); - } - - /** - * Adds a field of type {@link List} and records the element type for that list field. - * - *

The field name is also appended to the ordered field list. - * - * @param name field name - * @param parameter the list element type (e.g., {@code Integer.class}) - */ - public void addLinkedListField(String name, Class parameter) { - linkedListTypes.put(name, parameter); - addField(name, List.class); - } - - /** - * Adds a field definition. - * - *

The field is stored by name with its declared Java type and its name is appended to the - * ordered field list. If the same field name is added multiple times, the most recent type wins. - * - * @param name field name - * @param type declared Java type for values assigned to this field - */ - public void addField(String name, Class type) { - fields.put(name, type); - orderedFields.add(name); - } - - /** - * Adds common routing-related fields used by node-to-node messages. - * - *

Fields are identified by constants in {@link DMT}: {@code UID} ({@link Long}), {@code - * TARGET_LOCATION} ({@link Double}), {@code HTL} ({@link Short}), and {@code NODE_IDENTITY} - * ({@link ShortBuffer}). - */ - public void addRoutedToNodeMessageFields() { - addField(DMT.UID, Long.class); - addField(DMT.TARGET_LOCATION, Double.class); - addField(DMT.HTL, Short.class); - addField(DMT.NODE_IDENTITY, ShortBuffer.class); - } - - /** - * Verifies that a value is assignment-compatible with the declared type of field. - * - * @param fieldName the field to check - * @param fieldValue the value to test; {@code null} returns {@code false} - * @return {@code true} if {@code fieldValue.getClass()} is equal to, or a subclass/implementation - * of, the declared field type; {@code false} if the value is {@code null} or incompatible - * @throws IllegalStateException if {@code fieldName} is not defined for this type - */ - public boolean checkType(String fieldName, Object fieldValue) { - if (fieldValue == null) { - return false; - } - Class defClass = fields.get(fieldName); - if (defClass == null) { - throw new IllegalStateException( - "Cannot set field \"" - + fieldName - + "\" which is not defined" - + " in the message type \"" - + getName() - + "\"."); - } - Class valueClass = fieldValue.getClass(); - if (defClass == valueClass) return true; - return defClass.isAssignableFrom(valueClass); - } - - /** - * Returns the declared Java type for a field. - * - * @param field field name - * @return the {@link Class} for the field, or {@code null} if the name is not defined - */ - public Class typeOf(String field) { - return fields.get(field); - } - - /** - * Compares by {@code name} value equality. - * - *

Message types are registered by name; value equality keeps comparisons stable even if two - * instances reference distinct but equal strings. - */ - @Override - public boolean equals(Object o) { - if (!(o instanceof MessageType other)) { - return false; - } - return java.util.Objects.equals(other.name, name); - } - - /** - * Computes a hash code from the {@code name} string. - * - * @return {@code name.hashCode()} - */ - @Override - public int hashCode() { - return name.hashCode(); - } - - /** - * Resolves a registered message type by its hashed identifier. - * - *

When {@code dontLog} is {@code false}, unknown IDs are logged at INFO. Unknown types can - * occur due to version skew or malformed traffic and are not necessarily fatal. - * - * @param specID identifier derived from {@code name.hashCode()} - * @param dontLog if {@code true}, suppresses INFO logging when the ID is not present - * @return the matching {@code MessageType}, or {@code null} if not registered - */ - public static MessageType getSpec(Integer specID, boolean dontLog) { - MessageType id = _specs.get(specID); - if (id == null && !dontLog) LOG.info("Unrecognised message type received ({})", specID); - - return id; - } - - /** - * Returns the human-readable name for this type. - * - * @return the name used when the type was created - */ - public String getName() { - return name; - } - - /** - * Returns the mapping of field names to declared Java types. - * - *

The returned map is backed by this instance and is mutable; changes affect the message type. - * - * @return live map of field definitions - */ - public Map> getFields() { - return fields; - } - - /** - * Returns field names in their defined encoding order. - * - *

The returned list is backed by this instance and is mutable. - * - * @return live list of field names in order - */ - public List getOrderedFields() { - return orderedFields; - } - - /** - * Returns element types for fields declared as {@link List}. - * - *

The map keys are field names; values are the corresponding element {@link Class} objects. - * The returned map is backed by this instance and is mutable. - * - * @return live map of list field element types - */ - public Map> getLinkedListTypes() { - return linkedListTypes; - } - - /** - * Indicates whether this type is internal-only. - * - *

If {@code true}, incoming UDP messages with this spec are silently discarded. - * - * @return {@code true} if internal-only; {@code false} otherwise - */ - public boolean isInternalOnly() { - return internalOnly; - } - - /** - * Returns the default priority associated with this type. - * - *

Senders may raise the effective priority dynamically (e.g., for real-time traffic). - * - * @return default priority value - */ - public short getDefaultPriority() { - return priority; - } - - /** - * Estimates the maximum encoded size, in bytes, for simple messages. - * - *

This mirrors {@code Message.encodeToPacket}. It sums the fixed sizes for known field types - * and uses {@code 4 + 2*maxStringLength} for strings. This method is not suitable for complex - * structures such as nested collections beyond {@link List} placeholders. - * - * @param maxStringLength maximum number of characters assumed for string fields - * @return upper bound on the encoded size in bytes - * @throws IllegalArgumentException if any field type is unsupported by {@link Serializer#length} - */ - public int getMaxSize(int maxStringLength) { - // This method mirrors Message.encodeToPacket. - int length = 0; - length += 4; // _spec.getName().hashCode() - for (Map.Entry> entry : fields.entrySet()) { - length += Serializer.length(entry.getValue(), maxStringLength); - } - return length; - } - - /** - * Indicates whether this type is intended for lossy packet transport. - * - * @return {@code true} if lossy packet encoding is permitted - */ - public boolean isLossyPacketMessage() { - return isLossyPacketMessage; - } -} diff --git a/src/main/java/network/crypta/io/comm/NotConnectedException.java b/src/main/java/network/crypta/io/comm/NotConnectedException.java deleted file mode 100644 index 6c3a12a05ad..00000000000 --- a/src/main/java/network/crypta/io/comm/NotConnectedException.java +++ /dev/null @@ -1,50 +0,0 @@ -package network.crypta.io.comm; - -import java.io.Serial; -import network.crypta.support.LightweightException; - -/** - * Indicates that an operation requiring an active peer connection was attempted when no connection - * existed. - * - *

This exception commonly occurs when sending a message to a peer that is not currently - * connected. It is distinct from {@link DisconnectedException}, which signals that a previously - * established connection dropped during a blocking wait. As a {@link LightweightException}, stack - * traces may be omitted by default to keep the exception inexpensive in frequent control-flow - * paths. - * - * @author amphibian - * @see DisconnectedException - * @see PeerRestartedException - */ -public class NotConnectedException extends LightweightException { - @Serial private static final long serialVersionUID = -1; - - /** - * Constructs an instance with a detail message. - * - * @param string human-readable detail; may be {@code null}. - */ - public NotConnectedException(String string) { - super(string); - } - - /** Constructs an instance with no detail message. */ - public NotConnectedException() { - super(); - } - - /** - * Constructs an instance derived from a {@link DisconnectedException}. - * - *

The message is taken from {@code e.toString()}, and this exception's cause is initialized to - * {@code e}. Useful when translating a mid-wait disconnect into a "not connected" condition for - * higher layers. - * - * @param e the disconnection to wrap as the cause; must not be {@code null}. - */ - public NotConnectedException(DisconnectedException e) { - super(e.toString()); - initCause(e); - } -} diff --git a/src/main/java/network/crypta/io/comm/OpennetAnnounceRequest.java b/src/main/java/network/crypta/io/comm/OpennetAnnounceRequest.java deleted file mode 100644 index 06f9a3bc472..00000000000 --- a/src/main/java/network/crypta/io/comm/OpennetAnnounceRequest.java +++ /dev/null @@ -1,14 +0,0 @@ -package network.crypta.io.comm; - -/** - * Immutable metadata for an opennet announce request message. - * - * @param uid request identifier used to correlate responses - * @param transferUID transfer identifier for the noderef payload - * @param noderefLength unpadded noderef length in bytes - * @param paddedLength padded transfer length in bytes - * @param target target location in {@code [0.0, 1.0)} - * @param htl hop-to-live value for routing - */ -public record OpennetAnnounceRequest( - long uid, long transferUID, int noderefLength, int paddedLength, double target, short htl) {} diff --git a/src/main/java/network/crypta/io/comm/Peer.java b/src/main/java/network/crypta/io/comm/Peer.java deleted file mode 100644 index 428ee3f028c..00000000000 --- a/src/main/java/network/crypta/io/comm/Peer.java +++ /dev/null @@ -1,415 +0,0 @@ -package network.crypta.io.comm; - -import java.io.DataInput; -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.Serial; -import java.io.Serializable; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.Comparator; -import network.crypta.io.WritableToDataOutputStream; -import network.crypta.support.io.InetAddressIpv6FirstComparator; -import network.crypta.support.transport.ip.HostnameSyntaxException; -import network.crypta.support.transport.ip.IPUtil; - -/** - * Immutable network endpoint consisting of a host (DNS name or IP literal) and a TCP port. - * - *

The host is represented by {@link FreenetInetAddress}, which can hold either a DNS hostname or - * a resolved numeric address and defines multiple equality modes. This class mirrors those - * semantics via {@link #laxEquals(Object)}, {@link #equals(Object)}, and {@link - * #strictEquals(Object)} while also requiring the port numbers to match. - * - *

Instances created from a DNS name may re-resolve the address when explicitly requested (see - * {@link #getHandshakeAddress()}). Regular address lookups (see {@link #getAddress(boolean)}) - * prefer cached results to avoid unnecessary DNS requests. - */ -public final class Peer implements WritableToDataOutputStream { - - /** - * Thrown when a resolved address is considered local/non‑public and therefore not acceptable for - * the requested operation. - */ - public static class LocalAddressException extends Exception { - @Serial private static final long serialVersionUID = -1; - } - - /** Comparator that prefers hostname peers, then IPv6 before IPv4. See {@link PeerComparator}. */ - public static final PeerComparator PEER_COMPARATOR = new PeerComparator(); - - /** Legacy revision marker string. */ - public static final String VERSION = "$Id: Peer.java,v 1.4 2005/08/25 17:28:19 amphibian Exp $"; - - private final FreenetInetAddress addr; - private final int port; - - /** - * Constructs a peer by reading from a {@link DataInput} in the same format written by {@link - * #writeToDataOutputStream(DataOutputStream)}. - * - *

Format: serialized {@link FreenetInetAddress} followed by a 32‑bit port. The port must be in - * {@code [0, 65535]}. - * - * @param dis data input to read from - * @throws IOException if the input is malformed or the port is out of range - */ - public Peer(DataInput dis) throws IOException { - addr = new FreenetInetAddress(dis); - port = dis.readInt(); - if (port > 65535 || port < 0) throw new IOException("bogus port"); - } - - /** - * Constructs a peer from a {@link DataInput}, optionally validating hostname/IP syntax while - * reading the {@link FreenetInetAddress}. - * - * @param dis data input to read from - * @param checkHostnameOrIPSyntax when {@code true}, validate DNS hostname or IPv4 syntax during - * deserialization - * @throws HostnameSyntaxException if syntax validation is enabled and the host is not well‑formed - * @throws IOException if the input is malformed or the port is out of range - */ - public Peer(DataInput dis, boolean checkHostnameOrIPSyntax) - throws HostnameSyntaxException, IOException { - addr = new FreenetInetAddress(dis, checkHostnameOrIPSyntax); - port = dis.readInt(); - if (port > 65535 || port < 0) throw new IOException("bogus port"); - } - - /** - * Constructs a peer from a concrete {@link InetAddress} and port. The numeric address is - * considered primary and does not change with later DNS updates. - * - * @param address resolved numeric address - * @param port TCP port in {@code [0, 65535]} - * @throws IllegalArgumentException if {@code port} is outside the valid range - */ - public Peer(InetAddress address, int port) { - addr = new FreenetInetAddress(address); - this.port = port; - if (this.port > 65535 || this.port < 0) throw new IllegalArgumentException("bogus port"); - } - - /** - * Parses {@code "host:port"} where {@code host} is a DNS name or IP literal. When a DNS name is - * provided, the name is treated as primary; {@link #getHandshakeAddress()} may re‑resolve it. - * - *

The split uses the last colon to separate the port, which tolerates IPv6 literals when the - * port suffix is present. - * - * @param physical input in the form {@code :} - * @param allowUnknown when {@code true}, allow construction even if the DNS name cannot be - * resolved at this time - * @throws PeerParseException if the input is malformed, or the port is missing/invalid - * @throws UnknownHostException if {@code allowUnknown} is {@code false} and the DNS lookup fails - */ - public Peer(String physical, boolean allowUnknown) - throws PeerParseException, UnknownHostException { - int offset = physical.lastIndexOf(':'); // split on final ':' to tolerate IPv6 literals - if (offset < 0) throw new PeerParseException(); - String host = physical.substring(0, offset); - addr = new FreenetInetAddress(host, allowUnknown); - String strport = physical.substring(offset + 1); - try { - port = Integer.parseInt(strport); - if (port < 0 || port > 65535) throw new PeerParseException("Invalid port " + port); - } catch (NumberFormatException e) { - throw new PeerParseException(e); - } - } - - /** - * Parses {@code "host:port"} with optional hostname/IP syntax validation. Semantics are identical - * to {@link #Peer(String, boolean)} with the added syntax check step. - * - * @param physical input in the form {@code :} - * @param allowUnknown when {@code true}, allow construction even if the DNS name cannot be - * resolved at this time - * @param checkHostnameOrIPSyntax when {@code true}, validate the DNS hostname or IPv4 literal - * syntax - * @throws HostnameSyntaxException if syntax validation fails - * @throws PeerParseException if the input is malformed, or the port is missing/invalid - * @throws UnknownHostException if {@code allowUnknown} is {@code false} and the DNS lookup fails - */ - public Peer(String physical, boolean allowUnknown, boolean checkHostnameOrIPSyntax) - throws HostnameSyntaxException, PeerParseException, UnknownHostException { - int offset = physical.lastIndexOf(':'); // split on final ':' to tolerate IPv6 literals - if (offset < 0) throw new PeerParseException("No port number: \"" + physical + "\""); - String host = physical.substring(0, offset); - addr = new FreenetInetAddress(host, allowUnknown, checkHostnameOrIPSyntax); - String strport = physical.substring(offset + 1); - try { - port = Integer.parseInt(strport); - if (port < 0 || port > 65535) throw new PeerParseException("Invalid port " + port); - } catch (NumberFormatException e) { - throw new PeerParseException(e); - } - } - - /** - * Constructs a peer from an existing {@link FreenetInetAddress} and port. No copying is - * performed; the provided instance is kept as‑is. - * - * @param addr address holder; must be non‑null - * @param port TCP port in {@code [0, 65535]} - * @throws NullPointerException if {@code addr} is {@code null} - * @throws IllegalArgumentException if {@code port} is outside the valid range - */ - public Peer(FreenetInetAddress addr, int port) { - this.addr = addr; - if (addr == null) throw new NullPointerException(); - this.port = port; - if (this.port > 65535 || this.port < 0) throw new IllegalArgumentException("bogus port"); - } - - /** - * Returns {@code true} when the port is zero. This is used as a sentinel for an uninitialized or - * special peer in some contexts. - */ - public boolean isNull() { - return port == 0; - } - - /** - * Returns {@code true} if {@code o} is a {@code Peer} with the same port and an address that is - * equal under {@link FreenetInetAddress#laxEquals(FreenetInetAddress)}. - */ - public boolean laxEquals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof Peer peer)) { - return false; - } - - if (port != peer.port) { - return false; - } - return addr.laxEquals(peer.addr); - } - - /** - * Compares by port and address using {@link FreenetInetAddress#equals(Object)}. This is the - * default, non‑lax, non‑strict comparison. - */ - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof Peer peer)) { - return false; - } - - if (port != peer.port) { - return false; - } - return addr.equals(peer.addr); - } - - /** - * Returns {@code true} if {@code o} is a {@code Peer} with the same port and an address that is - * equal under {@link FreenetInetAddress#strictEquals(FreenetInetAddress)}. - */ - public boolean strictEquals(Object o) { - if (this == o) { - return true; - } - if (o == null) return false; - if (!(o instanceof Peer peer)) { - return false; - } - - if (port != peer.port) { - return false; - } - return addr.strictEquals(peer.addr); - } - - /** - * Returns the IP address, performing a DNS lookup if needed and allowed by {@link - * #getAddress(boolean)} semantics. - */ - public InetAddress getAddress() { - return getAddress(true); - } - - /** - * Returns the IP address. When {@code doDNSRequest} is {@code true} and no cached value exists, a - * DNS lookup may be performed. If a previous lookup exists, the cached value is returned without - * forcing a refresh. - * - * @param doDNSRequest whether a DNS lookup is permitted - * @return the resolved address, or {@code null} when unknown and a lookup is not performed - */ - public InetAddress getAddress(boolean doDNSRequest) { - return addr.getAddress(doDNSRequest); - } - - /** - * Returns the IP address with optional DNS lookup and local‑address filtering. - * - * @param doDNSRequest whether a DNS lookup is permitted - * @param allowLocal when {@code false}, reject addresses considered local/non‑public - * @return the resolved address, or {@code null} when unknown and a lookup is not performed - * @throws LocalAddressException if the resolved address is not acceptable and {@code allowLocal} - * is {@code false} - */ - public InetAddress getAddress(boolean doDNSRequest, boolean allowLocal) - throws LocalAddressException { - InetAddress a = addr.getAddress(doDNSRequest); - if (a == null) return null; - if (allowLocal || IPUtil.isValidAddress(a, false)) return a; - throw new LocalAddressException(); - } - - /** - * Forces a re‑lookup when the hostname is primary, even if a previous resolution exists. This is - * typically used before reconnection attempts when a dynamic DNS address may have changed. - */ - @SuppressWarnings("UnusedReturnValue") - public InetAddress getHandshakeAddress() { - return addr.getHandshakeAddress(); - } - - /** Computes a hash code consistent with {@link #equals(Object)}. */ - @Override - public int hashCode() { - return addr.hashCode() + port; - } - - /** - * Returns the TCP port number. - * - * @return port in {@code [0, 65535]} - */ - public int getPort() { - return port; - } - - /** - * Returns {@code host:port} using the underlying {@link FreenetInetAddress#toString()} for the - * host component. - */ - @Override - public String toString() { - return addr.toString() + ':' + port; - } - - /** - * Writes this peer to a {@link DataOutputStream}. The format matches the constructor that accepts - * a {@link DataInput}: serialized {@link FreenetInetAddress} then a 32‑bit port. - * - * @param dos output stream - * @throws IOException if writing fails - */ - @Override - public void writeToDataOutputStream(DataOutputStream dos) throws IOException { - addr.writeToDataOutputStream(dos); - dos.writeInt(port); - } - - /** - * Returns the underlying address holder for advanced queries. - * - * @return the {@link FreenetInetAddress} backing this peer - */ - public FreenetInetAddress getFreenetAddress() { - return addr; - } - - /** - * Indicates whether the address is considered a real Internet address. The check may perform a - * lookup based on {@code lookup} and may treat local addresses as acceptable based on {@code - * allowLocalAddresses}. - * - * @param lookup whether to perform a lookup if needed - * @param defaultVal value to return when the address is unknown - * @param allowLocalAddresses whether to treat local/private addresses as acceptable - * @return {@code true} if the address is acceptable per the above parameters - */ - public boolean isRealInternetAddress( - boolean lookup, boolean defaultVal, boolean allowLocalAddresses) { - return addr.isRealInternetAddress(lookup, defaultVal, allowLocalAddresses); - } - - /** - * Returns {@code host:port} but prefers a numeric IP representation for the host when available, - * avoiding DNS names. - */ - public String toStringPrefNumeric() { - return addr.toStringPrefNumeric() + ':' + port; - } - - /** - * Returns a peer where the hostname (if any) is dropped in favor of a numeric address. If no - * numeric address is known, returns {@code null}. If the underlying address is unchanged, this - * instance is returned. - * - * @return a peer without a hostname, or {@code null} if not possible - */ - public Peer dropHostName() { - FreenetInetAddress newAddr = addr.dropHostname(); - if (newAddr == null) return null; - if (!addr.equals(newAddr)) { - return new Peer(newAddr, port); - } else return this; - } - - /** - * Returns {@code true} when the address is IPv6. If the address is unknown, {@code defaultValue} - * is returned. - * - * @param defaultValue value to return when the address has not been resolved - */ - public boolean isIPv6(boolean defaultValue) { - if (addr == null) return defaultValue; - return addr.isIPv6(defaultValue); - } - - /** - * Comparator that orders peers as follows: - * - *

    - *
  • Peers with a hostname sort before peers without one. - *
  • If both have hostnames, they compare as equal (no further ordering by name). - *
  • Otherwise, IPv6 addresses sort before IPv4. - *
  • Ties are broken using {@link InetAddressIpv6FirstComparator#COMPARATOR} on the resolved - * addresses. - *
- * - *

This is not a strict total ordering. Callers that require a complete order should apply an - * additional tie‑breaker. - */ - public static class PeerComparator implements Comparator, Serializable { - @Serial private static final long serialVersionUID = 1L; - - /** - * Compares two peers according to {@link PeerComparator} rules. - * - * @return negative if {@code p0} should sort before {@code p1}; positive if after; zero when - * considered equivalent by this comparator - */ - @Override - public int compare(Peer p0, Peer p1) { - boolean hasHostnameP0 = p0.getFreenetAddress().hasHostname(); - boolean hasHostnameP1 = p1.getFreenetAddress().hasHostname(); - boolean isIpv6P0 = p0.isIPv6(false); // default used when the address is not yet resolved - boolean isIpv6P1 = p1.isIPv6(false); // default used when the address is not yet resolved - if (hasHostnameP0 && !hasHostnameP1) { - return -1; - } else if (!hasHostnameP0 && hasHostnameP1) { - return 1; - } else if (hasHostnameP0) { - return 0; - } - if (isIpv6P0 && !isIpv6P1) { - return -1; - } else if (!isIpv6P0 && isIpv6P1) { - return 1; - } - return InetAddressIpv6FirstComparator.COMPARATOR.compare(p0.getAddress(), p1.getAddress()); - } - } -} diff --git a/src/main/java/network/crypta/io/comm/PeerParseException.java b/src/main/java/network/crypta/io/comm/PeerParseException.java deleted file mode 100644 index d2e3a8d10a3..00000000000 --- a/src/main/java/network/crypta/io/comm/PeerParseException.java +++ /dev/null @@ -1,39 +0,0 @@ -package network.crypta.io.comm; - -import java.io.Serial; - -/** - * Indicates that a textual or structured peer description could not be parsed. - * - *

Thrown by code that converts external representations (for example, node references or address - * strings) into {@link Peer} instances when the input is syntactically invalid or missing required - * fields. - * - * @author amphibian - */ -public class PeerParseException extends Exception { - @Serial private static final long serialVersionUID = -1; - - /** - * Constructs an instance that wraps a lower-level cause. - * - * @param e the underlying parsing failure; may be {@code null}. - */ - public PeerParseException(Exception e) { - super(e); - } - - /** Constructs an instance with no detail message. */ - public PeerParseException() { - super(); - } - - /** - * Constructs an instance with a detail message. - * - * @param string human-readable detail; may be {@code null}. - */ - public PeerParseException(String string) { - super(string); - } -} diff --git a/src/main/java/network/crypta/io/comm/PeerRestartedException.java b/src/main/java/network/crypta/io/comm/PeerRestartedException.java deleted file mode 100644 index a7fa4c34d42..00000000000 --- a/src/main/java/network/crypta/io/comm/PeerRestartedException.java +++ /dev/null @@ -1,25 +0,0 @@ -package network.crypta.io.comm; - -import java.io.Serial; -import network.crypta.support.LightweightException; - -/** - * Indicates that the remote peer restarted during an in‑flight operation. - * - *

This specialized form of {@link DisconnectedException} is used when an established connection - * drops because the peer process has restarted. Typical triggers include attempting to send a - * throttled packet, waiting for an incoming reply, or otherwise interacting with a session whose - * {@link PeerContext#getBootID() boot ID} has changed. - * - *

As a descendant of {@link LightweightException} (via {@link DisconnectedException}), stack - * traces may be omitted by default to keep the exception inexpensive in frequent control‑flow - * paths. - * - * @see DisconnectedException - * @see NotConnectedException - * @see PeerContext#getBootID() - */ -public class PeerRestartedException extends DisconnectedException { - - @Serial private static final long serialVersionUID = 616182042289792833L; -} diff --git a/src/main/java/network/crypta/io/comm/ReferenceSignatureVerificationException.java b/src/main/java/network/crypta/io/comm/ReferenceSignatureVerificationException.java deleted file mode 100644 index 619ff8c95b9..00000000000 --- a/src/main/java/network/crypta/io/comm/ReferenceSignatureVerificationException.java +++ /dev/null @@ -1,39 +0,0 @@ -package network.crypta.io.comm; - -import java.io.Serial; - -/** - * Exception indicating that verification of a reference signature failed. - * - *

This exception is thrown when a cryptographic signature associated with a reference cannot be - * validated. Callers should treat this as an authentication failure and ignore or discard the - * unverified data. - * - * @author amphibian - */ -public class ReferenceSignatureVerificationException extends Exception { - @Serial private static final long serialVersionUID = -1; - - /** - * Creates an instance that wraps the underlying cause. - * - * @param e the cause of the verification failure; may be {@code null} - */ - public ReferenceSignatureVerificationException(Exception e) { - super(e); - } - - /** Creates an instance with no detail message or cause. */ - public ReferenceSignatureVerificationException() { - super(); - } - - /** - * Creates an instance with the specified detail message. - * - * @param string the detail message to include in this exception - */ - public ReferenceSignatureVerificationException(String string) { - super(string); - } -} diff --git a/src/main/java/network/crypta/io/comm/RetrievalException.java b/src/main/java/network/crypta/io/comm/RetrievalException.java deleted file mode 100644 index ac2b20b9486..00000000000 --- a/src/main/java/network/crypta/io/comm/RetrievalException.java +++ /dev/null @@ -1,151 +0,0 @@ -package network.crypta.io.comm; - -import java.io.Serial; -import network.crypta.support.LightweightException; - -/** - * Exception indicating that a block or related data could not be retrieved over the I/O - * communication layer. - * - *

The reason for the failure is encoded as an integer {@linkplain #getReason() reason code}. - * Human-readable forms are provided by {@link #getErrString()} and {@link #getErrString(int)}. The - * {@link #toString()} and {@link #getMessage()} methods include both the canonical reason string - * and an optional detail message. - * - *

This class extends {@link LightweightException}; consult that type for behavior differences - * from {@link Exception} (e.g., message formatting or stack handling). - * - * @author ian - */ -public class RetrievalException extends LightweightException { - @Serial private static final long serialVersionUID = 3257565105301500723L; - - /** Unspecified failure reason. */ - public static final int UNKNOWN = 0; - - /** Premature end of input while reading a block or message. */ - public static final int PREMATURE_EOF = 2; - - /** Underlying I/O error occurred during transfer. */ - public static final int IO_ERROR = 3; - - /** Sender terminated unexpectedly during the transfer. */ - public static final int SENDER_DIED = 5; - - /** Operation exceeded the allowed time. */ - public static final int TIMED_OUT = 4; - - /** The requested block already exists locally; retrieval not performed. */ - public static final int ALREADY_CACHED = 6; - - /** Sender disconnected before completion. */ - public static final int SENDER_DISCONNECTED = 7; - - /** Data insert is unavailable or not permitted for this operation. */ - public static final int NO_DATAINSERT = 8; - - /** Transfer was canceled by the receiver. */ - public static final int CANCELLED_BY_RECEIVER = 9; - - /** Receiver terminated unexpectedly during the transfer. */ - public static final int RECEIVER_DIED = 11; - - /** Could not send a block within the configured timeout. */ - public static final int UNABLE_TO_SEND_BLOCK_WITHIN_TIMEOUT = 12; - - /** Transfer aborted because the peer switched to turtle mode. */ - public static final int GONE_TO_TURTLE_MODE = 13; - - /** Transfer aborted because the turtle job/session was terminated. */ - public static final int TURTLE_KILLED = 14; - - // Numeric reason code; one of the public constants above. - final int reason; - // Optional human-readable detail for diagnostics (may be empty). - final String cause; - - /** - * Creates an instance with the given reason code. - * - * @param reason one of the public reason code constants defined in this class - */ - public RetrievalException(int reason) { - this.reason = reason; - this.cause = getErrString(reason); - } - - /** - * Creates an instance with the given reason code and a detail message. - * - *

If {@code cause} is {@code null}, empty, or the literal {@code "null"}, the canonical reason - * string from {@link #getErrString(int)} is used instead. - * - * @param reason one of the public reason code constants defined in this class - * @param cause optional detail message for logging or diagnostics - */ - public RetrievalException(int reason, String cause) { - this.reason = reason; - this.cause = - cause == null || cause.isEmpty() || cause.equals("null") ? getErrString(reason) : cause; - } - - /** - * Returns the numeric reason code associated with this failure. - * - * @return one of the public reason code constants defined in this class - */ - public int getReason() { - return reason; - } - - /** - * Returns a concise description in the form {@code REASON:detail}. The reason token contains no - * spaces, which simplifies parsing in logs and diagnostics. - */ - @Override - public String toString() { - return getErrString(reason) + ":" + cause; - } - - /** - * Returns the canonical reason token for this instance. - * - *

The token contains no spaces to remain stable in logs and machine parsing. - * - * @return a non-null, no-space canonical token for the reason code - */ - public String getErrString() { - return getErrString(reason); - } - - /** - * Maps a reason code to its canonical token. - * - * @param reason a numeric reason code - * @return a non-null, no-space canonical token describing the reason code; {@code "UNKNOWN (n)"} - * for unrecognized values - */ - public static String getErrString(int reason) { - return switch (reason) { - case PREMATURE_EOF -> "PREMATURE_EOF"; - case IO_ERROR -> "IO_ERROR"; - case SENDER_DIED -> "SENDER_DIED"; - case TIMED_OUT -> "TIMED_OUT"; - case ALREADY_CACHED -> "ALREADY_CACHED"; - case SENDER_DISCONNECTED -> "SENDER_DISCONNECTED"; - case NO_DATAINSERT -> "NO_DATAINSERT"; - case CANCELLED_BY_RECEIVER -> "CANCELLED_BY_RECEIVER"; - case UNKNOWN -> "UNKNOWN"; - case UNABLE_TO_SEND_BLOCK_WITHIN_TIMEOUT -> "UNABLE_TO_SEND_BLOCK_WITHIN_TIMEOUT"; - case GONE_TO_TURTLE_MODE -> "GONE_TO_TURTLE_MODE"; - case TURTLE_KILLED -> "TURTLE_KILLED"; - default -> "UNKNOWN (" + reason + ")"; - }; - } - - /** Returns the detail message. Equivalent to {@link #toString()}. */ - @Override - public String getMessage() { - return toString(); - } -} diff --git a/src/main/java/network/crypta/io/comm/TrafficClass.java b/src/main/java/network/crypta/io/comm/TrafficClass.java deleted file mode 100644 index a4ca6246e5e..00000000000 --- a/src/main/java/network/crypta/io/comm/TrafficClass.java +++ /dev/null @@ -1,72 +0,0 @@ -package network.crypta.io.comm; - -/** - * Differentiated Services Code Point (DSCP) values for use with {@link - * java.net.Socket#setTrafficClass(int)}. - * - *

Each constant encodes the 6-bit DSCP value left-shifted into the IPv4/IPv6 traffic class - * field; the two least significant bits (ECN) are {@code 0}. These values can be passed directly to - * {@code Socket#setTrafficClass(int)} on platforms that honor DSCP settings. - * - *

Naming follows common DSCP conventions: {@code CSx} (Class Selector), {@code AFxy} (Assured - * Forwarding), and a critical/expedited forwarding class. - * - * @see java.net.Socket#setTrafficClass(int) - * @see Differentiated services - */ -public enum TrafficClass { - BEST_EFFORT(0), - DSCP_CRITICAL(0xB8), - DSCP_AF11(0x28), - DSCP_AF12(0x30), - DSCP_AF13(0x38), - DSCP_AF21(0x48), - DSCP_AF22(0x50), - DSCP_AF23(0x52), - DSCP_AF31(0x58), - DSCP_AF32(0x70), - DSCP_AF33(0x78), - DSCP_AF41(0x88), - DSCP_AF42(0x90), - DSCP_AF43(0x98), - DSCP_CS0(0), - DSCP_CS1(0x20), - DSCP_CS2(0x40), - DSCP_CS3(0x60), - DSCP_CS4(0x80), - DSCP_CS5(0xA0), - DSCP_CS6(0xC0), - DSCP_CS7(0xE0), - RFC1349_IPTOS_LOWCOST(0x02), - RFC1349_IPTOS_RELIABILITY(0x04), - RFC1349_IPTOS_THROUGHPUT(0x08), - RFC1349_IPTOS_LOWDELAY(0x10); - - public final int value; - - TrafficClass(int tc) { - value = tc; - } - - public static TrafficClass getDefault() { - // Default: CS1 (often treated as lower priority but suitable for bulk throughput). - return TrafficClass.DSCP_CS1; - } - - public static TrafficClass fromNameOrValue(String tcName) { - // Accept either a symbolic enum name (case-insensitive) or a decimal integer value. - int tcParsed = -1; - try { - tcParsed = Integer.parseInt(tcName); - } catch (NumberFormatException _) { - // Not an integer; fall through and attempt name matching. - } - - for (TrafficClass t : TrafficClass.values()) { - if (t.toString().equalsIgnoreCase(tcName) || t.value == tcParsed) { - return t; - } - } - throw new IllegalArgumentException(); - } -} diff --git a/src/main/java/network/crypta/node/Version.java b/src/main/java/network/crypta/node/Version.java deleted file mode 100644 index 097bc4ab3bd..00000000000 --- a/src/main/java/network/crypta/node/Version.java +++ /dev/null @@ -1,506 +0,0 @@ -package network.crypta.node; - -import java.util.Arrays; -import network.crypta.support.Fields; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Version information and helpers for the Cryptad node. - * - *

This class is the canonical source for node identity values, wire-version descriptors, and - * compatibility decisions used during peer negotiation. It exposes both static constants and helper - * methods that parse and compare serialized version strings exchanged by peers. Callers use these - * helpers to answer whether a remote peer is acceptable, to extract build identifiers from - * user-visible version text, and to track the highest build observed in the network. - * - *

All methods are static and side effects are intentionally limited to updating a single - * in-process "highest seen build" counter. Returned version component arrays are defensive copies, - * so callers cannot mutate internal cached values. Logging is used for rejection diagnostics but - * does not alter compatibility outcomes. - */ -public final class Version { - /** Human-readable product name of the node. */ - public static final String NODE_NAME = "Cryptad"; - - /** Minimum acceptable build for stable Fred peers. */ - public static final int LAST_GOOD_FRED_STABLE_BUILD = 1; - - /** Protocol version the node speaks on the wire. */ - public static final String LAST_GOOD_FRED_PROTOCOL_VERSION = "1.0"; - - /** Git revision embedded at build time. */ - public static final String GIT_REVISION = "@git_rev@"; - - /** Minimum acceptable build number for Cryptad peers. */ - public static final int MIN_ACCEPTABLE_CRYPTAD_BUILD_NUMBER = 1; - - /** Minimum acceptable build number for Freenet peers. */ - public static final int MIN_ACCEPTABLE_FRED_BUILD_NUMBER = 1475; - - // Private constants - private static final String BUILD_NUMBER_STRING = "@build_number@"; - private static final String WIRE_NAME = "Fred"; - private static final String STABLE_FRED_NODE_VERSION = "0.7"; - private static final String[] EMPTY_VERSION_COMPONENTS = new String[0]; - - private static final int BUILD_NUMBER = computeBuildNumber(); - private static final Logger LOG = LoggerFactory.getLogger("network.crypta.node.Version"); - - private static volatile int highestSeenBuild = BUILD_NUMBER; - - private static final String[] CACHED_VERSION_COMPONENTS = { - NODE_NAME, - Integer.toString(BUILD_NUMBER), - LAST_GOOD_FRED_PROTOCOL_VERSION, - Integer.toString(BUILD_NUMBER) - }; - - private static final String[] CACHED_MIN_ACCEPTABLE_VERSION_COMPONENTS = { - WIRE_NAME, - STABLE_FRED_NODE_VERSION, - LAST_GOOD_FRED_PROTOCOL_VERSION, - Integer.toString(MIN_ACCEPTABLE_FRED_BUILD_NUMBER) - }; - - private Version() {} - - /** - * Returns the build number embedded at runtime. - * - * @return current node build number resolved during class initialization - */ - public static int currentBuildNumber() { - return BUILD_NUMBER; - } - - /** - * Returns the Git revision string embedded at build time. - * - * @return build-time Git revision token for this runtime - */ - public static String gitRevision() { - return GIT_REVISION; - } - - /** - * Returns the current node version tuple as components. - * - * @return a new array containing node name, build, protocol, and compatibility build fields - */ - public static String[] getVersionComponents() { - return Arrays.copyOf(CACHED_VERSION_COMPONENTS, CACHED_VERSION_COMPONENTS.length); - } - - /** - * Returns minimum acceptable peer-version components for Fred compatibility. - * - * @return a new array containing the minimum acceptable wire peer descriptor components - */ - public static String[] getMinAcceptableVersionComponents() { - return Arrays.copyOf( - CACHED_MIN_ACCEPTABLE_VERSION_COMPONENTS, CACHED_MIN_ACCEPTABLE_VERSION_COMPONENTS.length); - } - - /** - * Returns the current node version in serialized wire format. - * - * @return comma-separated version string advertised to remote peers - */ - public static String getVersionString() { - return Fields.commaList(getVersionComponents()); - } - - /** - * Returns the serialized minimum acceptable peer-version descriptor. - * - * @return comma-separated minimum acceptable version string - */ - public static String getMinAcceptableVersionString() { - return Fields.commaList(getMinAcceptableVersionComponents()); - } - - /** - * Checks whether a peer version string is compatible with this node. - * - * @param version peer-reported serialized version string to validate - * @return {@code true} when the version parses successfully and passes compatibility checks - */ - public static boolean isCompatibleVersion(String version) { - String[] v = parseVersionOrNull(version); - if (v.length == 0) { - return false; - } - if (rejectIfCryptadTooOld(v, version)) { - return false; - } - if (rejectIfFredTooOld(v, version)) { - return false; - } - if (LOG.isDebugEnabled()) { - LOG.debug("Accepting peer version string: {}", version); - } - return true; - } - - /** - * Checks compatibility using the peer version and its stated minimum acceptable version. - * - * @param versionStr peer-reported serialized version string - * @param lastGoodVersionStr peer-reported minimum acceptable version string - * @return {@code true} when both descriptors parse and satisfy compatibility rules - */ - public static boolean isCompatibleVersionWithLastGood( - String versionStr, String lastGoodVersionStr) { - String[] v = parseVersionOrNull(versionStr); - if (v.length == 0) { - return false; - } - String[] lgv = parseVersionOrNull(lastGoodVersionStr, "lastGoodVersion"); - if (lgv.length == 0) { - return false; - } - - if (NODE_NAME.equals(v[0]) - && NODE_NAME.equals(lgv[0]) - && !checkCryptadCompatibility(v, lgv, versionStr, lastGoodVersionStr)) { - return false; - } - - if (WIRE_NAME.equals(v[0]) && !checkFredCompatibility(v, lgv, versionStr, lastGoodVersionStr)) { - return false; - } - - if (LOG.isDebugEnabled()) { - LOG.debug("Accepting peer version with lastGood: {}", versionStr); - } - return true; - } - - /** - * Parses a version string and extracts its build number. - * - * @param version serialized peer version string to parse - * @return parsed build number extracted from the provided version descriptor - * @throws VersionParseException if the version string format is invalid - */ - public static int parseBuildNumberFromVersionStr(String version) throws VersionParseException { - if (version == null) { - LOG.error("version == null!", new Exception("error")); - throw new IllegalArgumentException("version == null"); - } - - String[] v = Fields.commaList(version); - if (v.length < 3 || hasInvalidProtocol(v[2])) { - throw new VersionParseException("not long enough or bad protocol: " + version); - } - - try { - if (NODE_NAME.equals(v[0])) { - return Integer.parseInt(v[1]); - } - if (WIRE_NAME.equals(v[0])) { - if (v.length == 3) { - throw new VersionParseException("Fred version missing build number: " + version); - } - return Integer.parseInt(v[3]); - } - throw new VersionParseException("unknown node name: " + v[0]); - } catch (NumberFormatException e) { - String bad = v.length > 3 ? v[3] : null; - VersionParseException wrapped = - new VersionParseException( - "Got NumberFormatException on " + bad + " : " + e + " for " + version); - wrapped.initCause(e); - throw wrapped; - } - } - - /** - * Parses a build number and falls back to a default when parsing fails. - * - * @param version serialized peer version string to parse - * @param defaultValue fallback value returned when parsing fails - * @return parsed build number, or {@code defaultValue} if parsing throws - */ - public static int parseBuildNumberFromVersionStr(String version, int defaultValue) { - try { - return parseBuildNumberFromVersionStr(version); - } catch (Exception _) { - return defaultValue; - } - } - - /** - * Records a peer version and updates the highest seen build when applicable. - * - * @param versionStr serialized peer version string observed from the network - */ - public static void seenVersion(String versionStr) { - String[] v = Fields.commaList(versionStr); - if (v.length < 3) { - return; - } - - int version; - try { - if (NODE_NAME.equals(v[0])) { - version = Integer.parseInt(v[1]); - } else if (WIRE_NAME.equals(v[0])) { - if (v.length == 3) { - return; - } - version = Integer.parseInt(v[3]); - } else { - return; - } - } catch (Exception _) { - return; - } - - if (version > highestSeenBuild) { - if (LOG.isDebugEnabled()) { - LOG.debug("New highest seen build: {}", version); - } - highestSeenBuild = version; - } - } - - /** - * Returns the highest peer build number observed in this process. - * - * @return highest build number accepted by {@link #seenVersion(String)} - */ - public static int getHighestSeenBuild() { - return highestSeenBuild; - } - - /** - * Returns whether parsed version components identify a Cryptad node. - * - * @param v parsed version components array to inspect - * @return {@code true} when the first component identifies {@link #NODE_NAME} - */ - public static boolean isCryptad(String[] v) { - return v.length >= 2 && NODE_NAME.equals(v[0]); - } - - /** - * Returns whether two parsed version arrays belong to compatible release series. - * - * @param v parsed peer version components - * @param lgv parsed peer "last good" version components - * @return {@code true} when both arrays represent a compatible version family - */ - @SuppressWarnings("unused") - public static boolean isCompatibleSeries(String[] v, String[] lgv) { - if (v.length < 2 || lgv.length < 2) { - return false; - } - return switch (v[0]) { - case NODE_NAME -> true; // Cryptad compatible with Fred - case WIRE_NAME -> v[1].equals(lgv[1]) && v.length >= 4 && lgv.length >= 4; - default -> false; - }; - } - - private static boolean hasInvalidProtocol(String protocol) { - return !LAST_GOOD_FRED_PROTOCOL_VERSION.equals(protocol); - } - - private static boolean checkCryptadCompatibility( - String[] v, String[] lgv, String versionStr, String lastGoodVersionStr) { - Integer version = parseIntOrNull(getOrNull(v, 1)); - Integer minVersion = parseIntOrNull(getOrNull(lgv, 1)); - return isNumberAtLeast(version, minVersion, versionStr, lastGoodVersionStr); - } - - private static boolean checkFredCompatibility( - String[] v, String[] lgv, String versionStr, String lastGoodVersionStr) { - if (lgv.length > 0 && NODE_NAME.equals(lgv[0])) { - return false; - } - if (rejectIfFredTooOld(v, versionStr)) { - return false; - } - Integer build = parseIntOrNull(getOrNull(v, 3)); - Integer minBuild = parseIntOrNull(getOrNull(lgv, 3)); - return isNumberAtLeast(build, minBuild, versionStr, lastGoodVersionStr); - } - - private static String[] parseVersionOrNull(String version) { - return parseVersionOrNull(version, "version"); - } - - private static String[] parseVersionOrNull(String version, String label) { - if (version == null) { - LOG.error("{} == null!", label, new Exception("error")); - return EMPTY_VERSION_COMPONENTS; - } - String[] v = Fields.commaList(version); - if (v.length < 3 || hasInvalidProtocol(v[2])) { - return EMPTY_VERSION_COMPONENTS; - } - return v; - } - - private static boolean isNumberAtLeast( - Integer actual, Integer min, String versionStr, String lastGoodVersionStr) { - if (actual == null || min == null) { - if (LOG.isDebugEnabled()) { - LOG.debug( - "Rejecting version due to non-numeric build (compat check): version={}" - + " lastGoodVersion={}", - versionStr, - lastGoodVersionStr); - } - return false; - } - - if (actual < min) { - if (LOG.isDebugEnabled()) { - LOG.debug( - "Rejecting version below minimum (compat check): version={} lastGoodVersion={}", - versionStr, - lastGoodVersionStr); - } - return false; - } - - return true; - } - - private static boolean rejectIfCryptadTooOld(String[] v, String original) { - if (!isCryptad(v)) { - return false; - } - Integer version = parseIntOrNull(getOrNull(v, 1)); - int req = MIN_ACCEPTABLE_CRYPTAD_BUILD_NUMBER; - - if (version == null) { - if (LOG.isDebugEnabled()) { - LOG.debug("Rejecting Cryptad version with non-numeric build: {}", original); - } - return true; - } - if (version < req) { - if (LOG.isDebugEnabled()) { - LOG.debug( - "Rejecting Cryptad version below minimum build: {}" - + " (minAcceptableCryptadBuildNumber={})", - original, - req); - } - return true; - } - return false; - } - - private static boolean rejectIfFredTooOld(String[] v, String original) { - if (!isFredStableVersion(v)) { - return false; - } - - Integer build = parseIntOrNull(getOrNull(v, 3)); - if (build == null) { - if (LOG.isDebugEnabled()) { - LOG.debug("Rejecting Fred stable version with non-numeric build: {}", original); - } - return true; - } - - if (build < LAST_GOOD_FRED_STABLE_BUILD) { - if (LOG.isDebugEnabled()) { - LOG.debug( - "Rejecting Fred stable version below last good build: {} (lastGoodStableBuild={})", - original, - LAST_GOOD_FRED_STABLE_BUILD); - } - return true; - } - return false; - } - - private static boolean isFredStableVersion(String[] v) { - return v.length >= 4 && WIRE_NAME.equals(v[0]) && STABLE_FRED_NODE_VERSION.equals(v[1]); - } - - /** - * Compares two build numbers considering node names first. - * - *

Cryptad nodes are always considered newer than Fred nodes. - * - * @param nodeName1 node-name component for the first build number - * @param buildNumber1 first build number to compare - * @param nodeName2 node-name component for the second build number - * @param buildNumber2 second build number to compare - * @return a negative value, zero, or a positive value following {@link Integer#compare(int, int)} - */ - public static int compareBuildNumbers( - String nodeName1, int buildNumber1, String nodeName2, int buildNumber2) { - if (nodeName1 == null || nodeName2 == null) { - return Integer.compare(buildNumber1, buildNumber2); - } - - if (NODE_NAME.equals(nodeName1) && WIRE_NAME.equals(nodeName2)) { - return 1; - } - if (WIRE_NAME.equals(nodeName1) && NODE_NAME.equals(nodeName2)) { - return -1; - } - return Integer.compare(buildNumber1, buildNumber2); - } - - /** - * Checks if a peer's build is at least the specified minimum build number, considering node name. - * - * @param nodeName peer node-name component - * @param buildNumber peer build number - * @param minBuildNumber minimum acceptable build number for non-Cryptad peers - * @return {@code true} when the peer satisfies minimum build requirements - */ - public static boolean isBuildAtLeast(String nodeName, int buildNumber, int minBuildNumber) { - if (NODE_NAME.equals(nodeName)) { - return true; - } - return buildNumber >= minBuildNumber; - } - - /** - * Extracts the node-name component from a serialized version string. - * - * @param version serialized version string to parse - * @return first version component, or {@code null} when missing or input is {@code null} - */ - public static String parseNodeNameFromVersionStr(String version) { - if (version == null) { - return null; - } - String[] parts = Fields.commaList(version); - return parts.length > 0 ? parts[0] : null; - } - - @SuppressWarnings("DataFlowIssue") - private static int computeBuildNumber() { - try { - return Integer.parseInt(BUILD_NUMBER_STRING); - } catch (NumberFormatException _) { - return 0; - } - } - - private static Integer parseIntOrNull(String s) { - if (s == null) { - return null; - } - try { - return Integer.parseInt(s); - } catch (NumberFormatException _) { - return null; - } - } - - private static String getOrNull(String[] arr, int index) { - return index >= 0 && index < arr.length ? arr[index] : null; - } -} diff --git a/src/main/java/network/crypta/node/VersionParseException.java b/src/main/java/network/crypta/node/VersionParseException.java deleted file mode 100644 index 66f03d00b75..00000000000 --- a/src/main/java/network/crypta/node/VersionParseException.java +++ /dev/null @@ -1,27 +0,0 @@ -package network.crypta.node; - -import java.io.Serial; - -/** - * Exception indicating that parsing a version string failed. - * - *

Used by version-processing utilities (for example, {@link Version}) when converting a textual - * version into numeric components such as an arbitrary build number. It is a checked exception so - * callers explicitly handle malformed or unsupported version formats. The class is immutable and - * carries no additional state beyond the detail message. - * - * @author toad - */ -public class VersionParseException extends Exception { - // Fixed UID to keep serialization compatibility across releases. - @Serial private static final long serialVersionUID = -19006235321212642L; - - /** - * Creates an instance with a human-readable detail message. - * - * @param msg detail describing the parse error context; may be {@code null}. - */ - public VersionParseException(String msg) { - super(msg); - } -} diff --git a/src/main/java/network/crypta/node/probe/Error.java b/src/main/java/network/crypta/node/probe/Error.java deleted file mode 100644 index 49a862a4406..00000000000 --- a/src/main/java/network/crypta/node/probe/Error.java +++ /dev/null @@ -1,151 +0,0 @@ -package network.crypta.node.probe; - -/** - * Enumerates well-known failure conditions that can occur while running a probe in the Crypta - * network. - * - *

Each constant represents a distinct class of failure that a probing node or a relaying node - * can report when a probe cannot be completed successfully. The enum is intentionally coupled with - * a stable {@linkplain #code numeric code} so that values can be serialized over the network - * without relying on fragile {@code name()} or ordinal-based encodings. Codes are small and - * consecutive, starting at zero. - * - *

Typical usage follows this pattern: - * - *

    - *
  • When emitting an error over the wire, write the {@link #code} value. - *
  • When decoding, validate with {@link #isValid(byte)} and then convert using {@link - * #valueOf(byte)}. If validation fails, treat the value as unknown for your context. - *
- * - *

This type is immutable and thread-safe. It has no mutable state and can be freely shared - * across threads. The mapping between codes and enum constants is fixed at compile time and does - * not change at runtime. - */ -@SuppressWarnings("JavaLangClash") -public enum Error { - /** - * The target node disconnected while the probe awaited a response. - * - *

This indicates that a network connection or session broke before a definitive outcome could - * be delivered. The disconnect can originate either from the remote peer voluntarily closing the - * connection or due to transport-layer failures, timeouts at lower layers, or process shutdowns. - * Retrying may succeed if the disconnection was transient. - */ - DISCONNECTED((byte) 0), - /** - * The receiving node rejected the probe because a local DoS/overload guard is active. - * - *

Nodes enforce admission control for probes to protect resources. When limits are exceeded, - * new probes are refused and this error is emitted. Backing off and retrying after a delay is the - * recommended strategy; immediate retries are likely to be rejected again while the guard is - * engaged. - */ - OVERLOAD((byte) 1), - /** - * The probe timed out before a response or terminal failure arrived. - * - *

Timeouts can result from long network paths, slow or congested links, or nodes that are - * alive but too busy to respond within the configured time budget. Upstream policies determine - * the actual timeout thresholds. Callers typically treat this as retryable with exponential - * backoff. - */ - TIMEOUT((byte) 2), - /** - * A node reported an error code that is not recognized by the local implementation. - * - *

When received locally, the unrecognized numeric value is preserved alongside this category - * to aid diagnostics. This generally indicates a version skew between peers or an extension that - * is not understood. The failure itself is terminal for the current probe; callers should avoid - * assuming any semantics beyond “not recognized”. - */ - UNKNOWN((byte) 3), - /** - * The remote node understood the request envelope but did not recognize the probe type. - * - *

This differs from protocol-level errors: for locally started probes an unknown type would be - * rejected earlier as a protocol error. Over the network it communicates that the peer does not - * implement the requested probe, which may happen with mixed-version deployments. - */ - UNRECOGNIZED_TYPE((byte) 4), - /** - * The node accepted and understood the request but failed to forward it to the next hop. - * - *

Forwarding can fail due to routing constraints, peer selection limits, or transient - * connectivity issues. Implementations usually cap the number of send attempts to prevent - * excessive retries. - * - * @see Probe#MAX_SEND_ATTEMPTS - */ - CANNOT_FORWARD((byte) 5); - - /** - * Stable numeric value that represents this constant on the wire. - * - *

Use this field when serializing an {@code Error} over the network. Do not rely on {@link - * Enum#name()} or ordinal values; those are unstable across refactorings and re-orderings. Codes - * are consecutive and begin at zero. - */ - public final byte code; - - private static final int MAX_CODE = Error.values().length; - - Error(byte code) { - this.code = code; - } - - /** - * Reports whether a numeric code corresponds to a defined {@code Error} value. - * - *

This helper enables fast validation without exceptions. It checks that the code falls within - * the inclusive lower bound and exclusive upper bound of known codes. The mapping is stable for a - * given build. A {@code true} outcome guarantees that {@link #valueOf(byte)} will succeed. - * - *

Usage example: - * - *

{@code
-   * if (Error.isValid(code)) {
-   *   var e = Error.valueOf(code);
-   *   // handle e
-   * } else {
-   *   // handle unknown code
-   * }
-   * }
- * - * @param code numeric value received from a peer or decoded from storage; values below zero or - * beyond the highest assigned code are invalid. - * @return {@code true} when {@code code} maps to a known constant; {@code false} when it does - * not, in which case callers should treat it as unknown. - */ - static boolean isValid(byte code) { - // Assumes codes are consecutive, start at zero, and all are valid. - return code >= 0 && code < MAX_CODE; - } - - /** - * Converts a numeric code to its corresponding {@code Error} constant. - * - *

This method performs a constant-time switch over the supported values and never returns - * {@code null}. It is idempotent for the same input and does not perform any I/O. Prefer calling - * {@link #isValid(byte)} first when handling untrusted input to avoid exceptions in hot paths. - * - * @param code numeric identifier previously obtained from {@link #code} during serialization or - * received from a peer; must be within the set of defined values. - * @return the non-null {@code Error} constant that matches {@code code}; ownership is shared and - * instances are the canonical enum singletons. - * @throws IllegalArgumentException if there is no constant with the requested {@code code}; - * callers should treat such cases as an unknown error category. - */ - static Error valueOf(byte code) throws IllegalArgumentException { - return switch (code) { - case 0 -> DISCONNECTED; - case 1 -> OVERLOAD; - case 2 -> TIMEOUT; - case 3 -> UNKNOWN; - case 4 -> UNRECOGNIZED_TYPE; - case 5 -> CANNOT_FORWARD; - default -> - throw new IllegalArgumentException("There is no ProbeError with code " + code + "."); - }; - } -} diff --git a/src/main/java/network/crypta/node/probe/Type.java b/src/main/java/network/crypta/node/probe/Type.java deleted file mode 100644 index b74e8c9bbfa..00000000000 --- a/src/main/java/network/crypta/node/probe/Type.java +++ /dev/null @@ -1,112 +0,0 @@ -package network.crypta.node.probe; - -/** - * Enumerates the different probe result types exchanged between nodes in the Crypta network. - * - *

Each constant identifies a specific class of diagnostic or status information that can be - * requested by a peer and returned by a responding node. The enum associates every type with a - * compact {@code byte} {@linkplain #code on-the-wire code} that is stable for serialization and - * deserialization. This allows efficient transmission while keeping the higher-level semantics - * explicit and type-safe in code. - * - *

Typical usage is to read a raw {@code byte} value from an inbound message, verify it with - * {@link #isValid(byte)}, and then obtain the corresponding {@link #valueOf(byte)} to branch on the - * enum constant. When emitting messages, use the {@link #code} of a constant instead of its ordinal - * value; the mapping is explicit and not tied to declaration order. Values outside the declared - * range are considered invalid and must be rejected by callers. - * - *

    - *
  • Mapping is stable: the {@link #code} is assigned explicitly and independent of {@code - * ordinal()}. - *
  • {@link #valueOf(byte)} throws {@link IllegalArgumentException} for unknown codes. - *
  • {@link #isValid(byte)} provides a fast pre-check without using exceptions for control flow. - *
- */ -public enum Type { - /** - * Requests or reports information about a node's bandwidth, such as capacity or recent - * utilization, subject to the probe protocol details. - */ - BANDWIDTH((byte) 0), - /** Reports the build identifier or version information for the running node software. */ - BUILD((byte) 1), - /** - * Exchanges a stable node identifier value as defined by the protocol, enabling peers to - * correlate responses with a specific node across requests. - */ - IDENTIFIER((byte) 2), - /** Provides summary statistics describing link length distributions in the network topology. */ - LINK_LENGTHS((byte) 3), - /** Reports coarse location or placement information used by the routing layer. */ - LOCATION((byte) 4), - /** Conveys the size or capacity of local persistent stores managed by the node. */ - STORE_SIZE((byte) 5), - /** Returns a node uptime measurement aggregated over a recent 48-hour window. */ - UPTIME_48H((byte) 6), - /** Returns a node uptime measurement aggregated over a recent 7-day window. */ - UPTIME_7D((byte) 7), - /** Reports request rejection statistics to aid diagnosis of overload or policy throttling. */ - REJECT_STATS((byte) 8), - /** - * Reports aggregated bulk output capacity usage, suitable for understanding sustained egress load - * relative to configured limits. - */ - OVERALL_BULK_OUTPUT_CAPACITY_USAGE((byte) 9); - - /** - * Compact, stable {@code byte} value used on the wire to represent this probe type. - * - *

The value is explicitly assigned per constant and is independent of {@link #ordinal()}. It - * is immutable and designed for serialization/deserialization; callers should not infer ordering - * or density beyond the fact that valid codes occupy the range {@code 0..(values().length-1)}. - */ - public final byte code; - - private static final int MAX_CODE = Type.values().length; - - Type(byte code) { - this.code = code; - } - - /** - * Checks whether {@link #valueOf(byte)} will succeed for the given code without throwing. - * - *

This helper exists to avoid using exceptions for control flow when parsing untrusted input. - * A return value of {@code true} indicates the code maps to one of the declared enum constants at - * the time of the call. The check is constant-time with respect to the number of enum constants. - * - * @param code the raw on-the-wire value to validate; values below zero are always invalid. - * @return {@code true} when the code corresponds to a declared constant; {@code false} otherwise. - */ - static boolean isValid(byte code) { - return code >= 0 && code < MAX_CODE; - } - - /** - * Determines the enum constant that corresponds to the supplied on-the-wire code. - * - *

This method performs a direct mapping from the compact {@code byte} representation to the - * strongly typed {@code Type}. It is idempotent and side effect free. Callers should prefer - * {@link #isValid(byte)} to guard inputs when handling data from untrusted peers. - * - * @param code the compact code to resolve; must match one of the declared constants. - * @return the {@code Type} constant corresponding to {@code code}; never {@code null}. - * @throws IllegalArgumentException if {@code code} does not correspond to any declared constant. - */ - static Type valueOf(byte code) throws IllegalArgumentException { - return switch (code) { - case 0 -> BANDWIDTH; - case 1 -> BUILD; - case 2 -> IDENTIFIER; - case 3 -> LINK_LENGTHS; - case 4 -> LOCATION; - case 5 -> STORE_SIZE; - case 6 -> UPTIME_48H; - case 7 -> UPTIME_7D; - case 8 -> REJECT_STATS; - case 9 -> OVERALL_BULK_OUTPUT_CAPACITY_USAGE; - default -> - throw new IllegalArgumentException("There is no ProbeType with code " + code + "."); - }; - } -} diff --git a/src/main/java/network/crypta/support/Serializer.java b/src/main/java/network/crypta/support/Serializer.java deleted file mode 100644 index 5e59971161d..00000000000 --- a/src/main/java/network/crypta/support/Serializer.java +++ /dev/null @@ -1,412 +0,0 @@ -package network.crypta.support; - -import java.io.DataInput; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import network.crypta.io.WritableToDataOutputStream; -import network.crypta.io.comm.Peer; -import network.crypta.keys.Key; -import network.crypta.keys.NodeCHK; -import network.crypta.keys.NodeSSK; - -/** - * Utility for serializing and deserializing a constrained set of value types to and from {@link - * java.io.DataInput} / {@link java.io.DataOutputStream}. - * - *

This class supports a small, explicit set of types used by the networking and messaging code: - * boxed primitives ({@link Boolean}, {@link Byte}, {@link Short}, {@link Integer}, {@link Long}, - * {@link Float}, {@link Double}), {@link String}, {@link List}, {@code double[]} and {@code - * float[]}, several support types ({@link Buffer}, {@link ShortBuffer}, {@link Peer}, {@link - * BitArray}), and selected key types ({@link NodeCHK}, {@link NodeSSK}, and {@link Key}). - * - *

Strings are encoded as a 32-bit length followed by that many 16-bit Java {@code char}s written - * via {@link java.io.DataOutputStream#writeChar(int)} (i.e., not modified UTF-8 and not {@link - * java.io.DataOutputStream#writeUTF(String)}). Booleans are encoded strictly as a single byte with - * value {@code 0} (false) or {@code 1} (true); other values are rejected at read time. Arrays use a - * compact length prefix: one unsigned byte for {@code double[]} (max 255 elements) and a 16-bit - * signed short for {@code float[]}. - * - *

The class is stateless and thread-safe. All methods are static; instantiation is prevented. - */ -public class Serializer { - private Serializer() {} - - /** Historical SCM identifier (kept for traceability). */ - public static final String VERSION = - "$Id: Serializer.java,v 1.5 2005/09/15 18:16:04 amphibian Exp $"; - - /** - * Upper bound, in bits, used when deserializing {@link BitArray} to prevent pathological - * allocations. - */ - public static final int MAX_BITARRAY_SIZE = SerializationLimits.MAX_BITARRAY_SIZE; - - /** Maximum allowed inbound variable-length payload, in bytes. */ - public static final int MAX_ARRAY_LENGTH = SerializationLimits.MAX_ARRAY_LENGTH; - - /** - * Reads a {@link List} whose elements are of {@code elementType} from the given {@link - * DataInput}. - * - *

The on-wire representation is a 32-bit element count followed by that many element values, - * each encoded using {@link #readFromDataInputStream(Class, DataInput)} with the supplied {@code - * elementType}. The method does not enforce an explicit upper bound on the element count; callers - * must ensure the producer is trusted or that inputs are reasonably bounded. - * - * @param elementType the expected element type. Must be one of the types recognized by {@link - * #readFromDataInputStream(Class, DataInput)}. - * @param dis the input to read from; not closed by this method. - * @return a new {@link List} containing the decoded elements in order. - * @throws IOException if an I/O error occurs, or an element payload is invalid for the declared - * type. - * @throws IllegalArgumentException if {@code elementType} is unsupported. - * @see #readFromDataInputStream(Class, DataInput) - * @see #writeToDataOutputStream(Object, DataOutputStream) - */ - public static List readListFromDataInputStream(Class elementType, DataInput dis) - throws IOException { - int length = dis.readInt(); - List ret = new ArrayList<>(Math.max(0, length)); - for (int x = 0; x < length; x++) { - ret.add(readFromDataInputStream(elementType, dis)); - } - return ret; - } - - /** - * Reads a single value of the requested {@code type} from a {@link DataInput}. - * - *

Supported types include boxed primitives ({@link Boolean}, {@link Byte}, {@link Short}, - * {@link Integer}, {@link Long}, {@link Float}, {@link Double}); {@link String}; support types - * ({@link Buffer}, {@link ShortBuffer}, {@link Peer}, {@link BitArray}); selected keys ({@link - * NodeCHK}, {@link NodeSSK}, {@link Key}); {@code double[]} and {@code float[]}. - * - *

Booleans are read in a strict form via a single byte: {@code 0} or {@code 1}. Strings are - * read as a 32-bit length (bounded by {@link #MAX_ARRAY_LENGTH}) plus that many 16-bit - * characters. - * - * @param type the class indicating the type to read. Must be one of the supported types listed - * above. - * @param dis the input to read from; not closed by this method. - * @return the decoded value; never {@code null}. - * @throws IOException if an I/O error occurs, or the next value is malformed for the requested - * type (for example, a boolean byte not equal to {@code 0} or {@code 1}, an invalid string - * length, or an oversized array). - * @throws IllegalArgumentException if {@code type} is unsupported. - */ - public static Object readFromDataInputStream(Class type, DataInput dis) throws IOException { - if (type.equals(Boolean.class)) { - return readStrictBoolean(dis); - } - - if (isNumericBoxed(type)) { - return readNumeric(type, dis); - } - - if (type.equals(String.class)) { - return readStringWithBounds(dis); - } - - if (isSpecializedSupportType(type)) { - return readSpecializedSupportType(type, dis); - } - - if (isKeyType(type)) { - // Use Key.read(...) rather than type-specific methods because write(...) writes the TYPE - // field. - return Key.read(dis); - } - - if (type.equals(double[].class)) { - return readDoubleArray(dis); - } - - if (type.equals(float[].class)) { - return readFloatArray(dis); - } - - throw new IllegalArgumentException("Unrecognised field type: " + type); - } - - private static Object readStrictBoolean(DataInput dis) throws IOException { - final byte bool = dis.readByte(); - // Read a single byte, not {@code readBoolean()}, to reject non-0/1 values. - return switch (bool) { - case 1 -> Boolean.TRUE; - case 0 -> Boolean.FALSE; - default -> throw new IOException("Boolean is non boolean value: " + bool); - }; - } - - private static boolean isNumericBoxed(Class type) { - return type.equals(Byte.class) - || type.equals(Short.class) - || type.equals(Integer.class) - || type.equals(Long.class) - || type.equals(Double.class) - || type.equals(Float.class); - } - - private static Object readNumeric(Class type, DataInput dis) throws IOException { - if (type.equals(Byte.class)) { - return dis.readByte(); - } - if (type.equals(Short.class)) { - return dis.readShort(); - } - if (type.equals(Integer.class)) { - return dis.readInt(); - } - if (type.equals(Long.class)) { - return dis.readLong(); - } - if (type.equals(Double.class)) { - return dis.readDouble(); - } - // Float.class - return dis.readFloat(); - } - - private static String readStringWithBounds(DataInput dis) throws IOException { - final int length = dis.readInt(); - // Limit length to MAX_ARRAY_LENGTH to avoid unreasonable or malicious sizes. Total byte - // accounting is left to callers because structures around the string are fixed-size. - if (length < 0 || length > MAX_ARRAY_LENGTH) { - throw new IOException("Invalid string length: " + length); - } - StringBuilder sb = new StringBuilder(length); - for (int x = 0; x < length; x++) { - sb.append(dis.readChar()); - } - return sb.toString(); - } - - private static boolean isSpecializedSupportType(Class type) { - return type.equals(Buffer.class) - || type.equals(ShortBuffer.class) - || type.equals(Peer.class) - || type.equals(BitArray.class); - } - - private static Object readSpecializedSupportType(Class type, DataInput dis) - throws IOException { - if (type.equals(Buffer.class)) { - return new Buffer(dis); - } - if (type.equals(ShortBuffer.class)) { - return new ShortBuffer(dis); - } - if (type.equals(Peer.class)) { - return new Peer(dis); - } - // BitArray.class - return new BitArray(dis, MAX_BITARRAY_SIZE); - } - - private static boolean isKeyType(Class type) { - return type.equals(NodeCHK.class) || type.equals(NodeSSK.class) || type.equals(Key.class); - } - - private static double[] readDoubleArray(DataInput dis) throws IOException { - // Length is stored in one unsigned byte; mask to avoid sign extension. - double[] array = new double[dis.readByte() & 0xFF]; - for (int i = 0; i < array.length; i++) array[i] = dis.readDouble(); - return array; - } - - private static float[] readFloatArray(DataInput dis) throws IOException { - final short length = dis.readShort(); - // Bound by max allowed bytes; each float is 4 bytes. - if (length < 0 || length > MAX_ARRAY_LENGTH / 4) { - throw new IOException("Invalid flat array length: " + length); - } - float[] array = new float[length]; - for (int i = 0; i < array.length; i++) array[i] = dis.readFloat(); - return array; - } - - /** - * Writes a single supported value to a {@link DataOutputStream} using the format recognized by - * {@link #readFromDataInputStream(Class, DataInput)}. - * - *

The {@code object} must be non-{@code null} and of a supported type. For lists, see {@link - * #writeList(List, DataOutputStream)} for details. Strings are written as a 32-bit length - * followed by UTF-16 {@code char}s via {@link DataOutputStream#writeChar(int)}. - * - * @param object the value to serialize; must be non-{@code null}. - * @param dos the destination stream; not closed by this method. - * @throws IOException if an I/O error occurs during writing. - * @throws IllegalArgumentException if {@code object} has an unsupported type or violates a format - * constraint (for example, a {@code double[]} longer than 255 elements). - */ - public static void writeToDataOutputStream(Object object, DataOutputStream dos) - throws IOException { - Class type = object.getClass(); - - if (isScalarBoxed(type)) { - writeScalar(object, dos); - return; - } - - if (WritableToDataOutputStream.class.isAssignableFrom(type)) { - ((WritableToDataOutputStream) object).writeToDataOutputStream(dos); - return; - } - - if (type.equals(String.class)) { - writeString((String) object, dos); - return; - } - - if (object instanceof List list) { - writeList(list, dos); - return; - } - - if (type.equals(double[].class)) { - writeDoubleArray((double[]) object, dos); - return; - } - - if (type.equals(float[].class)) { - writeFloatArray((float[]) object, dos); - return; - } - - throw new IllegalArgumentException("Unrecognised field type: " + type); - } - - private static boolean isScalarBoxed(Class type) { - return type.equals(Long.class) - || type.equals(Boolean.class) - || type.equals(Integer.class) - || type.equals(Short.class) - || type.equals(Double.class) - || type.equals(Float.class) - || type.equals(Byte.class); - } - - private static void writeScalar(Object object, DataOutputStream dos) throws IOException { - Class type = object.getClass(); - if (type.equals(Long.class)) { - dos.writeLong((Long) object); - return; - } - if (type.equals(Boolean.class)) { - dos.writeBoolean((Boolean) object); - return; - } - if (type.equals(Integer.class)) { - dos.writeInt((Integer) object); - return; - } - if (type.equals(Short.class)) { - dos.writeShort((Short) object); - return; - } - if (type.equals(Double.class)) { - dos.writeDouble((Double) object); - return; - } - if (type.equals(Float.class)) { - dos.writeFloat((Float) object); - return; - } - // Byte.class - dos.write((Byte) object); - } - - private static void writeString(String s, DataOutputStream dos) throws IOException { - dos.writeInt(s.length()); - for (int x = 0; x < s.length(); x++) { - dos.writeChar(s.charAt(x)); - } - } - - /** - * Serializes a {@link List} using a snapshot-and-write strategy to avoid concurrent modification - * races. - * - *

First, the method takes a stable snapshot of the list under synchronization on the list - * instance itself; then it releases the lock and writes the snapshot to the stream. Synchronizing - * on the list coordinates with callers that already use {@code synchronized(list)} during - * mutation and avoids {@link java.util.ConcurrentModificationException} and length/content - * mismatches while iterating. - * - * @param list the list to serialize; elements must be of a type supported by {@link - * #writeToDataOutputStream(Object, DataOutputStream)}. - * @param dos the destination stream; not closed by this method. - * @throws IOException if an I/O error occurs while writing an element. - */ - @SuppressWarnings({"java:S2445", "SynchronizationOnLocalVariableOrMethodParameter"}) - private static void writeList(final List list, DataOutputStream dos) throws IOException { - // Intentionally lock on the list instance, so external synchronization on the same - // object also applies. Snapshot under lock; perform I/O after releasing it to - // minimize contention. - final Object[] snapshot; - synchronized (list) { - snapshot = list.toArray(); - } - dos.writeInt(snapshot.length); - for (Object o : snapshot) { - writeToDataOutputStream(o, dos); - } - } - - private static void writeDoubleArray(double[] array, DataOutputStream dos) throws IOException { - // {@code writeByte} keeps the lower 8 bits; cap length to 255 elements. - if (array.length > 255) { - throw new IllegalArgumentException( - "Cannot serialize an array of more than 255 doubles; attempted to " - + "serialize " - + array.length - + "."); - } - dos.writeByte(array.length); - for (double element : array) dos.writeDouble(element); - } - - private static void writeFloatArray(float[] array, DataOutputStream dos) throws IOException { - dos.writeShort(array.length); - for (float element : array) dos.writeFloat(element); - } - - /** - * Returns the serialized size, in bytes, for fixed-size simple values. - * - *

For {@link String}, the result is an upper bound computed as {@code 4 + 2 * maxStringLength} - * (a 32-bit length plus two bytes per character). For types implementing {@link - * WritableToDataOutputStream} and for {@link List}, the size is unknown and this method throws - * {@link IllegalArgumentException}. - * - * @param type the type to measure. - * @param maxStringLength maximum characters to assume for strings when computing an upper bound. - * @return the exact byte size for fixed-size types or an upper bound for strings. - * @throws IllegalArgumentException if the type is unsupported or variable-length. - */ - public static int length(Class type, int maxStringLength) { - if (type.equals(Long.class)) { - return 8; - } else if (type.equals(Boolean.class)) { - return 1; - } else if (type.equals(Integer.class)) { - return 4; - } else if (type.equals(Short.class)) { - return 2; - } else if (type.equals(Double.class)) { - return 8; - } else if (WritableToDataOutputStream.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Unknown length for " + type); - } else if (type.equals(String.class)) { - return 4 + maxStringLength * 2; // Written as chars - } else if (List.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Unknown length for List"); - } else if (type.equals(Byte.class)) { - return 1; - } else { - throw new IllegalArgumentException("Unrecognised field type: " + type); - } - } -}