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_hdlc; 16 17 import dev.pigweed.pw_hdlc.CustomVarInt; 18 import dev.pigweed.pw_hdlc.Protocol; 19 import java.io.IOException; 20 import java.io.OutputStream; 21 import java.nio.ByteBuffer; 22 import java.util.zip.CRC32; 23 24 /** Encodes a payload with pigweed's HDLC format */ 25 public class Encoder { 26 private final CRC32 crc32 = new CRC32(); 27 private final OutputStream output; 28 29 /** Writes the payload as an HDLC unnumbered information (UI) frame. */ writeUiFrame(long address, ByteBuffer payload, OutputStream output)30 public static void writeUiFrame(long address, ByteBuffer payload, OutputStream output) 31 throws IOException { 32 Encoder encoder = new Encoder(output); 33 encoder.startUnnumberedFrame(address); 34 encoder.writeData(payload); 35 encoder.finishFrame(); 36 } 37 writeFrame(Frame frame, OutputStream output)38 public static void writeFrame(Frame frame, OutputStream output) throws IOException { 39 Encoder encoder = new Encoder(output); 40 encoder.startFrame(frame.getAddress(), frame.getControl()); 41 encoder.writeData(frame.getPayload()); 42 encoder.finishFrame(); 43 } 44 45 /** Returns the encoded size of a UI frame with the provided payload. */ getUiFrameEncodedSize(long address, byte[] payload)46 public static int getUiFrameEncodedSize(long address, byte[] payload) { 47 return getUiFrameEncodedSize(address, ByteBuffer.wrap(payload)); 48 } 49 getUiFrameEncodedSize(long address, ByteBuffer payload)50 public static int getUiFrameEncodedSize(long address, ByteBuffer payload) { 51 ByteCountingOutputStream output = new ByteCountingOutputStream(); 52 try { 53 writeUiFrame(address, payload, output); 54 } catch (IOException e) { 55 throw new AssertionError("ByteCountingOutputStream does not throw exceptions!", e); 56 } 57 return output.writtenBytes; 58 } 59 60 /** Returns the number of payload bytes that can be encoded in a UI frame of the given size. */ getMaxPayloadBytesInUiFrameOfSize( long address, byte[] payload, int maxEncodedUiFrameSizeBytes)61 public static int getMaxPayloadBytesInUiFrameOfSize( 62 long address, byte[] payload, int maxEncodedUiFrameSizeBytes) { 63 return getMaxPayloadBytesInUiFrameOfSize( 64 address, ByteBuffer.wrap(payload), maxEncodedUiFrameSizeBytes); 65 } 66 getMaxPayloadBytesInUiFrameOfSize( long address, ByteBuffer payload, int maxEncodedUiFrameSizeBytes)67 public static int getMaxPayloadBytesInUiFrameOfSize( 68 long address, ByteBuffer payload, int maxEncodedUiFrameSizeBytes) { 69 ByteCountingOutputStream output = new ByteCountingOutputStream(); 70 Encoder encoder = new Encoder(output); 71 int payloadBytesProcessed = 0; 72 73 try { 74 encoder.startUnnumberedFrame(address); 75 while (payload.hasRemaining()) { 76 encoder.write(payload.get()); 77 if (output.writtenBytes + encoder.getFrameSuffixSize() > maxEncodedUiFrameSizeBytes) { 78 break; 79 } 80 payloadBytesProcessed += 1; 81 } 82 } catch (IOException e) { 83 throw new AssertionError("ByteCountingOutputStream does not throw exceptions!", e); 84 } 85 return payloadBytesProcessed; 86 } 87 Encoder(OutputStream output)88 private Encoder(OutputStream output) { 89 this.output = output; 90 } 91 startUnnumberedFrame(long address)92 private void startUnnumberedFrame(long address) throws IOException { 93 startFrame(address, Protocol.UNNUMBERED_INFORMATION); 94 } 95 startFrame(long address, byte control)96 private void startFrame(long address, byte control) throws IOException { 97 crc32.reset(); 98 output.write(Protocol.FLAG); 99 writeVarLong(address); 100 byte[] wrappedControl = {control}; 101 writeData(ByteBuffer.wrap(wrappedControl)); 102 } 103 finishFrame()104 private void finishFrame() throws IOException { 105 writeCrc(); 106 output.write(Protocol.FLAG); 107 } 108 writeCrc()109 private void writeCrc() throws IOException { 110 long crc = crc32.getValue(); 111 byte[] buffer = {(byte) crc, (byte) (crc >> 8), (byte) (crc >> 16), (byte) (crc >> 24)}; 112 writeData(ByteBuffer.wrap(buffer)); 113 } 114 writeVarLong(long data)115 private void writeVarLong(long data) throws IOException { 116 ByteBuffer buffer = ByteBuffer.allocate(CustomVarInt.varLongSize(data)); 117 CustomVarInt.putVarLong(data, buffer); 118 buffer.rewind(); 119 writeData(buffer); 120 } 121 writeData(ByteBuffer data)122 private void writeData(ByteBuffer data) throws IOException { 123 while (data.hasRemaining()) { 124 byte b = data.get(); 125 write(b); 126 crc32.update(b); 127 } 128 } 129 write(byte b)130 private void write(byte b) throws IOException { 131 if (b == Protocol.FLAG) { 132 output.write(Protocol.ESCAPED_FLAG); 133 } else if (b == Protocol.ESCAPE) { 134 output.write(Protocol.ESCAPED_ESCAPE); 135 } else { 136 output.write(b); 137 } 138 } 139 getFrameSuffixSize()140 private int getFrameSuffixSize() { 141 int crc32Escapes = 0; 142 for (int value = (int) crc32.getValue(); value != 0; value >>>= 8) { 143 crc32Escapes += (value & 0xFF) == Protocol.FLAG || (value & 0xFF) == Protocol.ESCAPE ? 1 : 0; 144 } 145 return 4 /* CRC32 */ + crc32Escapes + 1 /* final flag byte */; 146 } 147 148 private static class ByteCountingOutputStream extends OutputStream { 149 private int writtenBytes; 150 151 @Override write(int b)152 public void write(int b) { 153 writtenBytes += 1; 154 } 155 } 156 }