Added logic to check Player- and Game-IDs according to new layout. (#2108)

Backwards compatible to old format.
This commit is contained in:
wrov
2020-03-18 21:55:57 +01:00
committed by GitHub
parent 701ddcb76b
commit e308f1fe0c
6 changed files with 205 additions and 9 deletions

View File

@ -0,0 +1,117 @@
package com.unciv.logic
import java.util.*
/**
* This class checks whether a Game- or Player-ID matches the old or new format.
* If old format is used, checks are skipped and input is returned.
* If new format is detected, prefix and checkDigit are checked and UUID returned.
*
* All input is returned trimmed.
*
* New format:
* G-UUID-CheckDigit for Game IDs
* P-UUID-CheckDigit for Player IDs
*
* Example:
* 2ddb3a34-0699-4126-b7a5-38603e665928
* Same ID in proposed new Player-ID format:
* P-2ddb3a34-0699-4126-b7a5-38603e665928-5
* Same ID in proposed new Game-ID format:
* G-2ddb3a34-0699-4126-b7a5-38603e665928-5
*/
class IdChecker {
companion object {
fun checkAndReturnPlayerUuid(playerId: String): String {
return checkAndReturnUuiId(playerId, "P")
}
fun checkAndReturnGameUuid(gameId: String): String {
return checkAndReturnUuiId(gameId, "G")
}
fun checkAndReturnUuiId(id: String, prefix: String): String {
val trimmedPlayerId = id.trim()
if (trimmedPlayerId.length == 40) { // length of a UUID (36) with pre- and postfix
if (!trimmedPlayerId.startsWith(prefix, true)) {
throw IllegalArgumentException("Not a valid ID. Does not start with prefix " + prefix)
}
val checkDigit = trimmedPlayerId.substring(trimmedPlayerId.lastIndex, trimmedPlayerId.lastIndex +1)
// remember, the format is: P-9e37e983-a676-4ecc-800e-ef8ec721a9b9-5
val shortenedPlayerId = trimmedPlayerId.substring(2, 38)
val calculatedCheckDigit = getCheckDigit(shortenedPlayerId).toString()
if (!calculatedCheckDigit.equals(checkDigit)) {
throw IllegalArgumentException("Not a valid ID. Checkdigit invalid.")
}
return shortenedPlayerId
} else if (trimmedPlayerId.length == 36) {
return trimmedPlayerId
}
throw IllegalArgumentException("Not a valid ID. Wrong length.")
}
/**
* Adapted from https://wiki.openmrs.org/display/docs/Check+Digit+Algorithm
*/
fun getCheckDigit(uuid: String): Int {
// allowable characters within identifier
val validChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVYWXZ-"
var idWithoutCheckdigit = uuid
// remove leading or trailing whitespace, convert to uppercase
idWithoutCheckdigit = idWithoutCheckdigit.trim().toUpperCase(Locale.ENGLISH)
// this will be a running total
var sum = 0
// loop through digits from right to left
for (i in idWithoutCheckdigit.indices) {
//set ch to "current" character to be processed
val ch = idWithoutCheckdigit.get(idWithoutCheckdigit.length - i - 1)
// throw exception for invalid characters
if (validChars.indexOf(ch) == -1)
throw IllegalArgumentException(
ch + " is an invalid character")
// our "digit" is calculated using ASCII value - 48
val digit = ch.toInt() - 48
// weight will be the current digit's contribution to
// the running total
var weight: Int
if (i % 2 == 0) {
// for alternating digits starting with the rightmost, we
// use our formula this is the same as multiplying x 2 and
// adding digits together for values 0 to 9. Using the
// following formula allows us to gracefully calculate a
// weight for non-numeric "digits" as well (from their
// ASCII value - 48).
weight = (2 * digit) - (digit / 5) * 9
} else {
// even-positioned digits just contribute their ascii
// value minus 48
weight = digit
}
// keep a running total of weights
sum += weight
}
// avoid sum less than 10 (if characters below "0" allowed,
// this could happen)
sum = Math.abs(sum) + 10
// check digit is amount needed to reach next number
// divisible by ten
val returnValue= (10 - (sum % 10)) % 10
return returnValue
}
}
}

View File

@ -3,9 +3,10 @@ package com.unciv.ui
import com.badlogic.gdx.Gdx import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.* import com.badlogic.gdx.scenes.scene2d.ui.*
import com.unciv.logic.GameSaver
import com.unciv.logic.GameInfo
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.GameInfo
import com.unciv.logic.GameSaver
import com.unciv.logic.IdChecker
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.pickerscreens.PickerScreen import com.unciv.ui.pickerscreens.PickerScreen
import com.unciv.ui.utils.* import com.unciv.ui.utils.*
@ -115,7 +116,7 @@ class MultiplayerScreen() : PickerScreen() {
fun addMultiplayerGame(gameId: String?, gameName: String = ""){ fun addMultiplayerGame(gameId: String?, gameName: String = ""){
try { try {
//since the gameId is a String it can contain anything and has to be checked //since the gameId is a String it can contain anything and has to be checked
UUID.fromString(gameId!!.trim()) UUID.fromString(IdChecker.checkAndReturnGameUuid(gameId!!))
} catch (ex: Exception) { } catch (ex: Exception) {
val errorPopup = Popup(this) val errorPopup = Popup(this)
errorPopup.addGoodSizedLabel("Invalid game ID!".tr()) errorPopup.addGoodSizedLabel("Invalid game ID!".tr())

View File

@ -9,6 +9,7 @@ import com.unciv.UncivGame
import com.unciv.logic.GameInfo import com.unciv.logic.GameInfo
import com.unciv.logic.GameSaver import com.unciv.logic.GameSaver
import com.unciv.logic.GameStarter import com.unciv.logic.GameStarter
import com.unciv.logic.IdChecker
import com.unciv.logic.civilization.PlayerType import com.unciv.logic.civilization.PlayerType
import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
@ -49,7 +50,7 @@ class NewGameScreen: PickerScreen(){
if (newGameParameters.isOnlineMultiplayer) { if (newGameParameters.isOnlineMultiplayer) {
for (player in newGameParameters.players.filter { it.playerType == PlayerType.Human }) { for (player in newGameParameters.players.filter { it.playerType == PlayerType.Human }) {
try { try {
UUID.fromString(player.playerId) UUID.fromString(IdChecker.checkAndReturnPlayerUuid(player.playerId))
} catch (ex: Exception) { } catch (ex: Exception) {
val invalidPlayerIdPopup = Popup(this) val invalidPlayerIdPopup = Popup(this)
invalidPlayerIdPopup.addGoodSizedLabel("Invalid player ID!".tr()).row() invalidPlayerIdPopup.addGoodSizedLabel("Invalid player ID!".tr()).row()

View File

@ -9,13 +9,13 @@ import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.scenes.scene2d.ui.TextField import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Align
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.IdChecker
import com.unciv.logic.civilization.PlayerType import com.unciv.logic.civilization.PlayerType
import com.unciv.models.metadata.GameParameters import com.unciv.models.metadata.GameParameters
import com.unciv.models.metadata.Player import com.unciv.models.metadata.Player
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.utils.* import com.unciv.ui.utils.*
import com.unciv.ui.utils.Popup
import java.util.* import java.util.*
class PlayerPickerTable(val newGameScreen: NewGameScreen, val newGameParameters: GameParameters): Table() { class PlayerPickerTable(val newGameScreen: NewGameScreen, val newGameParameters: GameParameters): Table() {
@ -72,8 +72,8 @@ class PlayerPickerTable(val newGameScreen: NewGameScreen, val newGameParameters:
fun onPlayerIdTextUpdated(){ fun onPlayerIdTextUpdated(){
try { try {
val uuid = UUID.fromString(playerIdTextfield.text) UUID.fromString(IdChecker.checkAndReturnPlayerUuid(playerIdTextfield.text))
player.playerId = playerIdTextfield.text player.playerId = playerIdTextfield.text.trim()
errorLabel.apply { setText("");setFontColor(Color.GREEN) } errorLabel.apply { setText("");setFontColor(Color.GREEN) }
} catch (ex: Exception) { } catch (ex: Exception) {
errorLabel.apply { setText("");setFontColor(Color.RED) } errorLabel.apply { setText("");setFontColor(Color.RED) }

View File

@ -3,9 +3,9 @@ package com.unciv.ui.worldscreen.mainmenu
import com.badlogic.gdx.Gdx import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.IdChecker
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.CivilopediaScreen import com.unciv.ui.CivilopediaScreen
import com.unciv.ui.victoryscreen.VictoryScreen
import com.unciv.ui.MultiplayerScreen import com.unciv.ui.MultiplayerScreen
import com.unciv.ui.mapeditor.LoadMapScreen import com.unciv.ui.mapeditor.LoadMapScreen
import com.unciv.ui.mapeditor.NewMapScreen import com.unciv.ui.mapeditor.NewMapScreen
@ -13,6 +13,7 @@ import com.unciv.ui.newgamescreen.NewGameScreen
import com.unciv.ui.saves.LoadGameScreen import com.unciv.ui.saves.LoadGameScreen
import com.unciv.ui.saves.SaveGameScreen import com.unciv.ui.saves.SaveGameScreen
import com.unciv.ui.utils.* import com.unciv.ui.utils.*
import com.unciv.ui.victoryscreen.VictoryScreen
import com.unciv.ui.worldscreen.WorldScreen import com.unciv.ui.worldscreen.WorldScreen
import java.util.* import java.util.*
import kotlin.concurrent.thread import kotlin.concurrent.thread
@ -105,7 +106,7 @@ class WorldScreenMenuPopup(val worldScreen: WorldScreen) : Popup(worldScreen) {
multiplayerPopup.addButton("Join Game") { multiplayerPopup.addButton("Join Game") {
val gameId = Gdx.app.clipboard.contents val gameId = Gdx.app.clipboard.contents
try { try {
UUID.fromString(gameId.trim()) UUID.fromString(IdChecker.checkAndReturnGameUuid(gameId))
} catch (ex: Exception) { } catch (ex: Exception) {
badGameIdLabel.setText("Invalid game ID!") badGameIdLabel.setText("Invalid game ID!")
badGameIdLabel.isVisible = true badGameIdLabel.isVisible = true

View File

@ -0,0 +1,76 @@
package com.unciv.testing
import com.unciv.logic.IdChecker
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(GdxTestRunner::class)
class IdHelperTests {
@Test
fun testCheckDigits() {
val correctString = "2ddb3a34-0699-4126-b7a5-38603e665928"
val inCorrectString1 = "2ddb3a34-0699-4126-b7a5-38603e665929"
val inCorrectString2 = "2ddb3a34-0969-4126-b7a5-38603e665928"
val inCorrectString3 = "2ddb3a34-0699-4126-b7a"
val inCorrectString4 = "0699-4126-b7a5-38603e665928-2ddb3a34"
val correctLuhn = IdChecker.getCheckDigit(correctString)
val correctLuhn2 = IdChecker.getCheckDigit(correctString)
val inCorrectLuhn1 = IdChecker.getCheckDigit(inCorrectString1)
val inCorrectLuhn2 = IdChecker.getCheckDigit(inCorrectString2)
val inCorrectLuhn3 = IdChecker.getCheckDigit(inCorrectString3)
val inCorrectLuhn4 = IdChecker.getCheckDigit(inCorrectString4)
Assert.assertEquals(correctLuhn, correctLuhn2)
Assert.assertNotEquals(inCorrectLuhn1, correctLuhn)
Assert.assertNotEquals(inCorrectLuhn2, correctLuhn)
Assert.assertNotEquals(inCorrectLuhn3, correctLuhn)
Assert.assertNotEquals(inCorrectLuhn4, correctLuhn)
Assert.assertNotEquals(inCorrectLuhn1, inCorrectLuhn2)
Assert.assertNotEquals(inCorrectLuhn1, inCorrectLuhn3)
Assert.assertNotEquals(inCorrectLuhn1, inCorrectLuhn4)
Assert.assertNotEquals(inCorrectLuhn2, inCorrectLuhn3)
Assert.assertNotEquals(inCorrectLuhn2, inCorrectLuhn4)
Assert.assertNotEquals(inCorrectLuhn3, inCorrectLuhn4)
}
@Test
fun testIdsSuccess() {
val correctString = "2ddb3a34-0699-4126-b7a5-38603e665928"
Assert.assertEquals(correctString, IdChecker.checkAndReturnPlayerUuid(correctString))
Assert.assertEquals("c872b8e0-f274-47d4-b761-ce684c5d224c", IdChecker.checkAndReturnGameUuid("c872b8e0-f274-47d4-b761-ce684c5d224c"))
Assert.assertEquals(correctString, IdChecker.checkAndReturnGameUuid("G-" + correctString + "-2"))
Assert.assertEquals(correctString, IdChecker.checkAndReturnPlayerUuid("P-" + correctString + "-2"))
}
@Test(expected = IllegalArgumentException::class)
fun testIdFailure1() {
IdChecker.checkAndReturnGameUuid("2ddb3a34-0699-4126-b7a5-38603e66592") // too short
}
@Test(expected = IllegalArgumentException::class)
fun testIdFailure2() {
IdChecker.checkAndReturnGameUuid("P-2ddb3a34-0699-4126-b7a5-38603e665928-2") // wrong prefix
}
@Test(expected = IllegalArgumentException::class)
fun testIdFailure3() {
IdChecker.checkAndReturnPlayerUuid("G-2ddb3a34-0699-4126-b7a5-38603e665928-2") // wrong prefix
}
@Test(expected = IllegalArgumentException::class)
fun testIdFailure4() {
IdChecker.checkAndReturnGameUuid("G-2ddb3a34-0699-4126-b7a5-38603e665928-3") // changed checkDigit
}
@Test(expected = IllegalArgumentException::class)
fun testIdFailure5() {
IdChecker.checkAndReturnGameUuid("G-2ddb3a34-0699-4126-b7a5-48603e665928-2") // changed uuid without changing checkdigit
}
}