implements packer PR-10 production trust and gates
This commit is contained in:
parent
1c418a454b
commit
1506858b25
@ -6,6 +6,8 @@ import p.packer.api.building.PackerBuildRequest;
|
|||||||
import p.packer.api.building.PackerBuildResult;
|
import p.packer.api.building.PackerBuildResult;
|
||||||
import p.packer.api.building.PackerBuildService;
|
import p.packer.api.building.PackerBuildService;
|
||||||
import p.packer.api.diagnostics.PackerDiagnostic;
|
import p.packer.api.diagnostics.PackerDiagnostic;
|
||||||
|
import p.packer.api.diagnostics.PackerDiagnosticCategory;
|
||||||
|
import p.packer.api.diagnostics.PackerDiagnosticSeverity;
|
||||||
import p.packer.api.events.PackerEventKind;
|
import p.packer.api.events.PackerEventKind;
|
||||||
import p.packer.api.events.PackerEventSink;
|
import p.packer.api.events.PackerEventSink;
|
||||||
import p.packer.api.events.PackerProgress;
|
import p.packer.api.events.PackerProgress;
|
||||||
@ -60,6 +62,17 @@ public final class FileSystemPackerBuildService implements PackerBuildService {
|
|||||||
final Path assetTableJson = buildDirectory.resolve("asset_table.json").toAbsolutePath().normalize();
|
final Path assetTableJson = buildDirectory.resolve("asset_table.json").toAbsolutePath().normalize();
|
||||||
final Path preloadJson = buildDirectory.resolve("preload.json").toAbsolutePath().normalize();
|
final Path preloadJson = buildDirectory.resolve("preload.json").toAbsolutePath().normalize();
|
||||||
final Path metadataJson = buildDirectory.resolve("asset_table_metadata.json").toAbsolutePath().normalize();
|
final Path metadataJson = buildDirectory.resolve("asset_table_metadata.json").toAbsolutePath().normalize();
|
||||||
|
final List<PackerDiagnostic> trustDiagnostics = new ArrayList<>(validateExistingOutputs(buildRequest, assetsArchive, metadataJson));
|
||||||
|
if (!trustDiagnostics.isEmpty()) {
|
||||||
|
final String summary = trustDiagnostics.getFirst().message();
|
||||||
|
events.emit(PackerEventKind.BUILD_FINISHED, summary, new PackerProgress(1.0d, false), List.of());
|
||||||
|
return new PackerBuildResult(
|
||||||
|
PackerOperationStatus.FAILED,
|
||||||
|
summary,
|
||||||
|
assetsArchive,
|
||||||
|
Map.of(),
|
||||||
|
trustDiagnostics);
|
||||||
|
}
|
||||||
events.emit(PackerEventKind.BUILD_STARTED, "Build started.", new PackerProgress(0.0d, false), List.of());
|
events.emit(PackerEventKind.BUILD_STARTED, "Build started.", new PackerProgress(0.0d, false), List.of());
|
||||||
final PackerBuildPlanResult planResult = buildPlanner.plan(buildRequest.project());
|
final PackerBuildPlanResult planResult = buildPlanner.plan(buildRequest.project());
|
||||||
if (planResult.plan() == null) {
|
if (planResult.plan() == null) {
|
||||||
@ -175,14 +188,76 @@ public final class FileSystemPackerBuildService implements PackerBuildService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return ((Map<String, Object>) new com.fasterxml.jackson.databind.ObjectMapper().readValue(Files.readString(metadataJson), Map.class))
|
final Map<String, Object> metadata = new com.fasterxml.jackson.databind.ObjectMapper().readValue(Files.readString(metadataJson), Map.class);
|
||||||
.getOrDefault("cache_key", "")
|
final Object schema = metadata.get("schema_version");
|
||||||
.toString();
|
if (!(schema instanceof Number) || ((Number) schema).intValue() != SCHEMA_VERSION) {
|
||||||
|
throw new IllegalArgumentException("Unsupported build metadata schema_version: " + schema);
|
||||||
|
}
|
||||||
|
return metadata.getOrDefault("cache_key", "").toString();
|
||||||
} catch (IOException exception) {
|
} catch (IOException exception) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<PackerDiagnostic> validateExistingOutputs(PackerBuildRequest request, Path assetsArchive, Path metadataJson) {
|
||||||
|
final List<PackerDiagnostic> diagnostics = new ArrayList<>();
|
||||||
|
if (Files.isRegularFile(metadataJson)) {
|
||||||
|
try {
|
||||||
|
final Map<?, ?> metadata = new com.fasterxml.jackson.databind.ObjectMapper().readValue(Files.readString(metadataJson), Map.class);
|
||||||
|
final Object schemaVersion = metadata.get("schema_version");
|
||||||
|
if (!(schemaVersion instanceof Number) || ((Number) schemaVersion).intValue() != SCHEMA_VERSION) {
|
||||||
|
diagnostics.add(new PackerDiagnostic(
|
||||||
|
PackerDiagnosticSeverity.ERROR,
|
||||||
|
PackerDiagnosticCategory.VERSIONING,
|
||||||
|
"Unsupported build metadata schema_version: " + schemaVersion,
|
||||||
|
metadataJson,
|
||||||
|
true));
|
||||||
|
}
|
||||||
|
} catch (IOException exception) {
|
||||||
|
diagnostics.add(new PackerDiagnostic(
|
||||||
|
PackerDiagnosticSeverity.ERROR,
|
||||||
|
PackerDiagnosticCategory.VERSIONING,
|
||||||
|
"Unable to read build metadata for version validation.",
|
||||||
|
metadataJson,
|
||||||
|
true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (request.incremental() && Files.isRegularFile(assetsArchive)) {
|
||||||
|
try {
|
||||||
|
final byte[] prelude = Files.readAllBytes(assetsArchive);
|
||||||
|
if (prelude.length < PRELUDE_SIZE) {
|
||||||
|
diagnostics.add(new PackerDiagnostic(
|
||||||
|
PackerDiagnosticSeverity.ERROR,
|
||||||
|
PackerDiagnosticCategory.VERSIONING,
|
||||||
|
"Existing assets.pa prelude is truncated.",
|
||||||
|
assetsArchive,
|
||||||
|
true));
|
||||||
|
} else {
|
||||||
|
final ByteBuffer buffer = ByteBuffer.wrap(prelude, 0, PRELUDE_SIZE).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
final byte[] magic = new byte[4];
|
||||||
|
buffer.get(magic);
|
||||||
|
final int schemaVersion = buffer.getInt();
|
||||||
|
if (!java.util.Arrays.equals(magic, MAGIC) || schemaVersion != SCHEMA_VERSION) {
|
||||||
|
diagnostics.add(new PackerDiagnostic(
|
||||||
|
PackerDiagnosticSeverity.ERROR,
|
||||||
|
PackerDiagnosticCategory.VERSIONING,
|
||||||
|
"Existing assets.pa uses an unsupported writer-side schema surface.",
|
||||||
|
assetsArchive,
|
||||||
|
true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException exception) {
|
||||||
|
diagnostics.add(new PackerDiagnostic(
|
||||||
|
PackerDiagnosticSeverity.ERROR,
|
||||||
|
PackerDiagnosticCategory.VERSIONING,
|
||||||
|
"Unable to inspect existing assets.pa for incremental trust validation.",
|
||||||
|
assetsArchive,
|
||||||
|
true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
private record EmittedArchive(
|
private record EmittedArchive(
|
||||||
byte[] bytes,
|
byte[] bytes,
|
||||||
String assetTableJson,
|
String assetTableJson,
|
||||||
|
|||||||
@ -122,13 +122,28 @@ public final class PackerAssetDeclarationParser {
|
|||||||
true));
|
true));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
values.add(value.asText().trim());
|
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));
|
result.put(entry.getKey(), List.copyOf(values));
|
||||||
});
|
});
|
||||||
return Map.copyOf(result);
|
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) {
|
private PackerDiagnostic missingOrInvalid(String fieldName, String expected, Path manifestPath) {
|
||||||
return new PackerDiagnostic(
|
return new PackerDiagnostic(
|
||||||
PackerDiagnosticSeverity.ERROR,
|
PackerDiagnosticSeverity.ERROR,
|
||||||
|
|||||||
@ -28,6 +28,9 @@ public final class FileSystemPackerRegistryRepository implements PackerRegistryR
|
|||||||
try {
|
try {
|
||||||
final RegistryDocument document = MAPPER.readValue(registryPath.toFile(), RegistryDocument.class);
|
final RegistryDocument document = MAPPER.readValue(registryPath.toFile(), RegistryDocument.class);
|
||||||
final int schemaVersion = document.schemaVersion <= 0 ? REGISTRY_SCHEMA_VERSION : document.schemaVersion;
|
final int schemaVersion = document.schemaVersion <= 0 ? REGISTRY_SCHEMA_VERSION : document.schemaVersion;
|
||||||
|
if (schemaVersion != REGISTRY_SCHEMA_VERSION) {
|
||||||
|
throw new PackerRegistryException("Unsupported registry schema_version: " + schemaVersion);
|
||||||
|
}
|
||||||
final List<PackerRegistryEntry> entries = new ArrayList<>();
|
final List<PackerRegistryEntry> entries = new ArrayList<>();
|
||||||
if (document.assets != null) {
|
if (document.assets != null) {
|
||||||
for (RegistryAssetDocument asset : document.assets) {
|
for (RegistryAssetDocument asset : document.assets) {
|
||||||
@ -94,7 +97,12 @@ public final class FileSystemPackerRegistryRepository implements PackerRegistryR
|
|||||||
if (root == null || root.isBlank()) {
|
if (root == null || root.isBlank()) {
|
||||||
throw new PackerRegistryException("Registry asset root must not be blank");
|
throw new PackerRegistryException("Registry asset root must not be blank");
|
||||||
}
|
}
|
||||||
return root.trim().replace('\\', '/');
|
final String normalized = root.trim().replace('\\', '/');
|
||||||
|
final Path normalizedPath = Path.of(normalized).normalize();
|
||||||
|
if (normalizedPath.isAbsolute() || normalizedPath.startsWith("..")) {
|
||||||
|
throw new PackerRegistryException("Registry asset root is outside the trusted assets boundary: " + normalized);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
|||||||
@ -369,7 +369,12 @@ public final class FileSystemPackerMutationService implements PackerMutationServ
|
|||||||
if (preview.targetAssetRoot() == null) {
|
if (preview.targetAssetRoot() == null) {
|
||||||
throw new PackerMutationException("Mutation preview does not define a target asset root");
|
throw new PackerMutationException("Mutation preview does not define a target asset root");
|
||||||
}
|
}
|
||||||
return preview.targetAssetRoot();
|
final Path targetRoot = preview.targetAssetRoot();
|
||||||
|
final Path projectRoot = preview.request().project().rootPath().toAbsolutePath().normalize();
|
||||||
|
if (!targetRoot.toAbsolutePath().normalize().startsWith(projectRoot.resolve("assets").toAbsolutePath().normalize())) {
|
||||||
|
throw new PackerMutationException("Mutation target root is outside the trusted assets boundary");
|
||||||
|
}
|
||||||
|
return targetRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void emit(
|
private void emit(
|
||||||
|
|||||||
@ -90,6 +90,37 @@ final class FileSystemPackerBuildServiceTest {
|
|||||||
assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.BUILD_FINISHED));
|
assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.BUILD_FINISHED));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildFailsOnUnsupportedMetadataSchema() throws Exception {
|
||||||
|
final Path projectRoot = createProject(tempDir.resolve("unsupported-metadata"));
|
||||||
|
Files.createDirectories(projectRoot.resolve("build"));
|
||||||
|
Files.writeString(projectRoot.resolve("build/asset_table_metadata.json"), """
|
||||||
|
{
|
||||||
|
"schema_version": 99,
|
||||||
|
"cache_key": "legacy"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
final FileSystemPackerBuildService service = new FileSystemPackerBuildService();
|
||||||
|
|
||||||
|
final var result = service.build(new PackerBuildRequest(new PackerProjectContext("unsupported-metadata", projectRoot), false));
|
||||||
|
|
||||||
|
assertEquals(PackerOperationStatus.FAILED, result.status());
|
||||||
|
assertTrue(result.diagnostics().stream().anyMatch(diagnostic -> diagnostic.message().contains("Unsupported build metadata schema_version")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void incrementalBuildFailsOnUnsupportedExistingArchivePrelude() throws Exception {
|
||||||
|
final Path projectRoot = createProject(tempDir.resolve("unsupported-archive"));
|
||||||
|
Files.createDirectories(projectRoot.resolve("build"));
|
||||||
|
Files.write(projectRoot.resolve("build/assets.pa"), "BAD!".getBytes(StandardCharsets.UTF_8));
|
||||||
|
final FileSystemPackerBuildService service = new FileSystemPackerBuildService();
|
||||||
|
|
||||||
|
final var result = service.build(new PackerBuildRequest(new PackerProjectContext("unsupported-archive", projectRoot), true));
|
||||||
|
|
||||||
|
assertEquals(PackerOperationStatus.FAILED, result.status());
|
||||||
|
assertTrue(result.diagnostics().stream().anyMatch(diagnostic -> diagnostic.message().contains("prelude is truncated")));
|
||||||
|
}
|
||||||
|
|
||||||
private ParsedArchive parseArchive(Path archivePath) throws Exception {
|
private ParsedArchive parseArchive(Path archivePath) throws Exception {
|
||||||
final byte[] bytes = Files.readAllBytes(archivePath);
|
final byte[] bytes = Files.readAllBytes(archivePath);
|
||||||
final ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
|
final ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|||||||
@ -1,14 +1,21 @@
|
|||||||
package p.packer.declarations;
|
package p.packer.declarations;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
import p.packer.api.diagnostics.PackerDiagnosticCategory;
|
import p.packer.api.diagnostics.PackerDiagnosticCategory;
|
||||||
import p.packer.testing.PackerFixtureLocator;
|
import p.packer.testing.PackerFixtureLocator;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
final class PackerAssetDeclarationParserTest {
|
final class PackerAssetDeclarationParserTest {
|
||||||
private final PackerAssetDeclarationParser parser = new PackerAssetDeclarationParser();
|
private final PackerAssetDeclarationParser parser = new PackerAssetDeclarationParser();
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void parsesValidDeclarationFixture() {
|
void parsesValidDeclarationFixture() {
|
||||||
final var result = parser.parse(PackerFixtureLocator.fixtureRoot("workspaces/managed-basic/assets/ui/atlas/asset.json"));
|
final var result = parser.parse(PackerFixtureLocator.fixtureRoot("workspaces/managed-basic/assets/ui/atlas/asset.json"));
|
||||||
@ -48,4 +55,24 @@ final class PackerAssetDeclarationParserTest {
|
|||||||
assertFalse(result.valid());
|
assertFalse(result.valid());
|
||||||
assertTrue(result.diagnostics().stream().anyMatch(diagnostic -> diagnostic.category() == PackerDiagnosticCategory.VERSIONING));
|
assertTrue(result.diagnostics().stream().anyMatch(diagnostic -> diagnostic.category() == PackerDiagnosticCategory.VERSIONING));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsUntrustedInputPaths() throws Exception {
|
||||||
|
final Path manifest = tempDir.resolve("asset.json");
|
||||||
|
Files.writeString(manifest, """
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"name": "bad_asset",
|
||||||
|
"type": "image_bank",
|
||||||
|
"inputs": { "sprites": ["../outside.png"] },
|
||||||
|
"output": { "format": "TILES/indexed_v1", "codec": "RAW" },
|
||||||
|
"preload": { "enabled": true }
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
final var result = parser.parse(manifest);
|
||||||
|
|
||||||
|
assertFalse(result.valid());
|
||||||
|
assertTrue(result.diagnostics().stream().anyMatch(diagnostic -> diagnostic.message().contains("untrusted path")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -106,6 +106,44 @@ final class PackerWorkspaceFoundationTest {
|
|||||||
assertTrue(exception.getMessage().contains("Unable to load registry"));
|
assertTrue(exception.getMessage().contains("Unable to load registry"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unsupportedRegistrySchemaFailsClearly() throws Exception {
|
||||||
|
final Path projectRoot = tempDir.resolve("main");
|
||||||
|
Files.createDirectories(projectRoot.resolve("assets/.prometeu"));
|
||||||
|
Files.writeString(projectRoot.resolve("assets/.prometeu/index.json"), """
|
||||||
|
{
|
||||||
|
"schema_version": 99,
|
||||||
|
"next_asset_id": 1,
|
||||||
|
"assets": []
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
final FileSystemPackerRegistryRepository repository = new FileSystemPackerRegistryRepository();
|
||||||
|
|
||||||
|
final PackerRegistryException exception = assertThrows(PackerRegistryException.class, () -> repository.load(project(projectRoot)));
|
||||||
|
|
||||||
|
assertTrue(exception.getMessage().contains("Unsupported registry schema_version"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void untrustedRegistryRootFailsClearly() throws Exception {
|
||||||
|
final Path projectRoot = tempDir.resolve("main");
|
||||||
|
Files.createDirectories(projectRoot.resolve("assets/.prometeu"));
|
||||||
|
Files.writeString(projectRoot.resolve("assets/.prometeu/index.json"), """
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"next_asset_id": 2,
|
||||||
|
"assets": [
|
||||||
|
{ "asset_id": 1, "asset_uuid": "uuid-1", "root": "../escape" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
final FileSystemPackerRegistryRepository repository = new FileSystemPackerRegistryRepository();
|
||||||
|
|
||||||
|
final PackerRegistryException exception = assertThrows(PackerRegistryException.class, () -> repository.load(project(projectRoot)));
|
||||||
|
|
||||||
|
assertTrue(exception.getMessage().contains("trusted assets boundary"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void lookupResolvesByIdUuidAndRootAndFailsOnMissingRoot() throws Exception {
|
void lookupResolvesByIdUuidAndRootAndFailsOnMissingRoot() throws Exception {
|
||||||
final Path projectRoot = tempDir.resolve("main");
|
final Path projectRoot = tempDir.resolve("main");
|
||||||
|
|||||||
@ -0,0 +1,133 @@
|
|||||||
|
package p.studio.workspaces.assets;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import p.packer.building.FileSystemPackerBuildService;
|
||||||
|
import p.packer.api.PackerProjectContext;
|
||||||
|
import p.packer.api.building.PackerBuildRequest;
|
||||||
|
import p.packer.api.doctor.PackerDoctorMode;
|
||||||
|
import p.packer.api.doctor.PackerDoctorRequest;
|
||||||
|
import p.packer.declarations.PackerAssetDeclarationParser;
|
||||||
|
import p.packer.declarations.PackerAssetDetailsService;
|
||||||
|
import p.packer.doctor.FileSystemPackerDoctorService;
|
||||||
|
import p.packer.foundation.PackerWorkspaceFoundation;
|
||||||
|
import p.packer.mutations.PackerProjectWriteCoordinator;
|
||||||
|
import p.packer.workspace.FileSystemPackerWorkspaceService;
|
||||||
|
import p.studio.events.StudioAssetsMutationAppliedEvent;
|
||||||
|
import p.studio.events.StudioEventBus;
|
||||||
|
import p.studio.events.StudioPackerEventAdapter;
|
||||||
|
import p.studio.events.StudioPackerOperationEvent;
|
||||||
|
import p.studio.events.StudioWorkspaceEventBus;
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
import p.studio.workspaces.WorkspaceId;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
final class PackerStudioIntegrationTest {
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void packerServicesAndStudioAdapterWorkTogetherEndToEnd() throws Exception {
|
||||||
|
final Path projectRoot = createProject(tempDir.resolve("main"));
|
||||||
|
final ProjectReference project = new ProjectReference("Main", "1.0.0", "pbs", 1, projectRoot);
|
||||||
|
final StudioEventBus globalBus = new StudioEventBus();
|
||||||
|
final StudioWorkspaceEventBus workspaceBus = new StudioWorkspaceEventBus(WorkspaceId.ASSETS, globalBus);
|
||||||
|
final StudioPackerEventAdapter packerAdapter = new StudioPackerEventAdapter(workspaceBus, project);
|
||||||
|
final List<StudioPackerOperationEvent> packerEvents = new ArrayList<>();
|
||||||
|
final List<StudioAssetsMutationAppliedEvent> mutationEvents = new ArrayList<>();
|
||||||
|
globalBus.subscribe(StudioPackerOperationEvent.class, packerEvents::add);
|
||||||
|
globalBus.subscribe(StudioAssetsMutationAppliedEvent.class, mutationEvents::add);
|
||||||
|
|
||||||
|
final PackerBackedAssetWorkspaceService readService = new PackerBackedAssetWorkspaceService(
|
||||||
|
new FileSystemPackerWorkspaceService(
|
||||||
|
new PackerWorkspaceFoundation(),
|
||||||
|
new PackerAssetDeclarationParser(),
|
||||||
|
packerAdapter));
|
||||||
|
final PackerBackedAssetWorkspaceMutationService mutationService = new PackerBackedAssetWorkspaceMutationService(
|
||||||
|
workspaceBus,
|
||||||
|
new PackerWorkspaceFoundation(),
|
||||||
|
new PackerAssetDetailsService(),
|
||||||
|
new PackerProjectWriteCoordinator());
|
||||||
|
final FileSystemPackerDoctorService doctorService = new FileSystemPackerDoctorService(
|
||||||
|
new FileSystemPackerWorkspaceService(
|
||||||
|
new PackerWorkspaceFoundation(),
|
||||||
|
new PackerAssetDeclarationParser(),
|
||||||
|
packerAdapter),
|
||||||
|
new PackerAssetDetailsService(),
|
||||||
|
packerAdapter);
|
||||||
|
final FileSystemPackerBuildService buildService = new FileSystemPackerBuildService(
|
||||||
|
new p.packer.building.PackerBuildPlanner(),
|
||||||
|
packerAdapter);
|
||||||
|
|
||||||
|
final AssetWorkspaceSnapshot snapshot = readService.loadWorkspace(project);
|
||||||
|
assertEquals(2, snapshot.assets().size());
|
||||||
|
|
||||||
|
final AssetWorkspaceAssetSummary orphan = snapshot.assets().stream()
|
||||||
|
.filter(asset -> asset.state() == AssetWorkspaceAssetState.ORPHAN)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
final AssetWorkspaceMutationPreview preview = mutationService.preview(project, orphan, AssetWorkspaceAction.REGISTER);
|
||||||
|
mutationService.apply(project, preview);
|
||||||
|
|
||||||
|
final var doctor = doctorService.doctor(new PackerDoctorRequest(
|
||||||
|
new PackerProjectContext(project.name(), project.rootPath()),
|
||||||
|
PackerDoctorMode.EXPANDED_WORKSPACE,
|
||||||
|
true));
|
||||||
|
final var build = buildService.build(new PackerBuildRequest(new PackerProjectContext(project.name(), project.rootPath()), false));
|
||||||
|
|
||||||
|
assertTrue(mutationEvents.stream().anyMatch(event -> event.action() == AssetWorkspaceAction.REGISTER));
|
||||||
|
assertTrue(doctor.status() == p.packer.api.PackerOperationStatus.SUCCESS || doctor.status() == p.packer.api.PackerOperationStatus.PARTIAL);
|
||||||
|
assertTrue(Files.isRegularFile(build.assetsArchive()));
|
||||||
|
assertTrue(packerEvents.stream().anyMatch(event -> event.kind() == p.packer.api.events.PackerEventKind.BUILD_FINISHED));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path createProject(Path projectRoot) throws Exception {
|
||||||
|
final Path managedRoot = projectRoot.resolve("assets/ui/atlas");
|
||||||
|
final Path orphanRoot = projectRoot.resolve("assets/orphans/ui_sounds");
|
||||||
|
Files.createDirectories(managedRoot.resolve("sprites"));
|
||||||
|
Files.createDirectories(orphanRoot.resolve("sources"));
|
||||||
|
Files.createDirectories(projectRoot.resolve("assets/.prometeu"));
|
||||||
|
Files.writeString(managedRoot.resolve("asset.json"), """
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"name": "ui_atlas",
|
||||||
|
"type": "image_bank",
|
||||||
|
"inputs": { "sprites": ["sprites/confirm.png"] },
|
||||||
|
"output": { "format": "TILES/indexed_v1", "codec": "RAW" },
|
||||||
|
"preload": { "enabled": true }
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
Files.writeString(orphanRoot.resolve("asset.json"), """
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"name": "ui_sounds",
|
||||||
|
"type": "sound_bank",
|
||||||
|
"inputs": { "sources": ["sources/confirm.wav"] },
|
||||||
|
"output": { "format": "SOUND/bank_v1", "codec": "RAW" },
|
||||||
|
"preload": { "enabled": false }
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
Files.writeString(managedRoot.resolve("sprites/confirm.png"), "png");
|
||||||
|
Files.writeString(orphanRoot.resolve("sources/confirm.wav"), "wav");
|
||||||
|
Files.writeString(projectRoot.resolve("assets/.prometeu/index.json"), """
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"next_asset_id": 2,
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"asset_id": 1,
|
||||||
|
"asset_uuid": "uuid-1",
|
||||||
|
"root": "ui/atlas"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
return projectRoot;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user