From 96cfd71e60c9bc05f546ffe596019da118d2d482 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 3 Jul 2023 20:11:01 +0200 Subject: [PATCH] Work on API structure and first baby steps of formatting the Authenticate output --- .../kotlin/org/pgpainless/wot/cli/WotCLI.kt | 51 ++++++++---- .../wot/cli/subcommands/AuthenticateCmd.kt | 41 +++++++++- .../wot/cli/subcommands/IdentifyCmd.kt | 3 +- .../pgpainless/wot/cli/subcommands/ListCmd.kt | 3 +- .../wot/cli/subcommands/LookupCmd.kt | 3 +- .../pgpainless/wot/cli/subcommands/PathCmd.kt | 3 +- .../cli/subcommands/AuthenticateCmdTest.kt | 58 ++++++++++++++ .../org/pgpainless/wot/api/AuthenticateAPI.kt | 38 ++++++++- .../pgpainless/wot/api/AuthenticationLevel.kt | 25 ++++++ .../org/pgpainless/wot/api/IdentifyAPI.kt | 10 ++- .../kotlin/org/pgpainless/wot/api/ListAPI.kt | 7 +- .../org/pgpainless/wot/api/LookupAPI.kt | 9 ++- .../kotlin/org/pgpainless/wot/api/PathAPI.kt | 24 +++++- .../kotlin/org/pgpainless/wot/api/WoTAPI.kt | 77 +++++++++++++++++++ .../org/pgpainless/wot/dijkstra/sq/Paths.kt | 13 +++- 15 files changed, 330 insertions(+), 35 deletions(-) create mode 100644 pgpainless-wot-cli/src/test/kotlin/org/pgpainless/wot/cli/subcommands/AuthenticateCmdTest.kt create mode 100644 pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/AuthenticationLevel.kt create mode 100644 pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/WoTAPI.kt diff --git a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/WotCLI.kt b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/WotCLI.kt index ea89898f..5469fb20 100644 --- a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/WotCLI.kt +++ b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/WotCLI.kt @@ -6,9 +6,10 @@ package org.pgpainless.wot.cli import org.pgpainless.certificate_store.PGPainlessCertD import org.pgpainless.util.DateUtil +import org.pgpainless.wot.api.WoTAPI import org.pgpainless.wot.cli.subcommands.* +import org.pgpainless.wot.dijkstra.sq.Fingerprint import org.pgpainless.wot.dijkstra.sq.ReferenceTime -import pgp.cert_d.PGPCertificateDirectory import pgp.cert_d.PGPCertificateStoreAdapter import pgp.cert_d.subkey_lookup.InMemorySubkeyLookupFactory import pgp.certificate_store.PGPCertificateStore @@ -65,25 +66,32 @@ class WotCLI: Callable { @Option(names = ["--time"], description = ["Reference time."]) var time: String? = null - fun getReferenceTime(): ReferenceTime { - return if (time == null) { - ReferenceTime.now() - } else { - val date = DateUtil.parseUTCDate(time) - ReferenceTime.timestamp(date) + private val referenceTime: ReferenceTime + get() { + return if (time == null) { + ReferenceTime.now() + } else { + val date = DateUtil.parseUTCDate(time) + ReferenceTime.timestamp(date) + } } - } - fun getCertificateStore(): PGPCertificateStore { - requireNotNull(certificateSource.pgpCertD) { - "Currently, only --cert-d is supported." + private val certificateStore: PGPCertificateStore + get() { + requireNotNull(certificateSource.pgpCertD) { + "Currently, only --cert-d is supported." + } + val certD = PGPainlessCertD.fileBased( + certificateSource.pgpCertD, + InMemorySubkeyLookupFactory()) + + return PGPCertificateStoreAdapter(certD) } - val certD = PGPainlessCertD.fileBased( - certificateSource.pgpCertD, - InMemorySubkeyLookupFactory()) - return PGPCertificateStoreAdapter(certD) - } + private val trustRoots: List + get() { + return trustRoot.map { Fingerprint(it) } + } /** * Execute the command. @@ -99,6 +107,17 @@ class WotCLI: Callable { return 0 } + val api: WoTAPI + get() { + return WoTAPI( + certStores = listOf(certificateStore), + trustRoots = trustRoots, + gossip = false, + certificationNetwork = false, + trustAmount = amount, + referenceTime = referenceTime) + } + companion object { @JvmStatic fun main(args: Array): Unit = exitProcess(CommandLine(WotCLI()).execute(*args)) diff --git a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/AuthenticateCmd.kt b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/AuthenticateCmd.kt index ac091996..b87c86f9 100644 --- a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/AuthenticateCmd.kt +++ b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/AuthenticateCmd.kt @@ -6,10 +6,13 @@ package org.pgpainless.wot.cli.subcommands import org.pgpainless.wot.api.AuthenticateAPI import org.pgpainless.wot.cli.WotCLI +import org.pgpainless.wot.dijkstra.sq.Fingerprint +import org.pgpainless.wot.dijkstra.sq.Path import picocli.CommandLine import picocli.CommandLine.Command import picocli.CommandLine.Parameters import picocli.CommandLine.ParentCommand +import java.text.SimpleDateFormat import java.util.concurrent.Callable @Command(name = "authenticate") @@ -27,12 +30,46 @@ class AuthenticateCmd: Callable { @CommandLine.Option(names = ["--email"], description = ["Consider all user-IDs that contain the given email address."]) var email = false + private val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd") + /** * Execute the command. * @return exit code */ override fun call(): Int { - val api = AuthenticateAPI() - TODO("Not yet implemented") + val result = parent.api.authenticate(AuthenticateAPI.Arguments( + Fingerprint(fingerprint), userId, email)) + print(formatResult(result)) + if (result.percentage < 100) { + return -1 + } + return 0 + } + + internal fun formatResult(result: AuthenticateAPI.Result): String { + if (result.percentage < 100) { + return "No paths found." + } + + val sb = StringBuilder() + sb.appendLine("[✓] ${result.fingerprint} ${result.userId}: fully authenticated (${result.percentage}%)") + for ((pIndex, path: Path) in result.paths.paths.withIndex()) { + sb.appendLine(" Path #${pIndex + 1} of ${result.paths.paths.size}, trust amount ${path.amount}:") + for ((cIndex, certification) in path.certifications.withIndex()) { + val issuerUserId = certification.issuer.userIds.keys.firstOrNull()?.let { " (\"${it}\")" } ?: "" + when (cIndex) { + 0 -> { + sb.appendLine(" ◯ ${certification.issuer.fingerprint}${issuerUserId}") + } + else -> { + sb.appendLine(" ├ ${certification.issuer.fingerprint}${issuerUserId}") + } + } + sb.appendLine(" │ certified the following binding on ${dateFormat.format(certification.creationTime)}") + } + sb.appendLine(" └ ${result.fingerprint} \"${result.userId}\"") + } + + return sb.toString() } } \ No newline at end of file diff --git a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/IdentifyCmd.kt b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/IdentifyCmd.kt index 7eaef04a..0011d30e 100644 --- a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/IdentifyCmd.kt +++ b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/IdentifyCmd.kt @@ -4,7 +4,6 @@ package org.pgpainless.wot.cli.subcommands -import org.pgpainless.wot.api.IdentifyAPI import org.pgpainless.wot.cli.WotCLI import picocli.CommandLine import picocli.CommandLine.Command @@ -26,7 +25,7 @@ class IdentifyCmd: Callable { * @return exit code */ override fun call(): Int { - val api = IdentifyAPI() + val api = parent.api TODO("Not yet implemented") } diff --git a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/ListCmd.kt b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/ListCmd.kt index 1d42a5fb..978d0cbb 100644 --- a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/ListCmd.kt +++ b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/ListCmd.kt @@ -4,7 +4,6 @@ package org.pgpainless.wot.cli.subcommands -import org.pgpainless.wot.api.ListAPI import org.pgpainless.wot.cli.WotCLI import picocli.CommandLine import picocli.CommandLine.Command @@ -22,7 +21,7 @@ class ListCmd: Callable { * @return exit code */ override fun call(): Int { - val api = ListAPI() + val api = parent.api TODO("Not yet implemented") } } \ No newline at end of file diff --git a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/LookupCmd.kt b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/LookupCmd.kt index 2209335d..4725b9e1 100644 --- a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/LookupCmd.kt +++ b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/LookupCmd.kt @@ -4,7 +4,6 @@ package org.pgpainless.wot.cli.subcommands -import org.pgpainless.wot.api.LookupAPI import org.pgpainless.wot.cli.WotCLI import picocli.CommandLine.* import java.util.concurrent.Callable @@ -27,7 +26,7 @@ class LookupCmd: Callable { * @return exit code */ override fun call(): Int { - val api = LookupAPI() + val api = parent.api TODO("Not yet implemented") } } \ No newline at end of file diff --git a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/PathCmd.kt b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/PathCmd.kt index a9048ae3..1120b11f 100644 --- a/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/PathCmd.kt +++ b/pgpainless-wot-cli/src/main/kotlin/org/pgpainless/wot/cli/subcommands/PathCmd.kt @@ -4,7 +4,6 @@ package org.pgpainless.wot.cli.subcommands -import org.pgpainless.wot.api.PathAPI import org.pgpainless.wot.cli.WotCLI import picocli.CommandLine import picocli.CommandLine.Command @@ -29,7 +28,7 @@ class PathCmd: Callable { * @return exit code */ override fun call(): Int { - val api = PathAPI() + val api = parent.api TODO("Not yet implemented") } } \ No newline at end of file diff --git a/pgpainless-wot-cli/src/test/kotlin/org/pgpainless/wot/cli/subcommands/AuthenticateCmdTest.kt b/pgpainless-wot-cli/src/test/kotlin/org/pgpainless/wot/cli/subcommands/AuthenticateCmdTest.kt new file mode 100644 index 00000000..e1e4072c --- /dev/null +++ b/pgpainless-wot-cli/src/test/kotlin/org/pgpainless/wot/cli/subcommands/AuthenticateCmdTest.kt @@ -0,0 +1,58 @@ +package org.pgpainless.wot.cli.subcommands + +import org.junit.jupiter.api.Test +import org.pgpainless.wot.api.AuthenticateAPI +import org.pgpainless.wot.dijkstra.sq.* +import java.text.SimpleDateFormat +import kotlin.test.assertEquals + +class AuthenticateCmdTest { + + @Test + fun testFormatting() { + val dateFormat = SimpleDateFormat("yyyy-MM-dd") + val cmd = AuthenticateCmd() + val paths = Paths() + val neal = CertSynopsis( + Fingerprint("F7173B3C7C685CD9ECC4191B74E445BA0E15C957"), + null, + RevocationState.notRevoked(), + mapOf( + Pair("Neal H. Walfield (Code Signing Key) ", RevocationState.notRevoked()) + ) + ) + val justus = CertSynopsis( + Fingerprint("CBCD8F030588653EEDD7E2659B7DD433F254904A"), + null, + RevocationState.notRevoked(), + mapOf( + Pair("Justus Winter ", RevocationState.notRevoked()) + ) + ) + val certification = Certification( + neal, + justus, + "Justus Winter ", + dateFormat.parse("2022-02-04"), + null, + true, + 120, + Depth.limited(0), + RegexSet.wildcard()) + paths.add(Path(neal, mutableListOf(certification), Depth.auto(0)), 120) + val testResult = AuthenticateAPI.Result( + Fingerprint("CBCD8F030588653EEDD7E2659B7DD433F254904A"), + "Justus Winter ", + 120, + paths) + + val formatted = cmd.formatResult(testResult) + assertEquals(buildString { + append("[✓] CBCD8F030588653EEDD7E2659B7DD433F254904A Justus Winter : fully authenticated (100%)\n") + append(" Path #1 of 1, trust amount 120:\n") + append(" ◯ F7173B3C7C685CD9ECC4191B74E445BA0E15C957 (\"Neal H. Walfield (Code Signing Key) \")\n") + append(" │ certified the following binding on 2022-02-04\n") + append(" └ CBCD8F030588653EEDD7E2659B7DD433F254904A \"Justus Winter \"\n") + }, formatted) + } +} \ No newline at end of file diff --git a/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/AuthenticateAPI.kt b/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/AuthenticateAPI.kt index ac652d53..5c5a7b1f 100644 --- a/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/AuthenticateAPI.kt +++ b/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/AuthenticateAPI.kt @@ -4,10 +4,46 @@ package org.pgpainless.wot.api +import org.pgpainless.wot.dijkstra.sq.Fingerprint +import org.pgpainless.wot.dijkstra.sq.Paths +import org.pgpainless.wot.dijkstra.sq.ReferenceTime + /** * Authenticate a binding. * A binding is a pair consisting of a certificate and a User ID. */ -class AuthenticateAPI { +interface AuthenticateAPI { + + /** + * Authenticate the binding between a fingerprint and a given userId. + * + * @param arguments arguments + */ + fun authenticate(arguments: Arguments): Result + + /** + * Bundle for arguments to the authenticate operation. + * @param fingerprint fingerprint of the certificate + * @param userId user-ID for which we want to authenticate a binding to the certificate + * @param email if true, consider [userId] to be an email address and consider all bindings containing it + */ + data class Arguments( + var fingerprint: Fingerprint, + var userId: String, + var email: Boolean = false) + + /** + * Authentication result. + * @param targetAmount the targeted trust amount required to achieve full authentication + * @param paths the number of paths + */ + data class Result(val fingerprint: Fingerprint, val userId: String, private val targetAmount: Int, val paths: Paths) { + + /** + * Percentage of authentication. 100% means fully authenticated binding. + */ + val percentage: Int + get() = paths.amount * 100 / targetAmount + } } diff --git a/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/AuthenticationLevel.kt b/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/AuthenticationLevel.kt new file mode 100644 index 00000000..07303a00 --- /dev/null +++ b/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/AuthenticationLevel.kt @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.wot.api + +/** + * Enum for different levels of Trust. + */ +enum class AuthenticationLevel(val amount: Int) { + /** + * With an amount of 40, a binding is considered partially trusted. + */ + Partially(40), + + /** + * An amount if 120 is sufficient to fully authenticate a binding. + */ + Fully(120), + + /** + * A trust amount of 240 means the binding is doubly authenticated. + */ + Doubly(240) +} \ No newline at end of file diff --git a/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/IdentifyAPI.kt b/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/IdentifyAPI.kt index 9ea4f064..647fdafb 100644 --- a/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/IdentifyAPI.kt +++ b/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/IdentifyAPI.kt @@ -4,6 +4,14 @@ package org.pgpainless.wot.api -class IdentifyAPI { +import org.pgpainless.wot.dijkstra.sq.Fingerprint +import org.pgpainless.wot.dijkstra.sq.Paths +interface IdentifyAPI { + + fun identify(arguments: Arguments): Result + + data class Arguments(val fingerprint: Fingerprint) + + data class Result(val paths: Paths) } diff --git a/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/ListAPI.kt b/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/ListAPI.kt index 88b253e6..cf64e056 100644 --- a/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/ListAPI.kt +++ b/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/ListAPI.kt @@ -4,6 +4,11 @@ package org.pgpainless.wot.api -class ListAPI { +import org.pgpainless.wot.dijkstra.sq.Paths +interface ListAPI { + + fun list(): Result + + data class Result(val paths: Paths) } diff --git a/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/LookupAPI.kt b/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/LookupAPI.kt index 55816d8c..09ff9f75 100644 --- a/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/LookupAPI.kt +++ b/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/LookupAPI.kt @@ -4,6 +4,13 @@ package org.pgpainless.wot.api -class LookupAPI { +import org.pgpainless.wot.dijkstra.sq.Paths +interface LookupAPI { + + fun lookup(arguments: Arguments): Result + + data class Arguments(val userId: String, val email: Boolean = false) + + data class Result(val paths: Paths) } diff --git a/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/PathAPI.kt b/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/PathAPI.kt index cac3b84c..01faa9f9 100644 --- a/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/PathAPI.kt +++ b/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/PathAPI.kt @@ -4,6 +4,28 @@ package org.pgpainless.wot.api -class PathAPI { +import org.pgpainless.wot.dijkstra.sq.Fingerprint +interface PathAPI { + + fun path(arguments: Arguments): Result + + data class Arguments(val rootFingerprint: Fingerprint, val pathFingerprints: List, val userId: String) + + interface Result { + + fun isSuccess(): Boolean + + class Success: Result { + override fun isSuccess(): Boolean { + return true + } + } + + data class Failure(val information: List): Result { + override fun isSuccess(): Boolean { + return false + } + } + } } diff --git a/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/WoTAPI.kt b/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/WoTAPI.kt new file mode 100644 index 00000000..ded80e0f --- /dev/null +++ b/pgpainless-wot/src/main/kotlin/org/pgpainless/wot/api/WoTAPI.kt @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.wot.api + +import org.pgpainless.certificate_store.PGPainlessCertD +import org.pgpainless.util.NotationRegistry +import org.pgpainless.wot.dijkstra.sq.Fingerprint +import org.pgpainless.wot.dijkstra.sq.ReferenceTime +import pgp.cert_d.PGPCertificateStoreAdapter +import pgp.cert_d.subkey_lookup.InMemorySubkeyLookupFactory +import pgp.certificate_store.PGPCertificateStore + +/** + * Web of Trust API, offering different operations. + * + * @param certStores one or more [PGPCertificateStores][PGPCertificateStore] to retrieve certificates from. + * @param trustRoots one or more [Fingerprints][Fingerprint] of trust-roots. + * @param gossip if true, consider all certificates as weakly trusted trust-roots + * @param certificationNetwork if true, all certifications are treated as delegations with infinite trust depth and no regular expressions + * @param trustAmount minimum trust amount + * @param referenceTime reference time at which the web of trust is evaluated + * @param knownNotationRegistry registry of known notations + */ +class WoTAPI( + val certStores: List = getDefaultCertStores(), + val trustRoots: List, + val gossip: Boolean = false, + val certificationNetwork: Boolean = false, + val trustAmount: Int = AuthenticationLevel.Fully.amount, + val referenceTime: ReferenceTime = ReferenceTime.now(), + val knownNotationRegistry: NotationRegistry = NotationRegistry() +): AuthenticateAPI, IdentifyAPI, ListAPI, LookupAPI, PathAPI { + + /** + * Secondary constructor, taking an [AuthenticationLevel] instead of an [Int]. + */ + constructor(certStores: List = getDefaultCertStores(), + trustRoots: List, + gossip: Boolean = false, + certificationNetwork: Boolean = false, + trustAmount: AuthenticationLevel = AuthenticationLevel.Fully, + referenceTime: ReferenceTime = ReferenceTime.now(), + knownNotationRegistry: NotationRegistry = NotationRegistry()): + this(certStores,trustRoots, gossip,certificationNetwork, trustAmount.amount, referenceTime, knownNotationRegistry) + + companion object { + @JvmStatic + fun getDefaultCertStores(): List { + val certD = PGPainlessCertD.fileBased(InMemorySubkeyLookupFactory()) + val asStore = PGPCertificateStoreAdapter(certD) + return listOf(asStore) + } + } + + override fun authenticate(arguments: AuthenticateAPI.Arguments): AuthenticateAPI.Result { + TODO("Not yet implemented") + } + + override fun identify(arguments: IdentifyAPI.Arguments): IdentifyAPI.Result { + TODO("Not yet implemented") + } + + override fun list(): ListAPI.Result { + TODO("Not yet implemented") + } + + override fun lookup(arguments: LookupAPI.Arguments): LookupAPI.Result { + TODO("Not yet implemented") + } + + override fun path(arguments: PathAPI.Arguments): PathAPI.Result { + TODO("Not yet implemented") + } + +} \ No newline at end of file diff --git a/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/dijkstra/sq/Paths.kt b/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/dijkstra/sq/Paths.kt index 7eea165d..77766f2c 100644 --- a/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/dijkstra/sq/Paths.kt +++ b/wot-dijkstra/src/main/kotlin/org/pgpainless/wot/dijkstra/sq/Paths.kt @@ -7,15 +7,20 @@ package org.pgpainless.wot.dijkstra.sq /** * List of individual [Paths][Path]. * - * @param paths list of paths + * @param _paths list of paths */ -class Paths(private val paths: MutableList) { +class Paths(private val _paths: MutableList) { /** * Empty collection of paths. */ constructor(): this(mutableListOf()) + val paths: List + get() { + return _paths.map { it.path } + } + /** * Add a [Path] to the list. * @@ -25,7 +30,7 @@ class Paths(private val paths: MutableList) { require(amount <= path.amount) { "Amount too small. TODO: Better error message" } - paths.add(Item(path, amount)) + _paths.add(Item(path, amount)) } /** @@ -33,7 +38,7 @@ class Paths(private val paths: MutableList) { */ val amount: Int get() { - return paths.sumOf { it.amount } + return _paths.sumOf { it.amount } } /**