diff --git a/api/src/main/java/org/jmisb/api/klv/BerDecoder.java b/api/src/main/java/org/jmisb/api/klv/BerDecoder.java index a6922dd81..e034ee0c8 100644 --- a/api/src/main/java/org/jmisb/api/klv/BerDecoder.java +++ b/api/src/main/java/org/jmisb/api/klv/BerDecoder.java @@ -1,7 +1,11 @@ package org.jmisb.api.klv; +import java.io.IOException; +import java.io.InputStream; + /** Decode data using Basic Encoding Rules (BER). */ public class BerDecoder { + private BerDecoder() {} /** @@ -77,4 +81,60 @@ public static BerField decode(byte[] data, int offset, boolean isOid) return new BerField(length, value); } + + /** + * Decode a field (length and value) from an InputStream. + * + * @param is InputStream with BER-encoded data at the tip + * @param isOid true if the data is encoded using BER-OID + * @return decoded The decoded field + * @throws IllegalArgumentException if the encoded data is invalid + */ + public static BerField decode(InputStream is, boolean isOid) throws IOException { + if (!isOid) { + return decodeBer(is); + } + + return decodeBerOid(is); + } + + private static BerField decodeBer(InputStream is) throws IOException { + int length = is.read(); + + if ((length & 0x80) == 0) { + // BER Short Form. If the first bit of the BER is 0 then the BER is 1-byte. + return new BerField(1, length); + } + + // BER Long Form (variable length) + int berLength = length & 0x7f; + int fullBerSize = berLength + 1; + byte[] data = new byte[berLength]; + int read = is.read(data, 0, data.length); + if (read != data.length) { + throw new IllegalArgumentException("BER parsing ran out of bytes"); + } + int len = 0; + for (int i = 0; i < berLength; ++i) { + int b = 0x00FF & data[i]; + len = (len << 8) | b; + } + length = len; + + return new BerField(fullBerSize, length); + } + + private static BerField decodeBerOid(InputStream is) throws IOException { + int read; + int value = 0; + int length = 0; + do { + read = is.read(); + int highbits = (value << 7); + int lowbits = (read & 0x7F); + value = highbits + lowbits; + length++; + } while ((read & 0x80) == 0x80); + return new BerField(length, value); + } } diff --git a/api/src/main/java/org/jmisb/api/klv/KlvParser.java b/api/src/main/java/org/jmisb/api/klv/KlvParser.java index a90a11d2a..2cccb2d01 100644 --- a/api/src/main/java/org/jmisb/api/klv/KlvParser.java +++ b/api/src/main/java/org/jmisb/api/klv/KlvParser.java @@ -1,8 +1,12 @@ package org.jmisb.api.klv; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.function.Consumer; import org.jmisb.api.common.KlvParseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,6 +17,73 @@ public class KlvParser { private KlvParser() {} + /** + * Parse an InputStream containing one or more {@link IMisbMessage}s. + * + *

This is an additional interface for parsing KLV metadata. It assumes that {@code is} + * contains one or more top-level messages, i.e., byte sequences starting with a Universal Label + * (UL). If a particular UL is unsupported it will be returned as a {@link RawMisbMessage}. + * + *

The supported UL are determined by the {@link MisbMessageFactory} singleton. + * + * @param is The input stream + * @param handler The resultant {@link IMisbMessage} objects streamed + * @param exceptionHandler The {@link KlvParseException} errors detected in the stream. + */ + public static void parseStream( + InputStream is, + Consumer handler, + Consumer exceptionHandler) + throws KlvParseException { + + // reusable key array to minimize garbage + byte[] key = new byte[UniversalLabel.LENGTH]; + + try { + while (true) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + // Read the UniversalLabel + int read = is.read(key, 0, key.length); + if (read < 0) { + break; + } + if (read != key.length) { + throw new KlvParseException( + "Read " + read + " bytes when expected " + key.length); + } + out.write(key); + + // Read the payload length + BerField length = BerDecoder.decode(is, false); + out.write(BerEncoder.encode(length.getValue())); + + // Read the payload + byte[] payload = new byte[length.getValue()]; + read = is.read(payload, 0, payload.length); + if (read == 0) { + break; + } + if (read != payload.length) { + throw new KlvParseException( + "Read " + read + " bytes when expected " + key.length); + } + out.write(payload); + + // hand off the IMisbMessage + byte[] buf = out.toByteArray(); + try { + IMisbMessage msg = MisbMessageFactory.getInstance().handleMessage(buf); + handler.accept(msg); + } catch (KlvParseException e) { + exceptionHandler.accept(e); + } + } + } catch (IOException e) { + throw new KlvParseException("IOException during stream parsing"); + } + } + /** * Parse a byte array containing one or more {@link IMisbMessage}s. * diff --git a/api/src/test/java/org/jmisb/api/klv/BerDecoderTest.java b/api/src/test/java/org/jmisb/api/klv/BerDecoderTest.java index 02fd85789..8c4b16bdd 100644 --- a/api/src/test/java/org/jmisb/api/klv/BerDecoderTest.java +++ b/api/src/test/java/org/jmisb/api/klv/BerDecoderTest.java @@ -1,5 +1,7 @@ package org.jmisb.api.klv; +import java.io.ByteArrayInputStream; +import java.io.IOException; import org.testng.Assert; import org.testng.annotations.Test; @@ -21,6 +23,24 @@ public void testShortFormLengthField() { Assert.assertEquals(l3.getLength(), 1); } + @Test + public void testShortFormLengthFieldInputStream() throws IOException { + // BER Short Form is always encoded in a single byte, and has its high order bit set to 0 + byte[] data = {0x00, 0x05, 0x7f}; // 1, 5, 127 + ByteArrayInputStream bais = new ByteArrayInputStream(data); + BerField l1 = BerDecoder.decode(bais, false); + BerField l2 = BerDecoder.decode(bais, false); + BerField l3 = BerDecoder.decode(bais, false); + + Assert.assertEquals(l1.getValue(), 0); + Assert.assertEquals(l2.getValue(), 5); + Assert.assertEquals(l3.getValue(), 127); + + Assert.assertEquals(l1.getLength(), 1); + Assert.assertEquals(l2.getLength(), 1); + Assert.assertEquals(l3.getLength(), 1); + } + @Test public void testLongFormLengthField() { byte[] data = { @@ -40,6 +60,26 @@ public void testLongFormLengthField() { Assert.assertEquals(l3.getLength(), 5); } + @Test + public void testLongFormLengthFieldInputStream() throws IOException { + byte[] data = { + (byte) 0x81, 0x05, (byte) 0x82, 0x01, (byte) 0x80, (byte) 0x84, 0x01, 0x01, 0x01, 0x01 + }; + + ByteArrayInputStream bais = new ByteArrayInputStream(data); + BerField l1 = BerDecoder.decode(bais, false); + BerField l2 = BerDecoder.decode(bais, false); + BerField l3 = BerDecoder.decode(bais, false); + + Assert.assertEquals(l1.getValue(), 5); + Assert.assertEquals(l2.getValue(), 384); + Assert.assertEquals(l3.getValue(), 16_843_009); + + Assert.assertEquals(l1.getLength(), 2); + Assert.assertEquals(l2.getLength(), 3); + Assert.assertEquals(l3.getLength(), 5); + } + @Test(expectedExceptions = IllegalArgumentException.class) public void testParseBufferOverrun() { byte[] data = {0x00, 0x05, 0x7f}; diff --git a/api/src/test/java/org/jmisb/api/klv/KlvParserTest.java b/api/src/test/java/org/jmisb/api/klv/KlvParserTest.java index 2da691420..b6b612582 100644 --- a/api/src/test/java/org/jmisb/api/klv/KlvParserTest.java +++ b/api/src/test/java/org/jmisb/api/klv/KlvParserTest.java @@ -4,6 +4,8 @@ import com.github.valfirst.slf4jtest.TestLogger; import com.github.valfirst.slf4jtest.TestLoggerFactory; +import java.io.ByteArrayInputStream; +import java.util.ArrayList; import java.util.List; import org.jmisb.api.common.KlvParseException; import org.testng.annotations.AfterMethod; @@ -62,6 +64,49 @@ private void doParse() throws KlvParseException { assertTrue(rawMessage.getIdentifiers().isEmpty()); } + @Test + public void checkParseStream() throws KlvParseException { + doParseStream(); + } + + private void doParseStream() throws KlvParseException { + byte[] bytes = + new byte[] { + 0x06, 0x0E, 0x2B, 0x34, 0x02, 0x0B, 0x01, 0x01, 0x0E, 0x01, 0x03, 0x01, 0x02, + 0x00, 0x00, 0x00, 0x09, 0x04, 0x02, 0x00, 0x4f, 0x01, 0x03, 0x1e, 0x2f, 0x3a + }; + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + List messages = new ArrayList<>(); + List errors = new ArrayList<>(); + KlvParser.parseStream(bais, messages::add, errors::add); + assertEquals(0, errors.size()); + if (!errors.isEmpty()) { + throw errors.get(0); + } + + assertNotNull(messages); + assertEquals(messages.size(), 1); + IMisbMessage message = messages.get(0); + assertNotNull(message); + assertEquals( + message.getUniversalLabel(), + new UniversalLabel( + new byte[] { + 0x06, 0x0E, 0x2B, 0x34, 0x02, 0x0B, 0x01, 0x01, 0x0E, 0x01, 0x03, 0x01, + 0x02, 0x00, 0x00, 0x00 + })); + assertEquals(message.displayHeader(), "Unknown"); + assertTrue(message instanceof RawMisbMessage); + RawMisbMessage rawMessage = (RawMisbMessage) message; + assertEquals( + rawMessage.getBytes(), + new byte[] { + 0x06, 0x0E, 0x2B, 0x34, 0x02, 0x0B, 0x01, 0x01, 0x0E, 0x01, 0x03, 0x01, 0x02, + 0x00, 0x00, 0x00, 0x09, 0x04, 0x02, 0x00, 0x4f, 0x01, 0x03, 0x1e, 0x2f, 0x3a + }); + assertTrue(rawMessage.getIdentifiers().isEmpty()); + } + @Test(expectedExceptions = KlvParseException.class) public void checkBadLength() throws KlvParseException { KlvParser.parseBytes(