Skip to content

Instantly share code, notes, and snippets.

@kyungw00k
Last active July 8, 2024 03:52
Show Gist options
  • Save kyungw00k/b9b65da40c66b1ded27a1644940e86fa to your computer and use it in GitHub Desktop.
Save kyungw00k/b9b65da40c66b1ded27a1644940e86fa to your computer and use it in GitHub Desktop.
A utility functions for encoding and decoding `Sec-Browsing-Topics` HTTP header values

SecBrowsingTopicsHeader Utility

This project provides a utility for encoding and decoding Sec-Browsing-Topics HTTP header values. It is based on the Topics API draft specification, which can be found here.

Overview

The SecBrowsingTopicsHeader object offers two primary functions:

  1. decode: Converts a Sec-Browsing-Topics header string into a list of BrowsingTopic objects.
  2. encode: Converts a list of Topic objects into a Sec-Browsing-Topics header string.

Classes and Functions

BrowsingTopic

A data class representing a browsing topic.

data class BrowsingTopic(
    val taxonomyVersion: Long,
    val modelVersion: Long,
    val topicId: Int,
    val configVersion: String = "chrome.1",
    val version: String = "$configVersion:$modelVersion:$taxonomyVersion"
)

SecBrowsingTopicsHeader

A utility object for encoding and decoding Sec-Browsing-Topics header values.

decode(headerValue: String): List[BrowsingTopic]

Parses the given Sec-Browsing-Topics header string and returns a list of BrowsingTopic objects.

  • Parameters:
    • headerValue: The Sec-Browsing-Topics header string.
  • Returns:
    • A list of BrowsingTopic objects.

encode(topics: List[Topic], configVersion: String = "chrome.1"): String

Encodes a list of Topic objects into a Sec-Browsing-Topics header string.

  • Parameters:
    • topics: The list of Topic objects.
    • configVersion: The configuration version string (default is "adfit.1").
  • Returns:
    • The Sec-Browsing-Topics header string.

Example Usage

import SecBrowsingTopicsHeader.BrowsingTopic

fun main() {
    val headerValue = "(1);v=chrome.1:1:2, ();p=P00000000000"
    val topics = SecBrowsingTopicsHeader.decode(headerValue)
    println(topics)  // Output: [BrowsingTopic(taxonomyVersion=2, modelVersion=1, topicId=1, configVersion=chrome.1)]

    val topicList = listOf(
        BrowsingTopic(1, 1, 1),
        BrowsingTopic(1, 1, 2),
        BrowsingTopic(1, 1, 100)
    )
    val encodedHeader = SecBrowsingTopicsHeader.encode(topicList, "chrome.1")
    println(encodedHeader)  // Output: The encoded header string
}

Installation

Include the necessary dependencies in your project to use this utility.

References

License

This project is licensed under the MIT License.

data class BrowsingTopic (
val taxonomyVersion: Long,
val modelVersion: Long,
val topicId: Int,
val configVersion: String = "chrome.1",
val version: String = "$configVersion:$modelVersion:$taxonomyVersion"
)
object SecBrowsingTopicsHeader {
fun decode(headerValue: String): List<BrowsingTopic> {
val topics = mutableListOf<BrowsingTopic>()
val entries = headerValue.split(", ").filter { it.isNotEmpty() && !it.startsWith("();p=") }
for (entry in entries) {
val parts = entry.split(";v=")
if (parts.size == 2) {
val topicIds = parts[0].removePrefix("(").removeSuffix(")").split(" ")
val versionParts = parts[1].split(":")
if (versionParts.size == 3) {
val configVersion = versionParts[0]
val modelVersion = versionParts[1].toLongOrNull() ?: 0L
val taxonomyVersion = versionParts[2].toLongOrNull() ?: 0L
topicIds.forEach { idStr ->
val topicId = idStr.toIntOrNull() ?: 0
topics.add(BrowsingTopic(taxonomyVersion, modelVersion, topicId, configVersion))
}
}
}
}
return topics
}
fun encode(topics: List<BrowsingTopic>, configVersion: String = "chrome.1"): String {
val versionsToTopics = mutableMapOf<String, MutableList<Int>>()
// Group topics by their version
topics.forEach { topic ->
val version = "${configVersion}:${topic.modelVersion}:${topic.taxonomyVersion}"
val topicId = topic.topicId
versionsToTopics.computeIfAbsent(version) { mutableListOf() }.add(topicId)
}
val topicsStructuredFieldsList = mutableListOf<String>()
// Create inner lists for each version
versionsToTopics.forEach { (version, topicIntegers) ->
val innerList = topicIntegers.joinToString(" ")
val entry = "($innerList);v=$version"
topicsStructuredFieldsList.add(entry)
}
// Calculate number of distinct versions
var numVersionsInEpochs = versionsToTopics.size
if (numVersionsInEpochs == 0) {
numVersionsInEpochs = 1
}
// Handle padding calculations
val maxNumberOfEpochs = 3
val topicMaxLength = 3 // Assuming max topic ID length of 3 digits
val listItemsSeparatorLength = 2
val perVersionedTopicsInnerListOverhead = 5
// The maximum version string length is the maximum possible string length of a version
// that a user agent could possibly generate in a given software release.
//
// For example, in Chrome’s experimentation phase, 13 was used for the maximum version
// string length to account for a version like chrome.1:1:11.
val versionMaxLength = configVersion.length + listItemsSeparatorLength + 3
val maxPaddingLength = maxNumberOfEpochs * topicMaxLength +
maxNumberOfEpochs - numVersionsInEpochs +
numVersionsInEpochs * perVersionedTopicsInnerListOverhead +
numVersionsInEpochs * versionMaxLength +
(numVersionsInEpochs - 1) * listItemsSeparatorLength
var paddingLength = maxPaddingLength
if (topicsStructuredFieldsList.isNotEmpty()) {
val serializedTopicsList = topicsStructuredFieldsList.joinToString(", ")
paddingLength -= serializedTopicsList.length
} else {
paddingLength += listItemsSeparatorLength
}
if (paddingLength < 0) {
paddingLength = 0
}
val paddedToken = "P" + "0".repeat(paddingLength)
val paddedEntry = "();p=$paddedToken"
topicsStructuredFieldsList.add(paddedEntry)
return topicsStructuredFieldsList.joinToString(", ")
}
}
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
class SecBrowsingTopicsHeaderTest {
@Test
fun testEncode() {
// Empty returned topics, and underlying epochs have same versions:
// ();p=P0000000000000000000000000000000
assertEquals(
"();p=P0000000000000000000000000000000",
SecBrowsingTopicsHeader.encode(
listOf(),
"chrome.1"
)
)
// One returned topic, and underlying epochs have same versions:
// (1);v=chrome.1:1:2, ();p=P00000000000
assertEquals(
"(1);v=chrome.1:1:2, ();p=P00000000000",
SecBrowsingTopicsHeader.encode(
listOf(
Topic(2, 1, 1)
),
"chrome.1"
)
)
// Two returned topics, and underlying epochs have same versions:
// (1 2);v=chrome.1:1:2, ();p=P000000000
assertEquals(
"(1 2);v=chrome.1:1:2, ();p=P000000000",
SecBrowsingTopicsHeader.encode(
listOf(
Topic(2, 1, 1),
Topic(2, 1, 2)
),
"chrome.1"
)
)
// Two returned topics, and underlying epochs have two different versions:
// (1);v=chrome.1:1:2, (1);v=chrome.1:1:4, ();p=P0000000000
assertEquals(
"(1);v=chrome.1:1:2, (1);v=chrome.1:1:4, ();p=P0000000000",
SecBrowsingTopicsHeader.encode(
listOf(
Topic(2, 1, 1),
Topic(4, 1, 1)
),
"chrome.1"
)
)
// Three returned topics, and underlying epochs have three different versions:
// (100);v=chrome.1:1:20, (200);v=chrome.1:1:40, (300);v=chrome.1:1:60, ();p=P
assertEquals(
"(100);v=chrome.1:1:20, (200);v=chrome.1:1:40, (300);v=chrome.1:1:60, ();p=P",
SecBrowsingTopicsHeader.encode(
listOf(
Topic(20, 1, 100),
Topic(40, 1, 200),
Topic(60, 1, 300)
),
"chrome.1"
)
)
}
@Test
fun testDecode() {
// Empty returned topics, and underlying epochs have same versions:
// ();p=P0000000000000000000000000000000
assertEquals(
/* expected = */ listOf<BrowsingTopic>(),
/* actual = */ SecBrowsingTopicsHeader.decode("();p=P0000000000000000000000000000000")
)
// One returned topic, and underlying epochs have same versions:
// (1);v=chrome.1:1:2, ();p=P00000000000
assertEquals(
/* expected = */ listOf<BrowsingTopic>(
BrowsingTopic(2, 1, 1)
),
/* actual = */ SecBrowsingTopicsHeader.decode("(1);v=chrome.1:1:2, ();p=P00000000000")
)
// Two returned topics, and underlying epochs have same versions:
// (1 2);v=chrome.1:1:2, ();p=P000000000
assertEquals(
/* expected = */ listOf<BrowsingTopic>(
BrowsingTopic(2, 1, 1),
BrowsingTopic(2, 1, 2)
),
/* actual = */ SecBrowsingTopicsHeader.decode("(1 2);v=chrome.1:1:2, ();p=P000000000")
)
// Two returned topics, and underlying epochs have two different versions:
// (1);v=chrome.1:1:2, (1);v=chrome.1:1:4, ();p=P0000000000
assertEquals(
/* expected = */ listOf<BrowsingTopic>(
BrowsingTopic(2, 1, 1),
BrowsingTopic(4, 1, 1)
),
/* actual = */ SecBrowsingTopicsHeader.decode("(1);v=chrome.1:1:2, (1);v=chrome.1:1:4, ();p=P0000000000")
)
// Three returned topics, and underlying epochs have three different versions:
// (100);v=chrome.1:1:20, (200);v=chrome.1:1:40, (300);v=chrome.1:1:60, ();p=P
assertEquals(
/* expected = */ listOf<BrowsingTopic>(
BrowsingTopic(20, 1, 100),
BrowsingTopic(40, 1, 200),
BrowsingTopic(60, 1, 300)
),
/* actual = */ SecBrowsingTopicsHeader.decode("(100);v=chrome.1:1:20, (200);v=chrome.1:1:40, (300);v=chrome.1:1:60, ();p=P")
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment