198 lines
8.7 KiB
Java
198 lines
8.7 KiB
Java
package p.packer.services;
|
|
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import p.packer.assets.AssetFamilyCatalog;
|
|
import p.packer.assets.OutputCodecCatalog;
|
|
import p.packer.diagnostics.PackerDiagnostic;
|
|
import p.packer.diagnostics.PackerDiagnosticCategory;
|
|
import p.packer.diagnostics.PackerDiagnosticSeverity;
|
|
import p.packer.models.PackerAssetDeclaration;
|
|
import p.packer.models.PackerAssetDeclarationParseResult;
|
|
|
|
import java.io.IOException;
|
|
import java.nio.file.Path;
|
|
import java.util.ArrayList;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
|
|
public final class PackerAssetDeclarationParser {
|
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
|
private static final int SUPPORTED_SCHEMA_VERSION = 1;
|
|
|
|
public PackerAssetDeclarationParseResult parse(Path assetManifestPath) {
|
|
final Path manifestPath = Objects.requireNonNull(assetManifestPath, "assetManifestPath").toAbsolutePath().normalize();
|
|
final List<PackerDiagnostic> diagnostics = new ArrayList<>();
|
|
final JsonNode root;
|
|
try {
|
|
root = MAPPER.readTree(manifestPath.toFile());
|
|
} catch (IOException exception) {
|
|
diagnostics.add(new PackerDiagnostic(
|
|
PackerDiagnosticSeverity.ERROR,
|
|
PackerDiagnosticCategory.STRUCTURAL,
|
|
"Unable to parse asset.json: " + exception.getMessage(),
|
|
manifestPath,
|
|
true));
|
|
return new PackerAssetDeclarationParseResult(null, diagnostics);
|
|
}
|
|
|
|
final Integer schemaVersion = requiredInt(root, "schema_version", diagnostics, manifestPath);
|
|
final String assetUuid = requiredText(root, "asset_uuid", diagnostics, manifestPath);
|
|
final String name = requiredText(root, "name", diagnostics, manifestPath);
|
|
final AssetFamilyCatalog assetFamily = requiredAssetFamily(root, diagnostics, manifestPath);
|
|
final Map<String, List<String>> inputsByRole = requiredInputs(root.path("inputs"), diagnostics, manifestPath);
|
|
final String outputFormat = requiredText(root.path("output"), "format", diagnostics, manifestPath);
|
|
final OutputCodecCatalog outputCodec = requiredOutputCodec(root.path("output"), diagnostics, manifestPath);
|
|
final Boolean preloadEnabled = requiredBoolean(root.path("preload"), "enabled", diagnostics, manifestPath);
|
|
|
|
if (schemaVersion != null && schemaVersion != SUPPORTED_SCHEMA_VERSION) {
|
|
diagnostics.add(new PackerDiagnostic(
|
|
PackerDiagnosticSeverity.ERROR,
|
|
PackerDiagnosticCategory.VERSIONING,
|
|
"Unsupported asset.json schema_version: " + schemaVersion,
|
|
manifestPath,
|
|
true));
|
|
}
|
|
|
|
if (diagnostics.stream().anyMatch(PackerDiagnostic::blocking)) {
|
|
return new PackerAssetDeclarationParseResult(null, diagnostics);
|
|
}
|
|
|
|
return new PackerAssetDeclarationParseResult(
|
|
new PackerAssetDeclaration(
|
|
schemaVersion,
|
|
assetUuid,
|
|
name,
|
|
assetFamily,
|
|
inputsByRole,
|
|
outputFormat,
|
|
outputCodec,
|
|
preloadEnabled),
|
|
diagnostics);
|
|
}
|
|
|
|
private Integer requiredInt(JsonNode node, String fieldName, List<PackerDiagnostic> diagnostics, Path manifestPath) {
|
|
final JsonNode field = node.path(fieldName);
|
|
if (!field.isInt()) {
|
|
diagnostics.add(missingOrInvalid(fieldName, "integer", manifestPath));
|
|
return null;
|
|
}
|
|
return field.intValue();
|
|
}
|
|
|
|
private String requiredText(JsonNode node, String fieldName, List<PackerDiagnostic> diagnostics, Path manifestPath) {
|
|
final JsonNode field = node.path(fieldName);
|
|
if (!field.isTextual() || field.asText().isBlank()) {
|
|
diagnostics.add(missingOrInvalid(fieldName, "non-blank string", manifestPath));
|
|
return null;
|
|
}
|
|
return field.asText().trim();
|
|
}
|
|
|
|
private AssetFamilyCatalog requiredAssetFamily(JsonNode node, List<PackerDiagnostic> diagnostics, Path manifestPath) {
|
|
final String manifestType = requiredText(node, "type", diagnostics, manifestPath);
|
|
if (manifestType == null) {
|
|
return null;
|
|
}
|
|
final AssetFamilyCatalog assetFamily = AssetFamilyCatalog.fromManifestType(manifestType);
|
|
if (assetFamily == AssetFamilyCatalog.UNKNOWN) {
|
|
diagnostics.add(new PackerDiagnostic(
|
|
PackerDiagnosticSeverity.ERROR,
|
|
PackerDiagnosticCategory.STRUCTURAL,
|
|
"Field 'type' must be one of: image_bank, palette_bank, sound_bank.",
|
|
manifestPath,
|
|
true));
|
|
return null;
|
|
}
|
|
return assetFamily;
|
|
}
|
|
|
|
private Boolean requiredBoolean(JsonNode node, String fieldName, List<PackerDiagnostic> diagnostics, Path manifestPath) {
|
|
final JsonNode field = node.path(fieldName);
|
|
if (!field.isBoolean()) {
|
|
diagnostics.add(missingOrInvalid(fieldName, "boolean", manifestPath));
|
|
return null;
|
|
}
|
|
return field.booleanValue();
|
|
}
|
|
|
|
private OutputCodecCatalog requiredOutputCodec(JsonNode node, List<PackerDiagnostic> diagnostics, Path manifestPath) {
|
|
final String codecValue = requiredText(node, "codec", diagnostics, manifestPath);
|
|
if (codecValue == null) {
|
|
return null;
|
|
}
|
|
final OutputCodecCatalog outputCodec = OutputCodecCatalog.fromManifestValue(codecValue);
|
|
if (outputCodec == OutputCodecCatalog.UNKNOWN) {
|
|
diagnostics.add(new PackerDiagnostic(
|
|
PackerDiagnosticSeverity.ERROR,
|
|
PackerDiagnosticCategory.STRUCTURAL,
|
|
"Field 'codec' must be one of: NONE.",
|
|
manifestPath,
|
|
true));
|
|
return null;
|
|
}
|
|
return outputCodec;
|
|
}
|
|
|
|
private Map<String, List<String>> requiredInputs(JsonNode inputsNode, List<PackerDiagnostic> diagnostics, Path manifestPath) {
|
|
if (!inputsNode.isObject()) {
|
|
diagnostics.add(missingOrInvalid("inputs", "object of input roles", manifestPath));
|
|
return Map.of();
|
|
}
|
|
|
|
final Map<String, List<String>> result = new LinkedHashMap<>();
|
|
inputsNode.fields().forEachRemaining(entry -> {
|
|
if (!entry.getValue().isArray()) {
|
|
diagnostics.add(new PackerDiagnostic(
|
|
PackerDiagnosticSeverity.ERROR,
|
|
PackerDiagnosticCategory.STRUCTURAL,
|
|
"Input role '" + entry.getKey() + "' must be an array of relative paths.",
|
|
manifestPath,
|
|
true));
|
|
return;
|
|
}
|
|
final List<String> values = new ArrayList<>();
|
|
entry.getValue().forEach(value -> {
|
|
if (!value.isTextual() || value.asText().isBlank()) {
|
|
diagnostics.add(new PackerDiagnostic(
|
|
PackerDiagnosticSeverity.ERROR,
|
|
PackerDiagnosticCategory.STRUCTURAL,
|
|
"Input role '" + entry.getKey() + "' must contain only non-blank path strings.",
|
|
manifestPath,
|
|
true));
|
|
return;
|
|
}
|
|
final String relativePath = value.asText().trim();
|
|
if (!isTrustedRelativePath(relativePath)) {
|
|
diagnostics.add(new PackerDiagnostic(
|
|
PackerDiagnosticSeverity.ERROR,
|
|
PackerDiagnosticCategory.STRUCTURAL,
|
|
"Input role '" + entry.getKey() + "' contains an untrusted path outside the asset root.",
|
|
manifestPath,
|
|
true));
|
|
return;
|
|
}
|
|
values.add(relativePath);
|
|
});
|
|
result.put(entry.getKey(), List.copyOf(values));
|
|
});
|
|
return Map.copyOf(result);
|
|
}
|
|
|
|
private boolean isTrustedRelativePath(String value) {
|
|
final Path path = Path.of(value).normalize();
|
|
return !path.isAbsolute() && !path.startsWith("..");
|
|
}
|
|
|
|
private PackerDiagnostic missingOrInvalid(String fieldName, String expected, Path manifestPath) {
|
|
return new PackerDiagnostic(
|
|
PackerDiagnosticSeverity.ERROR,
|
|
PackerDiagnosticCategory.STRUCTURAL,
|
|
"Field '" + fieldName + "' must be a " + expected + ".",
|
|
manifestPath,
|
|
true);
|
|
}
|
|
}
|