PublicSuffixListPlugin.kt (5539B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 package mozilla.components.gradle.plugins 6 7 import com.android.build.api.variant.AndroidComponentsExtension 8 import com.android.build.gradle.LibraryPlugin 9 import okio.Buffer 10 import okio.ByteString 11 import okio.ByteString.Companion.encodeUtf8 12 import org.gradle.api.DefaultTask 13 import org.gradle.api.GradleException 14 import org.gradle.api.InvalidUserDataException 15 import org.gradle.api.Plugin 16 import org.gradle.api.Project 17 import org.gradle.api.file.DirectoryProperty 18 import org.gradle.api.file.RegularFileProperty 19 import org.gradle.api.tasks.InputFile 20 import org.gradle.api.tasks.OutputDirectory 21 import org.gradle.api.tasks.TaskAction 22 import org.gradle.kotlin.dsl.withType 23 import java.io.File 24 import java.util.TreeSet 25 26 /** 27 * A self-contained, configuration-cache-compatible Gradle task to generate the Public Suffix List asset. 28 */ 29 abstract class GeneratePslAssetTask : DefaultTask() { 30 31 @get:InputFile 32 abstract val sourceFile: RegularFileProperty 33 34 @get:OutputDirectory 35 abstract val outputDir: DirectoryProperty 36 37 @TaskAction 38 fun generate() { 39 val source = sourceFile.get().asFile 40 if (!source.exists()) { 41 throw GradleException("Public Suffix List source file not found: ${source.absolutePath}") 42 } 43 44 val startTime = System.currentTimeMillis() 45 logger.info("PublicSuffixList> Executing generatePslAsset: Reading Public Suffix List from ${source.path}") 46 47 val listData = parsePublicSuffixList(source) 48 val newContent = buildBinaryContent(listData) 49 val destination = outputDir.file("publicsuffixes").get().asFile 50 51 logger.info("PublicSuffixList> Writing new Public Suffix List asset...") 52 destination.parentFile.mkdirs() 53 destination.writeBytes(newContent.toByteArray()) 54 55 val duration = System.currentTimeMillis() - startTime 56 logger.info("PublicSuffixList> Public Suffix List asset generation complete in ${duration}ms.") 57 } 58 59 private fun buildBinaryContent(data: PublicSuffixListData): ByteString { 60 val buffer = Buffer() 61 buffer.writeInt(data.totalRuleBytes) 62 for (domain in data.sortedRules) { 63 buffer.write(domain).writeByte('\n'.code) 64 } 65 buffer.writeInt(data.totalExceptionRuleBytes) 66 for (domain in data.sortedExceptionRules) { 67 buffer.write(domain).writeByte('\n'.code) 68 } 69 return buffer.readByteString() 70 } 71 72 private fun parsePublicSuffixList(sourceFile: File): PublicSuffixListData { 73 val data = PublicSuffixListData() 74 75 sourceFile.useLines { lines -> 76 lines.filter { it.isNotBlank() && !it.startsWith("//") } 77 .forEach { line -> 78 if (line.contains(WILDCARD_CHAR)) { 79 assertWildcardRule(line) 80 } 81 82 var rule = line.encodeUtf8() 83 if (rule.startsWith(EXCEPTION_RULE_MARKER)) { 84 rule = rule.substring(1) 85 // We use '\n' for end of value. 86 data.sortedExceptionRules.add(rule) 87 data.totalExceptionRuleBytes += rule.size + 1 88 } else { 89 data.sortedRules.add(rule) 90 // We use '\n' for end of value. 91 data.totalRuleBytes += rule.size + 1 92 } 93 } 94 } 95 96 return data 97 } 98 99 @Suppress("ThrowsCount") 100 private fun assertWildcardRule(rule: String) { 101 if (!rule.startsWith(WILDCARD_CHAR)) { 102 throw InvalidUserDataException("Wildcard is not in leftmost position") 103 } 104 if (rule.lastIndexOf(WILDCARD_CHAR) > 0) { 105 throw InvalidUserDataException("Rule contains multiple wildcards") 106 } 107 108 if (rule.length == 1) { 109 throw InvalidUserDataException("Rule wildcards the first level") 110 } 111 } 112 113 companion object { 114 private const val WILDCARD_CHAR = "*" 115 private val EXCEPTION_RULE_MARKER = "!".encodeUtf8() 116 } 117 } 118 119 abstract class PublicSuffixListExtension { 120 @get:InputFile 121 abstract val sourceFile: RegularFileProperty 122 } 123 124 class PublicSuffixListPlugin : Plugin<Project> { 125 override fun apply(project: Project) { 126 project.plugins.withType<LibraryPlugin>().configureEach { 127 val extension = project.extensions.create("publicSuffixList", PublicSuffixListExtension::class.java) 128 129 val generateTaskProvider = project.tasks.register("generatePslAsset", GeneratePslAssetTask::class.java) { 130 outputDir.set(project.layout.buildDirectory.dir("generated/assets/publicsuffixlist")) 131 sourceFile.set(extension.sourceFile) 132 } 133 134 val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java) 135 androidComponents.onVariants(androidComponents.selector().all()) { variant -> 136 variant.sources.assets?.addGeneratedSourceDirectory(generateTaskProvider, GeneratePslAssetTask::outputDir) 137 } 138 } 139 } 140 } 141 142 private data class PublicSuffixListData( 143 var totalRuleBytes: Int = 0, 144 var totalExceptionRuleBytes: Int = 0, 145 val sortedRules: TreeSet<ByteString> = TreeSet(), 146 val sortedExceptionRules: TreeSet<ByteString> = TreeSet(), 147 )