diff --git a/src/main/java/org/qortal/arbitrary/metadata/ArbitraryDataTransactionMetadata.java b/src/main/java/org/qortal/arbitrary/metadata/ArbitraryDataTransactionMetadata.java index 004e0ed3..d9dba037 100644 --- a/src/main/java/org/qortal/arbitrary/metadata/ArbitraryDataTransactionMetadata.java +++ b/src/main/java/org/qortal/arbitrary/metadata/ArbitraryDataTransactionMetadata.java @@ -7,6 +7,7 @@ import org.qortal.arbitrary.misc.Category; import org.qortal.repository.DataException; import org.qortal.utils.Base58; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; @@ -217,6 +218,25 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata { // Static helper methods + public static String trimUTF8String(String string, int maxLength) { + byte[] inputBytes = string.getBytes(StandardCharsets.UTF_8); + int length = Math.min(inputBytes.length, maxLength); + byte[] outputBytes = new byte[length]; + + System.arraycopy(inputBytes, 0, outputBytes, 0, length); + String result = new String(outputBytes, StandardCharsets.UTF_8); + + // check if last character is truncated + int lastIndex = result.length() - 1; + + if (lastIndex > 0 && result.charAt(lastIndex) != string.charAt(lastIndex)) { + // last character is truncated so remove the last character + return result.substring(0, lastIndex); + } + + return result; + } + public static String limitTitle(String title) { if (title == null) { return null; @@ -225,7 +245,7 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata { return null; } - return title.substring(0, Math.min(title.length(), MAX_TITLE_LENGTH)); + return trimUTF8String(title, MAX_TITLE_LENGTH); } public static String limitDescription(String description) { @@ -236,7 +256,7 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata { return null; } - return description.substring(0, Math.min(description.length(), MAX_DESCRIPTION_LENGTH)); + return trimUTF8String(description, MAX_DESCRIPTION_LENGTH); } public static List limitTags(List tags) { diff --git a/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionMetadataTests.java b/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionMetadataTests.java index 37da4e31..47c68b25 100644 --- a/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionMetadataTests.java +++ b/src/test/java/org/qortal/test/arbitrary/ArbitraryTransactionMetadataTests.java @@ -248,6 +248,47 @@ public class ArbitraryTransactionMetadataTests extends Common { } } + @Test + public void testUTF8Metadata() throws DataException, IOException, MissingDataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); + String publicKey58 = Base58.encode(alice.getPublicKey()); + String name = "TEST"; // Can be anything for this test + String identifier = null; // Not used for this test + Service service = Service.ARBITRARY_DATA; + int chunkSize = 100; + int dataLength = 900; // Actual data length will be longer due to encryption + + // Example (modified) strings from real world content + String title = "Доля юаня в трансграничных Доля юаня в трансграничных"; + String description = "Когда рыночек порешал"; + List tags = Arrays.asList("Доля", "юаня", "трансграничных"); + Category category = Category.OTHER; + + // Register the name to Alice + RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, ""); + transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp())); + TransactionUtils.signAndMint(repository, transactionData, alice); + + // Create PUT transaction + Path path1 = ArbitraryUtils.generateRandomDataPath(dataLength); + ArbitraryDataFile arbitraryDataFile = ArbitraryUtils.createAndMintTxn(repository, publicKey58, path1, name, + identifier, ArbitraryTransactionData.Method.PUT, service, alice, chunkSize, 0L, true, + title, description, tags, category); + + // Check the chunk count is correct + assertEquals(10, arbitraryDataFile.chunkCount()); + + // Check the metadata is correct + String expectedTrimmedTitle = "Доля юаня в трансграничных Доля юаня в тран"; + assertEquals(expectedTrimmedTitle, arbitraryDataFile.getMetadata().getTitle()); + assertEquals(description, arbitraryDataFile.getMetadata().getDescription()); + assertEquals(tags, arbitraryDataFile.getMetadata().getTags()); + assertEquals(category, arbitraryDataFile.getMetadata().getCategory()); + assertEquals("text/plain", arbitraryDataFile.getMetadata().getMimeType()); + } + } + @Test public void testMetadataLengths() throws DataException, IOException, MissingDataException { try (final Repository repository = RepositoryManager.getRepository()) {