Skip to content

Commit

Permalink
Use bytes to represent memo text (#259)
Browse files Browse the repository at this point in the history
The Java SDK assumes that all memo texts must be valid UTF 8 strings. However, that assumption is not valid. It turns out that any sequence of bytes is allowed to appear in the memo field (see stellar/go#2022 ).

Therefore, the correct representation for the memo field is a byte array.
  • Loading branch information
tamirms authored Dec 13, 2019
1 parent 335b91d commit 09d0c0d
Show file tree
Hide file tree
Showing 17 changed files with 174 additions and 71 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

As this project is pre 1.0, breaking changes may happen for minor version bumps. A breaking change will get clearly notified in this log.

## 0.12.0

* Represent memo text contents as bytes because a memo text may not be valid UTF-8 string (https://github.com/stellar/java-stellar-sdk/issues/257).
* Validate name length when constructing org.stellar.sdk.ManageDataOperation instances.
* Validate home domain length when constructing org.stellar.sdk.SetOptionsOperation instances.

## 0.11.0

* Fix bug in `org.stellar.sdk.requests.OperationsRequestBuilder.operation(long operationId)`. The method submitted an HTTP request to Horizon with the following path, /operation/<id> , but the correct path is /operations/<id>
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ apply plugin: 'com.github.ben-manes.versions' // gradle dependencyUpdates -Drevi
apply plugin: 'project-report' // gradle htmlDependencyReport

sourceCompatibility = 1.6
version = '0.11.0'
version = '0.12.0'
group = 'stellar'

jar {
Expand Down
13 changes: 7 additions & 6 deletions src/main/java/org/stellar/sdk/ManageDataOperation.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package org.stellar.sdk;

import com.google.common.base.Objects;
import org.stellar.sdk.xdr.DataValue;
import org.stellar.sdk.xdr.ManageDataOp;
import org.stellar.sdk.xdr.OperationType;
import org.stellar.sdk.xdr.String64;
import org.stellar.sdk.xdr.*;

import java.util.Arrays;

Expand All @@ -21,6 +18,10 @@ public class ManageDataOperation extends Operation {
private ManageDataOperation(String name, byte[] value) {
this.name = checkNotNull(name, "name cannot be null");
this.value = value;

if (new XdrString(this.name).getBytes().length > 64) {
throw new IllegalArgumentException("name cannot exceed 64 bytes");
}
}

/**
Expand All @@ -41,7 +42,7 @@ public byte[] getValue() {
org.stellar.sdk.xdr.Operation.OperationBody toOperationBody() {
ManageDataOp op = new ManageDataOp();
String64 name = new String64();
name.setString64(this.name);
name.setString64(new XdrString(this.name));
op.setDataName(name);

if (value != null) {
Expand All @@ -68,7 +69,7 @@ public static class Builder {
* @param op {@link ManageDataOp}
*/
Builder(ManageDataOp op) {
name = op.getDataName().getString64();
name = op.getDataName().getString64().toString();
if (op.getDataValue() != null) {
value = op.getDataValue().getDataValue();
} else {
Expand Down
11 changes: 10 additions & 1 deletion src/main/java/org/stellar/sdk/Memo.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ public static MemoText text(String text) {
return new MemoText(text);
}

/**
* Creates new {@link MemoText} instance.
* @param text
*/
public static MemoText text(byte[] text) {
return new MemoText(text);
}


/**
* Creates new {@link MemoId} instance.
* @param id
Expand Down Expand Up @@ -78,7 +87,7 @@ public static Memo fromXdr(org.stellar.sdk.xdr.Memo memo) {
case MEMO_ID:
return id(memo.getId().getUint64().longValue());
case MEMO_TEXT:
return text(memo.getText());
return text(memo.getText().getBytes());
case MEMO_HASH:
return hash(memo.getHash().getHash());
case MEMO_RETURN:
Expand Down
31 changes: 20 additions & 11 deletions src/main/java/org/stellar/sdk/MemoText.java
Original file line number Diff line number Diff line change
@@ -1,54 +1,63 @@
package org.stellar.sdk;

import com.google.common.base.Objects;
import org.stellar.sdk.xdr.MemoType;

import java.nio.charset.Charset;
import org.stellar.sdk.xdr.XdrString;

import static com.google.common.base.Preconditions.checkNotNull;

/**
* Represents MEMO_TEXT.
*/
public class MemoText extends Memo {
private String text;
private XdrString text;

public MemoText(String text) {
this.text = checkNotNull(text, "text cannot be null");
this(new XdrString(checkNotNull(text, "text cannot be null")));
}

public MemoText(byte[] text) {
this(new XdrString(checkNotNull(text, "text cannot be null")));
}

int length = text.getBytes((Charset.forName("UTF-8"))).length;
public MemoText(XdrString text) {
this.text = checkNotNull(text, "text cannot be null");
int length = this.text.getBytes().length;
if (length > 28) {
throw new MemoTooLongException("text must be <= 28 bytes. length=" + String.valueOf(length));
}
}

public String getText() {
return text;
return this.text.toString();
}

public byte[] getBytes() {
return this.text.getBytes();
}

@Override
org.stellar.sdk.xdr.Memo toXdr() {
org.stellar.sdk.xdr.Memo memo = new org.stellar.sdk.xdr.Memo();
memo.setDiscriminant(MemoType.MEMO_TEXT);
memo.setText(text);
memo.setText(this.text);
return memo;
}

@Override
public int hashCode() {
return Objects.hashCode(this.text);
return this.text.hashCode();
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MemoText memoText = (MemoText) o;
return Objects.equal(this.text, memoText.text);
return this.text.equals(memoText.text);
}

@Override
public String toString() {
return text == null ? "" : text;
return text == null ? "" : this.getText();
}
}
9 changes: 7 additions & 2 deletions src/main/java/org/stellar/sdk/SetOptionsOperation.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ private SetOptionsOperation(String inflationDestination, Integer clearFlags, Int
this.homeDomain = homeDomain;
this.signer = signer;
this.signerWeight = signerWeight;

if (this.homeDomain != null && new XdrString(this.homeDomain).getBytes().length > 32) {
throw new IllegalArgumentException("home domain cannot exceed 32 bytes");
}

}

/**
Expand Down Expand Up @@ -148,7 +153,7 @@ org.stellar.sdk.xdr.Operation.OperationBody toOperationBody() {
}
if (homeDomain != null) {
String32 homeDomain = new String32();
homeDomain.setString32(this.homeDomain);
homeDomain.setString32(new XdrString(this.homeDomain));
op.setHomeDomain(homeDomain);
}
if (signer != null) {
Expand Down Expand Up @@ -206,7 +211,7 @@ public static class Builder {
highThreshold = op.getHighThreshold().getUint32().intValue();
}
if (op.getHomeDomain() != null) {
homeDomain = op.getHomeDomain().getString32();
homeDomain = op.getHomeDomain().getString32().toString();
}
if (op.getSigner() != null) {
signer = op.getSigner().getKey();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
import com.google.gson.JsonParseException;

import org.stellar.sdk.Memo;
import org.stellar.sdk.xdr.TransactionEnvelope;
import org.stellar.sdk.xdr.XdrDataInputStream;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.reflect.Type;

public class TransactionDeserializer implements JsonDeserializer<TransactionResponse> {
Expand All @@ -31,12 +35,19 @@ public TransactionResponse deserialize(JsonElement json, Type typeOfT, JsonDeser
// representation of a transaction. That's why we need to handle a special case
// here.
if (memoType.equals("text")) {
JsonElement memoField = json.getAsJsonObject().get("memo");
if (memoField != null) {
memo = Memo.text(memoField.getAsString());
} else {
memo = Memo.text("");
// we obtain the memo text from the xdr because the bytes may not be valid utf8
String envelopeXdr = json.getAsJsonObject().get("envelope_xdr").getAsString();
BaseEncoding base64Encoding = BaseEncoding.base64();
byte[] bytes = base64Encoding.decode(envelopeXdr);
TransactionEnvelope transactionEnvelope = null;
try {
transactionEnvelope = TransactionEnvelope.decode(new XdrDataInputStream(new ByteArrayInputStream(bytes)));
} catch (IOException e) {
// JsonDeserializer<TransactionResponse> cannot throw IOExceptions
// so we must throw it as a runtime exception
throw new RuntimeException(e);
}
memo = Memo.text(transactionEnvelope.getTx().getMemo().getText().getBytes());
} else {
String memoValue = json.getAsJsonObject().get("memo").getAsString();
BaseEncoding base64Encoding = BaseEncoding.base64();
Expand Down
10 changes: 5 additions & 5 deletions src/main/java/org/stellar/sdk/xdr/Error.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,24 @@ public ErrorCode getCode() {
public void setCode(ErrorCode value) {
this.code = value;
}
private String msg;
public String getMsg() {
private XdrString msg;
public XdrString getMsg() {
return this.msg;
}
public void setMsg(String value) {
public void setMsg(XdrString value) {
this.msg = value;
}
public static void encode(XdrDataOutputStream stream, Error encodedError) throws IOException{
ErrorCode.encode(stream, encodedError.code);
stream.writeString(encodedError.msg);
encodedError.msg.encode(stream);
}
public void encode(XdrDataOutputStream stream) throws IOException {
encode(stream, this);
}
public static Error decode(XdrDataInputStream stream) throws IOException {
Error decodedError = new Error();
decodedError.code = ErrorCode.decode(stream);
decodedError.msg = stream.readString();
decodedError.msg = XdrString.decode(stream, 100);
return decodedError;
}
@Override
Expand Down
10 changes: 5 additions & 5 deletions src/main/java/org/stellar/sdk/xdr/Hello.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ public Hash getNetworkID() {
public void setNetworkID(Hash value) {
this.networkID = value;
}
private String versionStr;
public String getVersionStr() {
private XdrString versionStr;
public XdrString getVersionStr() {
return this.versionStr;
}
public void setVersionStr(String value) {
public void setVersionStr(XdrString value) {
this.versionStr = value;
}
private Integer listeningPort;
Expand Down Expand Up @@ -94,7 +94,7 @@ public static void encode(XdrDataOutputStream stream, Hello encodedHello) throws
Uint32.encode(stream, encodedHello.overlayVersion);
Uint32.encode(stream, encodedHello.overlayMinVersion);
Hash.encode(stream, encodedHello.networkID);
stream.writeString(encodedHello.versionStr);
encodedHello.versionStr.encode(stream);
stream.writeInt(encodedHello.listeningPort);
NodeID.encode(stream, encodedHello.peerID);
AuthCert.encode(stream, encodedHello.cert);
Expand All @@ -109,7 +109,7 @@ public static Hello decode(XdrDataInputStream stream) throws IOException {
decodedHello.overlayVersion = Uint32.decode(stream);
decodedHello.overlayMinVersion = Uint32.decode(stream);
decodedHello.networkID = Hash.decode(stream);
decodedHello.versionStr = stream.readString();
decodedHello.versionStr = XdrString.decode(stream, 100);
decodedHello.listeningPort = stream.readInt();
decodedHello.peerID = NodeID.decode(stream);
decodedHello.cert = AuthCert.decode(stream);
Expand Down
10 changes: 5 additions & 5 deletions src/main/java/org/stellar/sdk/xdr/Memo.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ public MemoType getDiscriminant() {
public void setDiscriminant(MemoType value) {
this.type = value;
}
private String text;
public String getText() {
private XdrString text;
public XdrString getText() {
return this.text;
}
public void setText(String value) {
public void setText(XdrString value) {
this.text = value;
}
private Uint64 id;
Expand Down Expand Up @@ -70,7 +70,7 @@ public static void encode(XdrDataOutputStream stream, Memo encodedMemo) throws I
case MEMO_NONE:
break;
case MEMO_TEXT:
stream.writeString(encodedMemo.text);
encodedMemo.text.encode(stream);
break;
case MEMO_ID:
Uint64.encode(stream, encodedMemo.id);
Expand All @@ -94,7 +94,7 @@ public static Memo decode(XdrDataInputStream stream) throws IOException {
case MEMO_NONE:
break;
case MEMO_TEXT:
decodedMemo.text = stream.readString();
decodedMemo.text = XdrString.decode(stream, 28);
break;
case MEMO_ID:
decodedMemo.id = Uint64.decode(stream);
Expand Down
10 changes: 5 additions & 5 deletions src/main/java/org/stellar/sdk/xdr/String32.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,22 @@

// ===========================================================================
public class String32 implements XdrElement {
private String string32;
public String getString32() {
private XdrString string32;
public XdrString getString32() {
return this.string32;
}
public void setString32(String value) {
public void setString32(XdrString value) {
this.string32 = value;
}
public static void encode(XdrDataOutputStream stream, String32 encodedString32) throws IOException {
stream.writeString(encodedString32.string32);
encodedString32.string32.encode(stream);
}
public void encode(XdrDataOutputStream stream) throws IOException {
encode(stream, this);
}
public static String32 decode(XdrDataInputStream stream) throws IOException {
String32 decodedString32 = new String32();
decodedString32.string32 = stream.readString();
decodedString32.string32 = XdrString.decode(stream, 32);
return decodedString32;
}
@Override
Expand Down
10 changes: 5 additions & 5 deletions src/main/java/org/stellar/sdk/xdr/String64.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,22 @@

// ===========================================================================
public class String64 implements XdrElement {
private String string64;
public String getString64() {
private XdrString string64;
public XdrString getString64() {
return this.string64;
}
public void setString64(String value) {
public void setString64(XdrString value) {
this.string64 = value;
}
public static void encode(XdrDataOutputStream stream, String64 encodedString64) throws IOException {
stream.writeString(encodedString64.string64);
encodedString64.string64.encode(stream);
}
public void encode(XdrDataOutputStream stream) throws IOException {
encode(stream, this);
}
public static String64 decode(XdrDataInputStream stream) throws IOException {
String64 decodedString64 = new String64();
decodedString64.string64 = stream.readString();
decodedString64.string64 = XdrString.decode(stream, 64);
return decodedString64;
}
@Override
Expand Down
7 changes: 0 additions & 7 deletions src/main/java/org/stellar/sdk/xdr/XdrDataInputStream.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,6 @@ public XdrDataInputStream(InputStream in) {
mIn = (XdrInputStream) super.in;
}

public String readString() throws IOException {
int l = readInt();
byte[] bytes = new byte[l];
read(bytes);
return new String(bytes, Charset.forName("UTF-8"));
}

public int[] readIntArray() throws IOException {
int l = readInt();
return readIntArray(l);
Expand Down
Loading

0 comments on commit 09d0c0d

Please sign in to comment.