| // Copyright 2022 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. |
| |
| package dev.pigweed.pw_hdlc; |
| |
| import com.google.common.io.BaseEncoding; |
| import dev.pigweed.pw_hdlc.Protocol; |
| import dev.pigweed.pw_log.Logger; |
| import java.nio.ByteBuffer; |
| import java.nio.ByteOrder; |
| import java.util.zip.CRC32; |
| |
| /** |
| * Decodes Pigweed's HDLC frames received over the wire and passes complete frames to the registered |
| * listener. |
| */ |
| public class Decoder { |
| private static final Logger logger = Logger.forClass(Decoder.class); |
| // Should be more than enough |
| private static final int MAX_FRAME_SIZE_BYTES = 4096; |
| |
| private final OnCompleteFrame listener; |
| private State state = State.INTER_FRAME; |
| private int currentFrameSize = 0; |
| private final byte[] buffer = new byte[MAX_FRAME_SIZE_BYTES]; |
| |
| /** OnCompleteFrame */ |
| public interface OnCompleteFrame { |
| void onCompleteFrame(Frame frame); |
| } |
| |
| enum State { |
| INTER_FRAME, |
| FRAME, |
| ESCAPE_FRAME, |
| } |
| |
| public Decoder(OnCompleteFrame listener) { |
| this.listener = listener; |
| } |
| |
| public void process(ByteBuffer buffer) { |
| while (buffer.hasRemaining()) { |
| process(buffer.get()); |
| } |
| } |
| |
| public void process(byte[] buffer) { |
| for (byte b : buffer) { |
| process(b); |
| } |
| } |
| |
| public void process(byte b) { |
| switch (state) { |
| case INTER_FRAME: |
| if (b == Protocol.FLAG) { |
| state = State.FRAME; |
| |
| // Report an error if non-flag bytes were read between frames. |
| if (currentFrameSize != 0) { |
| reset(); |
| return; |
| } |
| } else { |
| // Count bytes to track how many are discarded. |
| currentFrameSize += 1; |
| } |
| return; |
| case FRAME: { |
| if (b == Protocol.FLAG) { |
| if (checkFrame()) { |
| ByteBuffer message = ByteBuffer.wrap(buffer, 0, currentFrameSize); |
| logger.atInfo().log( |
| "Raw message %s", BaseEncoding.base16().encode(buffer, 0, currentFrameSize)); |
| Frame frame = Frame.parse(message); |
| if (frame != null) { |
| listener.onCompleteFrame(frame); |
| } |
| } |
| reset(); |
| } else if (b == Protocol.ESCAPE) { |
| state = State.ESCAPE_FRAME; |
| } else { |
| appendByte(b); |
| } |
| return; |
| } |
| case ESCAPE_FRAME: |
| // The flag character cannot be escaped; return an error. |
| if (b == Protocol.FLAG) { |
| state = State.FRAME; |
| reset(); |
| } |
| |
| if (b == Protocol.ESCAPE) { |
| // Two escape characters in a row is illegal -- invalidate this frame. |
| // The frame is reported abandoned when the next flag byte appears. |
| state = State.INTER_FRAME; |
| |
| // Count the escape byte so that the inter-frame state detects an error. |
| currentFrameSize += 1; |
| } else { |
| state = State.FRAME; |
| appendByte(escape(b)); |
| } |
| } |
| } |
| |
| private void reset() { |
| logger.atConfig().log("reset"); |
| currentFrameSize = 0; |
| } |
| |
| private void appendByte(byte b) { |
| if (currentFrameSize < MAX_FRAME_SIZE_BYTES) { |
| buffer[currentFrameSize] = b; |
| } |
| |
| // Always increase size: if it is larger than the buffer, overflow occurred. |
| currentFrameSize += 1; |
| } |
| |
| private static byte escape(byte b) { |
| return (byte) (b ^ Protocol.ESCAPE_CONSTANT); |
| } |
| |
| private boolean checkFrame() { |
| // Empty frames are not an error; repeated flag characters are okay. |
| if (currentFrameSize == 0) { |
| return true; |
| } |
| |
| if (currentFrameSize < Frame.MIN_FRAME_SIZE_BYTES) { |
| logger.atWarning().log( |
| "Frame length (%d) less than %d", currentFrameSize, Frame.MIN_FRAME_SIZE_BYTES); |
| return false; |
| } |
| |
| if (!verifyFrameCheckSequence()) { |
| logger.atWarning().log("Frame CRC verification failed"); |
| return false; |
| } |
| |
| if (currentFrameSize > MAX_FRAME_SIZE_BYTES) { |
| logger.atWarning().log("Frame too big (%d > %d)", currentFrameSize, MAX_FRAME_SIZE_BYTES); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| private boolean verifyFrameCheckSequence() { |
| int crcOffset = currentFrameSize - Frame.CRC_SIZE; |
| CRC32 crc32 = new CRC32(); |
| crc32.update(buffer, 0, crcOffset); |
| int actualCrc = (int) crc32.getValue(); |
| return actualCrc |
| == ByteBuffer.wrap(buffer, crcOffset, Frame.CRC_SIZE) |
| .order(ByteOrder.LITTLE_ENDIAN) |
| .asIntBuffer() |
| .get(); |
| } |
| } |