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