blob: 79cc8879aed0a4f564f061677a670c7e12fd1a24 [file] [log] [blame]
// 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();
}
}