blob: ae03741036fc686188259a00f71f91425b4f5d59 [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 dev.pigweed.pw_hdlc.CustomVarInt;
import dev.pigweed.pw_hdlc.Protocol;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.zip.CRC32;
/** Encodes a payload with pigweed's HDLC format */
public class Encoder {
private final CRC32 crc32 = new CRC32();
private final OutputStream output;
/** Writes the payload as an HDLC unnumbered information (UI) frame. */
public static void writeUiFrame(long address, ByteBuffer payload, OutputStream output)
throws IOException {
Encoder encoder = new Encoder(output);
encoder.startUnnumberedFrame(address);
encoder.writeData(payload);
encoder.finishFrame();
}
public static void writeFrame(Frame frame, OutputStream output) throws IOException {
Encoder encoder = new Encoder(output);
encoder.startFrame(frame.getAddress(), frame.getControl());
encoder.writeData(frame.getPayload());
encoder.finishFrame();
}
/** Returns the encoded size of a UI frame with the provided payload. */
public static int getUiFrameEncodedSize(long address, byte[] payload) {
return getUiFrameEncodedSize(address, ByteBuffer.wrap(payload));
}
public static int getUiFrameEncodedSize(long address, ByteBuffer payload) {
ByteCountingOutputStream output = new ByteCountingOutputStream();
try {
writeUiFrame(address, payload, output);
} catch (IOException e) {
throw new AssertionError("ByteCountingOutputStream does not throw exceptions!", e);
}
return output.writtenBytes;
}
/** Returns the number of payload bytes that can be encoded in a UI frame of the given size. */
public static int getMaxPayloadBytesInUiFrameOfSize(
long address, byte[] payload, int maxEncodedUiFrameSizeBytes) {
return getMaxPayloadBytesInUiFrameOfSize(
address, ByteBuffer.wrap(payload), maxEncodedUiFrameSizeBytes);
}
public static int getMaxPayloadBytesInUiFrameOfSize(
long address, ByteBuffer payload, int maxEncodedUiFrameSizeBytes) {
ByteCountingOutputStream output = new ByteCountingOutputStream();
Encoder encoder = new Encoder(output);
int payloadBytesProcessed = 0;
try {
encoder.startUnnumberedFrame(address);
while (payload.hasRemaining()) {
encoder.write(payload.get());
if (output.writtenBytes + encoder.getFrameSuffixSize() > maxEncodedUiFrameSizeBytes) {
break;
}
payloadBytesProcessed += 1;
}
} catch (IOException e) {
throw new AssertionError("ByteCountingOutputStream does not throw exceptions!", e);
}
return payloadBytesProcessed;
}
private Encoder(OutputStream output) {
this.output = output;
}
private void startUnnumberedFrame(long address) throws IOException {
startFrame(address, Protocol.UNNUMBERED_INFORMATION);
}
private void startFrame(long address, byte control) throws IOException {
crc32.reset();
output.write(Protocol.FLAG);
writeVarLong(address);
byte[] wrappedControl = {control};
writeData(ByteBuffer.wrap(wrappedControl));
}
private void finishFrame() throws IOException {
writeCrc();
output.write(Protocol.FLAG);
}
private void writeCrc() throws IOException {
long crc = crc32.getValue();
byte[] buffer = {(byte) crc, (byte) (crc >> 8), (byte) (crc >> 16), (byte) (crc >> 24)};
writeData(ByteBuffer.wrap(buffer));
}
private void writeVarLong(long data) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(CustomVarInt.varLongSize(data));
CustomVarInt.putVarLong(data, buffer);
buffer.rewind();
writeData(buffer);
}
private void writeData(ByteBuffer data) throws IOException {
while (data.hasRemaining()) {
byte b = data.get();
write(b);
crc32.update(b);
}
}
private void write(byte b) throws IOException {
if (b == Protocol.FLAG) {
output.write(Protocol.ESCAPED_FLAG);
} else if (b == Protocol.ESCAPE) {
output.write(Protocol.ESCAPED_ESCAPE);
} else {
output.write(b);
}
}
private int getFrameSuffixSize() {
int crc32Escapes = 0;
for (int value = (int) crc32.getValue(); value != 0; value >>>= 8) {
crc32Escapes += (value & 0xFF) == Protocol.FLAG || (value & 0xFF) == Protocol.ESCAPE ? 1 : 0;
}
return 4 /* CRC32 */ + crc32Escapes + 1 /* final flag byte */;
}
private static class ByteCountingOutputStream extends OutputStream {
private int writtenBytes;
@Override
public void write(int b) {
writtenBytes += 1;
}
}
}