diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 773bad4..783803f 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,10 +1,7 @@
-
-
-
-
+
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 0000000..efa4625
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 2f00e79..22f380b 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -4,6 +4,9 @@
+
+
+
diff --git a/.idea/modules.xml b/.idea/modules.xml
index ad80793..3986c39 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -3,11 +3,8 @@
-
-
-
-
-
+
+
\ No newline at end of file
diff --git a/.idea/modules/ETF-Java.iml b/.idea/modules/ETF-Java.iml
index 71c1d11..47d9110 100644
--- a/.idea/modules/ETF-Java.iml
+++ b/.idea/modules/ETF-Java.iml
@@ -1,5 +1,6 @@
-
+
+
diff --git a/.idea/modules/ETF-Java_main.iml b/.idea/modules/ETF-Java.main.iml
similarity index 62%
rename from .idea/modules/ETF-Java_main.iml
rename to .idea/modules/ETF-Java.main.iml
index 77aac3a..d79cc51 100644
--- a/.idea/modules/ETF-Java_main.iml
+++ b/.idea/modules/ETF-Java.main.iml
@@ -1,13 +1,14 @@
-
-
-
+
+
+
-
+
+
\ No newline at end of file
diff --git a/.idea/modules/ETF-Java.test.iml b/.idea/modules/ETF-Java.test.iml
new file mode 100644
index 0000000..8744c33
--- /dev/null
+++ b/.idea/modules/ETF-Java.test.iml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/ETF-Java_jmh.iml b/.idea/modules/ETF-Java_jmh.iml
index 7950568..fe6043c 100644
--- a/.idea/modules/ETF-Java_jmh.iml
+++ b/.idea/modules/ETF-Java_jmh.iml
@@ -1,6 +1,6 @@
-
+
diff --git a/.idea/modules/ETF-Java_test.iml b/.idea/modules/ETF-Java_test.iml
deleted file mode 100644
index 72b272e..0000000
--- a/.idea/modules/ETF-Java_test.iml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules/gui/gui.iml b/.idea/modules/gui/gui.iml
deleted file mode 100644
index be7c72e..0000000
--- a/.idea/modules/gui/gui.iml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules/gui/gui_main.iml b/.idea/modules/gui/gui_main.iml
deleted file mode 100644
index 07a155a..0000000
--- a/.idea/modules/gui/gui_main.iml
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules/gui/gui_test.iml b/.idea/modules/gui/gui_test.iml
deleted file mode 100644
index 0152b3a..0000000
--- a/.idea/modules/gui/gui_test.iml
+++ /dev/null
@@ -1,71 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index 870a180..05abab0 100644
--- a/README.md
+++ b/README.md
@@ -2,36 +2,15 @@
A Java parser/writer for the ETF format.
This api aims to provide a set of high-speed abstractions for ETF.
-Additionally, it supports multiple subsets of ETF: standard ETF, BERT-rpc
-and Hammer & Chisel's (Discord's) Loqui.
+
+Currently, its primary goal is to support the subset of ETF utilized by Discord (see: [Loqui](https://github.com/discord/loqui) and [erlpack](https://github.com/discord/erlpack))
+So the you should consider this the "minimum viable product" of an ETF parser for use with discord.
## Using ETF-Java
**TODO**
-## A note about performance
-While this project attempts to be as well optimized as possible, the JVM
-creates a limit to how fast this can be just due to how it manages memory.
-
-Performance can be improved via JNI or usage of the `Unsafe` class
-but these have issues which has kept me using pure java.
-
-Regarding JNI: I do not know enough C/C++ to properly optimize and there
-would still be a cost for interfacing natives with java (meaning that for
-smaller ETF data there won't be much of a benefit).
-
-Regarding `Unsafe`: Unsafe is *unsafe*. Meaning it is not guaranteed to
-work across java versions and jvm implementations. So despite its
-significant performance benefits, it's impossible to be sure that it will
-always work how I expect it to.
-
-## Caveats
-Due to java not implementing proper primitive unsigned data types, I have
-been forced to use much larger data types than should be necessary to
-prevent potential underflows (ex. `long` instead of `int` or `BigInteger`
-instead of `long`). This causes some strange internal data type handling
-and some slight overhead.
-
## Helpful Resources
* [ETF Documentation](http://erlang.org/doc/apps/erts/erl_ext_dist.html)
* [BERT Documentation](http://bert-rpc.org/)
-* [Loqui](https://github.com/hammerandchisel/loqui)
\ No newline at end of file
+* [Loqui](https://github.com/discord/loqui)
+* [erlpack](https://github.com/discord/erlpack)
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index f98d76b..1d3bd71 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,5 +1,5 @@
group 'com.austinv11.etf'
-version '1.0.0-SNAPSHOT'
+version '2.0.0-SNAPSHOT'
buildscript {
repositories {
@@ -8,13 +8,9 @@ buildscript {
url "https://plugins.gradle.org/m2/"
}
}
- dependencies {
- classpath "me.champeau.gradle:jmh-gradle-plugin:0.3.1"
- }
}
apply plugin: 'java'
-apply plugin: "me.champeau.gradle.jmh"
sourceCompatibility = 1.8
@@ -23,11 +19,7 @@ repositories {
}
dependencies {
- testCompile group: 'junit', name: 'junit', version: '4.11'
- testCompile 'org.json:json:20160810'
- testCompile 'com.google.code.gson:gson:2.8.0'
+ compile 'io.netty:netty-buffer:4.1.50.Final'
- jmh 'org.json:json:20160810'
- jmh 'com.google.code.gson:gson:2.8.0'
- jmh 'com.fasterxml.jackson.module:jackson-module-afterburner:2.8.7'
+ testCompile group: 'junit', name: 'junit', version: '4.11'
}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 2c5777a..11f5fc0 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Sun Mar 12 23:31:05 EDT 2017
+#Fri Jun 05 18:18:49 EDT 2020
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
diff --git a/settings.gradle b/settings.gradle
index ae3223a..f455a07 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,4 +1 @@
-rootProject.name = 'ETF-Java'
-include 'gui'
-include 'gui'
-
+rootProject.name = 'ETF-Java'
\ No newline at end of file
diff --git a/src/jmh/java/com/austinv11/etf/benchmarks/Benchmarks.java b/src/jmh/java/com/austinv11/etf/benchmarks/Benchmarks.java
deleted file mode 100644
index 3e80733..0000000
--- a/src/jmh/java/com/austinv11/etf/benchmarks/Benchmarks.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package com.austinv11.etf.benchmarks;
-
-import com.austinv11.etf.ETFConfig;
-import org.openjdk.jmh.annotations.*;
-
-import java.util.concurrent.TimeUnit;
-
-public class Benchmarks {
-
- @State(Scope.Benchmark)
- public static class Context {
-
- ETFConfig config;
-
- @Setup(Level.Trial)
- public void init() {
- config = new ETFConfig().setBert(false).setCompression(false)
- .setIncludeDistributionHeader(false).setIncludeHeader(false).setLoqui(true).setVersion(131);
- }
-
- @TearDown(Level.Trial)
- public void clean() {
- config = null;
- }
- }
-
- public static class TestClass {
-
- }
-
- @Benchmark
- @BenchmarkMode(Mode.All)
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- public void etf(Context context) {
- //TODO
- }
-}
diff --git a/src/main/java/com/austinv11/etf/ETFConfig.java b/src/main/java/com/austinv11/etf/ETFConfig.java
deleted file mode 100644
index 49a2f70..0000000
--- a/src/main/java/com/austinv11/etf/ETFConfig.java
+++ /dev/null
@@ -1,197 +0,0 @@
-package com.austinv11.etf;
-
-import com.austinv11.etf.parsing.ETFParser;
-import com.austinv11.etf.util.ETFConstants;
-import com.austinv11.etf.util.Mapper;
-import com.austinv11.etf.writing.ETFWriter;
-
-/**
- * This provides a clean way to configure etf handlers.
- */
-public class ETFConfig {
-
- private boolean bert = false;
- private int version = ETFConstants.VERSION;
- private boolean includeHeader = true;
- private boolean includeDistributionHeader = false;
- private boolean loqui = false;
- private boolean compress = false;
-
- /**
- * This returns whether this supports BERT.
- *
- * @return True if BERT is configured, false if otherwise.
- */
- public boolean isBert() {
- return bert;
- }
-
- /**
- * This sets whether this handles BERT.
- *
- * @param bert Whether to support BERT.
- * @return The current config instance (for chaining).
- *
- * @see com.austinv11.etf.util.BertCompatible
- * @see com.austinv11.etf.util.BertType
- */
- public ETFConfig setBert(boolean bert) {
- this.bert = bert;
- return this;
- }
-
- /**
- * This gets the version of ETF this is handling.
- *
- * @return The version.
- */
- public int getVersion() {
- return version;
- }
-
- /**
- * This sets the specific version of etf this is handling.
- * WARNING: No matter what version you set, the internal handlers will only respect {@link ETFConstants#VERSION}'s
- * spec.
- *
- * @param version The version integer.
- * @return The current config instance (for chaining).
- */
- public ETFConfig setVersion(int version) {
- this.version = version;
- return this;
- }
-
- /**
- * This returns whether headers are included.
- * NOTE This is NOT a distribution header setting.
- *
- * @return True if including a header.
- */
- public boolean isIncludingHeader() {
- return includeHeader;
- }
-
- /**
- * Sets whether this will include a header.
- * NOTE This is NOT a distribution header setting.
- *
- * @param includeHeader Set to true to include a header.
- * @return The current config instance (for chaining).
- */
- public ETFConfig setIncludeHeader(boolean includeHeader) {
- this.includeHeader = includeHeader;
- return this;
- }
-
- /**
- * This returns whether this config supports the Loqui
- * (Discord's) transport protocol.
- *
- * @return True if loqui handling is enabled, false if otherwise.
- */
- public boolean isLoqui() {
- return loqui;
- }
-
- /**
- * Sets whether this will have special handling for Loqui,
- * Discord's transport protocol.
- *
- * @param loqui Set to true to handle loqui types, false to ignore them.
- * @return The current config instance (for chaining).
- */
- public ETFConfig setLoqui(boolean loqui) {
- this.loqui = loqui;
- return this;
- }
-
- /**
- * This returns whether distribution headers are written.
- *
- * @return True when enabled, false when otherwise.
- */
- public boolean isIncludingDistributionHeader() {
- return includeDistributionHeader; //TODO
- }
-
- /**
- * This sets whether distribution headers are written.
- *
- * @param includeDistributionHeader Set to true to write distribution headers, false to not.
- * @return The current config instance (for chaining).
- */
- public ETFConfig setIncludeDistributionHeader(boolean includeDistributionHeader) {
- this.includeDistributionHeader = includeDistributionHeader;
- return this;
- }
-
- /**
- * This gets whether ETF should be compressed when written.
- *
- * @return True when etf is compressed when written.
- */
- public boolean isCompressing() {
- return compress;
- }
-
- /**
- * This sets whether ETF should be compressed when written.
- *
- * @param compress Set to true to compress, false to not.
- * @return The current config instance (for chaining).
- */
- public ETFConfig setCompression(boolean compress) {
- this.compress = compress;
- return this;
- }
-
- /**
- * This creates a new parser using the set configuration.
- *
- * @param data The data to parse.
- * @return The new parser instance.
- */
- public ETFParser createParser(byte[] data) {
- return createParser(data, false);
- }
-
- /**
- * This creates a new parser using the set configuration.
- *
- * @param data The data to parse.
- * @param partial Whether the data should be treated as partial (meaning no headers).
- * @return The new parser instance.
- */
- public ETFParser createParser(byte[] data, boolean partial) {
- return new ETFParser(data, this, partial);
- }
-
- /**
- * This creates a new writer using the set configuration.
- *
- * @return The new writer instance.
- */
- public ETFWriter createWriter() {
- return createWriter(false);
- }
-
- /**
- * This creates a new writer using the set configuration.
- *
- * @param partial Whether the data should be treated as partial (meaning no headers).
- * @return The new writer instance.
- */
- public ETFWriter createWriter(boolean partial) {
- return new ETFWriter(this, partial);
- }
-
- /**
- * This creates a new mapper using this configuration.
- *
- * @return The new mapper instance.
- */
- public Mapper createMapper() {
- return new Mapper(this);
- }
-}
diff --git a/src/main/java/com/austinv11/etf/EtfBuffer.java b/src/main/java/com/austinv11/etf/EtfBuffer.java
new file mode 100644
index 0000000..07044e2
--- /dev/null
+++ b/src/main/java/com/austinv11/etf/EtfBuffer.java
@@ -0,0 +1,509 @@
+package com.austinv11.etf;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+import java.io.Closeable;
+import java.nio.charset.StandardCharsets;
+
+import static com.austinv11.etf.Terms.*;
+
+/**
+ * A class which manages a buffer of ETF data.
+ */
+public class EtfBuffer implements Closeable {
+
+ private final ByteBuf buffer;
+
+ private EtfBuffer(ByteBuf buffer) {
+ buffer.retain();
+ this.buffer = buffer;
+ }
+
+ /**
+ * Builds a heap-bound ETF buffer.
+ * @return The buffer.
+ */
+ public static EtfBuffer onHeapBuffer() {
+ return new EtfBuffer(Unpooled.buffer());
+ }
+
+ /**
+ * Builds a heap-less ETF buffer.
+ * @return The buffer.
+ */
+ public static EtfBuffer directBuffer() {
+ return new EtfBuffer(Unpooled.directBuffer());
+ }
+
+ /**
+ * Wraps a byte buffer for reading only.
+ * @param bb The byte buffer to wrap.
+ * @return The buffer.
+ */
+ public static EtfBuffer wrapForRead(ByteBuf bb) {
+ return new EtfBuffer(bb);
+ }
+
+ /**
+ * Returns a read-only view over the internal buffer.
+ * @return The buffer.
+ */
+ public ByteBuf write() {
+ return buffer.asReadOnly();
+ }
+
+ /**
+ * Starts writing to the buffer. This isn't explicitly required, but ETF parsers may not understand the output
+ * unless you explicitly start writing (only once!).
+ * @return The buffer for chaining.
+ */
+ public EtfBuffer startWrite() {
+ buffer.writeByte(EtfConstants.VERSION);
+ return this;
+ }
+
+ /**
+ * Starts reading from the buffer.
+ * @return The buffer for reading.
+ */
+ public EtfBuffer startRead() {
+ assert buffer.readByte() == EtfConstants.VERSION;
+ return this;
+ }
+
+ /**
+ * Release the resources being used.
+ *
+ * @return The buffer for chaining.
+ */
+ public EtfBuffer endWrite() {
+ close();
+ return this;
+ }
+
+ /**
+ * Release the resources being used.
+ *
+ * @return The buffer for chaining.
+ */
+ public EtfBuffer endRead() {
+ close();
+ return this;
+ }
+
+ @Override
+ public void close() {
+ buffer.release();
+ }
+
+ private EtfBuffer write(byte... bytes) {
+ for (byte b : bytes) {
+ buffer.writeByte(b);
+ }
+ return this;
+ }
+
+ private EtfBuffer write(int... bytes) {
+ for (int b : bytes) {
+ buffer.writeByte(b);
+ }
+ return this;
+ }
+
+ private void requireType(byte type) {
+ if (peek() != type)
+ throw new EtfException("Unexpected type " + type);
+ buffer.readByte();
+ }
+
+ private byte requireEitherType(byte... types) {
+ byte peeked = peek();
+ for (byte t : types) {
+ if (t == peeked) {
+ return buffer.readByte();
+ }
+ }
+ throw new EtfException("Unexpected type " + peeked);
+ }
+
+ /**
+ * Peeks at the next byte.
+ * @return The next byte.
+ */
+ public byte peek() {
+ int i = buffer.readerIndex();
+ return buffer.getByte(i);
+ }
+
+ /**
+ * Writes a small integer (unsigned byte).
+ * @return The buffer for chaining.
+ */
+ public EtfBuffer writeSmallInt(byte b) {
+ return write(SMALL_INTEGER_EXT, b);
+ }
+
+ /**
+ * Reads a small integer (unsigned byte).
+ * @return The integer.
+ */
+ public byte readSmallInt() {
+ requireType(SMALL_INTEGER_EXT);
+ return buffer.readByte();
+ }
+
+ /**
+ * Writes a signed integer.
+ * @param i The integer.
+ * @return The buffer for chaining.
+ */
+ public EtfBuffer writeInt(int i) {
+ return write(INTEGER_EXT, ((i >>> 24) & 0xff),
+ ((i >>> 16) & 0xff),
+ ((i >>> 8) & 0xff),
+ (i & 0xff));
+ }
+
+ /**
+ * Reads a signed integer.
+ * @return The integer.
+ */
+ public int readInt() {
+ requireType(INTEGER_EXT);
+ return (buffer.readByte() << 24)
+ | (buffer.readByte() << 16)
+ | (buffer.readByte() << 8)
+ | buffer.readByte();
+ }
+
+ /**
+ * Indicates that a tuple will be written.
+ * @param count The number of tuple entries (as an unsigned integer). This indicates how many write operations
+ * will be counted as part of the tuple.
+ * @return The buffer for chaining.
+ */
+ public EtfBuffer startWriteTuple(int count) {
+ return startWriteTuple(count, count < 256);
+ }
+
+ /**
+ * Starts reading a tuple.
+ * @return The number of elements in the tuple.
+ */
+ public int startReadTuple() {
+ byte type = requireEitherType(SMALL_TUPLE_EXT, LARGE_TUPLE_EXT);
+ if (type == SMALL_TUPLE_EXT) {
+ return buffer.readUnsignedByte();
+ } else {
+ return (buffer.readByte() << 24)
+ | (buffer.readByte() << 16)
+ | (buffer.readByte() << 8)
+ | buffer.readByte();
+ }
+ }
+
+ /**
+ * Indicates that a tuple will be written.
+ * @param count The number of tuple entries (as an unsigned integer). This indicates how many write operations
+ * will be counted as part of the tuple.
+ * @param small Whether this should be explicitly set as a small tuple or large tuple.
+ * @return The buffer for chaining.
+ */
+ public EtfBuffer startWriteTuple(int count, boolean small) {
+ if (small) {
+ return write(SMALL_TUPLE_EXT, count);
+ } else {
+ return write(LARGE_TUPLE_EXT, ((count >>> 24) & 0xff),
+ ((count >>> 16) & 0xff),
+ ((count >>> 8) & 0xff),
+ (count & 0xff));
+ }
+ }
+
+ /**
+ * Indicates that a map will be written.
+ * @param count The number of K-V pairs (as an unsigned integer). This indicates the number of write opertions times
+ * 2 that will be counted as part of the map. The following elements are written as K1, V1, ..., Kn, Vn where n is
+ * the count.
+ * @return The buffer for chaining.
+ */
+ public EtfBuffer startWriteMap(int count) {
+ return write(MAP_EXT, ((count >>> 24) & 0xff),
+ ((count >>> 16) & 0xff),
+ ((count >>> 8) & 0xff),
+ (count & 0xff));
+ }
+
+ /**
+ * Starts reading a map.
+ * @return The number of K-V pairs in the map.
+ */
+ public int startReadMap() {
+ requireType(MAP_EXT);
+ return (buffer.readByte() << 24)
+ | (buffer.readByte() << 16)
+ | (buffer.readByte() << 8)
+ | buffer.readByte();
+ }
+
+ /**
+ * Writes nil.
+ *
+ * @return The buffer for chaining.
+ */
+ public EtfBuffer writeNil() {
+ return write(NIL_EXT);
+ }
+
+ /**
+ * Reads nil.
+ */
+ public void readNil() {
+ requireType(NIL_EXT);
+ }
+
+ /**
+ * Writes a string of bytes.
+ * @param bytes The bytes.
+ * @return The buffer for chaining.
+ */
+ public EtfBuffer writeString(byte[] bytes) {
+ write(STRING_EXT,
+ ((bytes.length >>> 8) & 0xff),
+ (bytes.length & 0xff));
+ return write(bytes);
+ }
+
+ /**
+ * Reads a string of bytes.
+ * @return The bytes.
+ */
+ public byte[] readString() {
+ requireType(STRING_EXT);
+ int count = (buffer.readByte() << 8)
+ | buffer.readByte();
+ byte[] bytes = new byte[count];
+ buffer.readBytes(bytes);
+ return bytes;
+ }
+
+ /**
+ * Starts writing a list. NOTE: A list in ETF is terminated with a tail value (so the list contains count + 1
+ * elements in total), this tail is usually nil (proper list).
+ * @param count The number of elements of the list (unsigned).
+ * @return The buffer for chaining.
+ *
+ * @see #writeNil()
+ */
+ public EtfBuffer startWriteList(int count) {
+ return write(LIST_EXT, ((count >>> 24) & 0xff),
+ ((count >>> 16) & 0xff),
+ ((count >>> 8) & 0xff),
+ (count & 0xff));
+ }
+
+ /**
+ * Starts reading a list. NOTE: A list in ETF is terminated with a tail value (so the list contains count + 1
+ * elements in total), this tail is usually nil (proper list).
+ * @return The number of elements (excluding the tail).
+ */
+ public int startReadList() {
+ requireType(LIST_EXT);
+ return (buffer.readByte() << 24)
+ | (buffer.readByte() << 16)
+ | (buffer.readByte() << 8)
+ | buffer.readByte();
+ }
+
+ /**
+ * Writes a floating point number.
+ *
+ * @param f The number.
+ * @return The buffer for chaining.
+ */
+ public EtfBuffer writeFloat(double f) {
+ long longBits = Double.doubleToLongBits(f);
+ return write(NEW_FLOAT_EXT,
+ (byte) ((longBits >>> 56) & 0xff), (byte) ((longBits >>> 48) & 0xff),
+ (byte) ((longBits >>> 40) & 0xff), (byte) ((longBits >>> 32) & 0xff),
+ (byte) ((longBits >>> 24) & 0xff), (byte) ((longBits >>> 16) & 0xff),
+ (byte) ((longBits >>> 8) & 0xff), (byte) (longBits & 0xff));
+ }
+
+ /**
+ * Reads a floating point number.
+ * @return The number.
+ */
+ public double readFloat() {
+ requireType(NEW_FLOAT_EXT);
+ long bits = ((long) buffer.readByte() << 56)
+ | ((long) buffer.readByte() << 48)
+ | ((long) buffer.readByte() << 40)
+ | ((long) buffer.readByte() << 32)
+ | ((long) buffer.readByte() << 24)
+ | ((long) buffer.readByte() << 16)
+ | ((long) buffer.readByte() << 8)
+ | buffer.readByte();
+ return Double.longBitsToDouble(bits);
+ }
+
+ /**
+ * Write a utf-8 atom.
+ * @param s The string to write.
+ * @return The buffer for chaining.
+ */
+ public EtfBuffer writeAtom(String s) {
+ byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
+ return writeAtom(bytes, bytes.length < 256);
+ }
+
+ /**
+ * Read a utf-8 atom. NOTE: In loqui (Discord), an atom of "nil" is a nil value and "true"/"false" atoms are
+ * booleans.
+ * @return The atom.
+ */
+ public String readAtom() {
+ byte type = requireEitherType(SMALL_ATOM_UTF8_EXT, ATOM_UTF8_EXT);
+
+ int length;
+ if (type == SMALL_TUPLE_EXT) {
+ length = buffer.readUnsignedByte();
+ } else {
+ length = (buffer.readByte() << 8)
+ | buffer.readByte();
+ }
+
+ byte[] bytes = new byte[length];
+ buffer.readBytes(bytes);
+ return new String(bytes, StandardCharsets.UTF_8);
+ }
+
+
+ /**
+ * Writes a utf-8 atom with explicit typing.
+ * @param s The string to write.
+ * @param small True if a small atom, false for a normal atom.
+ * @return The buffer for chaining.
+ */
+ public EtfBuffer writeAtom(String s, boolean small) {
+ return writeAtom(s.getBytes(StandardCharsets.UTF_8), small);
+ }
+
+ private EtfBuffer writeAtom(byte[] s, boolean small) {
+ if (small) {
+ write(SMALL_ATOM_UTF8_EXT, (s.length & 0xff));
+ } else {
+ write(ATOM_UTF8_EXT, ((s.length >>> 8) & 0xff), (s.length & 0xff));
+ }
+
+ for (byte b : s) {
+ write(b);
+ }
+ return this;
+ }
+
+ /**
+ * Writes a signed big integer.
+ * @param l The integer.
+ * @return The buffer for chaining.
+ */
+ public EtfBuffer writeBig(long l) {
+ return writeBig(l, l >= 0);
+ }
+
+ /**
+ * Writes an unsigned big integer.
+ * @param l The integer.
+ * @return The buffer for chaining.
+ */
+ public EtfBuffer writeBigUnsigned(long l) {
+ return writeBig(l, true); // Write as positive
+ }
+
+ // Implementation based on https://github.com/discord/erlpack/blob/master/cpp/encoder.h#L94
+ private EtfBuffer writeBig(long l, boolean isPositive) {
+ // Longs only support 64bit nums so a SMALL_BIG_EXT should work
+ write(SMALL_BIG_EXT);
+ byte[] buf = new byte[2 + 8]; // max size of the buffer, 2 meta bytes + the 8 bytes for a long
+ char bytes = 0;
+
+ while (l != 0) {
+ buf[2 + bytes] = (byte) (l & 0xFF);
+ l >>= 8;
+ bytes++;
+ }
+
+ buf[0] = (byte) bytes;
+ buf[1] = (byte) (isPositive ? 1 : 0);
+
+ buffer.writeBytes(buf, 0, 2 + bytes);
+
+ return this;
+ }
+
+ /**
+ * Reads a big integer.
+ * @return The integer.
+ */
+ public long readBig() {
+ requireType(SMALL_BIG_EXT);
+
+ short n = buffer.readUnsignedByte();
+ byte sign = buffer.readByte();
+
+ long big = 0;
+ for (short i = 0; i < n; i++) {
+ big |= buffer.readByte() << 8 * i;
+ }
+
+ if (big >= 0 && sign == 0) {
+ big *= -1L;
+ }
+
+ return big;
+ }
+
+ // Loqui (discord) specific differences
+
+ /**
+ * Writes a boolean as a small atom. (Loqui specific).
+ * @param bool The boolean.
+ * @return The buffer for chaining.
+ */
+ public EtfBuffer writeBoolean(boolean bool) {
+ return writeAtom(bool ? "true" : "false");
+ }
+
+ /**
+ * Reads a boolean from an atom. (Loqui specific).
+ * @return The boolean.
+ */
+ public boolean readBoolean() {
+ String atom = readAtom();
+ if ("true".equals(atom)) {
+ return true;
+ } else if ("false".equals(atom)) {
+ return false;
+ } else {
+ throw new EtfException("Atom is not a boolean!");
+ }
+ }
+
+ /**
+ * Writes "nil" as an atom. (Loqui specific encoding for nil).
+ * @return The buffer for chaining.
+ */
+ public EtfBuffer writeLoquiNil() {
+ return writeAtom("nil");
+ }
+
+ /**
+ * Reads "nil" as an atom. (Loqui specific).
+ */
+ public void readLoquiNil() {
+ if (!"nil".equals(readAtom())) {
+ throw new EtfException("Atom is not nil!");
+ }
+ }
+}
diff --git a/src/main/java/com/austinv11/etf/EtfConstants.java b/src/main/java/com/austinv11/etf/EtfConstants.java
new file mode 100644
index 0000000..2ad264b
--- /dev/null
+++ b/src/main/java/com/austinv11/etf/EtfConstants.java
@@ -0,0 +1,5 @@
+package com.austinv11.etf;
+
+public class EtfConstants {
+ public final static byte VERSION = (byte) 131; //The spec version this supports (131)
+}
diff --git a/src/main/java/com/austinv11/etf/EtfException.java b/src/main/java/com/austinv11/etf/EtfException.java
new file mode 100644
index 0000000..d655f6b
--- /dev/null
+++ b/src/main/java/com/austinv11/etf/EtfException.java
@@ -0,0 +1,12 @@
+package com.austinv11.etf;
+
+public class EtfException extends RuntimeException {
+
+ public EtfException() {
+
+ }
+
+ public EtfException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/austinv11/etf/common/TermTypes.java b/src/main/java/com/austinv11/etf/Terms.java
similarity index 76%
rename from src/main/java/com/austinv11/etf/common/TermTypes.java
rename to src/main/java/com/austinv11/etf/Terms.java
index cef4058..460c5c2 100644
--- a/src/main/java/com/austinv11/etf/common/TermTypes.java
+++ b/src/main/java/com/austinv11/etf/Terms.java
@@ -1,45 +1,29 @@
-package com.austinv11.etf.common;
+package com.austinv11.etf;
-import com.austinv11.etf.util.BertCompatible;
-
-/**
- * This represents an erlang term.
- */
-public class TermTypes {
-
- @BertCompatible
+public final class Terms {
public static final byte HEADER = 80;
public static final byte DISTRIBUTION_HEADER = 68;
public static final byte ATOM_CACHE_REF = 82;
- @BertCompatible
public static final byte SMALL_INTEGER_EXT = 97;
- @BertCompatible
public static final byte INTEGER_EXT = 98;
- @BertCompatible
+ @Deprecated // deprecated by ETF so we are not using it
public static final byte FLOAT_EXT = 99;
- @BertCompatible
+ @Deprecated // deprecated for ATOM_UTF8
public static final byte ATOM_EXT = 100;
public static final byte REFERENCE_EXT = 101;
public static final byte PORT_EXT = 102;
public static final byte PID_EXT = 103;
- @BertCompatible
public static final byte SMALL_TUPLE_EXT = 104;
- @BertCompatible
public static final byte LARGE_TUPLE_EXT = 105;
public static final byte MAP_EXT = 116;
- @BertCompatible
public static final byte NIL_EXT = 106;
- @BertCompatible
public static final byte STRING_EXT = 107;
- @BertCompatible
public static final byte LIST_EXT = 108;
- @BertCompatible
public static final byte BINARY_EXT = 109;
- @BertCompatible
public static final byte SMALL_BIG_EXT = 110;
- @BertCompatible
public static final byte LARGE_BIG_EXT = 111;
public static final byte NEW_REFERENCE_EXT = 114;
+ @Deprecated // deprecated for SMALL_ATOM_UTF8
public static final byte SMALL_ATOM_EXT = 115;
public static final byte FUN_EXT = 117;
public static final byte NEW_FUN_EXT = 112;
diff --git a/src/main/java/com/austinv11/etf/erlang/DistributionHeader.java b/src/main/java/com/austinv11/etf/erlang/DistributionHeader.java
deleted file mode 100644
index fea9330..0000000
--- a/src/main/java/com/austinv11/etf/erlang/DistributionHeader.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.austinv11.etf.erlang;
-
-import com.austinv11.etf.common.TermTypes;
-
-//TODO implement
-public class DistributionHeader implements ErlangObject {
-
- private final byte[] data;
-
- public DistributionHeader(byte[] data) {
- this.data = data;
- }
-
- @Override
- public byte type() {
- return TermTypes.DISTRIBUTION_HEADER;
- }
-}
diff --git a/src/main/java/com/austinv11/etf/erlang/ErlangList.java b/src/main/java/com/austinv11/etf/erlang/ErlangList.java
deleted file mode 100644
index 59528ec..0000000
--- a/src/main/java/com/austinv11/etf/erlang/ErlangList.java
+++ /dev/null
@@ -1,248 +0,0 @@
-package com.austinv11.etf.erlang;
-
-import com.austinv11.etf.common.TermTypes;
-import com.austinv11.etf.util.BertCompatible;
-
-import java.math.BigInteger;
-import java.util.AbstractList;
-
-/**
- * This represents an immutable ETF list.
- */
-@BertCompatible
-public class ErlangList extends AbstractList