blob: 9ce07391f9fa7e7a43632ca31cfbf8beed2e1a98 [file] [log] [blame]
/*
* Copyright (C) 2016 Square, Inc.
*
* 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 okio
import kotlin.jvm.JvmStatic
/** An indexed set of values that may be read with [BufferedSource.select]. */
class Options private constructor(
internal val byteStrings: Array<out ByteString>,
internal val trie: IntArray
) : AbstractList<ByteString>(), RandomAccess {
override val size: Int
get() = byteStrings.size
override fun get(index: Int) = byteStrings[index]
companion object {
@JvmStatic
fun of(vararg byteStrings: ByteString): Options {
if (byteStrings.isEmpty()) {
// With no choices we must always return -1. Create a trie that selects from an empty set.
return Options(arrayOf(), intArrayOf(0, -1))
}
// Sort the byte strings which is required when recursively building the trie. Map the sorted
// indexes to the caller's indexes.
val list = byteStrings.toMutableList()
list.sort()
val indexes = mutableListOf(*byteStrings.map { -1 }.toTypedArray())
byteStrings.forEachIndexed { callerIndex, byteString ->
val sortedIndex = list.binarySearch(byteString)
indexes[sortedIndex] = callerIndex
}
require(list[0].size > 0) { "the empty byte string is not a supported option" }
// Strip elements that will never be returned because they follow their own prefixes. For
// example, if the caller provides ["abc", "abcde"] we will never return "abcde" because we
// return as soon as we encounter "abc".
var a = 0
while (a < list.size) {
val prefix = list[a]
var b = a + 1
while (b < list.size) {
val byteString = list[b]
if (!byteString.startsWith(prefix)) break
require(byteString.size != prefix.size) { "duplicate option: $byteString" }
if (indexes[b] > indexes[a]) {
list.removeAt(b)
indexes.removeAt(b)
} else {
b++
}
}
a++
}
val trieBytes = Buffer()
buildTrieRecursive(node = trieBytes, byteStrings = list, indexes = indexes)
val trie = IntArray(trieBytes.intCount.toInt())
var i = 0
while (!trieBytes.exhausted()) {
trie[i++] = trieBytes.readInt()
}
return Options(byteStrings.copyOf() /* Defensive copy. */, trie)
}
/**
* Builds a trie encoded as an int array. Nodes in the trie are of two types: SELECT and SCAN.
*
* SELECT nodes are encoded as:
* - selectChoiceCount: the number of bytes to choose between (a positive int)
* - prefixIndex: the result index at the current position or -1 if the current position is not
* a result on its own
* - a sorted list of selectChoiceCount bytes to match against the input string
* - a heterogeneous list of selectChoiceCount result indexes (>= 0) or offsets (< 0) of the
* next node to follow. Elements in this list correspond to elements in the preceding list.
* Offsets are negative and must be multiplied by -1 before being used.
*
* SCAN nodes are encoded as:
* - scanByteCount: the number of bytes to match in sequence. This count is negative and must
* be multiplied by -1 before being used.
* - prefixIndex: the result index at the current position or -1 if the current position is not
* a result on its own
* - a list of scanByteCount bytes to match
* - nextStep: the result index (>= 0) or offset (< 0) of the next node to follow. Offsets are
* negative and must be multiplied by -1 before being used.
*
* This structure is used to improve locality and performance when selecting from a list of
* options.
*/
private fun buildTrieRecursive(
nodeOffset: Long = 0L,
node: Buffer,
byteStringOffset: Int = 0,
byteStrings: List<ByteString>,
fromIndex: Int = 0,
toIndex: Int = byteStrings.size,
indexes: List<Int>
) {
require(fromIndex < toIndex)
for (i in fromIndex until toIndex) {
require(byteStrings[i].size >= byteStringOffset)
}
var fromIndex = fromIndex
var from = byteStrings[fromIndex]
val to = byteStrings[toIndex - 1]
var prefixIndex = -1
// If the first element is already matched, that's our prefix.
if (byteStringOffset == from.size) {
prefixIndex = indexes[fromIndex]
fromIndex++
from = byteStrings[fromIndex]
}
if (from[byteStringOffset] != to[byteStringOffset]) {
// If we have multiple bytes to choose from, encode a SELECT node.
var selectChoiceCount = 1
for (i in fromIndex + 1 until toIndex) {
if (byteStrings[i - 1][byteStringOffset] != byteStrings[i][byteStringOffset]) {
selectChoiceCount++
}
}
// Compute the offset that childNodes will get when we append it to node.
val childNodesOffset = nodeOffset + node.intCount + 2 + (selectChoiceCount * 2)
node.writeInt(selectChoiceCount)
node.writeInt(prefixIndex)
for (i in fromIndex until toIndex) {
val rangeByte = byteStrings[i][byteStringOffset]
if (i == fromIndex || rangeByte != byteStrings[i - 1][byteStringOffset]) {
node.writeInt(rangeByte and 0xff)
}
}
val childNodes = Buffer()
var rangeStart = fromIndex
while (rangeStart < toIndex) {
val rangeByte = byteStrings[rangeStart][byteStringOffset]
var rangeEnd = toIndex
for (i in rangeStart + 1 until toIndex) {
if (rangeByte != byteStrings[i][byteStringOffset]) {
rangeEnd = i
break
}
}
if (rangeStart + 1 == rangeEnd &&
byteStringOffset + 1 == byteStrings[rangeStart].size
) {
// The result is a single index.
node.writeInt(indexes[rangeStart])
} else {
// The result is another node.
node.writeInt(-1 * (childNodesOffset + childNodes.intCount).toInt())
buildTrieRecursive(
nodeOffset = childNodesOffset,
node = childNodes,
byteStringOffset = byteStringOffset + 1,
byteStrings = byteStrings,
fromIndex = rangeStart,
toIndex = rangeEnd,
indexes = indexes
)
}
rangeStart = rangeEnd
}
node.writeAll(childNodes)
} else {
// If all of the bytes are the same, encode a SCAN node.
var scanByteCount = 0
for (i in byteStringOffset until minOf(from.size, to.size)) {
if (from[i] == to[i]) {
scanByteCount++
} else {
break
}
}
// Compute the offset that childNodes will get when we append it to node.
val childNodesOffset = nodeOffset + node.intCount + 2 + scanByteCount + 1
node.writeInt(-scanByteCount)
node.writeInt(prefixIndex)
for (i in byteStringOffset until byteStringOffset + scanByteCount) {
node.writeInt(from[i] and 0xff)
}
if (fromIndex + 1 == toIndex) {
// The result is a single index.
check(byteStringOffset + scanByteCount == byteStrings[fromIndex].size)
node.writeInt(indexes[fromIndex])
} else {
// The result is another node.
val childNodes = Buffer()
node.writeInt(-1 * (childNodesOffset + childNodes.intCount).toInt())
buildTrieRecursive(
nodeOffset = childNodesOffset,
node = childNodes,
byteStringOffset = byteStringOffset + scanByteCount,
byteStrings = byteStrings,
fromIndex = fromIndex,
toIndex = toIndex,
indexes = indexes
)
node.writeAll(childNodes)
}
}
}
private val Buffer.intCount get() = size / 4
}
}