lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

advisory_inventory.init.gradle (25694B)


      1 import groovy.json.JsonOutput
      2 import java.nio.ByteBuffer
      3 import java.nio.channels.FileChannel
      4 import java.nio.charset.StandardCharsets
      5 import java.nio.file.Files
      6 import java.nio.file.LinkOption
      7 import java.nio.file.Path
      8 import java.nio.file.Paths
      9 import java.nio.file.StandardOpenOption
     10 import java.nio.file.attribute.BasicFileAttributes
     11 import java.nio.file.attribute.PosixFileAttributes
     12 import java.nio.file.attribute.PosixFilePermissions
     13 import java.security.MessageDigest
     14 import java.util.HexFormat
     15 import org.gradle.api.Named
     16 import org.gradle.api.artifacts.component.ModuleComponentIdentifier
     17 import org.gradle.api.artifacts.component.ModuleComponentSelector
     18 import org.gradle.api.artifacts.component.ProjectComponentIdentifier
     19 import org.gradle.api.artifacts.component.ProjectComponentSelector
     20 import org.gradle.api.artifacts.result.ResolvedDependencyResult
     21 import org.gradle.api.artifacts.result.UnresolvedDependencyResult
     22 
     23 final long MAX_ARTIFACT_BYTES = 17_179_869_184L
     24 final long MAX_TOTAL_ARTIFACT_BYTES = 17_179_869_184L
     25 final int MAX_JSON_BYTES = 67_108_864
     26 final int STREAM_BUFFER_BYTES = 65_536
     27 final int MAX_EXTERNAL_VARIANT_DEPTH = 4
     28 final int MAX_COMPONENTS = 65_536
     29 final int MAX_DEPENDENCY_RESULTS = 262_144
     30 final int MAX_ARTIFACTS = 65_536
     31 final int MAX_VARIANTS_PER_COMPONENT = 4_096
     32 final int MAX_ATTRIBUTES = 1_024
     33 final int MAX_CAPABILITIES = 1_024
     34 final int MAX_REJECTED_VERSIONS = 1_024
     35 final Set<String> ADMITTED_EXTENSIONS = [
     36     "aar", "jar", "js", "klib", "module", "pom", "wasm", "zip"
     37 ] as Set
     38 
     39 def authorities = [
     40     "harvestcircle": [
     41         build_root: ".",
     42         workloads: [
     43             ":app:design_system|desktopRuntimeClasspath": "app_design_system",
     44             ":app:desktop|runtimeClasspath": "app_desktop",
     45             ":app:shared|desktopRuntimeClasspath": "app_shared",
     46             ":tools:design_catalog|desktopRuntimeClasspath": "tools_design_catalog",
     47         ],
     48     ],
     49     "harvestcircle-build-logic": [
     50         build_root: "build-logic",
     51         workloads: [
     52             ":contracts|runtimeClasspath": "build_logic_contracts",
     53             ":plugins|runtimeClasspath": "build_logic_plugins",
     54         ],
     55     ],
     56 ]
     57 
     58 def fail = { ->
     59     throw new GradleException("governed advisory graph projection failed")
     60 }
     61 
     62 def safeString = { Object value, int maximum, boolean allowEmpty = false ->
     63     if (!(value instanceof CharSequence)) {
     64         fail()
     65     }
     66     String text = value.toString()
     67     if ((!allowEmpty && text.isEmpty())
     68         || text.getBytes(StandardCharsets.UTF_8).length > maximum
     69         || text.codePoints().anyMatch { codePoint ->
     70             codePoint > 0x7f || Character.isISOControl(codePoint)
     71         }) {
     72         fail()
     73     }
     74     text
     75 }
     76 
     77 def nullableString = { Object value, int maximum, boolean allowEmpty = false ->
     78     value == null ? null : safeString(value, maximum, allowEmpty)
     79 }
     80 
     81 Closure<String> canonicalJson
     82 canonicalJson = { Object value ->
     83     if (value == null || value instanceof Boolean || value instanceof Number
     84         || value instanceof CharSequence) {
     85         return JsonOutput.toJson(value)
     86     }
     87     if (value instanceof Map) {
     88         def keys = value.keySet().collect { key ->
     89             if (!(key instanceof String)) {
     90                 fail()
     91             }
     92             safeString(key, 256)
     93         }.sort()
     94         return "{" + keys.collect { key ->
     95             JsonOutput.toJson(key) + ":" + canonicalJson(value[key])
     96         }.join(",") + "}"
     97     }
     98     if (value instanceof Collection) {
     99         return "[" + value.collect { entry -> canonicalJson(entry) }.join(",") + "]"
    100     }
    101     fail()
    102 }
    103 
    104 def boundedSize = { Collection values, int maximum, boolean allowEmpty = true ->
    105     int count = values.size()
    106     if ((!allowEmpty && count == 0) || count > maximum) {
    107         fail()
    108     }
    109     count
    110 }
    111 
    112 def canonicalSortedSet = { Collection rows, int maximum, boolean allowEmpty = true ->
    113     boundedSize(rows, maximum, allowEmpty)
    114     def byIdentity = new TreeMap<String, Object>()
    115     rows.each { row ->
    116         String identity = canonicalJson(row)
    117         byIdentity[identity] = row
    118     }
    119     if (byIdentity.size() > maximum || (!allowEmpty && byIdentity.isEmpty())) {
    120         fail()
    121     }
    122     byIdentity.values().toList()
    123 }
    124 
    125 def canonicalSortedRows = { Collection rows, int maximum, boolean allowEmpty = true ->
    126     boundedSize(rows, maximum, allowEmpty)
    127     def byIdentity = new TreeMap<String, Object>()
    128     rows.each { row ->
    129         String identity = canonicalJson(row)
    130         if (byIdentity.put(identity, row) != null) {
    131             fail()
    132         }
    133     }
    134     if (byIdentity.size() != rows.size()) {
    135         fail()
    136     }
    137     byIdentity.values().toList()
    138 }
    139 
    140 def attributeValue = { Object value ->
    141     if (value instanceof Named) {
    142         return safeString(value.name, 512)
    143     }
    144     if (value instanceof Enum) {
    145         return safeString(value.name(), 512)
    146     }
    147     if (value instanceof CharSequence || value instanceof Number || value instanceof Boolean) {
    148         return safeString(String.valueOf(value), 512, true)
    149     }
    150     fail()
    151 }
    152 
    153 def attributeRows = { attributes ->
    154     def keys = attributes.keySet()
    155     int expectedCount = boundedSize(keys, MAX_ATTRIBUTES)
    156     def rows = keys.collect { attribute ->
    157         [
    158             name: safeString(attribute.name, 512),
    159             value: attributeValue(attributes.getAttribute(attribute)),
    160         ]
    161     }.sort { left, right ->
    162         left.name <=> right.name ?: left.value <=> right.value
    163     }
    164     if (rows.size() != expectedCount
    165         || rows.collect { row -> row.name }.toSet().size() != rows.size()) {
    166         fail()
    167     }
    168     rows
    169 }
    170 
    171 def capabilityRows = { capabilities ->
    172     int expectedCount = boundedSize(capabilities, MAX_CAPABILITIES)
    173     def rows = capabilities.collect { capability ->
    174         [
    175             group: safeString(capability.group, 256, true),
    176             name: safeString(capability.name, 256),
    177             version: nullableString(capability.version, 128, true),
    178         ]
    179     }
    180     if (rows.size() != expectedCount) {
    181         fail()
    182     }
    183     canonicalSortedSet(rows, MAX_CAPABILITIES)
    184 }
    185 
    186 Closure<Map> variantRow
    187 variantRow = { variant, int depth = 0 ->
    188     if (depth > MAX_EXTERNAL_VARIANT_DEPTH) {
    189         fail()
    190     }
    191     def external = variant.externalVariant
    192     [
    193         attributes: attributeRows(variant.attributes),
    194         capabilities: capabilityRows(variant.capabilities),
    195         external_variant: external.present ? variantRow(external.get(), depth + 1) : null,
    196     ]
    197 }
    198 
    199 def variantEnvelope = { Collection variants ->
    200     def selected = canonicalSortedSet(variants, MAX_VARIANTS_PER_COMPONENT, false)
    201     [selected: selected]
    202 }
    203 
    204 def requiredProperty = { String name ->
    205     def value = gradle.startParameter.projectProperties[name]
    206     if (value == null) {
    207         fail()
    208     }
    209     safeString(value, 4_096)
    210 }
    211 
    212 def logicalBuildRoot = { String invocationBuildRoot, String buildPath ->
    213     String admitted = safeString(buildPath, 256)
    214     if (admitted == ":") {
    215         return invocationBuildRoot
    216     }
    217     if (admitted == ":build-logic") {
    218         return "build-logic"
    219     }
    220     fail()
    221 }
    222 
    223 def componentCore = { identifier, String invocationBuildRoot ->
    224     if (identifier instanceof ModuleComponentIdentifier) {
    225         return [
    226             build_root: null,
    227             group: safeString(identifier.group, 256),
    228             kind: "module",
    229             name: safeString(identifier.module, 256),
    230             project_path: null,
    231             version: safeString(identifier.version, 128),
    232         ]
    233     }
    234     if (identifier instanceof ProjectComponentIdentifier) {
    235         return [
    236             build_root: logicalBuildRoot(invocationBuildRoot, identifier.build.buildPath),
    237             group: null,
    238             kind: "project",
    239             name: null,
    240             project_path: safeString(identifier.projectPath, 256),
    241             version: null,
    242         ]
    243     }
    244     fail()
    245 }
    246 
    247 def selectorRow = { selector, String invocationBuildRoot ->
    248     if (!selector.capabilitySelectors.isEmpty()) {
    249         fail()
    250     }
    251     if (selector instanceof ModuleComponentSelector) {
    252         def constraint = selector.versionConstraint
    253         def rejectedVersions = constraint.rejectedVersions
    254         int rejectedCount = boundedSize(rejectedVersions, MAX_REJECTED_VERSIONS)
    255         def rejected = rejectedVersions.collect { version ->
    256             safeString(version, 512)
    257         }
    258         if (rejected.size() != rejectedCount) {
    259             fail()
    260         }
    261         return [
    262             attributes: attributeRows(selector.attributes),
    263             build_root: null,
    264             capabilities: capabilityRows(selector.requestedCapabilities),
    265             group: safeString(selector.group, 256),
    266             kind: "module",
    267             name: safeString(selector.module, 256),
    268             project_path: null,
    269             version: safeString(selector.version, 512, true),
    270             version_constraint: [
    271                 branch: nullableString(constraint.branch, 256, true),
    272                 preferred: safeString(constraint.preferredVersion, 512, true),
    273                 rejected: canonicalSortedSet(rejected, MAX_REJECTED_VERSIONS),
    274                 required: safeString(constraint.requiredVersion, 512, true),
    275                 strict: safeString(constraint.strictVersion, 512, true),
    276             ],
    277         ]
    278     }
    279     if (selector instanceof ProjectComponentSelector) {
    280         return [
    281             attributes: attributeRows(selector.attributes),
    282             build_root: logicalBuildRoot(invocationBuildRoot, selector.buildPath),
    283             capabilities: capabilityRows(selector.requestedCapabilities),
    284             group: null,
    285             kind: "project",
    286             name: null,
    287             project_path: safeString(selector.projectPath, 256),
    288             version: null,
    289             version_constraint: null,
    290         ]
    291     }
    292     fail()
    293 }
    294 
    295 def sameBasicIdentity = { BasicFileAttributes left, BasicFileAttributes right ->
    296     left.fileKey() != null
    297         && right.fileKey() != null
    298         && left.fileKey() == right.fileKey()
    299         && left.isRegularFile()
    300         && right.isRegularFile()
    301         && left.size() == right.size()
    302         && left.lastModifiedTime() == right.lastModifiedTime()
    303         && left.creationTime() == right.creationTime()
    304 }
    305 
    306 def digestBytes = { byte[] bytes ->
    307     HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes))
    308 }
    309 
    310 def observeRegularFile = { Path path, long maximum ->
    311     if (!path.isAbsolute() || path != path.normalize()) {
    312         fail()
    313     }
    314     BasicFileAttributes before = Files.readAttributes(
    315         path, BasicFileAttributes, LinkOption.NOFOLLOW_LINKS
    316     )
    317     if (!before.isRegularFile() || before.isSymbolicLink() || before.fileKey() == null
    318         || before.size() <= 0L || before.size() > maximum) {
    319         fail()
    320     }
    321     def digest = MessageDigest.getInstance("SHA-256")
    322     long observed = 0L
    323     FileChannel channel = FileChannel.open(
    324         path, StandardOpenOption.READ, LinkOption.NOFOLLOW_LINKS
    325     )
    326     try {
    327         if (channel.size() != before.size()) {
    328             fail()
    329         }
    330         ByteBuffer buffer = ByteBuffer.allocate(STREAM_BUFFER_BYTES)
    331         while (true) {
    332             int count = channel.read(buffer)
    333             if (count < 0) {
    334                 break
    335             }
    336             if (count == 0) {
    337                 fail()
    338             }
    339             observed = Math.addExact(observed, (long) count)
    340             if (observed > maximum) {
    341                 fail()
    342             }
    343             buffer.flip()
    344             digest.update(buffer)
    345             buffer.clear()
    346         }
    347         if (observed != before.size() || channel.position() != observed
    348             || channel.size() != observed) {
    349             fail()
    350         }
    351     } finally {
    352         channel.close()
    353     }
    354     BasicFileAttributes after = Files.readAttributes(
    355         path, BasicFileAttributes, LinkOption.NOFOLLOW_LINKS
    356     )
    357     if (!sameBasicIdentity(before, after) || after.size() != observed) {
    358         fail()
    359     }
    360     [
    361         byte_length: observed,
    362         sha256: HexFormat.of().formatHex(digest.digest()),
    363     ]
    364 }
    365 
    366 def artifactCorrelationKey = { artifact, String invocationBuildRoot ->
    367     Path source = artifact.file.toPath().toAbsolutePath().normalize()
    368     canonicalJson([
    369         component: componentCore(artifact.id.componentIdentifier, invocationBuildRoot),
    370         source_path: safeString(source.toString(), 4_096),
    371     ])
    372 }
    373 
    374 def moduleVersionRow = { moduleVersion ->
    375     if (moduleVersion == null) {
    376         fail()
    377     }
    378     def identifier = moduleVersion.id
    379     if (identifier == null) {
    380         fail()
    381     }
    382     [
    383         group: safeString(identifier.group, 256),
    384         name: safeString(identifier.name, 256),
    385         version: safeString(identifier.version, 128),
    386     ]
    387 }
    388 
    389 def writeCreateNew = { Path output, byte[] bytes ->
    390     if (!output.isAbsolute() || output != output.normalize()
    391         || output.fileName == null || output.parent == null
    392         || bytes.length <= 0 || bytes.length > MAX_JSON_BYTES) {
    393         fail()
    394     }
    395     safeString(output.fileName.toString(), 256)
    396     Path parent = output.parent
    397     BasicFileAttributes parentBefore = Files.readAttributes(
    398         parent, BasicFileAttributes, LinkOption.NOFOLLOW_LINKS
    399     )
    400     if (!parentBefore.isDirectory() || parentBefore.isSymbolicLink()
    401         || parentBefore.fileKey() == null
    402         || Files.exists(output, LinkOption.NOFOLLOW_LINKS)) {
    403         fail()
    404     }
    405     def outputOptions = [
    406         StandardOpenOption.CREATE_NEW,
    407         StandardOpenOption.WRITE,
    408         LinkOption.NOFOLLOW_LINKS,
    409     ] as Set
    410     def outputPermissions = PosixFilePermissions.fromString("rw-------")
    411     FileChannel channel = FileChannel.open(
    412         output, outputOptions, PosixFilePermissions.asFileAttribute(outputPermissions)
    413     )
    414     try {
    415         ByteBuffer buffer = ByteBuffer.wrap(bytes)
    416         while (buffer.hasRemaining()) {
    417             if (channel.write(buffer) <= 0) {
    418                 fail()
    419             }
    420         }
    421         channel.force(true)
    422         if (channel.position() != bytes.length || channel.size() != bytes.length) {
    423             fail()
    424         }
    425     } finally {
    426         channel.close()
    427     }
    428     PosixFileAttributes outputAttributes = Files.readAttributes(
    429         output, PosixFileAttributes, LinkOption.NOFOLLOW_LINKS
    430     )
    431     if (!outputAttributes.isRegularFile() || outputAttributes.isSymbolicLink()
    432         || outputAttributes.fileKey() == null
    433         || outputAttributes.permissions() != outputPermissions) {
    434         fail()
    435     }
    436     BasicFileAttributes parentAfter = Files.readAttributes(
    437         parent, BasicFileAttributes, LinkOption.NOFOLLOW_LINKS
    438     )
    439     if (parentBefore.fileKey() != parentAfter.fileKey()
    440         || !parentAfter.isDirectory() || parentAfter.isSymbolicLink()) {
    441         fail()
    442     }
    443     def observed = observeRegularFile(output, MAX_JSON_BYTES)
    444     if (observed.byte_length != bytes.length || observed.sha256 != digestBytes(bytes)) {
    445         fail()
    446     }
    447 }
    448 
    449 gradle.afterProject { project, state ->
    450     if (state.failure != null) {
    451         return
    452     }
    453     def authority = authorities[project.rootProject.name]
    454     if (authority == null) {
    455         return
    456     }
    457     def matching = authority.workloads.findAll { key, ignored ->
    458         key.startsWith(project.path + "|")
    459     }
    460     if (matching.isEmpty()) {
    461         return
    462     }
    463     if (project.tasks.findByName("rshrAdvisoryGraph") != null) {
    464         fail()
    465     }
    466     project.tasks.register("rshrAdvisoryGraph") {
    467         doLast {
    468             String targetConfiguration = requiredProperty("rshrAdvisoryConfiguration")
    469             String outputArgument = requiredProperty("rshrAdvisoryOutput")
    470             String authorityKey = project.path + "|" + targetConfiguration
    471             String workloadId = authority.workloads[authorityKey]
    472             String expectedTask = project.path + ":rshrAdvisoryGraph"
    473             if (workloadId == null
    474                 || gradle.startParameter.taskNames != [expectedTask]
    475                 || !gradle.startParameter.offline) {
    476                 fail()
    477             }
    478             def configuration = project.configurations.findByName(targetConfiguration)
    479             if (configuration == null || !configuration.canBeResolved) {
    480                 fail()
    481             }
    482 
    483             String invocationBuildRoot = authority.build_root
    484             def resolution = configuration.incoming.resolutionResult
    485             def allDependencies = resolution.allDependencies
    486             int dependencyCount = boundedSize(
    487                 allDependencies, MAX_DEPENDENCY_RESULTS
    488             )
    489             def dependencyResults = allDependencies.toList()
    490             if (dependencyResults.size() != dependencyCount) {
    491                 fail()
    492             }
    493             dependencyResults.each { dependency ->
    494                 if (dependency instanceof UnresolvedDependencyResult
    495                     || !(dependency instanceof ResolvedDependencyResult)) {
    496                     fail()
    497                 }
    498             }
    499 
    500             def componentStates = new TreeMap<String, Object>()
    501             def allComponents = resolution.allComponents
    502             int componentCount = boundedSize(allComponents, MAX_COMPONENTS, false)
    503             allComponents.each { component ->
    504                 Map core = componentCore(component.id, invocationBuildRoot)
    505                 String key = canonicalJson(core)
    506                 if (componentStates.containsKey(key)) {
    507                     fail()
    508                 }
    509                 def rawVariants = component.variants
    510                 int variantCount = boundedSize(
    511                     rawVariants, MAX_VARIANTS_PER_COMPONENT
    512                 )
    513                 def variants = rawVariants.collect { variant -> variantRow(variant) }
    514                 if (variants.size() != variantCount) {
    515                     fail()
    516                 }
    517                 componentStates[key] = [
    518                     component: component,
    519                     core: core,
    520                     variants: variants,
    521                 ]
    522             }
    523             if (componentStates.size() != componentCount) {
    524                 fail()
    525             }
    526             def appendVariant = { Map componentState, Map selectedVariant ->
    527                 if (componentState.variants.size() >= MAX_VARIANTS_PER_COMPONENT) {
    528                     fail()
    529                 }
    530                 componentState.variants.add(selectedVariant)
    531             }
    532             def rootComponent = resolution.rootComponent.get()
    533             Map rootCore = componentCore(rootComponent.id, invocationBuildRoot)
    534             String rootKey = canonicalJson(rootCore)
    535             if (!componentStates.containsKey(rootKey)) {
    536                 fail()
    537             }
    538             appendVariant(
    539                 componentStates[rootKey], variantRow(resolution.rootVariant.get())
    540             )
    541 
    542             def edgeRows = []
    543             dependencyResults.each { dependency ->
    544                 Map from = componentCore(dependency.from.id, invocationBuildRoot)
    545                 Map to = componentCore(dependency.selected.id, invocationBuildRoot)
    546                 String fromKey = canonicalJson(from)
    547                 String toKey = canonicalJson(to)
    548                 if (!componentStates.containsKey(fromKey)
    549                     || !componentStates.containsKey(toKey)) {
    550                     fail()
    551                 }
    552                 Map selectedVariant = variantRow(dependency.resolvedVariant)
    553                 appendVariant(componentStates[toKey], selectedVariant)
    554                 edgeRows.add([
    555                     constraint: dependency.constraint,
    556                     from: from,
    557                     requested: selectorRow(dependency.requested, invocationBuildRoot),
    558                     selected_variant: selectedVariant,
    559                     to: to,
    560                 ])
    561             }
    562             if (edgeRows.size() != dependencyCount) {
    563                 fail()
    564             }
    565             edgeRows = canonicalSortedRows(edgeRows, MAX_DEPENDENCY_RESULTS)
    566 
    567             def artifactCollection = configuration.incoming.artifacts
    568             if (!artifactCollection.failures.isEmpty()) {
    569                 fail()
    570             }
    571             def resolvedArtifacts = artifactCollection.artifacts
    572             int artifactCount = boundedSize(resolvedArtifacts, MAX_ARTIFACTS)
    573             def legacyResolution = configuration.resolvedConfiguration
    574             if (legacyResolution.hasError()) {
    575                 fail()
    576             }
    577             legacyResolution.rethrowFailure()
    578             def legacyArtifacts = legacyResolution.resolvedArtifacts
    579             int legacyArtifactCount = boundedSize(legacyArtifacts, MAX_ARTIFACTS)
    580             if (legacyArtifactCount != artifactCount) {
    581                 fail()
    582             }
    583             def legacyByKey = new TreeMap<String, Object>()
    584             legacyArtifacts.each { artifact ->
    585                 String key = artifactCorrelationKey(artifact, invocationBuildRoot)
    586                 def metadata = [
    587                     artifact_id: artifact.id,
    588                     artifact_type: safeString(artifact.type, 64),
    589                     classifier: nullableString(artifact.classifier, 128),
    590                     extension: safeString(artifact.extension, 64),
    591                     logical_name: safeString(artifact.name, 256),
    592                     module_version: moduleVersionRow(artifact.moduleVersion),
    593                 ]
    594                 if (!ADMITTED_EXTENSIONS.contains(metadata.extension)
    595                     || legacyByKey.put(key, metadata) != null) {
    596                     fail()
    597                 }
    598             }
    599             if (legacyByKey.size() != legacyArtifactCount) {
    600                 fail()
    601             }
    602             long totalArtifactBytes = 0L
    603             def artifactRows = resolvedArtifacts.collect { artifact ->
    604                 Map component = componentCore(
    605                     artifact.id.componentIdentifier, invocationBuildRoot
    606                 )
    607                 Map variantOwner = componentCore(artifact.variant.owner, invocationBuildRoot)
    608                 String componentKey = canonicalJson(component)
    609                 if (componentKey != canonicalJson(variantOwner)
    610                     || !componentStates.containsKey(componentKey)) {
    611                     fail()
    612                 }
    613                 Map selectedVariant = variantRow(artifact.variant)
    614                 appendVariant(componentStates[componentKey], selectedVariant)
    615                 Path source = artifact.file.toPath().toAbsolutePath().normalize()
    616                 String fileName = safeString(source.fileName.toString(), 256)
    617                 int separator = fileName.lastIndexOf('.')
    618                 if (separator <= 0 || separator == fileName.length() - 1) {
    619                     fail()
    620                 }
    621                 String extension = safeString(fileName.substring(separator + 1), 64)
    622                 if (!ADMITTED_EXTENSIONS.contains(extension)) {
    623                     fail()
    624                 }
    625                 String correlationKey = artifactCorrelationKey(
    626                     artifact, invocationBuildRoot
    627                 )
    628                 def metadata = legacyByKey.remove(correlationKey)
    629                 if (metadata == null || metadata.artifact_id != artifact.id
    630                     || metadata.extension != extension) {
    631                     fail()
    632                 }
    633                 def observation = observeRegularFile(source, MAX_ARTIFACT_BYTES)
    634                 totalArtifactBytes = Math.addExact(
    635                     totalArtifactBytes, (long) observation.byte_length
    636                 )
    637                 if (totalArtifactBytes > MAX_TOTAL_ARTIFACT_BYTES) {
    638                     fail()
    639                 }
    640                 [
    641                     artifact_name: fileName,
    642                     artifact_type: metadata.artifact_type,
    643                     classifier: metadata.classifier,
    644                     component: component,
    645                     extension: extension,
    646                     group: component.group,
    647                     logical_name: metadata.logical_name,
    648                     name: component.name,
    649                     module_version: metadata.module_version,
    650                     observed_byte_length: observation.byte_length,
    651                     observed_sha256: observation.sha256,
    652                     source_path: safeString(source.toString(), 4_096),
    653                     variant: selectedVariant,
    654                     version: component.version,
    655                 ]
    656             }
    657             if (artifactRows.size() != artifactCount
    658                 || !legacyByKey.isEmpty()
    659                 || !artifactCollection.failures.isEmpty()) {
    660                 fail()
    661             }
    662             artifactRows = canonicalSortedRows(artifactRows, MAX_ARTIFACTS)
    663 
    664             def componentRows = componentStates.collect { key, value ->
    665                 value.core + [
    666                     root: key == rootKey,
    667                     variant: variantEnvelope(value.variants),
    668                 ]
    669             }.sort { left, right ->
    670                 canonicalJson(left) <=> canonicalJson(right)
    671             }
    672             if (componentRows.size() != componentCount
    673                 || componentRows.count { row -> row.root } != 1) {
    674                 fail()
    675             }
    676 
    677             def manifest = [
    678                 schema: "radroots.gradle-advisory-graph.v1",
    679                 workload_id: workloadId,
    680                 build_root: invocationBuildRoot,
    681                 project_path: project.path,
    682                 configuration: targetConfiguration,
    683                 components: componentRows,
    684                 edges: edgeRows,
    685                 artifacts: artifactRows,
    686             ]
    687             byte[] bytes = canonicalJson(manifest).getBytes(StandardCharsets.UTF_8)
    688             writeCreateNew(Paths.get(outputArgument), bytes)
    689         }
    690     }
    691 }