ArchitectureArtifactEnvelope.java

package com.taxonomy.export.artifact;

import java.util.Objects;
import java.util.regex.Pattern;

/**
 * Deterministic envelope binding an export payload to one immutable snapshot and
 * one explicit loss profile.
 */
public record ArchitectureArtifactEnvelope(
        String schemaVersion,
        String artifactId,
        ArchitectureArtifactFormat format,
        ArchitectureArtifactSource source,
        String mediaType,
        String fileName,
        String payloadSha256,
        ArchitectureArtifactLossManifest lossManifest,
        String payload) {

    public static final String SCHEMA_VERSION = "1.0";

    private static final Pattern SHA256 =
            Pattern.compile("[0-9a-f]{64}");
    private static final Pattern SAFE_FILE_NAME =
            Pattern.compile("[A-Za-z0-9][A-Za-z0-9._-]{0,127}");

    public ArchitectureArtifactEnvelope {
        if (!SCHEMA_VERSION.equals(schemaVersion)) {
            throw new IllegalArgumentException("Unsupported schemaVersion");
        }
        artifactId = requireSha256(artifactId, "artifactId");
        format = Objects.requireNonNull(format, "format");
        source = Objects.requireNonNull(source, "source");
        if (!format.mediaType().equals(mediaType)) {
            throw new IllegalArgumentException(
                    "mediaType does not match format " + format);
        }
        fileName = requireFileName(fileName, format);
        payloadSha256 = requireSha256(payloadSha256, "payloadSha256");
        lossManifest = Objects.requireNonNull(lossManifest, "lossManifest");
        if (payload == null || payload.isBlank()) {
            throw new IllegalArgumentException("payload must not be blank");
        }
        String expectedPayloadSha256 =
                ArchitectureArtifactEnvelopeFactory.payloadSha256(payload);
        if (!payloadSha256.equals(expectedPayloadSha256)) {
            throw new IllegalArgumentException(
                    "payloadSha256 does not match payload");
        }
        String expectedArtifactId =
                ArchitectureArtifactEnvelopeFactory.artifactId(
                        format,
                        source,
                        fileName,
                        payloadSha256,
                        lossManifest);
        if (!artifactId.equals(expectedArtifactId)) {
            throw new IllegalArgumentException(
                    "artifactId does not match envelope identity");
        }
    }

    static String requireFileName(
            String fileName,
            ArchitectureArtifactFormat format) {
        if (fileName == null
                || !SAFE_FILE_NAME.matcher(fileName).matches()
                || !fileName.endsWith(format.fileSuffix())) {
            throw new IllegalArgumentException(
                    "Unsafe or format-incompatible fileName");
        }
        return fileName;
    }

    private static String requireSha256(String value, String field) {
        if (value == null || !SHA256.matcher(value).matches()) {
            throw new IllegalArgumentException(
                    field + " must be a lowercase SHA-256 value");
        }
        return value;
    }
}