prometeu-studio/prometeu-packer/src/main/java/p/packer/foundation/FileSystemPackerRegistryRepository.java

136 lines
5.8 KiB
Java

package p.packer.foundation;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import p.packer.api.PackerProjectContext;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public final class FileSystemPackerRegistryRepository implements PackerRegistryRepository {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final int REGISTRY_SCHEMA_VERSION = 1;
@Override
public PackerRegistryState load(PackerProjectContext project) {
final Path registryPath = PackerWorkspacePaths.registryPath(project);
if (!Files.isRegularFile(registryPath)) {
return emptyState();
}
try {
final RegistryDocument document = MAPPER.readValue(registryPath.toFile(), RegistryDocument.class);
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<>();
if (document.assets != null) {
for (RegistryAssetDocument asset : document.assets) {
if (asset == null) {
continue;
}
entries.add(new PackerRegistryEntry(
asset.assetId,
asset.assetUuid,
normalizeRoot(asset.root),
asset.includedInBuild == null || asset.includedInBuild));
}
}
validateEntries(entries);
final int nextAssetId = document.nextAssetId > 0
? document.nextAssetId
: entries.stream().mapToInt(PackerRegistryEntry::assetId).max().orElse(0) + 1;
return new PackerRegistryState(
schemaVersion,
nextAssetId,
entries.stream().sorted(Comparator.comparingInt(PackerRegistryEntry::assetId)).toList());
} catch (IOException exception) {
throw new PackerRegistryException("Unable to load registry: " + registryPath, exception);
}
}
@Override
public void save(PackerProjectContext project, PackerRegistryState state) {
final Path registryDirectory = PackerWorkspacePaths.registryDirectory(project);
final Path registryPath = PackerWorkspacePaths.registryPath(project);
validateEntries(state.assets());
try {
Files.createDirectories(registryDirectory);
final RegistryDocument document = new RegistryDocument();
document.schemaVersion = state.schemaVersion();
document.nextAssetId = state.nextAssetId();
document.assets = state.assets().stream()
.map(entry -> new RegistryAssetDocument(
entry.assetId(),
entry.assetUuid(),
entry.root(),
entry.includedInBuild()))
.toList();
MAPPER.writerWithDefaultPrettyPrinter().writeValue(registryPath.toFile(), document);
} catch (IOException exception) {
throw new PackerRegistryException("Unable to save registry: " + registryPath, exception);
}
}
private PackerRegistryState emptyState() {
return new PackerRegistryState(REGISTRY_SCHEMA_VERSION, 1, List.of());
}
private void validateEntries(List<PackerRegistryEntry> entries) {
final Set<Integer> assetIds = new HashSet<>();
final Set<String> assetUuids = new HashSet<>();
final Set<String> roots = new HashSet<>();
for (PackerRegistryEntry entry : entries) {
if (!assetIds.add(entry.assetId())) {
throw new PackerRegistryException("Duplicate asset_id in registry: " + entry.assetId());
}
if (!assetUuids.add(entry.assetUuid())) {
throw new PackerRegistryException("Duplicate asset_uuid in registry: " + entry.assetUuid());
}
if (!roots.add(entry.root())) {
throw new PackerRegistryException("Duplicate asset root in registry: " + entry.root());
}
}
}
private String normalizeRoot(String root) {
if (root == null || root.isBlank()) {
throw new PackerRegistryException("Registry asset root must not be blank");
}
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)
private static final class RegistryDocument {
@JsonProperty("schema_version")
public int schemaVersion;
@JsonProperty("next_asset_id")
public int nextAssetId;
@JsonProperty("assets")
public List<RegistryAssetDocument> assets = List.of();
}
@JsonIgnoreProperties(ignoreUnknown = true)
private record RegistryAssetDocument(
@JsonProperty("asset_id") int assetId,
@JsonProperty("asset_uuid") String assetUuid,
@JsonProperty("root") String root,
@JsonProperty("included_in_build") Boolean includedInBuild) {
}
}