diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Base58.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base58.java
index 90ce8df77..1a7a6444a 100644
--- a/hutool-core/src/main/java/cn/hutool/core/codec/Base58.java
+++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base58.java
@@ -1,36 +1,37 @@
package cn.hutool.core.codec;
-import java.math.BigInteger;
+import cn.hutool.core.exceptions.UtilException;
+import cn.hutool.core.exceptions.ValidateException;
+
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
/**
- * @author lin
- * Inspired from https://github.com/adamcaudill/Base58Check/blob/master/src/Base58Check/Base58CheckEncoding.cs
* Base58工具类,提供Base58的编码和解码方案
+ * 参考: https://github.com/Anujraval24/Base58Encoding
+ * 规范见:https://en.bitcoin.it/wiki/Base58Check_encoding
+ *
+ * @author lin, looly
* @since 5.7.22
*/
public class Base58 {
-
- private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
- private static final char[] ALPHABET_ARRAY = ALPHABET.toCharArray();
- private static final BigInteger BASE_SIZE = BigInteger.valueOf(ALPHABET_ARRAY.length);
private static final int CHECKSUM_SIZE = 4;
// -------------------------------------------------------------------- encode
/**
- * Base58编码
+ * Base58编码
+ * 包含版本位和校验位
*
- * @param data 被编码的数组,添加校验和。
+ * @param version 编码版本,{@code null}表示不包含版本位
+ * @param data 被编码的数组,添加校验和。
* @return 编码后的字符串
- * @since 5.7.22
*/
- public static String encode(byte[] data) throws NoSuchAlgorithmException {
- return encodePlain(addChecksum(data));
+ public static String encodeChecked(Integer version, byte[] data) {
+ return encode(addChecksum(version, data));
}
/**
@@ -38,114 +39,114 @@ public class Base58 {
*
* @param data 被编码的数据,不带校验和。
* @return 编码后的字符串
- * @since 5.7.22
*/
- public static String encodePlain(byte[] data) {
- BigInteger intData;
- try {
- intData = new BigInteger(1, data);
- } catch (NumberFormatException e) {
- return "";
- }
- StringBuilder result = new StringBuilder();
- while (intData.compareTo(BigInteger.ZERO) > 0) {
- BigInteger[] quotientAndRemainder = intData.divideAndRemainder(BASE_SIZE);
- BigInteger quotient = quotientAndRemainder[0];
- BigInteger remainder = quotientAndRemainder[1];
- intData = quotient;
- result.insert(0, ALPHABET_ARRAY[remainder.intValue()]);
- }
- for (int i = 0; i < data.length && data[i] == 0; i++) {
- result.insert(0, '1');
- }
- return result.toString();
+ public static String encode(byte[] data) {
+ return Base58Codec.INSTANCE.encode(data);
}
// -------------------------------------------------------------------- decode
/**
- * Base58编码
+ * Base58解码
+ * 解码包含标志位验证和版本呢位去除
*
* @param encoded 被解码的base58字符串
* @return 解码后的bytes
- * @since 5.7.22
+ * @throws ValidateException 标志位验证错误抛出此异常
*/
- public static byte[] decode(String encoded) throws NoSuchAlgorithmException {
- byte[] valueWithChecksum = decodePlain(encoded);
- byte[] value = verifyAndRemoveChecksum(valueWithChecksum);
- if (value == null) {
- throw new IllegalArgumentException("Base58 checksum is invalid");
+ public static byte[] decodeChecked(CharSequence encoded) throws ValidateException {
+ try {
+ return decodeChecked(encoded, true);
+ } catch (ValidateException ignore) {
+ return decodeChecked(encoded, false);
}
- return value;
}
/**
- * Base58编码
+ * Base58解码
+ * 解码包含标志位验证和版本呢位去除
*
- * @param encoded 被解码的base58字符串
+ * @param encoded 被解码的base58字符串
+ * @param withVersion 是否包含版本位
* @return 解码后的bytes
- * @since 5.7.22
+ * @throws ValidateException 标志位验证错误抛出此异常
*/
- public static byte[] decodePlain(String encoded) {
- if (encoded.length() == 0) {
- return new byte[0];
- }
- BigInteger intData = BigInteger.ZERO;
- int leadingZeros = 0;
- for (int i = 0; i < encoded.length(); i++) {
- char current = encoded.charAt(i);
- int digit = ALPHABET.indexOf(current);
- if (digit == -1) {
- throw new IllegalArgumentException(String.format("Invalid Base58 character `%c` at position %d", current, i));
- }
- intData = (intData.multiply(BASE_SIZE)).add(BigInteger.valueOf(digit));
- }
+ public static byte[] decodeChecked(CharSequence encoded, boolean withVersion) throws ValidateException {
+ byte[] valueWithChecksum = decode(encoded);
+ return verifyAndRemoveChecksum(valueWithChecksum, withVersion);
+ }
- for (int i = 0; i < encoded.length(); i++) {
- char current = encoded.charAt(i);
- if (current == '1') {
- leadingZeros++;
- } else {
- break;
- }
+ /**
+ * Base58解码
+ *
+ * @param encoded 被编码的base58字符串
+ * @return 解码后的bytes
+ */
+ public static byte[] decode(CharSequence encoded) {
+ return Base58Codec.INSTANCE.decode(encoded);
+ }
+
+ /**
+ * 验证并去除验证位和版本位
+ *
+ * @param data 编码的数据
+ * @param withVersion 是否包含版本位
+ * @return 载荷数据
+ */
+ private static byte[] verifyAndRemoveChecksum(byte[] data, boolean withVersion) {
+ final byte[] payload = Arrays.copyOfRange(data, withVersion ? 1 : 0, data.length - CHECKSUM_SIZE);
+ final byte[] checksum = Arrays.copyOfRange(data, data.length - CHECKSUM_SIZE, data.length);
+ final byte[] expectedChecksum = checksum(payload);
+ if (false == Arrays.equals(checksum, expectedChecksum)) {
+ throw new ValidateException("Base58 checksum is invalid");
}
- byte[] bytesData;
- if (intData.equals(BigInteger.ZERO)) {
- bytesData = new byte[0];
+ return payload;
+ }
+
+ /**
+ * 数据 + 校验码
+ *
+ * @param version 版本,{@code null}表示不添加版本位
+ * @param payload Base58数据(不含校验码)
+ * @return Base58数据
+ */
+ private static byte[] addChecksum(Integer version, byte[] payload) {
+ final byte[] addressBytes;
+ if (null != version) {
+ addressBytes = new byte[1 + payload.length + CHECKSUM_SIZE];
+ addressBytes[0] = (byte) version.intValue();
+ System.arraycopy(payload, 0, addressBytes, 1, payload.length);
} else {
- bytesData = intData.toByteArray();
+ addressBytes = new byte[payload.length + CHECKSUM_SIZE];
+ System.arraycopy(payload, 0, addressBytes, 0, payload.length);
}
- //Should we cut the sign byte ? - https://bitcoinj.googlecode.com/git-history/216deb2d35d1a128a7f617b91f2ca35438aae546/lib/src/com/google/bitcoin/core/Base58.java
- boolean stripSignByte = bytesData.length > 1 && bytesData[0] == 0 && bytesData[1] < 0;
- byte[] decoded = new byte[bytesData.length - (stripSignByte ? 1 : 0) + leadingZeros];
- System.arraycopy(bytesData, stripSignByte ? 1 : 0, decoded, leadingZeros, decoded.length - leadingZeros);
- return decoded;
+ final byte[] checksum = checksum(payload);
+ System.arraycopy(checksum, 0, addressBytes, addressBytes.length - CHECKSUM_SIZE, CHECKSUM_SIZE);
+ return addressBytes;
}
- private static byte[] verifyAndRemoveChecksum(byte[] data) throws NoSuchAlgorithmException {
- byte[] value = Arrays.copyOfRange(data, 0, data.length - CHECKSUM_SIZE);
- byte[] checksum = Arrays.copyOfRange(data, data.length - CHECKSUM_SIZE, data.length);
- byte[] expectedChecksum = getChecksum(value);
- return Arrays.equals(checksum, expectedChecksum) ? value : null;
- }
-
- private static byte[] addChecksum(byte[] data) throws NoSuchAlgorithmException {
- byte[] checksum = getChecksum(data);
- byte[] result = new byte[data.length + checksum.length];
- System.arraycopy(data, 0, result, 0, data.length);
- System.arraycopy(checksum, 0, result, data.length, checksum.length);
- return result;
- }
-
- private static byte[] getChecksum(byte[] data) throws NoSuchAlgorithmException {
- byte[] hash = hash256(data);
- hash = hash256(hash);
+ /**
+ * 获取校验码
+ * 计算规则为对数据进行两次sha256计算,然后取{@link #CHECKSUM_SIZE}长度
+ *
+ * @param data 数据
+ * @return 校验码
+ */
+ private static byte[] checksum(byte[] data) {
+ byte[] hash = hash256(hash256(data));
return Arrays.copyOfRange(hash, 0, CHECKSUM_SIZE);
}
- private static byte[] hash256(byte[] data) throws NoSuchAlgorithmException {
- MessageDigest md = MessageDigest.getInstance("SHA-256");
- md.update(data);
- return md.digest();
+ /**
+ * 计算数据的SHA-256值
+ *
+ * @param data 数据
+ * @return sha-256值
+ */
+ private static byte[] hash256(byte[] data) {
+ try {
+ return MessageDigest.getInstance("SHA-256").digest(data);
+ } catch (NoSuchAlgorithmException e) {
+ throw new UtilException(e);
+ }
}
-
}
diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Base58Codec.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base58Codec.java
new file mode 100644
index 000000000..6e6d02d82
--- /dev/null
+++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base58Codec.java
@@ -0,0 +1,142 @@
+package cn.hutool.core.codec;
+
+import cn.hutool.core.util.StrUtil;
+
+import java.util.Arrays;
+
+/**
+ * Base58编码器
+ * 此编码器不包括校验码、版本等信息
+ *
+ * @author lin, looly
+ * @since 5.7.22
+ */
+public class Base58Codec implements Encoder, Decoder {
+
+ public static Base58Codec INSTANCE = new Base58Codec();
+
+ private final char[] alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray();
+ private final char ENCODED_ZERO = alphabet[0];
+ private final int[] lookup = initLookup();
+
+ /**
+ * Base58编码
+ *
+ * @param data 被编码的数据,不带校验和。
+ * @return 编码后的字符串
+ */
+ @Override
+ public String encode(byte[] data) {
+ if (null == data) {
+ return null;
+ }
+ if (data.length == 0) {
+ return StrUtil.EMPTY;
+ }
+ // 计算开头0的个数
+ int zeroCount = 0;
+ while (zeroCount < data.length && data[zeroCount] == 0) {
+ ++zeroCount;
+ }
+ // 将256位编码转换为58位编码
+ data = Arrays.copyOf(data, data.length); // since we modify it in-place
+ final char[] encoded = new char[data.length * 2]; // upper bound
+ int outputStart = encoded.length;
+ for (int inputStart = zeroCount; inputStart < data.length; ) {
+ encoded[--outputStart] = alphabet[divmod(data, inputStart, 256, 58)];
+ if (data[inputStart] == 0) {
+ ++inputStart; // optimization - skip leading zeros
+ }
+ }
+ // Preserve exactly as many leading encoded zeros in output as there were leading zeros in input.
+ while (outputStart < encoded.length && encoded[outputStart] == ENCODED_ZERO) {
+ ++outputStart;
+ }
+ while (--zeroCount >= 0) {
+ encoded[--outputStart] = ENCODED_ZERO;
+ }
+ // Return encoded string (including encoded leading zeros).
+ return new String(encoded, outputStart, encoded.length - outputStart);
+ }
+
+ /**
+ * 解码给定的Base58字符串
+ *
+ * @param encoded Base58编码字符串
+ * @return 解码后的bytes
+ * @throws IllegalArgumentException 非标准Base58字符串
+ */
+ @Override
+ public byte[] decode(CharSequence encoded) throws IllegalArgumentException {
+ if (encoded.length() == 0) {
+ return new byte[0];
+ }
+ // Convert the base58-encoded ASCII chars to a base58 byte sequence (base58 digits).
+ final byte[] input58 = new byte[encoded.length()];
+ for (int i = 0; i < encoded.length(); ++i) {
+ char c = encoded.charAt(i);
+ int digit = c < 128 ? lookup[c] : -1;
+ if (digit < 0) {
+ throw new IllegalArgumentException(StrUtil.format("Invalid char '{}' at [{}]", c, i));
+ }
+ input58[i] = (byte) digit;
+ }
+ // Count leading zeros.
+ int zeros = 0;
+ while (zeros < input58.length && input58[zeros] == 0) {
+ ++zeros;
+ }
+ // Convert base-58 digits to base-256 digits.
+ byte[] decoded = new byte[encoded.length()];
+ int outputStart = decoded.length;
+ for (int inputStart = zeros; inputStart < input58.length; ) {
+ decoded[--outputStart] = divmod(input58, inputStart, 58, 256);
+ if (input58[inputStart] == 0) {
+ ++inputStart; // optimization - skip leading zeros
+ }
+ }
+ // Ignore extra leading zeroes that were added during the calculation.
+ while (outputStart < decoded.length && decoded[outputStart] == 0) {
+ ++outputStart;
+ }
+ // Return decoded data (including original number of leading zeros).
+ return Arrays.copyOfRange(decoded, outputStart - zeros, decoded.length);
+ }
+
+ /**
+ * 初始化字符序号查找表
+ *
+ * @return 字符序号查找表
+ */
+ private int[] initLookup() {
+ final int[] lookup = new int['z' + 1];
+ Arrays.fill(lookup, -1);
+ for (int i = 0; i < alphabet.length; i++)
+ lookup[alphabet[i]] = i;
+ return lookup;
+ }
+
+ /**
+ * Divides a number, represented as an array of bytes each containing a single digit
+ * in the specified base, by the given divisor. The given number is modified in-place
+ * to contain the quotient, and the return value is the remainder.
+ *
+ * @param number the number to divide
+ * @param firstDigit the index within the array of the first non-zero digit
+ * (this is used for optimization by skipping the leading zeros)
+ * @param base the base in which the number's digits are represented (up to 256)
+ * @param divisor the number to divide by (up to 256)
+ * @return the remainder of the division operation
+ */
+ private static byte divmod(byte[] number, int firstDigit, int base, int divisor) {
+ // this is just long division which accounts for the base of the input digits
+ int remainder = 0;
+ for (int i = firstDigit; i < number.length; i++) {
+ int digit = (int) number[i] & 0xFF;
+ int temp = remainder * base + digit;
+ number[i] = (byte) (temp / divisor);
+ remainder = temp % divisor;
+ }
+ return (byte) remainder;
+ }
+}
diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Base62Codec.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base62Codec.java
index 0dbc1fadd..2386de005 100644
--- a/hutool-core/src/main/java/cn/hutool/core/codec/Base62Codec.java
+++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base62Codec.java
@@ -12,7 +12,7 @@ import java.io.Serializable;
* @author Looly, Sebastian Ruhleder, sebastian@seruco.io
* @since 4.5.9
*/
-public class Base62Codec implements Serializable{
+public class Base62Codec implements Encoder, Decoder, Serializable{
private static final long serialVersionUID = 1L;
private static final int STANDARD_BASE = 256;
@@ -86,6 +86,7 @@ public class Base62Codec implements Serializable{
* @param message 被编码的消息
* @return Base62内容
*/
+ @Override
public byte[] encode(byte[] message) {
final byte[] indices = convert(message, STANDARD_BASE, TARGET_BASE);
return translate(indices, alphabet);
@@ -97,6 +98,7 @@ public class Base62Codec implements Serializable{
* @param encoded Base62内容
* @return 消息
*/
+ @Override
public byte[] decode(byte[] encoded) {
final byte[] prepared = translate(encoded, lookup);
return convert(prepared, TARGET_BASE, STANDARD_BASE);
@@ -177,4 +179,4 @@ public class Base62Codec implements Serializable{
return (int) Math.ceil((Math.log(sourceBase) / Math.log(targetBase)) * inputLength);
}
// --------------------------------------------------------------------------------------------------------------- Private method end
-}
\ No newline at end of file
+}
diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Base64Decoder.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base64Decoder.java
index 91dd741e8..20d0baad9 100644
--- a/hutool-core/src/main/java/cn/hutool/core/codec/Base64Decoder.java
+++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base64Decoder.java
@@ -18,15 +18,6 @@ public class Base64Decoder {
private static final byte PADDING = -2;
/** Base64解码表,共128位,-1表示非base64字符,-2表示padding */
- // private static final byte[] DECODE_TABLE2 = {
- // -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
- // -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
- // -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
- // 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
- // -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
- // 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
- // -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
- // 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1 };
private static final byte[] DECODE_TABLE = {
// 0 1 2 3 4 5 6 7 8 9 A B C D E F
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f
diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Base64Encoder.java b/hutool-core/src/main/java/cn/hutool/core/codec/Base64Encoder.java
index 608dbc4bf..c1ec7a6d6 100644
--- a/hutool-core/src/main/java/cn/hutool/core/codec/Base64Encoder.java
+++ b/hutool-core/src/main/java/cn/hutool/core/codec/Base64Encoder.java
@@ -11,7 +11,7 @@ import java.nio.charset.Charset;
* @author looly
* @since 3.2.0
*/
-public class Base64Encoder {
+public class Base64Encoder{
private static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8;
/** 标准编码表 */
diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Decoder.java b/hutool-core/src/main/java/cn/hutool/core/codec/Decoder.java
new file mode 100644
index 000000000..b1f9e0a57
--- /dev/null
+++ b/hutool-core/src/main/java/cn/hutool/core/codec/Decoder.java
@@ -0,0 +1,20 @@
+package cn.hutool.core.codec;
+
+/**
+ * 解码接口
+ *
+ * @param 被解码的数据类型
+ * @param 解码后的数据类型
+ * @author looly
+ * @since 5.7.22
+ */
+public interface Decoder {
+
+ /**
+ * 执行解码
+ *
+ * @param data 被解码的数据
+ * @return 解码后的数据
+ */
+ R decode(T data);
+}
diff --git a/hutool-core/src/main/java/cn/hutool/core/codec/Encoder.java b/hutool-core/src/main/java/cn/hutool/core/codec/Encoder.java
new file mode 100644
index 000000000..ca05d0ac3
--- /dev/null
+++ b/hutool-core/src/main/java/cn/hutool/core/codec/Encoder.java
@@ -0,0 +1,20 @@
+package cn.hutool.core.codec;
+
+/**
+ * 编码接口
+ *
+ * @param 被编码的数据类型
+ * @param 编码后的数据类型
+ * @author looly
+ * @since 5.7.22
+ */
+public interface Encoder {
+
+ /**
+ * 执行编码
+ *
+ * @param encoded 被编码的数据
+ * @return 编码后的数据
+ */
+ R encode(T encoded);
+}
diff --git a/hutool-core/src/test/java/cn/hutool/core/codec/Base58Test.java b/hutool-core/src/test/java/cn/hutool/core/codec/Base58Test.java
index d27a43180..0dab5faf2 100644
--- a/hutool-core/src/test/java/cn/hutool/core/codec/Base58Test.java
+++ b/hutool-core/src/test/java/cn/hutool/core/codec/Base58Test.java
@@ -4,31 +4,37 @@ import org.junit.Assert;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
-import java.security.NoSuchAlgorithmException;
public class Base58Test {
+
@Test
- public void testEncode() throws NoSuchAlgorithmException {
+ public void encodeCheckedTest() {
String a = "hello world";
- String encode = Base58.encode(a.getBytes(StandardCharsets.UTF_8));
+ String encode = Base58.encodeChecked(0, a.getBytes());
+ Assert.assertEquals(1 + "3vQB7B6MrGQZaxCuFg4oh", encode);
+
+ // 无版本位
+ encode = Base58.encodeChecked(null, a.getBytes());
Assert.assertEquals("3vQB7B6MrGQZaxCuFg4oh", encode);
}
@Test
- public void testEncodePlain() {
+ public void encodeTest() {
String a = "hello world";
- String encode = Base58.encodePlain(a.getBytes(StandardCharsets.UTF_8));
+ String encode = Base58.encode(a.getBytes(StandardCharsets.UTF_8));
Assert.assertEquals("StV1DL6CwTryKyV", encode);
}
@Test
- public void testDecode() throws NoSuchAlgorithmException {
+ public void decodeCheckedTest() {
String a = "3vQB7B6MrGQZaxCuFg4oh";
- byte[] decode = Base58.decode(a);
+ byte[] decode = Base58.decodeChecked(1 + a);
+ Assert.assertArrayEquals("hello world".getBytes(StandardCharsets.UTF_8),decode);
+ decode = Base58.decodeChecked(a);
Assert.assertArrayEquals("hello world".getBytes(StandardCharsets.UTF_8),decode);
}
@Test
- public void testDecodePlain() {
+ public void testDecode() {
String a = "StV1DL6CwTryKyV";
- byte[] decode = Base58.decodePlain(a);
+ byte[] decode = Base58.decode(a);
Assert.assertArrayEquals("hello world".getBytes(StandardCharsets.UTF_8),decode);
}
}
diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/KeyUtil.java b/hutool-crypto/src/main/java/cn/hutool/crypto/KeyUtil.java
index 90ef70540..518f09917 100644
--- a/hutool-crypto/src/main/java/cn/hutool/crypto/KeyUtil.java
+++ b/hutool-crypto/src/main/java/cn/hutool/crypto/KeyUtil.java
@@ -1,5 +1,6 @@
package cn.hutool.crypto;
+import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Assert;
@@ -22,6 +23,7 @@ import java.io.InputStream;
import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
+import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
@@ -988,4 +990,14 @@ public class KeyUtil {
throw new CryptoException(e);
}
}
+
+ /**
+ * 将密钥编码为Base64格式
+ * @param key 密钥
+ * @return Base64格式密钥
+ * @since 5.7.22
+ */
+ public static String toBase64(Key key){
+ return Base64.encode(key.getEncoded());
+ }
}