1 // Copyright 2023 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 #pragma once 15 16 #include <optional> 17 18 #include "pw_bytes/span.h" 19 #include "pw_chrono/system_clock.h" 20 #include "pw_result/result.h" 21 #include "pw_transfer/internal/protocol.h" 22 #include "pw_transfer/transfer.pwpb.h" 23 24 namespace pw::transfer::internal { 25 26 class Chunk { 27 public: 28 using Type = transfer::pwpb::Chunk::Type; 29 30 class Identifier { 31 public: is_session()32 constexpr bool is_session() const { return type_ == kSession; } is_desired_session()33 constexpr bool is_desired_session() const { return type_ == kDesired; } is_legacy()34 constexpr bool is_legacy() const { return type_ == kLegacy; } 35 value()36 constexpr uint32_t value() const { return value_; } 37 38 private: 39 friend class Chunk; 40 Session(uint32_t value)41 static constexpr Identifier Session(uint32_t value) { 42 return Identifier(kSession, value); 43 } Desired(uint32_t value)44 static constexpr Identifier Desired(uint32_t value) { 45 return Identifier(kDesired, value); 46 } Legacy(uint32_t value)47 static constexpr Identifier Legacy(uint32_t value) { 48 return Identifier(kLegacy, value); 49 } 50 51 enum IdType { 52 kSession, 53 kDesired, 54 kLegacy, 55 }; 56 Identifier(IdType type,uint32_t value)57 constexpr Identifier(IdType type, uint32_t value) 58 : type_(type), value_(value) {} 59 60 IdType type_; 61 uint32_t value_; 62 }; 63 64 // Partially decodes a transfer chunk to find its transfer context identifier. 65 // Depending on the protocol version and type of chunk, this may be one of 66 // several proto fields. 67 static Result<Identifier> ExtractIdentifier(ConstByteSpan message); 68 69 // Constructs a new chunk with the given transfer protocol version. All fields 70 // are initialized to their zero values. Chunk(ProtocolVersion version,Type type)71 constexpr Chunk(ProtocolVersion version, Type type) 72 : Chunk(version, std::optional<Type>(type)) {} 73 74 // Parses a chunk from a serialized protobuf message. 75 static Result<Chunk> Parse(ConstByteSpan message); 76 77 // Creates a terminating status chunk within a transfer. Final(ProtocolVersion version,uint32_t session_id,Status status)78 static Chunk Final(ProtocolVersion version, 79 uint32_t session_id, 80 Status status) { 81 return Chunk(version, Type::kCompletion) 82 .set_session_id(session_id) 83 .set_status(status); 84 } 85 86 // Encodes the chunk to the specified buffer, returning a span of the 87 // serialized data on success. 88 Result<ConstByteSpan> Encode(ByteSpan buffer) const; 89 90 // Returns the size of the serialized chunk based on the fields currently set 91 // within the chunk object. 92 size_t EncodedSize() const; 93 94 void LogChunk(bool received, 95 pw::chrono::SystemClock::duration rate_limit = 96 chrono::SystemClock::duration::zero()) const; 97 set_session_id(uint32_t session_id)98 constexpr Chunk& set_session_id(uint32_t session_id) { 99 session_id_ = session_id; 100 return *this; 101 } 102 set_desired_session_id(uint32_t session_id)103 constexpr Chunk& set_desired_session_id(uint32_t session_id) { 104 desired_session_id_ = session_id; 105 return *this; 106 } 107 set_resource_id(uint32_t resource_id)108 constexpr Chunk& set_resource_id(uint32_t resource_id) { 109 resource_id_ = resource_id; 110 return *this; 111 } 112 set_protocol_version(ProtocolVersion version)113 constexpr Chunk& set_protocol_version(ProtocolVersion version) { 114 protocol_version_ = version; 115 return *this; 116 } 117 set_window_end_offset(uint32_t window_end_offset)118 constexpr Chunk& set_window_end_offset(uint32_t window_end_offset) { 119 window_end_offset_ = window_end_offset; 120 return *this; 121 } 122 set_max_chunk_size_bytes(uint32_t max_chunk_size_bytes)123 constexpr Chunk& set_max_chunk_size_bytes(uint32_t max_chunk_size_bytes) { 124 max_chunk_size_bytes_ = max_chunk_size_bytes; 125 return *this; 126 } 127 set_min_delay_microseconds(uint32_t min_delay_microseconds)128 constexpr Chunk& set_min_delay_microseconds(uint32_t min_delay_microseconds) { 129 min_delay_microseconds_ = min_delay_microseconds; 130 return *this; 131 } 132 set_offset(uint32_t offset)133 constexpr Chunk& set_offset(uint32_t offset) { 134 offset_ = offset; 135 return *this; 136 } 137 set_payload(ConstByteSpan payload)138 constexpr Chunk& set_payload(ConstByteSpan payload) { 139 payload_ = payload; 140 return *this; 141 } 142 set_remaining_bytes(uint64_t remaining_bytes)143 constexpr Chunk& set_remaining_bytes(uint64_t remaining_bytes) { 144 remaining_bytes_ = remaining_bytes; 145 return *this; 146 } 147 set_initial_offset(uint32_t offset)148 constexpr Chunk& set_initial_offset(uint32_t offset) { 149 initial_offset_ = offset; 150 return *this; 151 } 152 153 // TODO(frolv): For some reason, the compiler complains if this setter is 154 // marked constexpr. Leaving it off for now, but this should be investigated 155 // and fixed. set_status(Status status)156 Chunk& set_status(Status status) { 157 status_ = status; 158 return *this; 159 } 160 session_id()161 constexpr uint32_t session_id() const { 162 if (desired_session_id_.has_value()) { 163 return desired_session_id_.value(); 164 } 165 return session_id_; 166 } 167 desired_session_id()168 constexpr std::optional<uint32_t> desired_session_id() const { 169 return desired_session_id_; 170 } 171 resource_id()172 constexpr std::optional<uint32_t> resource_id() const { 173 if (is_legacy()) { 174 // In the legacy protocol, resource_id and session_id are the same (i.e. 175 // transfer_id). 176 return session_id_; 177 } 178 179 return resource_id_; 180 } 181 window_end_offset()182 constexpr uint32_t window_end_offset() const { return window_end_offset_; } offset()183 constexpr uint32_t offset() const { return offset_; } status()184 constexpr std::optional<Status> status() const { return status_; } 185 has_payload()186 constexpr bool has_payload() const { return !payload_.empty(); } payload()187 constexpr ConstByteSpan payload() const { return payload_; } 188 max_chunk_size_bytes()189 constexpr std::optional<uint32_t> max_chunk_size_bytes() const { 190 return max_chunk_size_bytes_; 191 } min_delay_microseconds()192 constexpr std::optional<uint32_t> min_delay_microseconds() const { 193 return min_delay_microseconds_; 194 } remaining_bytes()195 constexpr std::optional<uint64_t> remaining_bytes() const { 196 return remaining_bytes_; 197 } 198 protocol_version()199 constexpr ProtocolVersion protocol_version() const { 200 return protocol_version_; 201 } 202 is_legacy()203 constexpr bool is_legacy() const { 204 return protocol_version_ == ProtocolVersion::kLegacy; 205 } 206 type()207 constexpr Type type() const { 208 // Legacy protocol chunks may not have a type, but newer versions always 209 // will. Try to deduce the type of a legacy chunk without one set. 210 if (!is_legacy() || type_.has_value()) { 211 return type_.value(); 212 } 213 214 // The type-less legacy transfer protocol doesn't support handshakes or 215 // continuation parameters. Therefore, there are only three possible chunk 216 // types: start, data, and retransmit. 217 if (IsInitialChunk()) { 218 return Type::kStart; 219 } 220 221 if (has_payload()) { 222 return Type::kData; 223 } 224 225 return Type::kParametersRetransmit; 226 } 227 initial_offset()228 constexpr uint32_t initial_offset() const { return initial_offset_; } 229 230 // Returns true if this parameters chunk is requesting that the transmitter 231 // transmit from its set offset instead of simply ACKing. RequestsTransmissionFromOffset()232 constexpr bool RequestsTransmissionFromOffset() const { 233 if (is_legacy() && !type_.has_value()) { 234 return true; 235 } 236 237 return type_.value() == Type::kParametersRetransmit || 238 type_.value() == Type::kStartAckConfirmation || 239 type_.value() == Type::kStart; 240 } 241 IsInitialChunk()242 constexpr bool IsInitialChunk() const { 243 if (protocol_version_ >= ProtocolVersion::kVersionTwo) { 244 return type_ == Type::kStart; 245 } 246 247 // In legacy versions of the transfer protocol, the chunk type is not always 248 // set. Infer that a chunk is initial if it has an offset of 0 and no data 249 // or status. 250 return type_ == Type::kStart || 251 (offset_ == 0 && !has_payload() && !status_.has_value()); 252 } 253 IsTerminatingChunk()254 constexpr bool IsTerminatingChunk() const { 255 return type_ == Type::kCompletion || (is_legacy() && status_.has_value()); 256 } 257 258 // The final chunk from the transmitter sets remaining_bytes to 0 in both Read 259 // and Write transfers. IsFinalTransmitChunk()260 constexpr bool IsFinalTransmitChunk() const { return remaining_bytes_ == 0u; } 261 262 // Returns true if this chunk is part of an initial transfer handshake. IsInitialHandshakeChunk()263 constexpr bool IsInitialHandshakeChunk() const { 264 return type_ == Type::kStart || type_ == Type::kStartAck || 265 type_ == Type::kStartAckConfirmation; 266 } 267 268 private: Chunk(ProtocolVersion version,std::optional<Type> type)269 constexpr Chunk(ProtocolVersion version, std::optional<Type> type) 270 : session_id_(0), 271 desired_session_id_(std::nullopt), 272 resource_id_(std::nullopt), 273 window_end_offset_(0), 274 max_chunk_size_bytes_(std::nullopt), 275 min_delay_microseconds_(std::nullopt), 276 offset_(0), 277 initial_offset_(0), 278 payload_({}), 279 remaining_bytes_(std::nullopt), 280 status_(std::nullopt), 281 type_(type), 282 protocol_version_(version) {} 283 Chunk()284 constexpr Chunk() : Chunk(ProtocolVersion::kUnknown, std::nullopt) {} 285 286 // Returns true if this chunk should write legacy protocol fields to the 287 // serialized message. 288 // 289 // The first chunk of a transfer (type TRANSFER_START) is a special case: as 290 // we do not yet know what version of the protocol the other end is speaking, 291 // every legacy field must be encoded alongside newer ones to ensure that the 292 // chunk is processable. Following a response, the common protocol version 293 // will be determined and fields omitted as necessary. ShouldEncodeLegacyFields()294 constexpr bool ShouldEncodeLegacyFields() const { 295 return is_legacy() || type_ == Type::kStart; 296 } 297 298 uint32_t session_id_; 299 std::optional<uint32_t> desired_session_id_; 300 std::optional<uint32_t> resource_id_; 301 uint32_t window_end_offset_; 302 std::optional<uint32_t> max_chunk_size_bytes_; 303 std::optional<uint32_t> min_delay_microseconds_; 304 uint32_t offset_; 305 uint32_t initial_offset_; 306 ConstByteSpan payload_; 307 std::optional<uint64_t> remaining_bytes_; 308 std::optional<Status> status_; 309 std::optional<Type> type_; 310 ProtocolVersion protocol_version_; 311 }; 312 313 } // namespace pw::transfer::internal 314