diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e1621fa7..afbbcdd42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/ , but the correct path is /operations/ diff --git a/build.gradle b/build.gradle index 68fe7db9d..48a73eb62 100644 --- a/build.gradle +++ b/build.gradle @@ -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 { diff --git a/src/main/java/org/stellar/sdk/ManageDataOperation.java b/src/main/java/org/stellar/sdk/ManageDataOperation.java index 38e5fb5be..d11422bd0 100644 --- a/src/main/java/org/stellar/sdk/ManageDataOperation.java +++ b/src/main/java/org/stellar/sdk/ManageDataOperation.java @@ -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; @@ -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"); + } } /** @@ -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) { @@ -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 { diff --git a/src/main/java/org/stellar/sdk/Memo.java b/src/main/java/org/stellar/sdk/Memo.java index b0a5f9f35..6fb34f083 100644 --- a/src/main/java/org/stellar/sdk/Memo.java +++ b/src/main/java/org/stellar/sdk/Memo.java @@ -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 @@ -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: diff --git a/src/main/java/org/stellar/sdk/MemoText.java b/src/main/java/org/stellar/sdk/MemoText.java index 0dcb4b2c3..e355a7fc2 100644 --- a/src/main/java/org/stellar/sdk/MemoText.java +++ b/src/main/java/org/stellar/sdk/MemoText.java @@ -1,9 +1,7 @@ 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; @@ -11,32 +9,43 @@ * 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 @@ -44,11 +53,11 @@ 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(); } } diff --git a/src/main/java/org/stellar/sdk/SetOptionsOperation.java b/src/main/java/org/stellar/sdk/SetOptionsOperation.java index ba2574305..ecbda7f81 100644 --- a/src/main/java/org/stellar/sdk/SetOptionsOperation.java +++ b/src/main/java/org/stellar/sdk/SetOptionsOperation.java @@ -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"); + } + } /** @@ -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) { @@ -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(); diff --git a/src/main/java/org/stellar/sdk/responses/TransactionDeserializer.java b/src/main/java/org/stellar/sdk/responses/TransactionDeserializer.java index 098f07e65..428ef7e93 100644 --- a/src/main/java/org/stellar/sdk/responses/TransactionDeserializer.java +++ b/src/main/java/org/stellar/sdk/responses/TransactionDeserializer.java @@ -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 { @@ -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 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(); diff --git a/src/main/java/org/stellar/sdk/xdr/Error.java b/src/main/java/org/stellar/sdk/xdr/Error.java index 7d3fc80d9..319085d20 100644 --- a/src/main/java/org/stellar/sdk/xdr/Error.java +++ b/src/main/java/org/stellar/sdk/xdr/Error.java @@ -26,16 +26,16 @@ 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); @@ -43,7 +43,7 @@ public void encode(XdrDataOutputStream stream) throws IOException { 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 diff --git a/src/main/java/org/stellar/sdk/xdr/Hello.java b/src/main/java/org/stellar/sdk/xdr/Hello.java index 4a5b59db5..c188a5f8c 100644 --- a/src/main/java/org/stellar/sdk/xdr/Hello.java +++ b/src/main/java/org/stellar/sdk/xdr/Hello.java @@ -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; @@ -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); @@ -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); diff --git a/src/main/java/org/stellar/sdk/xdr/Memo.java b/src/main/java/org/stellar/sdk/xdr/Memo.java index d7e343850..a6099df79 100644 --- a/src/main/java/org/stellar/sdk/xdr/Memo.java +++ b/src/main/java/org/stellar/sdk/xdr/Memo.java @@ -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; @@ -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); @@ -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); diff --git a/src/main/java/org/stellar/sdk/xdr/String32.java b/src/main/java/org/stellar/sdk/xdr/String32.java index 3b16cf7df..ea7115b30 100644 --- a/src/main/java/org/stellar/sdk/xdr/String32.java +++ b/src/main/java/org/stellar/sdk/xdr/String32.java @@ -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 diff --git a/src/main/java/org/stellar/sdk/xdr/String64.java b/src/main/java/org/stellar/sdk/xdr/String64.java index f5cffd245..a2a145bfc 100644 --- a/src/main/java/org/stellar/sdk/xdr/String64.java +++ b/src/main/java/org/stellar/sdk/xdr/String64.java @@ -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 diff --git a/src/main/java/org/stellar/sdk/xdr/XdrDataInputStream.java b/src/main/java/org/stellar/sdk/xdr/XdrDataInputStream.java index da75c84d9..ae317dd58 100644 --- a/src/main/java/org/stellar/sdk/xdr/XdrDataInputStream.java +++ b/src/main/java/org/stellar/sdk/xdr/XdrDataInputStream.java @@ -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); diff --git a/src/main/java/org/stellar/sdk/xdr/XdrDataOutputStream.java b/src/main/java/org/stellar/sdk/xdr/XdrDataOutputStream.java index 672a01a76..9bb857a64 100644 --- a/src/main/java/org/stellar/sdk/xdr/XdrDataOutputStream.java +++ b/src/main/java/org/stellar/sdk/xdr/XdrDataOutputStream.java @@ -14,12 +14,6 @@ public XdrDataOutputStream(OutputStream out) { mOut = (XdrOutputStream) super.out; } - public void writeString(String s) throws IOException { - byte[] chars = s.getBytes(Charset.forName("UTF-8")); - writeInt(chars.length); - write(chars); - } - public void writeIntArray(int[] a) throws IOException { writeInt(a.length); writeIntArray(a, a.length); diff --git a/src/main/java/org/stellar/sdk/xdr/XdrString.java b/src/main/java/org/stellar/sdk/xdr/XdrString.java new file mode 100644 index 000000000..d15e01e7e --- /dev/null +++ b/src/main/java/org/stellar/sdk/xdr/XdrString.java @@ -0,0 +1,58 @@ +package org.stellar.sdk.xdr; + +import java.io.IOException; +import java.io.InvalidClassException; +import java.nio.charset.Charset; +import java.util.Arrays; + +public class XdrString implements XdrElement { + private byte[] bytes; + + public XdrString(byte[] bytes) { + this.bytes = bytes; + } + + public XdrString(String text) { + this.bytes = text.getBytes(Charset.forName("UTF-8")); + } + + @Override + public void encode(XdrDataOutputStream stream) throws IOException { + stream.writeInt(this.bytes.length); + stream.write(this.bytes, 0, this.bytes.length); + } + + public static XdrString decode(XdrDataInputStream stream, int maxSize) throws IOException { + int size = stream.readInt(); + if (size > maxSize) { + throw new InvalidClassException("String length "+size+" exceeds max size "+maxSize); + } + byte[] bytes = new byte[size]; + stream.read(bytes); + return new XdrString(bytes); + } + + public byte[] getBytes() { + return this.bytes; + } + + @Override + public int hashCode() { + return Arrays.hashCode(this.bytes); + } + + @Override + public boolean equals(Object object) { + if (object == null || !(object instanceof XdrString)) { + return false; + } + + XdrString other = (XdrString) object; + return Arrays.equals(this.bytes, other.bytes); + } + + @Override + public String toString() { + return new String(bytes, Charset.forName("UTF-8")); + } +} diff --git a/src/test/java/org/stellar/sdk/xdr/XdrDataStreamTest.java b/src/test/java/org/stellar/sdk/XdrDataStreamTest.java similarity index 76% rename from src/test/java/org/stellar/sdk/xdr/XdrDataStreamTest.java rename to src/test/java/org/stellar/sdk/XdrDataStreamTest.java index 748cd9e58..e1bc10712 100644 --- a/src/test/java/org/stellar/sdk/xdr/XdrDataStreamTest.java +++ b/src/test/java/org/stellar/sdk/XdrDataStreamTest.java @@ -1,12 +1,13 @@ -package org.stellar.sdk.xdr; +package org.stellar.sdk; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.nio.charset.StandardCharsets; import static org.junit.Assert.assertEquals; import org.junit.Test; +import org.stellar.sdk.xdr.XdrDataInputStream; +import org.stellar.sdk.xdr.XdrDataOutputStream; public class XdrDataStreamTest { @@ -16,16 +17,17 @@ public static String backAndForthXdrStreaming(String inputString) throws IOExcep //String to XDR ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); XdrDataOutputStream xdrOutputStream = new XdrDataOutputStream(byteOutputStream); - xdrOutputStream.writeString(inputString); + + org.stellar.sdk.xdr.Memo xdrMemo = Memo.text(inputString).toXdr(); + xdrMemo.encode(xdrOutputStream); byte[] xdrByteOutput = byteOutputStream.toByteArray(); //XDR back to String XdrDataInputStream xdrInputStream = new XdrDataInputStream(new ByteArrayInputStream(xdrByteOutput)); - String outputString = xdrInputStream.readString(); - - return outputString; + xdrMemo = org.stellar.sdk.xdr.Memo.decode(xdrInputStream); + return xdrMemo.getText().toString(); } @Test @@ -43,7 +45,7 @@ public void backAndForthXdrStreamingWithNonStandardAscii() throws IOException { @Test public void backAndForthXdrStreamingWithAllNonStandardAscii() throws IOException { - String memo = "øûý™€♠♣♥†‡µ¢£€"; + String memo = "øûý™€♠♣♥†‡"; assertEquals(memo, backAndForthXdrStreaming(memo)); } } diff --git a/src/test/java/org/stellar/sdk/xdr/TransactionDecodeTest.java b/src/test/java/org/stellar/sdk/xdr/TransactionDecodeTest.java index 6eab65c87..442e31879 100644 --- a/src/test/java/org/stellar/sdk/xdr/TransactionDecodeTest.java +++ b/src/test/java/org/stellar/sdk/xdr/TransactionDecodeTest.java @@ -4,6 +4,7 @@ import org.junit.Test; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Arrays; @@ -59,4 +60,18 @@ public void testTransactionEnvelopeWithMemo() throws IOException { assertTrue(Arrays.equals(new byte[]{'G', 'O', 'L', 'D'}, transactionEnvelope.getTx().getOperations()[0].getBody().getPaymentOp().getAsset().getAlphaNum4().getAssetCode().getAssetCode4())); } + @Test + public void testRoundtrip() throws IOException { + String txBody = "AAAAAM6jLgjKjuXxWkir4M7v0NqoOfODXcFnn6AGlP+d4RxAAAAAZAAIiE4AAAABAAAAAAAAAAEAAAAcyKMl+WDSzuttWkF2DvzKAkkEqeSZ4cZihjGJEAAAAAEAAAAAAAAAAQAAAAAgECmBaDwiRPE1z2vAE36J+45toU/ZxdvpR38tc0HvmgAAAAAAAAAAAJiWgAAAAAAAAAABneEcQAAAAECeXDKebJoAbST1T2AbDBui9K0TbSM8sfbhXUAZ2ROAoCRs5cG1pRvY+ityyPWFEKPd7+3qEupavkAZ/+L7/28G"; + BaseEncoding base64Encoding = BaseEncoding.base64(); + byte[] bytes = base64Encoding.decode(txBody); + + TransactionEnvelope transactionEnvelope = TransactionEnvelope.decode(new XdrDataInputStream(new ByteArrayInputStream(bytes))); + ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); + + transactionEnvelope.encode(new XdrDataOutputStream(byteOutputStream)); + String serialized = base64Encoding.encode(byteOutputStream.toByteArray()); + assertEquals(serialized, txBody); + } + }