Extended encoding basics

works for basic subject/body messages, no attachments and no other features supported yet
This commit is contained in:
Christian Basler 2016-11-30 17:26:22 +01:00
parent 831e4bcbcc
commit 0bb455d433
6 changed files with 457 additions and 29 deletions

View File

@ -25,6 +25,7 @@ artifacts {
dependencies { dependencies {
compile 'org.slf4j:slf4j-api:1.7.12' compile 'org.slf4j:slf4j-api:1.7.12'
compile 'org.msgpack:msgpack-core:0.8.11'
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
testCompile 'org.hamcrest:hamcrest-library:1.3' testCompile 'org.hamcrest:hamcrest-library:1.3'
testCompile 'org.mockito:mockito-core:1.10.19' testCompile 'org.mockito:mockito-core:1.10.19'

View File

@ -18,17 +18,23 @@ package ch.dissem.bitmessage.entity;
import ch.dissem.bitmessage.entity.payload.Msg; import ch.dissem.bitmessage.entity.payload.Msg;
import ch.dissem.bitmessage.entity.payload.Pubkey.Feature; import ch.dissem.bitmessage.entity.payload.Pubkey.Feature;
import ch.dissem.bitmessage.entity.valueobject.Attachment;
import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.exception.ApplicationException;
import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.factory.Factory;
import ch.dissem.bitmessage.utils.*; import ch.dissem.bitmessage.utils.Decode;
import ch.dissem.bitmessage.utils.Encode;
import ch.dissem.bitmessage.utils.TTL;
import ch.dissem.bitmessage.utils.UnixTime;
import java.io.*; import java.io.*;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.*; import java.util.*;
import java.util.Collections;
import static ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED;
import static ch.dissem.bitmessage.entity.Plaintext.Encoding.SIMPLE;
import static ch.dissem.bitmessage.utils.Singleton.cryptography; import static ch.dissem.bitmessage.utils.Singleton.cryptography;
/** /**
@ -42,6 +48,7 @@ public class Plaintext implements Streamable {
private final long encoding; private final long encoding;
private final byte[] message; private final byte[] message;
private final byte[] ackData; private final byte[] ackData;
private ExtendedEncoding extendedData;
private ObjectMessage ackMessage; private ObjectMessage ackMessage;
private Object id; private Object id;
private InventoryVector inventoryVector; private InventoryVector inventoryVector;
@ -143,6 +150,10 @@ public class Plaintext implements Streamable {
return labels; return labels;
} }
public Encoding getEncoding() {
return Encoding.fromCode(encoding);
}
public long getStream() { public long getStream() {
return from.getStream(); return from.getStream();
} }
@ -299,7 +310,13 @@ public class Plaintext implements Streamable {
public String getSubject() { public String getSubject() {
Scanner s = new Scanner(new ByteArrayInputStream(message), "UTF-8"); Scanner s = new Scanner(new ByteArrayInputStream(message), "UTF-8");
String firstLine = s.nextLine(); String firstLine = s.nextLine();
if (encoding == 2) { if (encoding == EXTENDED.code) {
if (getExtendedData().getMessage() == null) {
return null;
} else {
return extendedData.getMessage().getSubject();
}
} else if (encoding == SIMPLE.code) {
return firstLine.substring("Subject:".length()).trim(); return firstLine.substring("Subject:".length()).trim();
} else if (firstLine.length() > 50) { } else if (firstLine.length() > 50) {
return firstLine.substring(0, 50).trim() + "..."; return firstLine.substring(0, 50).trim() + "...";
@ -309,9 +326,16 @@ public class Plaintext implements Streamable {
} }
public String getText() { public String getText() {
if (encoding == EXTENDED.code) {
if (getExtendedData().getMessage() == null) {
return null;
} else {
return extendedData.getMessage().getBody();
}
} else {
try { try {
String text = new String(message, "UTF-8"); String text = new String(message, "UTF-8");
if (encoding == 2) { if (encoding == SIMPLE.code) {
return text.substring(text.indexOf("\nBody:") + 6); return text.substring(text.indexOf("\nBody:") + 6);
} }
return text; return text;
@ -319,6 +343,31 @@ public class Plaintext implements Streamable {
throw new ApplicationException(e); throw new ApplicationException(e);
} }
} }
}
protected ExtendedEncoding getExtendedData() {
if (extendedData == null && encoding == EXTENDED.code) {
// TODO: make sure errors are properly handled
extendedData = ExtendedEncoding.unzip(message);
}
return extendedData;
}
public List<InventoryVector> getParents() {
if (getExtendedData() == null || extendedData.getMessage() == null) {
return Collections.emptyList();
} else {
return extendedData.getMessage().getParents();
}
}
public List<Attachment> getFiles() {
if (getExtendedData() == null || extendedData.getMessage() == null) {
return Collections.emptyList();
} else {
return extendedData.getMessage().getFiles();
}
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
@ -386,7 +435,7 @@ public class Plaintext implements Streamable {
} }
public enum Encoding { public enum Encoding {
IGNORE(0), TRIVIAL(1), SIMPLE(2); IGNORE(0), TRIVIAL(1), SIMPLE(2), EXTENDED(3);
long code; long code;
@ -397,6 +446,15 @@ public class Plaintext implements Streamable {
public long getCode() { public long getCode() {
return code; return code;
} }
public static Encoding fromCode(long code) {
for (Encoding e : values()) {
if (e.getCode() == code) {
return e;
}
}
return null;
}
} }
public enum Status { public enum Status {
@ -517,9 +575,15 @@ public class Plaintext implements Streamable {
return this; return this;
} }
public Builder message(ExtendedEncoding message) {
this.encoding = EXTENDED.getCode();
this.message = message.zip();
return this;
}
public Builder message(String subject, String message) { public Builder message(String subject, String message) {
try { try {
this.encoding = Encoding.SIMPLE.getCode(); this.encoding = SIMPLE.getCode();
this.message = ("Subject:" + subject + '\n' + "Body:" + message).getBytes("UTF-8"); this.message = ("Subject:" + subject + '\n' + "Body:" + message).getBytes("UTF-8");
} catch (UnsupportedEncodingException e) { } catch (UnsupportedEncodingException e) {
throw new ApplicationException(e); throw new ApplicationException(e);

View File

@ -0,0 +1,95 @@
package ch.dissem.bitmessage.entity.valueobject;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Objects;
/**
* A "file" attachment as used by extended encoding type messages. Could either be an attachment,
* or used inline to be used by a HTML message, for example.
*/
public class Attachment implements Serializable {
private static final long serialVersionUID = 7319139427666943189L;
private String name;
private byte[] data;
private String type;
private Disposition disposition;
public String getName() {
return name;
}
public byte[] getData() {
return data;
}
public String getType() {
return type;
}
public Disposition getDisposition() {
return disposition;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Attachment that = (Attachment) o;
return Objects.equals(name, that.name) &&
Arrays.equals(data, that.data) &&
Objects.equals(type, that.type) &&
disposition == that.disposition;
}
@Override
public int hashCode() {
return Objects.hash(name, data, type, disposition);
}
private enum Disposition {
inline, attachment
}
public static final class Builder {
private String name;
private byte[] data;
private String type;
private Disposition disposition;
public Builder name(String name) {
this.name = name;
return this;
}
public Builder data(byte[] data) {
this.data = data;
return this;
}
public Builder type(String type) {
this.type = type;
return this;
}
public Builder inline() {
this.disposition = Disposition.inline;
return this;
}
public Builder attachment() {
this.disposition = Disposition.attachment;
return this;
}
public Attachment build() {
Attachment attachment = new Attachment();
attachment.type = this.type;
attachment.disposition = this.disposition;
attachment.data = this.data;
attachment.name = this.name;
return attachment;
}
}
}

View File

@ -0,0 +1,203 @@
package ch.dissem.bitmessage.entity.valueobject;
import ch.dissem.bitmessage.exception.ApplicationException;
import org.msgpack.core.MessagePack;
import org.msgpack.core.MessagePacker;
import org.msgpack.core.MessageUnpacker;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
/**
* Extended encoding message object.
*/
public class ExtendedEncoding implements Serializable {
private static final long serialVersionUID = 3876871488247305200L;
private Message message;
public ExtendedEncoding(Message message) {
this.message = message;
}
private ExtendedEncoding() {
}
public Message getMessage() {
return message;
}
public byte[] zip() {
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
DeflaterOutputStream zipper = new DeflaterOutputStream(out)) {
MessagePacker packer = MessagePack.newDefaultPacker(zipper);
// FIXME: this should work for trivial cases
if (message != null) {
message.pack(packer);
}
packer.close();
zipper.close();
return out.toByteArray();
} catch (IOException e) {
throw new ApplicationException(e);
}
}
public static ExtendedEncoding unzip(byte[] zippedData) {
ExtendedEncoding result = new ExtendedEncoding();
try (InflaterInputStream unzipper = new InflaterInputStream(new ByteArrayInputStream(zippedData))) {
MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(unzipper);
int mapSize = unpacker.unpackMapHeader();
for (int i = 0; i < mapSize; i++) {
String key = unpacker.unpackString();
switch (key) {
case "":
switch (unpacker.unpackString()) {
case "message":
result.message = new Message();
break;
}
break;
case "subject":
result.message.subject = unpacker.unpackString();
break;
case "body":
result.message.body = unpacker.unpackString();
break;
default:
break;
}
}
} catch (IOException e) {
throw new ApplicationException(e);
}
return result;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ExtendedEncoding that = (ExtendedEncoding) o;
return Objects.equals(message, that.message);
}
@Override
public int hashCode() {
return Objects.hash(message);
}
public static class Message implements Serializable {
private static final long serialVersionUID = -2724977231484285467L;
private String subject;
private String body;
private List<InventoryVector> parents;
private List<Attachment> files;
private Message() {
parents = Collections.emptyList();
files = Collections.emptyList();
}
private Message(Builder builder) {
subject = builder.subject;
body = builder.body;
parents = Collections.unmodifiableList(builder.parents);
files = Collections.unmodifiableList(builder.files);
}
public String getSubject() {
return subject;
}
public String getBody() {
return body;
}
public List<InventoryVector> getParents() {
return parents;
}
public List<Attachment> getFiles() {
return files;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Message message = (Message) o;
return Objects.equals(subject, message.subject) &&
Objects.equals(body, message.body) &&
Objects.equals(parents, message.parents) &&
Objects.equals(files, message.files);
}
@Override
public int hashCode() {
return Objects.hash(subject, body, parents, files);
}
public void pack(MessagePacker packer) throws IOException {
packer.packMapHeader(3);
packer.packString("");
packer.packString("message");
packer.packString("subject");
packer.packString(subject);
packer.packString("body");
packer.packString(body);
}
public static class Builder {
private String subject;
private String body;
private List<InventoryVector> parents = new LinkedList<>();
private List<Attachment> files = new LinkedList<>();
private Builder() {
}
public Builder subject(String subject) {
this.subject = subject;
return this;
}
public Builder body(String body) {
this.body = body;
return this;
}
public Builder addParent(InventoryVector iv) {
parents.add(iv);
return this;
}
public Builder addFile(Attachment file) {
files.add(file);
return this;
}
public ExtendedEncoding build() {
return new ExtendedEncoding(new Message(this));
}
}
}
public static class Builder {
public Message.Builder message() {
return new Message.Builder();
}
// TODO: vote (etc.?)
}
}

File diff suppressed because one or more lines are too long

View File

@ -17,6 +17,7 @@
package ch.dissem.bitmessage.entity; package ch.dissem.bitmessage.entity;
import ch.dissem.bitmessage.entity.payload.*; import ch.dissem.bitmessage.entity.payload.*;
import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.factory.Factory;
@ -98,6 +99,31 @@ public class SerializationTest extends TestBase {
assertEquals(p1, p2); assertEquals(p1, p2);
} }
@Test
public void ensurePlaintextWithExtendedEncodingIsSerializedAndDeserializedCorrectly() throws Exception {
Plaintext p1 = new Plaintext.Builder(MSG)
.from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"))
.to(TestUtils.loadContact())
.message(new ExtendedEncoding.Builder().message()
.subject("Subject")
.body("Message")
.build())
.ackData("ackMessage".getBytes())
.signature(new byte[0])
.build();
ByteArrayOutputStream out = new ByteArrayOutputStream();
p1.write(out);
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
Plaintext p2 = Plaintext.read(MSG, in);
// Received is automatically set on deserialization, so we'll need to set it to 0
Field received = Plaintext.class.getDeclaredField("received");
received.setAccessible(true);
received.set(p2, 0L);
assertEquals(p1, p2);
}
@Test @Test
public void ensurePlaintextWithAckMessageIsSerializedAndDeserializedCorrectly() throws Exception { public void ensurePlaintextWithAckMessageIsSerializedAndDeserializedCorrectly() throws Exception {
Plaintext p1 = new Plaintext.Builder(MSG) Plaintext p1 = new Plaintext.Builder(MSG)