pw_hdlc: Calculate frame size before writing

This updates the WriteUIFrame function to calculate the worst-case
encoded frame size in advance and not write any data if it exceeds
the stream's write limit.

Change-Id: I315a2a94dbf19d6f3d021ecdee66d5329313f1d9
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/29485
Reviewed-by: Wyatt Hepler <hepler@google.com>
Commit-Queue: Alexei Frolov <frolv@google.com>
diff --git a/pw_hdlc/BUILD b/pw_hdlc/BUILD
index 3dbde8c..8316bb8 100644
--- a/pw_hdlc/BUILD
+++ b/pw_hdlc/BUILD
@@ -26,6 +26,7 @@
     srcs = [
         "decoder.cc",
         "encoder.cc",
+        "public/pw_hdlc/internal/encoder.h",
         "pw_hdlc_private/protocol.h",
         "rpc_packets.cc",
     ],
diff --git a/pw_hdlc/BUILD.gn b/pw_hdlc/BUILD.gn
index 7522842..18a1946 100644
--- a/pw_hdlc/BUILD.gn
+++ b/pw_hdlc/BUILD.gn
@@ -53,16 +53,17 @@
   public = [ "public/pw_hdlc/encoder.h" ]
   sources = [
     "encoder.cc",
+    "public/pw_hdlc/internal/encoder.h",
     "pw_hdlc_private/protocol.h",
   ]
   public_deps = [
     "$dir_pw_stream:sys_io_stream",
     dir_pw_bytes,
+    dir_pw_checksum,
     dir_pw_status,
     dir_pw_stream,
     dir_pw_sys_io,
   ]
-  deps = [ dir_pw_checksum ]
   friend = [ ":*" ]
 }
 
diff --git a/pw_hdlc/encoder.cc b/pw_hdlc/encoder.cc
index ae30f02..b0d209e 100644
--- a/pw_hdlc/encoder.cc
+++ b/pw_hdlc/encoder.cc
@@ -21,13 +21,13 @@
 #include <span>
 
 #include "pw_bytes/endian.h"
-#include "pw_checksum/crc32.h"
+#include "pw_hdlc/internal/encoder.h"
 #include "pw_hdlc_private/protocol.h"
 
 using std::byte;
 
 namespace pw::hdlc {
-namespace {
+namespace internal {
 
 // Indicates this an information packet with sequence numbers set to 0.
 constexpr byte kUnusedControl = byte{0};
@@ -42,31 +42,6 @@
   return writer.Write(b);
 }
 
-// Encodes and writes HDLC frames.
-class Encoder {
- public:
-  constexpr Encoder(stream::Writer& output) : writer_(output) {}
-
-  // Writes the header for an I-frame. After successfully calling
-  // StartInformationFrame, WriteData may be called any number of times.
-  [[maybe_unused]] Status StartInformationFrame(uint8_t address);
-
-  // Writes the header for an U-frame. After successfully calling
-  // StartUnnumberedFrame, WriteData may be called any number of times.
-  Status StartUnnumberedFrame(uint8_t address);
-
-  // Writes data for an ongoing frame. Must only be called after a successful
-  // StartInformationFrame call, and prior to a FinishFrame() call.
-  Status WriteData(ConstByteSpan data);
-
-  // Finishes a frame. Writes the frame check sequence and a terminating flag.
-  Status FinishFrame();
-
- private:
-  stream::Writer& writer_;
-  checksum::Crc32 fcs_;
-};
-
 Status Encoder::StartInformationFrame(uint8_t address) {
   fcs_.clear();
   if (Status status = writer_.Write(kFlag); !status.ok()) {
@@ -117,12 +92,27 @@
   return writer_.Write(kFlag);
 }
 
-}  // namespace
+size_t Encoder::MaxEncodedSize(uint8_t address, ConstByteSpan payload) {
+  constexpr size_t kFcsMaxSize = 8;  // Worst case FCS: 0x7e7e7e7e.
+  size_t encoded_address_size = NeedsEscaping(std::byte{address}) ? 2 : 1;
+  size_t encoded_payload_size =
+      payload.size() +
+      std::count_if(payload.begin(), payload.end(), NeedsEscaping);
+
+  return encoded_address_size + encoded_payload_size + kFcsMaxSize;
+}
+
+}  // namespace internal
 
 Status WriteUIFrame(uint8_t address,
                     ConstByteSpan payload,
                     stream::Writer& writer) {
-  Encoder encoder(writer);
+  if (internal::Encoder::MaxEncodedSize(address, payload) >
+      writer.ConservativeWriteLimit()) {
+    return Status::ResourceExhausted();
+  }
+
+  internal::Encoder encoder(writer);
 
   if (Status status = encoder.StartUnnumberedFrame(address); !status.ok()) {
     return status;
diff --git a/pw_hdlc/encoder_test.cc b/pw_hdlc/encoder_test.cc
index f3ce559..ead3bfc 100644
--- a/pw_hdlc/encoder_test.cc
+++ b/pw_hdlc/encoder_test.cc
@@ -20,6 +20,7 @@
 
 #include "gtest/gtest.h"
 #include "pw_bytes/array.h"
+#include "pw_hdlc/internal/encoder.h"
 #include "pw_hdlc_private/protocol.h"
 #include "pw_stream/memory_stream.h"
 
@@ -154,10 +155,53 @@
       kFlag));
 }
 
-TEST_F(WriteUnnumberedFrame, WriterError) {
+TEST_F(WriteUnnumberedFrame, PayloadTooLarge_WritesNothing) {
   constexpr auto data = bytes::Initialized<sizeof(buffer_)>(0x7e);
   EXPECT_EQ(Status::ResourceExhausted(), WriteUIFrame(kAddress, data, writer_));
+  EXPECT_EQ(0u, writer_.bytes_written());
+}
+
+class ErrorWriter : public stream::Writer {
+ private:
+  Status DoWrite(ConstByteSpan) override { return Status::Unimplemented(); }
+};
+
+TEST(WriteUnnumberedFrame, WriterError) {
+  ErrorWriter writer;
+  EXPECT_EQ(Status::Unimplemented(),
+            WriteUIFrame(kAddress, bytes::Array<0x01>(), writer));
 }
 
 }  // namespace
+
+namespace internal {
+namespace {
+
+constexpr uint8_t kEscapeAddress = 0x7d;
+
+TEST(Encoder, MaxEncodedSize_EmptyPayload) {
+  EXPECT_EQ(9u, Encoder::MaxEncodedSize(kAddress, {}));
+  EXPECT_EQ(10u, Encoder::MaxEncodedSize(kEscapeAddress, {}));
+}
+
+TEST(Encoder, MaxEncodedSize_PayloadWithoutEscapes) {
+  constexpr auto data = bytes::Array<0x00, 0x01, 0x02, 0x03>();
+  EXPECT_EQ(13u, Encoder::MaxEncodedSize(kAddress, data));
+  EXPECT_EQ(14u, Encoder::MaxEncodedSize(kEscapeAddress, data));
+}
+
+TEST(Encoder, MaxEncodedSize_PayloadWithOneEscape) {
+  constexpr auto data = bytes::Array<0x00, 0x01, 0x7e, 0x03>();
+  EXPECT_EQ(14u, Encoder::MaxEncodedSize(kAddress, data));
+  EXPECT_EQ(15u, Encoder::MaxEncodedSize(kEscapeAddress, data));
+}
+
+TEST(Encoder, MaxEncodedSize_PayloadWithAllEscapes) {
+  constexpr auto data = bytes::Initialized<8>(0x7e);
+  EXPECT_EQ(25u, Encoder::MaxEncodedSize(kAddress, data));
+  EXPECT_EQ(26u, Encoder::MaxEncodedSize(kEscapeAddress, data));
+}
+
+}  // namespace
+}  // namespace internal
 }  // namespace pw::hdlc
diff --git a/pw_hdlc/public/pw_hdlc/internal/encoder.h b/pw_hdlc/public/pw_hdlc/internal/encoder.h
new file mode 100644
index 0000000..a7a61ad
--- /dev/null
+++ b/pw_hdlc/public/pw_hdlc/internal/encoder.h
@@ -0,0 +1,50 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_checksum/crc32.h"
+#include "pw_stream/stream.h"
+
+namespace pw::hdlc::internal {
+
+// Encodes and writes HDLC frames.
+class Encoder {
+ public:
+  constexpr Encoder(stream::Writer& output) : writer_(output) {}
+
+  // Writes the header for an I-frame. After successfully calling
+  // StartInformationFrame, WriteData may be called any number of times.
+  [[maybe_unused]] Status StartInformationFrame(uint8_t address);
+
+  // Writes the header for an U-frame. After successfully calling
+  // StartUnnumberedFrame, WriteData may be called any number of times.
+  Status StartUnnumberedFrame(uint8_t address);
+
+  // Writes data for an ongoing frame. Must only be called after a successful
+  // StartInformationFrame call, and prior to a FinishFrame() call.
+  Status WriteData(ConstByteSpan data);
+
+  // Finishes a frame. Writes the frame check sequence and a terminating flag.
+  Status FinishFrame();
+
+  // Runs a pass through a payload, returning the worst-case encoded size for a
+  // frame containing it. Does not calculate CRC to improve efficiency.
+  static size_t MaxEncodedSize(uint8_t address, ConstByteSpan payload);
+
+ private:
+  stream::Writer& writer_;
+  checksum::Crc32 fcs_;
+};
+
+}  // namespace pw::hdlc::internal