/* Copyright 2022 Tusky Contributors * * This file is a part of Tusky. * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation; either version 3 of the * License, or (at your option) any later version. * * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. * * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ package com.keylesspalace.tusky.util import android.net.Uri import androidx.annotation.VisibleForTesting import androidx.core.net.toUri /** * Represents one link and its parameters from the link header of an HTTP message. * * @see [RFC5988](https://tools.ietf.org/html/rfc5988) */ class HttpHeaderLink @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) constructor( uri: String ) { data class Parameter(val name: String, val value: String?) private val parameters: MutableList = ArrayList() val uri: Uri = uri.toUri() private data class ValueResult(val value: String, val end: Int = -1) companion object { private fun findEndOfQuotedString(line: String, start: Int): Int { var i = start while (i < line.length) { val c = line[i] if (c == '\\') { i += 1 } else if (c == '"') { return i } i++ } return -1 } private fun parseValue(line: String, start: Int): ValueResult { val foundIndex = line.indexOfAny(charArrayOf(';', ',', '"'), start, false) if (foundIndex == -1) { return ValueResult(line.substring(start).trim()) } val c = line[foundIndex] return if (c == ';' || c == ',') { ValueResult(line.substring(start, foundIndex).trim(), foundIndex) } else { var quoteEnd = findEndOfQuotedString(line, foundIndex + 1) if (quoteEnd == -1) { quoteEnd = line.length } ValueResult(line.substring(foundIndex + 1, quoteEnd).trim(), quoteEnd) } } private fun parseParameters(line: String, start: Int, link: HttpHeaderLink): Int { var i = start while (i < line.length) { val foundIndex = line.indexOfAny(charArrayOf('=', ','), i, false) if (foundIndex == -1) { return -1 } else if (line[foundIndex] == ',') { return foundIndex } val name = line.substring(line.indexOf(';', i) + 1, foundIndex).trim() val result = parseValue(line, foundIndex) val value = result.value val parameter = Parameter(name, value) link.parameters.add(parameter) i = if (result.end == -1) { return -1 } else { result.end } } return -1 } /** * @param line the entire link header, not including the initial "Link:" * @return all links found in the header */ fun parse(line: String?): List { val links: MutableList = mutableListOf() line ?: return links var i = 0 while (i < line.length) { val uriEnd = line.indexOf('>', i) val uri = line.substring(line.indexOf('<', i) + 1, uriEnd) val link = HttpHeaderLink(uri) links.add(link) val parseEnd = parseParameters(line, uriEnd, link) i = if (parseEnd == -1) { break } else { parseEnd } i++ } return links } /** * @param links intended to be those returned by parse() * @param relationType of the parameter "rel", commonly "next" or "prev" * @return the link matching the given relation type */ fun findByRelationType( links: List, relationType: String ): HttpHeaderLink? { return links.find { link -> link.parameters.any { parameter -> parameter.name == "rel" && parameter.value == relationType } } } } }