1 // Copyright 2022 The Pigweed Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); you may not 4 // use this file except in compliance with the License. You may obtain a copy of 5 // the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 // License for the specific language governing permissions and limitations under 13 // the License. 14 15 package dev.pigweed.pw_transfer; 16 17 import com.google.auto.value.AutoValue; 18 import com.google.protobuf.ByteString; 19 import dev.pigweed.pw_rpc.Status; 20 import java.util.Map; 21 import java.util.OptionalInt; 22 import java.util.OptionalLong; 23 24 /** 25 * Abstraction of the Chunk proto that supports different protocol versions. 26 */ 27 @AutoValue 28 abstract class VersionedChunk { 29 // Invalid session ID used for legacy chunks that do not map to a known session ID. 30 public static final int UNKNOWN_SESSION_ID = 0; 31 version()32 public abstract ProtocolVersion version(); 33 type()34 public abstract Chunk.Type type(); 35 sessionId()36 public abstract int sessionId(); 37 resourceId()38 public abstract OptionalInt resourceId(); 39 offset()40 public abstract int offset(); 41 initialOffset()42 public abstract int initialOffset(); 43 windowEndOffset()44 public abstract int windowEndOffset(); 45 data()46 public abstract ByteString data(); 47 remainingBytes()48 public abstract OptionalLong remainingBytes(); 49 maxChunkSizeBytes()50 public abstract OptionalInt maxChunkSizeBytes(); 51 minDelayMicroseconds()52 public abstract OptionalInt minDelayMicroseconds(); 53 status()54 public abstract OptionalInt status(); 55 desiredSessionId()56 public abstract OptionalInt desiredSessionId(); 57 builder()58 public static Builder builder() { 59 return new AutoValue_VersionedChunk.Builder() 60 .setSessionId(UNKNOWN_SESSION_ID) 61 .setOffset(0) 62 .setWindowEndOffset(0) 63 .setData(ByteString.EMPTY) 64 .setInitialOffset(0); 65 } 66 67 @AutoValue.Builder 68 public abstract static class Builder { setVersion(ProtocolVersion version)69 public abstract Builder setVersion(ProtocolVersion version); 70 setType(Chunk.Type type)71 public abstract Builder setType(Chunk.Type type); 72 setSessionId(int sessionId)73 public abstract Builder setSessionId(int sessionId); 74 setResourceId(int resourceId)75 public abstract Builder setResourceId(int resourceId); 76 setOffset(int offset)77 public abstract Builder setOffset(int offset); 78 setInitialOffset(int initialOffset)79 public abstract Builder setInitialOffset(int initialOffset); 80 setWindowEndOffset(int windowEndOffset)81 public abstract Builder setWindowEndOffset(int windowEndOffset); 82 setData(ByteString data)83 public abstract Builder setData(ByteString data); 84 setRemainingBytes(long remainingBytes)85 public abstract Builder setRemainingBytes(long remainingBytes); 86 setMaxChunkSizeBytes(int maxChunkSizeBytes)87 public abstract Builder setMaxChunkSizeBytes(int maxChunkSizeBytes); 88 setMinDelayMicroseconds(int minDelayMicroseconds)89 public abstract Builder setMinDelayMicroseconds(int minDelayMicroseconds); 90 setStatus(Status status)91 public final Builder setStatus(Status status) { 92 return setStatus(status.code()); 93 } 94 setStatus(int statusCode)95 abstract Builder setStatus(int statusCode); 96 setDesiredSessionId(int desiredSessionId)97 abstract Builder setDesiredSessionId(int desiredSessionId); 98 build()99 public abstract VersionedChunk build(); 100 } 101 fromMessage(Chunk chunk, Map<Integer, Integer> legacyIdToSessionId)102 public static VersionedChunk fromMessage(Chunk chunk, Map<Integer, Integer> legacyIdToSessionId) { 103 Builder builder = builder(); 104 105 ProtocolVersion version; 106 if (chunk.hasProtocolVersion()) { 107 if (chunk.getProtocolVersion() < ProtocolVersion.values().length) { 108 version = ProtocolVersion.values()[chunk.getProtocolVersion()]; 109 } else { 110 version = ProtocolVersion.UNKNOWN; 111 } 112 } else if (chunk.hasSessionId()) { 113 version = ProtocolVersion.VERSION_TWO; 114 } else { 115 version = ProtocolVersion.LEGACY; 116 } 117 builder.setVersion(version); 118 119 if (chunk.hasType()) { 120 builder.setType(chunk.getType()); 121 } else if (chunk.getOffset() == 0 && chunk.getData().isEmpty() && !chunk.hasStatus()) { 122 builder.setType(Chunk.Type.START); 123 } else if (!chunk.getData().isEmpty()) { 124 builder.setType(Chunk.Type.DATA); 125 } else if (chunk.hasStatus()) { 126 builder.setType(Chunk.Type.COMPLETION); 127 } else { 128 builder.setType(Chunk.Type.PARAMETERS_RETRANSMIT); 129 } 130 131 // For legacy chunks, use the transfer ID as both the resource and session IDs. 132 if (version == ProtocolVersion.LEGACY) { 133 builder.setSessionId( 134 legacyIdToSessionId.getOrDefault(chunk.getTransferId(), UNKNOWN_SESSION_ID)); 135 builder.setResourceId(chunk.getTransferId()); 136 if (chunk.hasStatus()) { 137 builder.setType(Chunk.Type.COMPLETION); 138 } 139 } else { 140 builder.setSessionId(chunk.getSessionId()); 141 } 142 143 if (chunk.hasDesiredSessionId()) { 144 builder.setDesiredSessionId(chunk.getDesiredSessionId()); 145 } 146 147 builder.setOffset((int) chunk.getOffset()).setData(chunk.getData()); 148 149 builder.setInitialOffset((int) chunk.getInitialOffset()); 150 151 if (chunk.hasResourceId()) { 152 builder.setResourceId(chunk.getResourceId()); 153 } 154 if (chunk.hasPendingBytes()) { 155 builder.setWindowEndOffset((int) chunk.getOffset() + chunk.getPendingBytes()); 156 } else { 157 builder.setWindowEndOffset(chunk.getWindowEndOffset()); 158 } 159 if (chunk.hasRemainingBytes()) { 160 builder.setRemainingBytes(chunk.getRemainingBytes()); 161 } 162 if (chunk.hasMaxChunkSizeBytes()) { 163 builder.setMaxChunkSizeBytes(chunk.getMaxChunkSizeBytes()); 164 } 165 if (chunk.hasMinDelayMicroseconds()) { 166 builder.setMinDelayMicroseconds(chunk.getMinDelayMicroseconds()); 167 } 168 if (chunk.hasStatus()) { 169 builder.setStatus(chunk.getStatus()); 170 } 171 return builder.build(); 172 } 173 createInitialChunk( ProtocolVersion desiredVersion, int resourceId, int sessionId)174 public static VersionedChunk.Builder createInitialChunk( 175 ProtocolVersion desiredVersion, int resourceId, int sessionId) { 176 VersionedChunk.Builder builder = 177 builder().setVersion(desiredVersion).setType(Chunk.Type.START).setResourceId(resourceId); 178 179 if (desiredVersion != ProtocolVersion.LEGACY) { 180 builder.setDesiredSessionId(sessionId); 181 } 182 183 return builder; 184 } 185 toMessage()186 public Chunk toMessage() { 187 Chunk.Builder chunk = Chunk.newBuilder() 188 .setType(type()) 189 .setOffset(offset()) 190 .setWindowEndOffset(windowEndOffset()) 191 .setData(data()) 192 .setInitialOffset(initialOffset()); 193 194 remainingBytes().ifPresent(chunk::setRemainingBytes); 195 maxChunkSizeBytes().ifPresent(chunk::setMaxChunkSizeBytes); 196 minDelayMicroseconds().ifPresent(chunk::setMinDelayMicroseconds); 197 status().ifPresent(chunk::setStatus); 198 desiredSessionId().ifPresent(chunk::setDesiredSessionId); 199 200 // The resourceId is only needed for START chunks. 201 if (type() == Chunk.Type.START) { 202 chunk.setResourceId(resourceId().getAsInt()); // resourceId must be set for start chunks 203 } 204 205 // session_id did not exist in the legacy protocol, so don't send it. 206 if (version() != ProtocolVersion.LEGACY && sessionId() != UNKNOWN_SESSION_ID) { 207 chunk.setSessionId(sessionId()); 208 } 209 210 if (shouldEncodeLegacyFields()) { 211 chunk.setTransferId(resourceId().getAsInt()); // resourceId must be set for legacy transfers 212 213 if (chunk.getWindowEndOffset() != 0) { 214 chunk.setPendingBytes(chunk.getWindowEndOffset() - offset()); 215 } 216 } 217 218 if (isInitialHandshakeChunk()) { 219 chunk.setProtocolVersion(version().ordinal()); 220 } 221 222 return chunk.build(); 223 } 224 isInitialHandshakeChunk()225 private boolean isInitialHandshakeChunk() { 226 return version() == ProtocolVersion.VERSION_TWO 227 && (type() == Chunk.Type.START || type() == Chunk.Type.START_ACK 228 || type() == Chunk.Type.START_ACK_CONFIRMATION); 229 } 230 requestsTransmissionFromOffset()231 public final boolean requestsTransmissionFromOffset() { 232 return type() == Chunk.Type.PARAMETERS_RETRANSMIT || type() == Chunk.Type.START_ACK_CONFIRMATION 233 || type() == Chunk.Type.START; 234 } 235 shouldEncodeLegacyFields()236 private boolean shouldEncodeLegacyFields() { 237 return version() == ProtocolVersion.LEGACY || type() == Chunk.Type.START; 238 } 239 } 240