1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.libraries.pcc.chronicle.api.policy.capabilities 18 19 import com.android.libraries.pcc.chronicle.api.policy.annotation.Annotation 20 21 /** A base class for all the store capabilities. */ 22 sealed class Capability(val tag: String) { 23 enum class Comparison { LessStrict, Equivalent, Stricter } 24 isEquivalentnull25 open fun isEquivalent(other: Capability): Boolean { 26 return when (other) { 27 is Range -> toRange().isEquivalent(other) 28 else -> compare(other) == Comparison.Equivalent 29 } 30 } 31 containsnull32 open fun contains(other: Capability): Boolean { 33 return when (other) { 34 is Range -> toRange().contains(other) 35 else -> isEquivalent(other) 36 } 37 } isLessStrictnull38 fun isLessStrict(other: Capability) = compare(other) == Comparison.LessStrict 39 fun isSameOrLessStrict(other: Capability) = compare(other) != Comparison.Stricter 40 fun isStricter(other: Capability) = compare(other) == Comparison.Stricter 41 fun isSameOrStricter(other: Capability) = compare(other) != Comparison.LessStrict 42 43 fun compare(other: Capability): Comparison { 44 if (tag != other.tag) throw IllegalArgumentException("Cannot compare different Capabilities") 45 return when (this) { 46 is Persistence -> compare(other as Persistence) 47 is Encryption -> compare(other as Encryption) 48 is Ttl -> compare(other as Ttl) 49 is Queryable -> compare(other as Queryable) 50 is Shareable -> compare(other as Shareable) 51 is Range -> throw UnsupportedOperationException( 52 "Capability.Range comparison not supported yet." 53 ) 54 } 55 } 56 57 /** 58 * Returns its own tag if this is an individual capability, or the tag of the inner capability, 59 * if this is a range. 60 */ getRealTagnull61 fun getRealTag(): String { 62 return when (tag) { 63 Capability.Range.TAG -> (this as Capability.Range).min.tag 64 else -> tag 65 } 66 } 67 isCompatiblenull68 fun isCompatible(other: Capability): Boolean { 69 return getRealTag() == other.getRealTag() 70 } 71 toRangenull72 open fun toRange() = Range(this, this) 73 74 /** Capability describing persistence requirement for the store. */ 75 data class Persistence(val kind: Kind) : Capability(TAG) { 76 enum class Kind { None, InMemory, OnDisk, Unrestricted } 77 78 fun compare(other: Persistence): Comparison { 79 return when { 80 kind.ordinal < other.kind.ordinal -> Comparison.Stricter 81 kind.ordinal > other.kind.ordinal -> Comparison.LessStrict 82 else -> Comparison.Equivalent 83 } 84 } 85 86 companion object { 87 const val TAG = "persistence" 88 val UNRESTRICTED = Persistence(Kind.Unrestricted) 89 val ON_DISK = Persistence(Kind.OnDisk) 90 val IN_MEMORY = Persistence(Kind.InMemory) 91 val NONE = Persistence(Kind.None) 92 93 val ANY = Range(Persistence.UNRESTRICTED, Persistence.NONE) 94 95 fun fromAnnotations(annotations: List<Annotation>): Persistence? { 96 val kinds = mutableSetOf<Kind>() 97 for (annotation in annotations) { 98 when (annotation.name) { 99 "onDisk", "persistent" -> { 100 if (annotation.params.size > 0) { 101 throw IllegalArgumentException( 102 "Unexpected parameter for $annotation.name capability annotation" 103 ) 104 } 105 kinds.add(Kind.OnDisk) 106 } 107 "inMemory", "tiedToArc", "tiedToRuntime" -> { 108 if (annotation.params.size > 0) { 109 throw IllegalArgumentException( 110 "Unexpected parameter for $annotation.name capability annotation" 111 ) 112 } 113 kinds.add(Kind.InMemory) 114 } 115 } 116 } 117 return when (kinds.size) { 118 0 -> null 119 1 -> Persistence(kinds.elementAt(0)) 120 else -> throw IllegalArgumentException( 121 "Containing multiple persistence capabilities: $annotations" 122 ) 123 } 124 } 125 } 126 } 127 128 /** Capability describing retention policy of the store. */ 129 sealed class Ttl(count: Int, val isInfinite: Boolean = false) : Capability(TAG) { 130 /** Number of minutes for retention, or -1 for infinite. */ 131 val minutes: Int = count * when (this) { 132 is Minutes -> 1 133 is Hours -> 60 134 is Days -> 60 * 24 135 is Infinite -> 1 // returns -1 because Infinite `count` is -1. 136 } 137 138 /** Number of milliseconds for retention, or -1 for infinite. */ 139 val millis: Long = if (this is Infinite) -1 else minutes * MILLIS_IN_MIN 140 141 init { <lambda>null142 require(count > 0 || isInfinite) { 143 "must be either positive count or infinite, " + 144 "but got count=$count and isInfinite=$isInfinite" 145 } 146 } 147 comparenull148 fun compare(other: Ttl): Comparison { 149 return when { 150 (isInfinite && other.isInfinite) || millis == other.millis -> Comparison.Equivalent 151 isInfinite -> Comparison.LessStrict 152 other.isInfinite -> Comparison.Stricter 153 millis < other.millis -> Comparison.Stricter 154 else -> Comparison.LessStrict 155 } 156 } 157 158 data class Minutes(val count: Int) : Ttl(count) 159 data class Hours(val count: Int) : Ttl(count) 160 data class Days(val count: Int) : Ttl(count) 161 data class Infinite(val count: Int = TTL_INFINITE) : Ttl(count, true) 162 163 companion object { 164 const val TAG = "ttl" 165 const val UNINITIALIZED_TIMESTAMP: Long = -1 166 const val TTL_INFINITE = -1 167 const val MILLIS_IN_MIN = 60 * 1000L 168 169 val ANY = Range(Ttl.Infinite(), Ttl.Minutes(1)) 170 171 private val TTL_PATTERN = 172 "^([0-9]+)[ ]*(day[s]?|hour[s]?|minute[s]?|[d|h|m])$".toRegex() 173 fromStringnull174 fun fromString(ttlStr: String): Ttl { 175 val ttlMatch = requireNotNull(TTL_PATTERN.matchEntire(ttlStr.trim())) { 176 "Invalid TTL $ttlStr." 177 } 178 val (_, count, units) = ttlMatch.groupValues 179 // Note: consider using idiomatic KT types: 180 // https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.time/-duration-unit/ 181 return when (units.trim()) { 182 "m", "minute", "minutes" -> Ttl.Minutes(count.toInt()) 183 "h", "hour", "hours" -> Ttl.Hours(count.toInt()) 184 "d", "day", "days" -> Ttl.Days(count.toInt()) 185 else -> throw IllegalStateException("Invalid TTL units: $units") 186 } 187 } 188 fromAnnotationsnull189 fun fromAnnotations(annotations: List<Annotation>): Ttl? { 190 val ttls = annotations.filter { it.name == "ttl" } 191 return when (ttls.size) { 192 0 -> null 193 1 -> { 194 if (ttls.elementAt(0).params.size > 1) { 195 throw IllegalArgumentException("Unexpected parameter for Ttl Capability annotation") 196 } 197 Capability.Ttl.fromString(ttls.elementAt(0).getStringParam("value")) 198 } 199 else -> throw IllegalArgumentException( 200 "Containing multiple ttl capabilities: $annotations" 201 ) 202 } 203 } 204 } 205 } 206 207 /** Capability describing whether the store needs to be encrypted. */ 208 data class Encryption(val value: Boolean) : Capability(TAG) { comparenull209 fun compare(other: Encryption): Comparison { 210 return when { 211 value == other.value -> Comparison.Equivalent 212 value -> Comparison.Stricter 213 else -> Comparison.LessStrict 214 } 215 } 216 217 companion object { 218 const val TAG = "encryption" 219 val ANY = Range(Encryption(false), Encryption(true)) 220 fromAnnotationsnull221 fun fromAnnotations(annotations: List<Annotation>): Encryption? { 222 val filtered = annotations.filter { it.name == "encrypted" } 223 return when (filtered.size) { 224 0 -> null 225 1 -> { 226 if (filtered.elementAt(0).params.size > 0) { 227 throw IllegalArgumentException("Unexpected parameter for Encryption annotation") 228 } 229 Capability.Encryption(true) 230 } 231 else -> throw IllegalArgumentException( 232 "Containing multiple encryption capabilities: $annotations" 233 ) 234 } 235 } 236 } 237 } 238 239 /** Capability describing whether the store needs to be queryable. */ 240 data class Queryable(val value: Boolean) : Capability(TAG) { comparenull241 fun compare(other: Queryable): Comparison { 242 return when { 243 value == other.value -> Comparison.Equivalent 244 value -> Comparison.Stricter 245 else -> Comparison.LessStrict 246 } 247 } 248 249 companion object { 250 const val TAG = "queryable" 251 val ANY = Range(Queryable(false), Queryable(true)) 252 fromAnnotationsnull253 fun fromAnnotations(annotations: List<Annotation>): Queryable? { 254 val filtered = annotations.filter { it.name == "queryable" } 255 return when (filtered.size) { 256 0 -> null 257 1 -> { 258 if (filtered.elementAt(0).params.size > 0) { 259 throw IllegalArgumentException("Unexpected parameter for Queryable annotation") 260 } 261 Capability.Queryable(true) 262 } 263 else -> throw IllegalArgumentException( 264 "Containing multiple queryable capabilities: $annotations" 265 ) 266 } 267 } 268 } 269 } 270 271 /** Capability describing whether the store needs to be shareable across arcs. */ 272 data class Shareable(val value: Boolean) : Capability(TAG) { comparenull273 fun compare(other: Shareable): Comparison { 274 return when { 275 value == other.value -> Comparison.Equivalent 276 value -> Comparison.Stricter 277 else -> Comparison.LessStrict 278 } 279 } 280 281 companion object { 282 const val TAG = "shareable" 283 val ANY = Range(Shareable(false), Shareable(true)) 284 fromAnnotationsnull285 fun fromAnnotations(annotations: List<Annotation>): Shareable? { 286 val filtered = annotations.filter { 287 arrayOf("shareable", "tiedToRuntime").contains(it.name) 288 } 289 return when (filtered.size) { 290 0 -> null 291 1 -> { 292 if (filtered.elementAt(0).params.size > 0) { 293 throw IllegalArgumentException("Unexpected parameter for Shareable annotation") 294 } 295 Capability.Shareable(true) 296 } 297 else -> throw IllegalArgumentException( 298 "Containing multiple shareable capabilities: $annotations" 299 ) 300 } 301 } 302 } 303 } 304 305 data class Range(val min: Capability, val max: Capability) : Capability(TAG) { 306 init { <lambda>null307 require(min.isSameOrLessStrict(max)) { 308 "Minimum capability in a range must be equivalent or less strict than maximum." 309 } 310 } 311 isEquivalentnull312 override fun isEquivalent(other: Capability): Boolean { 313 return when (other) { 314 is Range -> min.isEquivalent(other.min) && max.isEquivalent(other.max) 315 else -> min.isEquivalent(other) && max.isEquivalent(other) 316 } 317 } 318 containsnull319 override fun contains(other: Capability): Boolean { 320 return when (other) { 321 is Range -> 322 min.isSameOrLessStrict(other.min) && max.isSameOrStricter(other.max) 323 else -> min.isSameOrLessStrict(other) && max.isSameOrStricter(other) 324 } 325 } 326 toRangenull327 override fun toRange() = this 328 329 companion object { 330 const val TAG = "range" 331 } 332 } 333 } 334