forked from GitHub-Mirror/riotX-android
canonicalize
This commit is contained in:
parent
1436667e7d
commit
e70fd8e351
@ -45,9 +45,11 @@ android {
|
|||||||
|
|
||||||
debug {
|
debug {
|
||||||
// Set to true to log privacy or sensible data, such as token
|
// Set to true to log privacy or sensible data, such as token
|
||||||
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"
|
// TODO Set to false
|
||||||
|
buildConfigField "boolean", "LOG_PRIVATE_DATA", "true"
|
||||||
|
|
||||||
// Set to BODY instead of NONE to enable logging
|
// Set to BODY instead of NONE to enable logging
|
||||||
|
//TODO Revert BODY
|
||||||
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.BODY"
|
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.BODY"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,154 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.util
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import im.vector.matrix.android.InstrumentedTest
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
internal class JsonCanonicalizerTest : InstrumentedTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun identityTest() {
|
||||||
|
listOf(
|
||||||
|
"{}",
|
||||||
|
"""{"a":true}""",
|
||||||
|
"""{"a":false}""",
|
||||||
|
"""{"a":1}""",
|
||||||
|
"""{"a":1.2}""",
|
||||||
|
"""{"a":null}""",
|
||||||
|
"""{"a":[]}""",
|
||||||
|
"""{"a":["b":"c"]}""",
|
||||||
|
"""{"a":["c":"b","d":"e"]}""",
|
||||||
|
"""{"a":["d":"b","c":"e"]}"""
|
||||||
|
).forEach {
|
||||||
|
assertEquals(it,
|
||||||
|
JsonCanonicalizer.canonicalize(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun reorderTest() {
|
||||||
|
assertEquals("""{"a":true,"b":false}""",
|
||||||
|
JsonCanonicalizer.canonicalize("""{"b":false,"a":true}"""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun realSampleTest() {
|
||||||
|
assertEquals("""{"algorithms":["m.megolm.v1.aes-sha2","m.olm.v1.curve25519-aes-sha2"],"device_id":"VSCUNFSOUI","keys":{"curve25519:VSCUNFSOUI":"utyOjnhiQ73qNhi9HlN0OgWIowe5gthTS8r0r9TcJ3o","ed25519:VSCUNFSOUI":"qNhEt+Yggaajet0hX/FjTRLfySgs65ldYyomm7PIx6U"},"user_id":"@benoitx:matrix.org"}""",
|
||||||
|
JsonCanonicalizer.canonicalize("""{"algorithms":["m.megolm.v1.aes-sha2","m.olm.v1.curve25519-aes-sha2"],"device_id":"VSCUNFSOUI","user_id":"@benoitx:matrix.org","keys":{"curve25519:VSCUNFSOUI":"utyOjnhiQ73qNhi9HlN0OgWIowe5gthTS8r0r9TcJ3o","ed25519:VSCUNFSOUI":"qNhEt+Yggaajet0hX/FjTRLfySgs65ldYyomm7PIx6U"}}"""))
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Test from https://matrix.org/docs/spec/appendices.html#examples
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun matrixOrg001Test() {
|
||||||
|
assertEquals("""{}""",
|
||||||
|
JsonCanonicalizer.canonicalize("""{}"""))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun matrixOrg002Test() {
|
||||||
|
assertEquals("""{"one":1,"two":"Two"}""",
|
||||||
|
JsonCanonicalizer.canonicalize("""{
|
||||||
|
"one": 1,
|
||||||
|
"two": "Two"
|
||||||
|
}"""))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun matrixOrg003Test() {
|
||||||
|
assertEquals("""{"a":"1","b":"2"}""",
|
||||||
|
JsonCanonicalizer.canonicalize("""{
|
||||||
|
"b": "2",
|
||||||
|
"a": "1"
|
||||||
|
}"""))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun matrixOrg004Test() {
|
||||||
|
assertEquals("""{"a":"1","b":"2"}""",
|
||||||
|
JsonCanonicalizer.canonicalize("""{"b":"2","a":"1"}"""))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun matrixOrg005Test() {
|
||||||
|
assertEquals("""{"auth":{"mxid":"@john.doe:example.com","profile":{"display_name":"John Doe","three_pids":[{"address":"john.doe@example.org","medium":"email"},{"address":"123456789","medium":"msisdn"}]},"success":true}}""",
|
||||||
|
JsonCanonicalizer.canonicalize("""{
|
||||||
|
"auth": {
|
||||||
|
"success": true,
|
||||||
|
"mxid": "@john.doe:example.com",
|
||||||
|
"profile": {
|
||||||
|
"display_name": "John Doe",
|
||||||
|
"three_pids": [
|
||||||
|
{
|
||||||
|
"medium": "email",
|
||||||
|
"address": "john.doe@example.org"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"medium": "msisdn",
|
||||||
|
"address": "123456789"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"""))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun matrixOrg006Test() {
|
||||||
|
assertEquals("""{"a":"日本語"}""",
|
||||||
|
JsonCanonicalizer.canonicalize("""{
|
||||||
|
"a": "日本語"
|
||||||
|
}"""))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun matrixOrg007Test() {
|
||||||
|
assertEquals("""{"日":1,"本":2}""",
|
||||||
|
JsonCanonicalizer.canonicalize("""{
|
||||||
|
"本": 2,
|
||||||
|
"日": 1
|
||||||
|
}"""))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun matrixOrg008Test() {
|
||||||
|
assertEquals("""{"a":"日"}""",
|
||||||
|
JsonCanonicalizer.canonicalize("{\"a\": \"\u65E5\"}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun matrixOrg009Test() {
|
||||||
|
assertEquals("""{"a":null}""",
|
||||||
|
JsonCanonicalizer.canonicalize("""{
|
||||||
|
"a": null
|
||||||
|
}"""))
|
||||||
|
}
|
||||||
|
}
|
@ -73,7 +73,6 @@ interface CryptoService {
|
|||||||
|
|
||||||
fun getDeviceInfo(userId: String, deviceId: String?, callback: MatrixCallback<MXDeviceInfo?>)
|
fun getDeviceInfo(userId: String, deviceId: String?, callback: MatrixCallback<MXDeviceInfo?>)
|
||||||
|
|
||||||
// TODO move elsewhere
|
|
||||||
fun reRequestRoomKeyForEvent(event: Event)
|
fun reRequestRoomKeyForEvent(event: Event)
|
||||||
|
|
||||||
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody)
|
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody)
|
||||||
|
@ -43,7 +43,10 @@ import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
|
|||||||
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
|
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
|
||||||
import im.vector.matrix.android.internal.crypto.model.*
|
import im.vector.matrix.android.internal.crypto.model.*
|
||||||
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
|
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.*
|
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedMessage
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.*
|
import im.vector.matrix.android.internal.crypto.tasks.*
|
||||||
import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService
|
import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService
|
||||||
@ -1293,11 +1296,11 @@ internal class CryptoManager(
|
|||||||
* @param event the event
|
* @param event the event
|
||||||
*/
|
*/
|
||||||
fun onToDeviceEvent(event: Event) {
|
fun onToDeviceEvent(event: Event) {
|
||||||
mSasVerificationService.onToDeviceEvent(event)
|
if (event.type == EventType.ROOM_KEY || event.type == EventType.FORWARDED_ROOM_KEY) {
|
||||||
|
getDecryptingThreadHandler().post {
|
||||||
if (TextUtils.equals(event.type, EventType.ROOM_KEY) || TextUtils.equals(event.type, EventType.FORWARDED_ROOM_KEY)) {
|
onRoomKeyEvent(event)
|
||||||
getDecryptingThreadHandler().post { onRoomKeyEvent(event) }
|
}
|
||||||
} else if (TextUtils.equals(event.type, EventType.ROOM_KEY_REQUEST)) {
|
} else if (event.type == EventType.ROOM_KEY_REQUEST) {
|
||||||
encryptingThreadHandler.post {
|
encryptingThreadHandler.post {
|
||||||
mIncomingRoomKeyRequestManager.onRoomKeyRequestEvent(event)
|
mIncomingRoomKeyRequestManager.onRoomKeyRequestEvent(event)
|
||||||
}
|
}
|
||||||
@ -1490,7 +1493,7 @@ internal class CryptoManager(
|
|||||||
|
|
||||||
// Try to keep at most half that number on the server. This leaves the
|
// Try to keep at most half that number on the server. This leaves the
|
||||||
// rest of the slots free to hold keys that have been claimed from the
|
// rest of the slots free to hold keys that have been claimed from the
|
||||||
// server but we haven't recevied a message for.
|
// server but we haven't received a message for.
|
||||||
// If we run out of slots when generating new keys then olm will
|
// If we run out of slots when generating new keys then olm will
|
||||||
// discard the oldest private keys first. This will eventually clean
|
// discard the oldest private keys first. This will eventually clean
|
||||||
// out stale private keys that won't receive a message.
|
// out stale private keys that won't receive a message.
|
||||||
|
@ -28,15 +28,14 @@ import im.vector.matrix.android.api.session.events.model.toModel
|
|||||||
import im.vector.matrix.android.internal.crypto.*
|
import im.vector.matrix.android.internal.crypto.*
|
||||||
import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting
|
import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting
|
||||||
import im.vector.matrix.android.internal.crypto.algorithms.MXDecryptionResult
|
import im.vector.matrix.android.internal.crypto.algorithms.MXDecryptionResult
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXOlmSessionResult
|
import im.vector.matrix.android.internal.crypto.model.MXOlmSessionResult
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
import im.vector.matrix.android.internal.crypto.CryptoManager
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.ForwardedRoomKeyContent
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
|
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.ForwardedRoomKeyContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
@ -239,7 +238,7 @@ internal class MXMegolmDecryption : IMXDecrypting {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TextUtils.equals(event.type, EventType.FORWARDED_ROOM_KEY)) {
|
if (event.type == EventType.FORWARDED_ROOM_KEY) {
|
||||||
Timber.d("## onRoomKeyEvent(), forward adding key : roomId " + roomKeyContent.roomId + " sessionId " + roomKeyContent.sessionId
|
Timber.d("## onRoomKeyEvent(), forward adding key : roomId " + roomKeyContent.roomId + " sessionId " + roomKeyContent.sessionId
|
||||||
+ " sessionKey " + roomKeyContent.sessionKey) // from " + event);
|
+ " sessionKey " + roomKeyContent.sessionKey) // from " + event);
|
||||||
val forwardedRoomKeyContent = event.content.toModel<ForwardedRoomKeyContent>()!!
|
val forwardedRoomKeyContent = event.content.toModel<ForwardedRoomKeyContent>()!!
|
||||||
|
@ -23,6 +23,7 @@ import im.vector.matrix.android.internal.network.parsing.UriMoshiAdapter
|
|||||||
import im.vector.matrix.android.internal.session.sync.model.UserAccountData
|
import im.vector.matrix.android.internal.session.sync.model.UserAccountData
|
||||||
import im.vector.matrix.android.internal.session.sync.model.UserAccountDataDirectMessages
|
import im.vector.matrix.android.internal.session.sync.model.UserAccountDataDirectMessages
|
||||||
import im.vector.matrix.android.internal.session.sync.model.UserAccountDataFallback
|
import im.vector.matrix.android.internal.session.sync.model.UserAccountDataFallback
|
||||||
|
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||||
|
|
||||||
object MoshiProvider {
|
object MoshiProvider {
|
||||||
|
|
||||||
@ -48,10 +49,16 @@ object MoshiProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun <T> getCanonicalJson(type: Class<T>, o: T): String {
|
fun <T> getCanonicalJson(type: Class<T>, o: T): String {
|
||||||
val adadpter = moshi.adapter<T>(type)
|
val adapter = moshi.adapter<T>(type)
|
||||||
|
|
||||||
// FIXME It is not canonical...
|
val json = adapter.toJson(o)
|
||||||
return adadpter.toJson(o)
|
|
||||||
|
// Canonicalize manually
|
||||||
|
val can = JsonCanonicalizer.canonicalize(json)
|
||||||
|
|
||||||
|
val jsonSafe = can.replace("\\/", "/")
|
||||||
|
|
||||||
|
return jsonSafe
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,21 +17,24 @@
|
|||||||
package im.vector.matrix.android.internal.session.sync
|
package im.vector.matrix.android.internal.session.sync
|
||||||
|
|
||||||
import im.vector.matrix.android.internal.crypto.CryptoManager
|
import im.vector.matrix.android.internal.crypto.CryptoManager
|
||||||
|
import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService
|
||||||
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
|
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
|
||||||
import im.vector.matrix.android.internal.session.sync.model.ToDeviceSyncResponse
|
import im.vector.matrix.android.internal.session.sync.model.ToDeviceSyncResponse
|
||||||
|
|
||||||
|
|
||||||
internal class CryptoSyncHandler(private val crypto: CryptoManager) {
|
internal class CryptoSyncHandler(private val cryptoManager: CryptoManager,
|
||||||
|
private val sasVerificationService: DefaultSasVerificationService) {
|
||||||
|
|
||||||
fun handleToDevice(toDevice: ToDeviceSyncResponse) {
|
fun handleToDevice(toDevice: ToDeviceSyncResponse) {
|
||||||
toDevice.events?.forEach {
|
toDevice.events?.forEach {
|
||||||
crypto.onToDeviceEvent(it)
|
sasVerificationService.onToDeviceEvent(it)
|
||||||
|
cryptoManager.onToDeviceEvent(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSyncCompleted(syncResponse: SyncResponse, fromToken: String?, catchingUp: Boolean) {
|
fun onSyncCompleted(syncResponse: SyncResponse, fromToken: String?, catchingUp: Boolean) {
|
||||||
crypto.onSyncCompleted(syncResponse, fromToken, catchingUp)
|
cryptoManager.onSyncCompleted(syncResponse, fromToken, catchingUp)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -48,7 +48,7 @@ internal class SyncModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
CryptoSyncHandler(get())
|
CryptoSyncHandler(get(), get())
|
||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
|
@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.util
|
||||||
|
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build canonical Json
|
||||||
|
* Doc: https://matrix.org/docs/spec/appendices.html#canonical-json
|
||||||
|
*/
|
||||||
|
object JsonCanonicalizer {
|
||||||
|
|
||||||
|
fun canonicalize(json: String): String {
|
||||||
|
var can: String? = null
|
||||||
|
try {
|
||||||
|
val _json = JSONObject(json)
|
||||||
|
|
||||||
|
can = _canonicalize(_json)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
Timber.e(e, "Unable to canonicalize")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (can == null) {
|
||||||
|
Timber.e("Error")
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
|
return can
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonicalize a JsonElement element
|
||||||
|
*
|
||||||
|
* @param src the src
|
||||||
|
* @return the canonicalize element
|
||||||
|
*/
|
||||||
|
private fun _canonicalize(src: Any?): String? {
|
||||||
|
// sanity check
|
||||||
|
if (null == src) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
when (src) {
|
||||||
|
is JSONArray -> {
|
||||||
|
// Canonicalize each element of the array
|
||||||
|
val srcArray = src as JSONArray?
|
||||||
|
val result = StringBuilder("[")
|
||||||
|
|
||||||
|
for (i in 0 until srcArray!!.length()) {
|
||||||
|
result.append(_canonicalize(srcArray.get(i)))
|
||||||
|
if (i < srcArray.length() - 1) {
|
||||||
|
result.append(",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.append("]")
|
||||||
|
|
||||||
|
return result.toString()
|
||||||
|
}
|
||||||
|
is JSONObject -> {
|
||||||
|
// Sort the attributes by name, and the canonicalize each element of the object
|
||||||
|
val result = StringBuilder("{")
|
||||||
|
|
||||||
|
val attributes = TreeSet<String>()
|
||||||
|
for (entry in src.keys()) {
|
||||||
|
attributes.add(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (attribute in attributes.withIndex()) {
|
||||||
|
result.append("\"")
|
||||||
|
.append(attribute.value)
|
||||||
|
.append("\"")
|
||||||
|
.append(":")
|
||||||
|
.append(_canonicalize(src[attribute.value]))
|
||||||
|
|
||||||
|
if (attribute.index < attributes.size - 1) {
|
||||||
|
result.append(",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.append("}")
|
||||||
|
|
||||||
|
return result.toString()
|
||||||
|
}
|
||||||
|
is String -> return "\"" + src.toString() + "\""
|
||||||
|
else -> return src.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user