feat(pki): build revocation checkpoints with bounded memory

Build immutable revocation checkpoints from frozen current-index views
using bounded external sorting and canonical multi-pass merging.

Keep the transition log authoritative while preserving exact historical
revision binding, atomic checkpoint publication, and bounded resources.
This commit is contained in:
2026-08-02 12:56:16 +02:00
parent 7b63f139bf
commit 8582ad6fd4
6 changed files with 2271 additions and 12 deletions

View File

@@ -114,6 +114,32 @@ final class FilesystemRevocationCheckpoint implements AutoCloseable {
Objects.requireNonNull(faults, "faults");
Binding.requireCurrentPublication(recovery, coverage);
Binding.validate(logPath, recovery, coverage);
return publishBound(directory, coverage, source, operations, faults,
checkpoint -> Binding.validateGeneration(checkpoint, logPath, recovery));
}
/* default */ static PublishedGeneration publishHistorical(
Path directory,
FilesystemRevocationCurrentIndex.FrozenSnapshot frozen,
SortedSource source) throws IOException {
Objects.requireNonNull(directory, "directory");
Objects.requireNonNull(frozen, "frozen");
Objects.requireNonNull(source, "source");
Coverage coverage = frozen.coverage();
return publishBound(
directory, coverage, source,
DefaultPublicationOperations.INSTANCE, FaultInjector.NONE,
checkpoint -> HistoricalBinding.validateGeneration(
checkpoint, frozen));
}
private static PublishedGeneration publishBound(
Path directory,
Coverage coverage,
SortedSource source,
PublicationOperations operations,
FaultInjector faults,
GenerationValidator validator) throws IOException {
Files.createDirectories(directory);
requireDirectory(directory);
long declaredCount = source.entryCount();
@@ -126,7 +152,7 @@ final class FilesystemRevocationCheckpoint implements AutoCloseable {
Path target = directory.resolve(finalName(coverage.coveredRevision(), build.generationId()));
boolean published = false;
try {
Binding.validateGeneration(temporary, logPath, recovery);
validator.validate(temporary);
published = publishAtomically(temporary, target, operations, faults);
forceDirectory(directory, operations, faults);
return new PublishedGeneration(target, build.generationId(), coverage.coveredRevision(), published);
@@ -142,6 +168,13 @@ final class FilesystemRevocationCheckpoint implements AutoCloseable {
}
}
/** Exact post-build binding check selected by the publication boundary. */
@FunctionalInterface
private interface GenerationValidator {
/** Validates the completed temporary generation before publication. */
void validate(Path checkpoint) throws IOException;
}
/* default */ static Optional<FilesystemRevocationCheckpoint> discover(
Path directory,
Path logPath,
@@ -652,6 +685,66 @@ final class FilesystemRevocationCheckpoint implements AutoCloseable {
}
/** Exact set-equality proof between a generation and its immutable frozen index. */
private static final class HistoricalBinding {
private static void validateGeneration(
Path checkpointPath,
FilesystemRevocationCurrentIndex.FrozenSnapshot frozen) throws IOException {
try (FileChannel checkpoint = FileChannel.open(
checkpointPath, StandardOpenOption.READ)) {
RevocationCheckpointCodec codec = new RevocationCheckpointCodec();
RevocationCheckpointCodec.ValidatedFile validated = codec.validate(checkpoint);
Coverage expectedCoverage = frozen.coverage();
Coverage actualCoverage = Coverage.from(validated.header());
if (!actualCoverage.equals(expectedCoverage)) {
throw new IOException("Checkpoint coverage changed during construction");
}
if (validated.header().entryCount() != frozen.entryCount()) {
throw new IOException("Checkpoint count differs from its frozen index");
}
validateEntries(frozen, checkpoint, codec, validated);
}
}
private static void validateEntries(
FilesystemRevocationCurrentIndex.FrozenSnapshot frozen,
FileChannel checkpoint,
RevocationCheckpointCodec checkpointCodec,
RevocationCheckpointCodec.ValidatedFile validated) throws IOException {
long offset = RevocationCheckpointCodec.HEADER_BYTES;
RevocationCheckpointCodec.SequentialDecoder decoder =
checkpointCodec.sequentialDecoder();
for (long index = 0L; index < validated.header().entryCount(); index++) {
RevocationCheckpointCodec.DecodedRecord decoded = decoder.read(
checkpoint, offset, validated.trailerOffset());
validateEntry(frozen, decoded.entry());
offset = decoded.nextOffset();
}
}
private static void validateEntry(
FilesystemRevocationCurrentIndex.FrozenSnapshot frozen,
RevocationCheckpointCodec.CurrentStateEntry entry) throws IOException {
RevocationTransitionFrameCodec.CompleteRecord frame = frozen.lookup(
entry.credentialId()).orElseThrow(
() -> new IOException(
"Checkpoint entry is absent from its frozen index"));
if (!frame.data().credentialId().equals(entry.credentialId())
|| frame.data().globalRevision() != entry.globalRevision()
|| frame.data().transition().revision() != entry.credentialRevision()
|| !frame.commitment().equals(entry.transitionCommitment())
|| !RevocationTransitionFrameCodec.transitionsEqual(
frame.data().transition(), entry.transition())
|| !new FrameBounds(frame.recordOffset(), frame.recordEnd()).equals(
new FrameBounds(entry.frameStart(), entry.frameEnd()))) {
throw new IOException("Checkpoint entry disagrees with its frozen current state");
}
}
private record FrameBounds(long start, long end) {
}
}
/** Streaming discovery keeps one candidate descriptor and repeats only for ambiguity fallback. */
private static final class Discovery {
private static Optional<FilesystemRevocationCheckpoint> discover(

View File

@@ -0,0 +1,934 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.pki.impl.fs;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.UUID;
import java.util.logging.Logger;
/**
* Builds one immutable checkpoint from a frozen disk copy of the derived current index.
*
* <p>Run formation and merge fan-in are explicitly bounded. Temporary run files reuse
* the strict checkpoint record codec but remain outside the discoverable checkpoint
* directory. The authoritative revocation log is only read and is never modified.</p>
*/
final class FilesystemRevocationCheckpointBuilder {
private static final Logger LOGGER =
Logger.getLogger(FilesystemRevocationCheckpointBuilder.class.getName());
private static final String CLEANUP_WARNING =
"Revocation checkpoint temporary cleanup was incomplete";
private static final long ZERO = 0L;
private FilesystemRevocationCheckpointBuilder() {
// Static package implementation.
}
/* default */ static FilesystemRevocationCheckpoint.PublishedGeneration build(
FilesystemRevocationCurrentIndex index,
Path checkpointDirectory,
Configuration configuration) throws IOException {
return build(index, checkpointDirectory, configuration, FaultInjector.NONE);
}
/* default */ static FilesystemRevocationCheckpoint.PublishedGeneration build(
FilesystemRevocationCurrentIndex index,
Path checkpointDirectory,
Configuration configuration,
FaultInjector faults) throws IOException {
Objects.requireNonNull(index, "index");
Objects.requireNonNull(checkpointDirectory, "checkpointDirectory");
Objects.requireNonNull(configuration, "configuration");
Objects.requireNonNull(faults, "faults");
configuration.requireValid();
try (Workspace workspace = Workspace.create(
configuration.temporaryDirectory(), faults)) {
return buildInWorkspace(
index, checkpointDirectory, configuration, faults, workspace);
}
}
private static FilesystemRevocationCheckpoint.PublishedGeneration buildInWorkspace(
FilesystemRevocationCurrentIndex index,
Path checkpointDirectory,
Configuration configuration,
FaultInjector faults,
Workspace workspace) throws IOException {
faults.fail(FaultPoint.BEFORE_FREEZE);
try (FrozenBuild frozenBuild = FrozenBuild.open(
index, workspace.frozenPath(), configuration.transferBufferBytes(), faults)) {
FilesystemRevocationCheckpoint.PublishedGeneration result = buildFrozen(
checkpointDirectory, configuration, faults, workspace,
frozenBuild.snapshot());
workspace.markOutcomeAuthoritative();
frozenBuild.markOutcomeAuthoritative();
return result;
}
}
private static FilesystemRevocationCheckpoint.PublishedGeneration buildFrozen(
Path checkpointDirectory,
Configuration configuration,
FaultInjector faults,
Workspace workspace,
FilesystemRevocationCurrentIndex.FrozenSnapshot frozen) throws IOException {
RunFormation formation = new RunFormation(
workspace, frozen.coverage(), configuration, faults);
long runCount = formation.createRuns(frozen);
Path finalRun = new MergePass(
workspace, frozen.coverage(), configuration, faults)
.merge(runCount);
faults.fail(FaultPoint.BEFORE_PUBLICATION);
FilesystemRevocationCheckpoint.SortedSource source = runCount == ZERO
? new EmptySource() : new RunSource(finalRun);
return FilesystemRevocationCheckpoint.publishHistorical(
checkpointDirectory, frozen, source);
}
private static void warnCleanup() {
try {
LOGGER.warning(CLEANUP_WARNING);
} catch (IllegalStateException ignored) {
// Advisory logging cannot change an already published derived checkpoint.
}
}
/** Explicit resource bounds and temporary location for one build. */
/* default */ record Configuration(
long maxEncodedRunBytes,
int maxOpenMergeInputs,
int transferBufferBytes,
Path temporaryDirectory) {
Configuration {
Objects.requireNonNull(temporaryDirectory, "temporaryDirectory");
if (maxEncodedRunBytes <= ZERO || maxOpenMergeInputs < 2
|| transferBufferBytes <= 0) {
throw new IllegalArgumentException("Invalid revocation checkpoint builder bounds");
}
}
private void requireValid() throws IOException {
if (Files.exists(temporaryDirectory, LinkOption.NOFOLLOW_LINKS)
&& (!Files.isDirectory(temporaryDirectory, LinkOption.NOFOLLOW_LINKS)
|| Files.isSymbolicLink(temporaryDirectory))) {
throw new IOException("Revocation checkpoint temporary root is invalid");
}
}
}
/** Deterministic builder lifecycle faults; not production API. */
/* default */ @FunctionalInterface
interface FaultInjector {
FaultInjector NONE = point -> { };
/** Fails at one deterministic lifecycle boundary. */
void fail(FaultPoint point) throws IOException;
/** Observes the bounded entry buffer after one encoded entry is admitted. */
default void observeRunBuffer(long entryCount, long encodedBytes) {
// Optional structural test observation.
}
/** Observes one merge group after all of its bounded readers are open. */
default void observeOpenMergeInputs(int openInputs) {
// Optional structural test observation.
}
/** Observes one completed temporary run and its strict encoded size. */
default void observeTemporaryRun(int pass, long entryCount, long encodedBytes) {
// Optional structural test observation.
}
/** Observes population conservation across one complete merge pass. */
default void observeMergePass(
int pass,
long inputRuns,
long outputRuns,
long inputEntries,
long outputEntries) {
// Optional structural test observation.
}
/** Records one strict UTF-8 encoding used to create a merge head. */
default void observeMergeHeadEncoding() {
// Optional structural test observation.
}
/** Observes one completed strict run before it is consumed or published. */
default void afterRunWritten(Path run) throws IOException {
// Optional deterministic corruption seam.
}
}
/** Finite fault points for frozen-copy, run, merge, publication, and cleanup tests. */
/* default */ enum FaultPoint {
BEFORE_FREEZE,
FROZEN_COPY,
FROZEN_FORCE,
FROZEN_CLOSE,
RUN_WRITE,
RUN_FORCE,
MERGE_WRITE,
MERGE_FORCE,
BEFORE_PUBLICATION,
CLEANUP
}
/** Forms individually authenticated sorted runs with a bounded entry buffer. */
private static final class RunFormation {
private final Workspace workspace;
private final FilesystemRevocationCheckpoint.Coverage coverage;
private final Configuration configuration;
private final FaultInjector faults;
private RunFormation(
Workspace workspace,
FilesystemRevocationCheckpoint.Coverage coverage,
Configuration configuration,
FaultInjector faults) {
this.workspace = workspace;
this.coverage = coverage;
this.configuration = configuration;
this.faults = faults;
}
private long createRuns(FilesystemRevocationCurrentIndex.FrozenSnapshot frozen)
throws IOException {
if (frozen.entryCount() == ZERO) {
return ZERO;
}
long run = ZERO;
try (FilesystemRevocationCurrentIndex.FrozenCursor cursor = frozen.openCursor();
RunAccumulator accumulator = new RunAccumulator(
workspace.measurementPath(), coverage,
configuration.maxEncodedRunBytes(), faults)) {
while (cursor.advance()) {
if (accumulator.add(cursor.current())) {
writeRun(accumulator.drain(), workspace.runPath(0, run));
run = addExact(run, 1L, "Revocation checkpoint run count overflow");
}
}
if (!accumulator.isEmpty()) {
writeRun(accumulator.drain(), workspace.runPath(0, run));
run = addExact(run, 1L, "Revocation checkpoint run count overflow");
}
}
return run;
}
private void writeRun(List<SortableEntry> entries, Path path) throws IOException {
entries.sort(SortableEntry.ORDER);
byte[] previous = null;
try (FileChannel channel = FileChannel.open(path,
StandardOpenOption.CREATE_NEW, StandardOpenOption.READ,
StandardOpenOption.WRITE)) {
RevocationCheckpointCodec codec = new RevocationCheckpointCodec();
RevocationCheckpointCodec.Encoder encoder = codec.encoder(
channel, header(coverage, entries.size()));
for (SortableEntry sortable : entries) {
if (previous != null
&& RevocationCheckpointCodec.compareUnsigned(
previous, sortable.identity()) >= 0) {
throw new IOException("Frozen revocation index contains duplicate identities");
}
faults.fail(FaultPoint.RUN_WRITE);
encoder.write(sortable.entry());
previous = sortable.identity();
}
encoder.finish();
faults.fail(FaultPoint.RUN_FORCE);
channel.force(true);
codec.validate(channel);
}
faults.observeTemporaryRun(0, entries.size(), Files.size(path));
faults.afterRunWritten(path);
}
}
/** Measures actual encoded records while retaining only one configured run. */
private static final class RunAccumulator implements AutoCloseable {
private final Path measurementPath;
private final FilesystemRevocationCheckpoint.Coverage coverage;
private final long limit;
private final FaultInjector faults;
private List<SortableEntry> entries = new ArrayList<>();
private FileChannel channel;
private RevocationCheckpointCodec.Encoder encoder;
private RunAccumulator(
Path measurementPath,
FilesystemRevocationCheckpoint.Coverage coverage,
long limit,
FaultInjector faults) throws IOException {
this.measurementPath = measurementPath;
this.coverage = coverage;
this.limit = limit;
this.faults = faults;
openMeasurement();
}
private boolean add(RevocationCheckpointCodec.CurrentStateEntry entry)
throws IOException {
encoder.write(entry);
entries.add(new SortableEntry(
RevocationCheckpointCodec.strictUtf8(entry.credentialId().value()), entry));
long encodedBytes = channel.position() - RevocationCheckpointCodec.HEADER_BYTES;
faults.observeRunBuffer(entries.size(), encodedBytes);
return encodedBytes >= limit;
}
private boolean isEmpty() {
return entries.isEmpty();
}
private List<SortableEntry> drain() throws IOException {
List<SortableEntry> result = entries;
entries = new ArrayList<>();
channel.close();
Files.deleteIfExists(measurementPath);
openMeasurement();
return result;
}
private void openMeasurement() throws IOException {
channel = FileChannel.open(measurementPath,
StandardOpenOption.CREATE_NEW, StandardOpenOption.READ,
StandardOpenOption.WRITE);
encoder = new RevocationCheckpointCodec().encoder(
channel, header(coverage, Long.MAX_VALUE));
}
@Override
public void close() throws IOException {
IOException failure = null;
try {
channel.close();
} catch (IOException closeFailure) {
failure = closeFailure;
}
try {
Files.deleteIfExists(measurementPath);
} catch (IOException cleanupFailure) {
failure = appendFailure(failure, cleanupFailure);
}
if (failure != null) {
throw failure;
}
}
}
private record SortableEntry(
byte[] identity,
RevocationCheckpointCodec.CurrentStateEntry entry) {
private static final Comparator<SortableEntry> ORDER =
(first, second) -> RevocationCheckpointCodec.compareUnsigned(
first.identity, second.identity);
private SortableEntry {
identity = identity.clone();
Objects.requireNonNull(entry, "entry");
}
@Override
public byte[] identity() {
return identity.clone();
}
}
/** Multi-pass bounded fan-in merge over strict temporary run files. */
private static final class MergePass {
private final Workspace workspace;
private final FilesystemRevocationCheckpoint.Coverage coverage;
private final Configuration configuration;
private final FaultInjector faults;
private MergePass(
Workspace workspace,
FilesystemRevocationCheckpoint.Coverage coverage,
Configuration configuration,
FaultInjector faults) {
this.workspace = workspace;
this.coverage = coverage;
this.configuration = configuration;
this.faults = faults;
}
private Path merge(long initialRunCount) throws IOException {
long runCount = initialRunCount;
int pass = 0;
while (runCount > 1L) {
long outputCount = ZERO;
long inputEntries = ZERO;
long outputEntries = ZERO;
long first = ZERO;
while (first < runCount) {
int groupSize = (int) Math.min(
(long) configuration.maxOpenMergeInputs(), runCount - first);
Path output = workspace.runPath(
addExact(pass, 1, "Revocation checkpoint merge pass overflow"),
outputCount);
long mergedEntries = mergeGroup(pass, first, groupSize, output);
inputEntries = addExact(
inputEntries, mergedEntries,
"Revocation checkpoint merge input population overflow");
outputEntries = addExact(
outputEntries, mergedEntries,
"Revocation checkpoint merge output population overflow");
outputCount = addExact(
outputCount, 1L, "Revocation checkpoint merge count overflow");
first = addExact(first, groupSize,
"Revocation checkpoint merge offset overflow");
}
int completedPass = addExact(
pass, 1, "Revocation checkpoint merge pass overflow");
faults.observeMergePass(
completedPass, runCount, outputCount, inputEntries, outputEntries);
runCount = outputCount;
pass = completedPass;
}
return initialRunCount == ZERO ? null : workspace.runPath(pass, ZERO);
}
private long mergeGroup(
int pass, long first, int groupSize, Path output) throws IOException {
long count;
try (MergeInputs inputs = MergeInputs.open(
workspace, pass, first, groupSize, faults)) {
count = inputs.entryCount();
try (FileChannel channel = FileChannel.open(output,
StandardOpenOption.CREATE_NEW, StandardOpenOption.READ,
StandardOpenOption.WRITE)) {
RevocationCheckpointCodec codec = new RevocationCheckpointCodec();
RevocationCheckpointCodec.Encoder encoder =
codec.encoder(channel, header(coverage, count));
Queue<RunHead> queue = inputs.initialQueue(faults);
byte[] previous = null;
while (!queue.isEmpty()) {
RunHead head = queue.remove();
byte[] identity = head.identity;
if (previous != null
&& RevocationCheckpointCodec.compareUnsigned(
previous, identity) >= 0) {
throw new IOException("Revocation checkpoint merge contains duplicates");
}
faults.fail(FaultPoint.MERGE_WRITE);
encoder.write(head.entry());
previous = identity;
if (head.input().advance()) {
queue.add(RunHead.create(
head.input(), head.input().current(), faults));
}
}
encoder.finish();
faults.fail(FaultPoint.MERGE_FORCE);
channel.force(true);
codec.validate(channel);
}
}
for (int index = 0; index < groupSize; index++) {
Files.delete(workspace.runPath(pass, addExact(
first, index, "Revocation checkpoint merge input overflow")));
}
faults.observeTemporaryRun(
addExact(pass, 1, "Revocation checkpoint merge pass overflow"),
count, Files.size(output));
return count;
}
}
/** Owns at most the configured fan-in of run readers. */
private static final class MergeInputs implements AutoCloseable {
private final List<RunCursor> inputs;
private final long entryCount;
private MergeInputs(List<RunCursor> inputs, long entryCount) {
this.inputs = inputs;
this.entryCount = entryCount;
}
private static MergeInputs open(
Workspace workspace,
int pass,
long first,
int groupSize,
FaultInjector faults) throws IOException {
List<RunCursor> inputs = new ArrayList<>(groupSize);
long count = ZERO;
try {
for (int index = 0; index < groupSize; index++) {
RunCursor input = RunCursor.open(
workspace.runPath(pass, addExact(
first, index,
"Revocation checkpoint merge input overflow")));
inputs.add(input);
count = addExact(count, input.entryCount(),
"Revocation checkpoint merge entry count overflow");
}
faults.observeOpenMergeInputs(inputs.size());
return new MergeInputs(inputs, count);
} catch (IOException failure) {
closeInputs(inputs, failure);
throw failure;
}
}
private long entryCount() {
return entryCount;
}
private Queue<RunHead> initialQueue(FaultInjector faults) throws IOException {
Queue<RunHead> queue = new PriorityQueue<>(
Math.max(1, inputs.size()), RunHead.ORDER);
for (RunCursor input : inputs) {
if (input.advance()) {
queue.add(RunHead.create(input, input.current(), faults));
}
}
return queue;
}
@Override
public void close() throws IOException {
IOException failure = null;
for (RunCursor input : inputs) {
try {
input.close();
} catch (IOException closeFailure) {
failure = appendFailure(failure, closeFailure);
}
}
if (failure != null) {
throw failure;
}
}
private static void closeInputs(List<RunCursor> inputs, IOException failure) {
for (RunCursor input : inputs) {
try {
input.close();
} catch (IOException closeFailure) {
failure.addSuppressed(closeFailure);
}
}
}
}
private record RunHead(
RunCursor input,
RevocationCheckpointCodec.CurrentStateEntry entry,
byte[] identity) {
private static final Comparator<RunHead> ORDER = (first, second) ->
RevocationCheckpointCodec.compareUnsigned(
first.identity, second.identity);
private RunHead {
Objects.requireNonNull(input, "input");
Objects.requireNonNull(entry, "entry");
identity = identity.clone();
}
private static RunHead create(
RunCursor input,
RevocationCheckpointCodec.CurrentStateEntry entry,
FaultInjector faults) {
faults.observeMergeHeadEncoding();
return new RunHead(input, entry, RevocationCheckpointCodec.strictUtf8(
entry.credentialId().value()));
}
@Override
public byte[] identity() {
return identity.clone();
}
}
/** Sequential reader for one validated temporary run. */
private static final class RunCursor {
private final FileChannel channel;
private final RevocationCheckpointCodec.SequentialDecoder decoder;
private final long trailerOffset;
private final long entryCount;
private long offset = RevocationCheckpointCodec.HEADER_BYTES;
private long readCount;
private RevocationCheckpointCodec.CurrentStateEntry current;
private RunCursor(
FileChannel channel,
RevocationCheckpointCodec.SequentialDecoder decoder,
long trailerOffset,
long entryCount) {
this.channel = channel;
this.decoder = decoder;
this.trailerOffset = trailerOffset;
this.entryCount = entryCount;
}
private static RunCursor open(Path path) throws IOException {
FileChannel channel = FileChannel.open(path, StandardOpenOption.READ);
try {
RevocationCheckpointCodec codec = new RevocationCheckpointCodec();
RevocationCheckpointCodec.ValidatedFile validated = codec.validate(channel);
return new RunCursor(
channel, codec.sequentialDecoder(), validated.trailerOffset(),
validated.header().entryCount());
} catch (IOException failure) {
try {
channel.close();
} catch (IOException closeFailure) {
failure.addSuppressed(closeFailure);
}
throw failure;
}
}
private long entryCount() {
return entryCount;
}
private boolean advance() throws IOException {
if (readCount == entryCount) {
current = null;
return false;
}
RevocationCheckpointCodec.DecodedRecord decoded =
decoder.read(channel, offset, trailerOffset);
current = decoded.entry();
offset = decoded.nextOffset();
readCount = addExact(
readCount, 1L, "Revocation checkpoint run count overflow");
return true;
}
private RevocationCheckpointCodec.CurrentStateEntry current() {
if (current == null) {
throw new IllegalStateException("Temporary revocation run has no current entry");
}
return current;
}
private void close() throws IOException {
channel.close();
}
}
/** Final merged run transferred to the existing checkpoint publisher. */
private static final class RunSource implements FilesystemRevocationCheckpoint.SortedSource {
private final Path path;
private final long count;
private boolean opened;
private boolean closed;
private RunSource(Path path) throws IOException {
this.path = Objects.requireNonNull(path, "path");
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
count = new RevocationCheckpointCodec().validate(channel).header().entryCount();
}
}
@Override
public long entryCount() {
requireOpen();
return count;
}
@Override
public FilesystemRevocationCheckpoint.SourceCursor openCursor() throws IOException {
requireOpen();
if (opened) {
throw new IllegalStateException("Revocation run source cursor is already opened");
}
opened = true;
return new SourceCursorAdapter(RunCursor.open(path));
}
@Override
public void close() {
closed = true;
}
private void requireOpen() {
if (closed) {
throw new IllegalStateException("Revocation run source is closed");
}
}
}
/** Transfers one temporary run reader into the checkpoint source cursor contract. */
private static final class SourceCursorAdapter
implements FilesystemRevocationCheckpoint.SourceCursor {
private final RunCursor cursor;
private SourceCursorAdapter(RunCursor cursor) {
this.cursor = cursor;
}
@Override
public boolean advance() throws IOException {
return cursor.advance();
}
@Override
public RevocationCheckpointCodec.CurrentStateEntry current() {
return cursor.current();
}
@Override
public void close() throws IOException {
cursor.close();
}
}
/** Zero-entry source for an authenticated genesis checkpoint. */
private static final class EmptySource implements FilesystemRevocationCheckpoint.SortedSource {
@Override
public long entryCount() {
return ZERO;
}
@Override
public FilesystemRevocationCheckpoint.SourceCursor openCursor() {
return new EmptyCursor();
}
@Override
public void close() {
// No resource.
}
}
/** Stateless cursor paired with the zero-entry source. */
private static final class EmptyCursor implements FilesystemRevocationCheckpoint.SourceCursor {
@Override
public boolean advance() {
return false;
}
@Override
public RevocationCheckpointCodec.CurrentStateEntry current() {
throw new IllegalStateException("Empty revocation source has no current entry");
}
@Override
public void close() {
// No resource.
}
}
/** One isolated build directory; cleanup never touches published generations. */
private static final class Workspace implements AutoCloseable {
private final Path root;
private final FaultInjector faults;
private boolean closed;
private boolean outcomeAuthoritative;
private Workspace(Path root, FaultInjector faults) {
this.root = root;
this.faults = faults;
}
private static Workspace create(Path temporaryRoot, FaultInjector faults)
throws IOException {
Files.createDirectories(temporaryRoot);
if (!Files.isDirectory(temporaryRoot, LinkOption.NOFOLLOW_LINKS)
|| Files.isSymbolicLink(temporaryRoot)) {
throw new IOException("Revocation checkpoint temporary root is invalid");
}
Path root = temporaryRoot.resolve(".checkpoint-build-" + UUID.randomUUID());
Files.createDirectory(root);
return new Workspace(root, faults);
}
private Path frozenPath() {
return root.resolve("frozen-index");
}
private Path measurementPath() {
return root.resolve("measurement");
}
private Path runPath(int pass, long run) {
return root.resolve("p-" + pass + "-r-" + run + ".run");
}
private void markOutcomeAuthoritative() {
outcomeAuthoritative = true;
}
@Override
public void close() throws IOException {
if (closed) {
return;
}
closed = true;
IOException failure = null;
try {
faults.fail(FaultPoint.CLEANUP);
} catch (IOException cleanupFailure) {
failure = cleanupFailure;
}
try (DirectoryStream<Path> paths = Files.newDirectoryStream(root)) {
for (Path path : paths) {
try {
Files.deleteIfExists(path);
} catch (IOException cleanupFailure) {
failure = appendFailure(failure, cleanupFailure);
}
}
} catch (IOException cleanupFailure) {
failure = appendFailure(failure, cleanupFailure);
}
try {
Files.deleteIfExists(root);
} catch (IOException cleanupFailure) {
failure = appendFailure(failure, cleanupFailure);
}
if (failure != null) {
if (outcomeAuthoritative) {
warnCleanup();
} else {
throw failure;
}
}
}
}
/** Owns the immutable index copy and preserves an established publication outcome. */
private static final class FrozenBuild implements AutoCloseable {
private final FilesystemRevocationCurrentIndex.FrozenSnapshot snapshot;
private final FaultInjector faults;
private boolean outcomeAuthoritative;
private boolean closed;
private FrozenBuild(
FilesystemRevocationCurrentIndex.FrozenSnapshot snapshot,
FaultInjector faults) {
this.snapshot = snapshot;
this.faults = faults;
}
private static FrozenBuild open(
FilesystemRevocationCurrentIndex index,
Path frozenPath,
int transferBufferBytes,
FaultInjector faults) throws IOException {
FilesystemRevocationCurrentIndex.FrozenSnapshot snapshot = index.freeze(
frozenPath, transferBufferBytes, point -> {
if (point == FilesystemRevocationCurrentIndex.FrozenFaultPoint.COPY) {
faults.fail(FaultPoint.FROZEN_COPY);
} else {
faults.fail(FaultPoint.FROZEN_FORCE);
}
});
return new FrozenBuild(snapshot, faults);
}
private FilesystemRevocationCurrentIndex.FrozenSnapshot snapshot() {
return snapshot;
}
private void markOutcomeAuthoritative() {
outcomeAuthoritative = true;
}
@Override
public void close() throws IOException {
if (closed) {
return;
}
closed = true;
IOException failure = null;
try {
faults.fail(FaultPoint.FROZEN_CLOSE);
} catch (IOException injected) {
failure = injected;
}
try {
snapshot.close();
} catch (IOException closeFailure) {
failure = appendFailure(failure, closeFailure);
}
if (failure != null) {
if (outcomeAuthoritative) {
warnCleanup();
} else {
throw failure;
}
}
}
}
private static RevocationCheckpointCodec.HeaderData header(
FilesystemRevocationCheckpoint.Coverage coverage, long count) {
return new RevocationCheckpointCodec.HeaderData(
coverage.storeId(), coverage.coveredRevision(), coverage.finalRecordStart(),
coverage.coveredBoundary(), coverage.globalCommitment(), count);
}
private static long addExact(long first, long second, String message) throws IOException {
try {
return Math.addExact(first, second);
} catch (ArithmeticException overflow) {
throw new IOException(message, overflow);
}
}
private static int addExact(int first, int second, String message) throws IOException {
try {
return Math.addExact(first, second);
} catch (ArithmeticException overflow) {
throw new IOException(message, overflow);
}
}
private static IOException appendFailure(IOException primary, IOException secondary) {
if (primary == null) {
return secondary;
}
primary.addSuppressed(secondary);
return primary;
}
}

View File

@@ -111,6 +111,7 @@ final class FilesystemRevocationCurrentIndex implements AutoCloseable {
private static final HexFormat HEX = HexFormat.of();
private final Path indexPath;
private final Path logPath;
private final MetadataStoreId storeId;
private final Configuration configuration;
private final PublicationOperations operations;
@@ -126,6 +127,7 @@ final class FilesystemRevocationCurrentIndex implements AutoCloseable {
private FilesystemRevocationCurrentIndex(
Path indexPath,
Path logPath,
MetadataStoreId storeId,
Configuration configuration,
PublicationOperations operations,
@@ -135,6 +137,7 @@ final class FilesystemRevocationCurrentIndex implements AutoCloseable {
FileChannel logChannel,
SelectedSuperblock selected) {
this.indexPath = indexPath;
this.logPath = logPath;
this.storeId = storeId;
this.configuration = configuration;
this.operations = operations;
@@ -172,13 +175,14 @@ final class FilesystemRevocationCurrentIndex implements AutoCloseable {
IoOperations.requireRegular(indexPath, "Revocation current index");
IoOperations.requireRegular(logPath, "Revocation transition log");
try (OpenResources resources = OpenResources.acquire(indexPath, logPath)) {
return openLocked(indexPath, expectedStoreId, configuration,
return openLocked(indexPath, logPath, expectedStoreId, configuration,
operations, faults, resources);
}
}
private static FilesystemRevocationCurrentIndex openLocked(
Path indexPath,
Path logPath,
MetadataStoreId expectedStoreId,
Configuration configuration,
PublicationOperations operations,
@@ -191,7 +195,7 @@ final class FilesystemRevocationCurrentIndex implements AutoCloseable {
SelectedSuperblock selected = CandidateSelection.select(
resources, expectedStoreId, candidates);
FilesystemRevocationCurrentIndex opened = new FilesystemRevocationCurrentIndex(
indexPath, expectedStoreId, configuration, operations, faults,
indexPath, logPath, expectedStoreId, configuration, operations, faults,
resources.indexChannel(), resources.stableLock(),
resources.logChannel(), selected);
opened.new IndexAccess().applySuffix();
@@ -241,7 +245,7 @@ final class FilesystemRevocationCurrentIndex implements AutoCloseable {
IoOperations.forceDirectory(parent, operations, faults);
try (OpenResources resources = OpenResources.acquireLocked(
stable, indexPath, logPath)) {
return openLocked(indexPath, expectedStoreId, configuration,
return openLocked(indexPath, logPath, expectedStoreId, configuration,
operations, faults, resources);
}
} finally {
@@ -322,6 +326,43 @@ final class FilesystemRevocationCurrentIndex implements AutoCloseable {
}
}
/* default */ FrozenSnapshot freeze(Path snapshotPath, int transferBufferBytes)
throws IOException {
return freeze(snapshotPath, transferBufferBytes, FrozenFaultInjector.NONE);
}
/* default */ FrozenSnapshot freeze(
Path snapshotPath,
int transferBufferBytes,
FrozenFaultInjector faults) throws IOException {
Objects.requireNonNull(snapshotPath, "snapshotPath");
Objects.requireNonNull(faults, "faults");
if (transferBufferBytes <= 0) {
throw new IllegalArgumentException("Freeze transfer buffer must be positive");
}
lifecycleLock.lock();
try {
requireOperational();
try (FrozenResources resources = FrozenResources.create(snapshotPath, logPath)) {
long expectedSize = IoOperations.expectedSize(active.capacity());
faults.fail(FrozenFaultPoint.COPY);
IoOperations.copyExact(
indexChannel, resources.indexChannel(), expectedSize, transferBufferBytes);
faults.fail(FrozenFaultPoint.FORCE);
resources.indexChannel().force(true);
IndexValidation.validate(
resources.indexChannel(), resources.logChannel(), storeId, active);
FrozenSnapshot snapshot = new FrozenSnapshot(
snapshotPath, logPath,
resources.indexChannel(), resources.logChannel(), active);
resources.transferOwnership();
return snapshot;
}
} finally {
lifecycleLock.unlock();
}
}
@Override
public void close() throws IOException {
lifecycleLock.lock();
@@ -879,6 +920,34 @@ final class FilesystemRevocationCurrentIndex implements AutoCloseable {
}
}
private static void copyExact(
FileChannel source,
FileChannel target,
long length,
int transferBufferBytes) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(transferBufferBytes);
long offset = 0L;
while (offset < length) {
buffer.clear();
buffer.limit((int) Math.min((long) buffer.capacity(), length - offset));
int read = source.read(buffer, offset);
if (read <= 0) {
throw new IOException("Revocation current-index freeze made no read progress");
}
buffer.flip();
long writeOffset = offset;
while (buffer.hasRemaining()) {
int written = target.write(buffer, writeOffset);
if (written <= 0) {
throw new IOException("Revocation current-index freeze made no write progress");
}
writeOffset += written;
}
offset = writeOffset;
}
target.truncate(length);
}
private static void requireExpectedSize(FileChannel channel, long capacity) throws IOException {
if (channel.size() != expectedSize(capacity)) {
throw new IOException("Revocation current index physical size is invalid");
@@ -1428,6 +1497,270 @@ final class FilesystemRevocationCurrentIndex implements AutoCloseable {
}
}
/** Deterministic frozen-copy failure seam; package access is intentional for tests. */
/* default */ @FunctionalInterface
interface FrozenFaultInjector {
FrozenFaultInjector NONE = point -> { };
/** Fails at one copy or force boundary. */
void fail(FrozenFaultPoint point) throws IOException;
}
/** Finite lifecycle boundaries for the frozen derived-index copy. */
/* default */ enum FrozenFaultPoint {
COPY,
FORCE
}
/** Immutable physical-slot copy used by bounded checkpoint construction. */
/* default */ static final class FrozenSnapshot implements AutoCloseable {
private final Path path;
private final Path logPath;
private final FileChannel indexChannel;
private final FileChannel logChannel;
private final Superblock state;
private boolean closed;
private FrozenSnapshot(
Path path,
Path logPath,
FileChannel indexChannel,
FileChannel logChannel,
Superblock state) {
this.path = path;
this.logPath = logPath;
this.indexChannel = indexChannel;
this.logChannel = logChannel;
this.state = state;
}
/* default */ long entryCount() {
requireOpen();
return state.entryCount();
}
/* default */ FilesystemRevocationCheckpoint.Coverage coverage() {
requireOpen();
return new FilesystemRevocationCheckpoint.Coverage(
state.storeId(), state.coveredRevision(), state.finalRecordStart(),
state.coveredBoundary(), state.coveredCommitment());
}
/* default */ FrozenCursor openCursor() {
requireOpen();
return new PhysicalCursor(this);
}
/* default */ Path logPath() {
requireOpen();
return logPath;
}
/* default */ Optional<RevocationTransitionFrameCodec.CompleteRecord> lookup(
PkiId credentialId) throws IOException {
Objects.requireNonNull(credentialId, "credentialId");
requireOpen();
long slot = IndexValidation.probeSlot(
indexChannel, logChannel, state, credentialId);
if (slot == NO_SLOT) {
return Optional.empty();
}
Cell cell = IndexOperations.activeCell(
indexChannel, slot, state.coveredRevision()).cell();
return Optional.of(IndexValidation.readFrame(logChannel, cell));
}
private void requireOpen() {
if (closed || !indexChannel.isOpen() || !logChannel.isOpen()) {
throw new IllegalStateException("Frozen revocation current index is closed");
}
}
@Override
public void close() throws IOException {
if (closed) {
return;
}
closed = true;
IOException failure = null;
try {
logChannel.close();
} catch (IOException closeFailure) {
failure = closeFailure;
}
try {
indexChannel.close();
} catch (IOException closeFailure) {
failure = IoOperations.appendFailure(failure, closeFailure);
}
try {
Files.deleteIfExists(path);
} catch (IOException closeFailure) {
failure = IoOperations.appendFailure(failure, closeFailure);
}
if (failure != null) {
throw failure;
}
}
}
/** One-entry-at-a-time physical-slot cursor over a frozen index. */
/* default */ interface FrozenCursor extends AutoCloseable {
/** Advances to the next occupied physical slot. */
boolean advance() throws IOException;
/** Returns the entry selected by the most recent successful advancement. */
RevocationCheckpointCodec.CurrentStateEntry current();
@Override
void close();
}
/** Cursor state remains constant regardless of frozen index population. */
private static final class PhysicalCursor implements FrozenCursor {
private final FrozenSnapshot owner;
private long slot;
private long emitted;
private RevocationCheckpointCodec.CurrentStateEntry current;
private boolean closed;
private PhysicalCursor(FrozenSnapshot owner) {
this.owner = owner;
}
@Override
public boolean advance() throws IOException {
requireOpen();
while (slot < owner.state.capacity()) {
Cell cell = IndexOperations.activeCell(
owner.indexChannel, slot, owner.state.coveredRevision()).cell();
slot++;
if (cell.state() == SlotState.OCCUPIED) {
RevocationTransitionFrameCodec.CompleteRecord record =
IndexValidation.readFrame(owner.logChannel, cell);
emitted = IoOperations.addExact(
emitted, 1L, ENTRY_COUNT_OVERFLOW);
if (emitted > owner.state.entryCount()) {
throw new IOException("Frozen revocation index contains excess entries");
}
current = checkpointEntry(record);
return true;
}
}
if (emitted != owner.state.entryCount()) {
throw new IOException("Frozen revocation index entry count is inconsistent");
}
current = null;
return false;
}
@Override
public RevocationCheckpointCodec.CurrentStateEntry current() {
requireOpen();
if (current == null) {
throw new IllegalStateException("Frozen revocation cursor has no current entry");
}
return current;
}
@Override
public void close() {
closed = true;
current = null;
}
private void requireOpen() {
if (closed) {
throw new IllegalStateException("Frozen revocation cursor is closed");
}
owner.requireOpen();
}
private static RevocationCheckpointCodec.CurrentStateEntry checkpointEntry(
RevocationTransitionFrameCodec.CompleteRecord record) {
return new RevocationCheckpointCodec.CurrentStateEntry(
record.data().credentialId(), record.data().globalRevision(),
record.data().transition().revision(), record.commitment(),
record.data().transition(), record.recordOffset(), record.recordEnd());
}
}
/** Owns a frozen copy and independent log reader until snapshot transfer. */
private static final class FrozenResources implements AutoCloseable {
private final Path path;
private final FileChannel indexChannel;
private final FileChannel logChannel;
private boolean transferred;
private FrozenResources(
Path path, FileChannel indexChannel, FileChannel logChannel) {
this.path = path;
this.indexChannel = indexChannel;
this.logChannel = logChannel;
}
private static FrozenResources create(Path path, Path logPath) throws IOException {
FileChannel index = FileChannel.open(path,
StandardOpenOption.CREATE_NEW, StandardOpenOption.READ,
StandardOpenOption.WRITE, LinkOption.NOFOLLOW_LINKS);
try {
FileChannel log = FileChannel.open(
logPath, StandardOpenOption.READ, LinkOption.NOFOLLOW_LINKS);
return new FrozenResources(path, index, log);
} catch (IOException failure) {
try {
index.close();
} catch (IOException closeFailure) {
failure.addSuppressed(closeFailure);
}
try {
Files.deleteIfExists(path);
} catch (IOException cleanupFailure) {
failure.addSuppressed(cleanupFailure);
}
throw failure;
}
}
private FileChannel indexChannel() {
return indexChannel;
}
private FileChannel logChannel() {
return logChannel;
}
private void transferOwnership() {
transferred = true;
}
@Override
public void close() throws IOException {
if (transferred) {
return;
}
IOException failure = null;
try {
logChannel.close();
} catch (IOException closeFailure) {
failure = closeFailure;
}
try {
indexChannel.close();
} catch (IOException closeFailure) {
failure = IoOperations.appendFailure(failure, closeFailure);
}
try {
Files.deleteIfExists(path);
} catch (IOException cleanupFailure) {
failure = IoOperations.appendFailure(failure, cleanupFailure);
}
if (failure != null) {
throw failure;
}
}
}
/** Stable adjacent lock whose inode is never replaced with an index generation. */
private static final class StableIndexLock implements AutoCloseable {
private final FileChannel channel;

View File

@@ -60,6 +60,7 @@ final class FsPaths {
private static final String BINARY_EXTENSION = ".bin";
private static final String BY_ID = "by-id";
private static final String REVOCATIONS_DIRECTORY = "revocations";
/* default */ static final String VERSION_FILE = "VERSION";
/* default */ static final String LOCK_DIR = ".lock";
@@ -166,7 +167,8 @@ final class FsPaths {
/* default */ Path revocationDir(final PkiId credentialId) {
Objects.requireNonNull(credentialId, "credentialId");
return this.root.resolve("revocations").resolve("by-credential").resolve(FsUtil.safeId(credentialId));
return this.root.resolve(REVOCATIONS_DIRECTORY).resolve("by-credential")
.resolve(FsUtil.safeId(credentialId));
}
/* default */ Path revocationJournal(final PkiId credentialId) {
@@ -174,19 +176,23 @@ final class FsPaths {
}
/* default */ Path revocationTransitionLog() {
return this.root.resolve("revocations").resolve("transitions.log");
return this.root.resolve(REVOCATIONS_DIRECTORY).resolve("transitions.log");
}
/* default */ Path revocationCheckpointDirectory() {
return this.root.resolve("revocations").resolve("checkpoints");
return this.root.resolve(REVOCATIONS_DIRECTORY).resolve("checkpoints");
}
/* default */ Path revocationCheckpointWorkDirectory() {
return this.root.resolve(REVOCATIONS_DIRECTORY).resolve("checkpoint-work");
}
/* default */ Path revocationCurrentIndex() {
return this.root.resolve("revocations").resolve("current-state.idx");
return this.root.resolve(REVOCATIONS_DIRECTORY).resolve("current-state.idx");
}
/* default */ Path revocationCurrentIndexLock() {
return this.root.resolve("revocations").resolve("current-state.idx.lock");
return this.root.resolve(REVOCATIONS_DIRECTORY).resolve("current-state.idx.lock");
}
/* default */ Path revocationSnapshotRoot() {

View File

@@ -0,0 +1,893 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.pki.impl.fs;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.attr.AttributeId;
import zeroecho.pki.api.attr.AttributeValue;
import zeroecho.pki.api.revocation.RevocationReason;
import zeroecho.pki.api.revocation.RevocationState;
import zeroecho.pki.api.revocation.RevocationTransition;
import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
import zeroecho.core.io.CancellationSignal;
import zeroecho.pki.spi.store.MetadataStoreId;
final class FilesystemRevocationCheckpointBuilderTest {
private static final MetadataStoreId STORE_ID =
new MetadataStoreId("102030405060708090a0b0c0d0e0f001");
private static final FilesystemRevocationCurrentIndex.Configuration INDEX_CONFIGURATION =
new FilesystemRevocationCurrentIndex.Configuration(2L, 1, 2);
@TempDir
private Path temporaryDirectory;
@Test
void boundedMultiPassBuildPublishesCanonicalCurrentState() throws Exception {
System.out.print("boundedMultiPassBuildPublishesCanonicalCurrentState ");
try (Fixture fixture = fixture("multi-pass")) {
List<PkiId> identities = List.of(
new PkiId("credential:zeta"), new PkiId("credential:beta"),
new PkiId("credential:eta"), new PkiId("credential:alpha"),
new PkiId("credential:delta"), new PkiId("credential:gamma"),
new PkiId("credential:epsilon"));
fixture.appendAll(identities);
AtomicInteger mergeWrites = new AtomicInteger();
FilesystemRevocationCheckpointBuilder.FaultInjector faults = point -> {
if (point == FilesystemRevocationCheckpointBuilder.FaultPoint.MERGE_WRITE) {
mergeWrites.incrementAndGet();
}
};
try (FilesystemRevocationCurrentIndex index = fixture.rebuild()) {
FilesystemRevocationCheckpoint.PublishedGeneration generation =
FilesystemRevocationCheckpointBuilder.build(
index, fixture.checkpoints(), fixture.builderConfiguration(), faults);
try (FilesystemRevocationCheckpoint checkpoint = FilesystemRevocationCheckpoint.open(
generation.path(), fixture.logPath(), fixture.log().scan())) {
assertEquals(identities.size(), checkpoint.entryCount());
assertEquals(List.of(
"credential:alpha", "credential:beta", "credential:delta",
"credential:epsilon", "credential:eta", "credential:gamma",
"credential:zeta"), collect(checkpoint));
}
}
assertTrue(mergeWrites.get() > identities.size());
assertTrue(directoryEmpty(fixture.workRoot()));
}
System.out.println("...ok");
}
@Test
void frozenSnapshotRemainsAtCapturedRevisionDuringLiveUpdate() throws Exception {
System.out.print("frozenSnapshotRemainsAtCapturedRevisionDuringLiveUpdate ");
try (Fixture fixture = fixture("frozen")) {
PkiId first = new PkiId("credential:first");
PkiId second = new PkiId("credential:second");
fixture.log().append(first, held(1L, 1L));
try (FilesystemRevocationCurrentIndex index = fixture.rebuild()) {
Path frozenPath = fixture.workRoot().resolve("manual-frozen");
Files.createDirectories(fixture.workRoot());
try (FilesystemRevocationCurrentIndex.FrozenSnapshot frozen =
index.freeze(frozenPath, 31)) {
RevocationTransitionFrameCodec.CompleteRecord appended =
fixture.log().append(second, held(1L, 2L));
index.update(appended);
assertEquals(1L, frozen.coverage().coveredRevision());
assertEquals(List.of(first), collect(frozen));
}
assertFalse(Files.exists(frozenPath));
assertEquals(2L, index.coveredGlobalRevision());
}
}
System.out.println("...ok");
}
@Test
void logAppendAfterFreezeProducesValidHistoricalCheckpoint() throws Exception {
System.out.print("logAppendAfterFreezeProducesValidHistoricalCheckpoint ");
try (Fixture fixture = fixture("historical")) {
PkiId first = new PkiId("credential:first");
fixture.log().append(first, held(1L, 1L));
AtomicBoolean appended = new AtomicBoolean();
FilesystemRevocationCheckpointBuilder.FaultInjector faults = point -> {
if (point == FilesystemRevocationCheckpointBuilder.FaultPoint.BEFORE_PUBLICATION
&& appended.compareAndSet(false, true)) {
fixture.log().append(
new PkiId("credential:later"), held(1L, 2L));
}
};
try (FilesystemRevocationCurrentIndex index = fixture.rebuild()) {
FilesystemRevocationCheckpoint.PublishedGeneration generation =
FilesystemRevocationCheckpointBuilder.build(
index, fixture.checkpoints(), fixture.builderConfiguration(), faults);
FilesystemRevocationLog.RecoveryResult recovery = fixture.log().scan();
try (FilesystemRevocationCheckpoint checkpoint = FilesystemRevocationCheckpoint.open(
generation.path(), fixture.logPath(), recovery)) {
assertEquals(1L, checkpoint.coveredRevision());
assertEquals(List.of(first.value()), collect(checkpoint));
}
assertEquals(2L, recovery.globalRevision());
}
}
System.out.println("...ok");
}
@Test
void emptyIndexBuildsGenesisCheckpoint() throws Exception {
System.out.print("emptyIndexBuildsGenesisCheckpoint ");
try (Fixture fixture = fixture("empty")) {
try (FilesystemRevocationCurrentIndex index = fixture.rebuild()) {
FilesystemRevocationCheckpoint.PublishedGeneration generation =
FilesystemRevocationCheckpointBuilder.build(
index, fixture.checkpoints(), fixture.builderConfiguration());
try (FilesystemRevocationCheckpoint checkpoint = FilesystemRevocationCheckpoint.open(
generation.path(), fixture.logPath(), fixture.log().scan())) {
assertEquals(0L, checkpoint.coveredRevision());
assertEquals(0L, checkpoint.entryCount());
}
}
assertTrue(directoryEmpty(fixture.workRoot()));
}
System.out.println("...ok");
}
@Test
void runFailurePublishesNothingAndCleansWorkspace() throws Exception {
System.out.print("runFailurePublishesNothingAndCleansWorkspace ");
try (Fixture fixture = fixture("failure")) {
fixture.log().append(new PkiId("credential:first"), held(1L, 1L));
FilesystemRevocationCheckpointBuilder.FaultInjector faults = point -> {
if (point == FilesystemRevocationCheckpointBuilder.FaultPoint.RUN_WRITE) {
throw new IOException("injected run failure");
}
};
try (FilesystemRevocationCurrentIndex index = fixture.rebuild()) {
assertThrows(IOException.class, () ->
FilesystemRevocationCheckpointBuilder.build(
index, fixture.checkpoints(),
fixture.builderConfiguration(), faults));
assertEquals(1L, index.coveredGlobalRevision());
}
assertFalse(hasCheckpoint(fixture.checkpoints()));
assertTrue(directoryEmpty(fixture.workRoot()));
}
System.out.println("...ok");
}
@Test
void boundsAreValidatedWithoutCreatingWorkspace() throws Exception {
System.out.print("boundsAreValidatedWithoutCreatingWorkspace ");
Path work = temporaryDirectory.resolve("invalid-work");
assertThrows(IllegalArgumentException.class, () ->
new FilesystemRevocationCheckpointBuilder.Configuration(0L, 2, 64, work));
assertThrows(IllegalArgumentException.class, () ->
new FilesystemRevocationCheckpointBuilder.Configuration(64L, 1, 64, work));
assertThrows(IllegalArgumentException.class, () ->
new FilesystemRevocationCheckpointBuilder.Configuration(64L, 2, 0, work));
assertFalse(Files.exists(work));
System.out.println("...ok");
}
@Test
void frozenCopyAndForceFailuresReleaseBuilderResources() throws Exception {
System.out.print("frozenCopyAndForceFailuresReleaseBuilderResources ");
try (Fixture fixture = fixture("frozen-failures")) {
PkiId identity = new PkiId("credential:frozen-failure");
fixture.log().append(identity, held(1L, 1L));
try (FilesystemRevocationCurrentIndex index = fixture.rebuild()) {
for (FilesystemRevocationCheckpointBuilder.FaultPoint failurePoint : List.of(
FilesystemRevocationCheckpointBuilder.FaultPoint.FROZEN_COPY,
FilesystemRevocationCheckpointBuilder.FaultPoint.FROZEN_FORCE)) {
FilesystemRevocationCheckpointBuilder.FaultInjector faults = point -> {
if (point == failurePoint) {
throw new IOException("injected frozen failure");
}
};
assertThrows(IOException.class, () ->
FilesystemRevocationCheckpointBuilder.build(
index, fixture.checkpoints(),
fixture.builderConfiguration(), faults));
assertEquals(identity,
index.lookup(identity).orElseThrow().data().credentialId());
assertTrue(directoryEmpty(fixture.workRoot()));
}
FilesystemRevocationCheckpoint.PublishedGeneration generation =
FilesystemRevocationCheckpointBuilder.build(
index, fixture.checkpoints(), fixture.builderConfiguration());
assertTrue(Files.isRegularFile(generation.path()));
}
}
System.out.println("...ok");
}
@Test
void runAndMergeForceFailuresPreservePreviousGeneration() throws Exception {
System.out.print("runAndMergeForceFailuresPreservePreviousGeneration ");
try (Fixture fixture = fixture("force-failures")) {
PkiId first = new PkiId("credential:first");
PkiId second = new PkiId("credential:second");
fixture.log().append(first, held(1L, 1L));
try (FilesystemRevocationCurrentIndex index = fixture.rebuild()) {
FilesystemRevocationCheckpoint.PublishedGeneration previous =
FilesystemRevocationCheckpointBuilder.build(
index, fixture.checkpoints(), fixture.builderConfiguration());
RevocationTransitionFrameCodec.CompleteRecord appended =
fixture.log().append(second, held(1L, 2L));
index.update(appended);
for (FilesystemRevocationCheckpointBuilder.FaultPoint failurePoint : List.of(
FilesystemRevocationCheckpointBuilder.FaultPoint.RUN_FORCE,
FilesystemRevocationCheckpointBuilder.FaultPoint.MERGE_FORCE)) {
FilesystemRevocationCheckpointBuilder.FaultInjector faults = point -> {
if (point == failurePoint) {
throw new IOException("injected temporary force failure");
}
};
assertThrows(IOException.class, () ->
FilesystemRevocationCheckpointBuilder.build(
index, fixture.checkpoints(),
fixture.builderConfiguration(), faults));
assertTrue(Files.isRegularFile(previous.path()));
assertTrue(directoryEmpty(fixture.workRoot()));
}
try (FilesystemRevocationCheckpoint checkpoint =
FilesystemRevocationCheckpoint.open(
previous.path(), fixture.logPath(), fixture.log().scan())) {
assertEquals(List.of(first.value()), collect(checkpoint));
}
}
}
System.out.println("...ok");
}
@Test
void boundedResourcesAndRepeatedBuildAreDeterministic() throws Exception {
System.out.print("boundedResourcesAndRepeatedBuildAreDeterministic ");
try (Fixture fixture = fixture("bounded-observation")) {
fixture.appendAll(List.of(
new PkiId("credential:h"), new PkiId("credential:d"),
new PkiId("credential:f"), new PkiId("credential:b"),
new PkiId("credential:g"), new PkiId("credential:c"),
new PkiId("credential:e"), new PkiId("credential:a")));
AtomicLong maximumRunEntries = new AtomicLong();
AtomicLong maximumRunBytes = new AtomicLong();
AtomicLong maximumTemporaryBytes = new AtomicLong();
AtomicLong initialRunEntries = new AtomicLong();
AtomicLong expectedHeadEncodings = new AtomicLong();
AtomicLong actualHeadEncodings = new AtomicLong();
AtomicInteger maximumOpenInputs = new AtomicInteger();
AtomicInteger initialRunCount = new AtomicInteger();
AtomicInteger mergePassCount = new AtomicInteger();
FilesystemRevocationCheckpointBuilder.FaultInjector observer =
new FilesystemRevocationCheckpointBuilder.FaultInjector() {
@Override
public void fail(FilesystemRevocationCheckpointBuilder.FaultPoint point) {
// Observation only.
}
@Override
public void observeRunBuffer(long entryCount, long encodedBytes) {
maximumRunEntries.accumulateAndGet(entryCount, Math::max);
maximumRunBytes.accumulateAndGet(encodedBytes, Math::max);
}
@Override
public void observeOpenMergeInputs(int openInputs) {
maximumOpenInputs.accumulateAndGet(openInputs, Math::max);
}
@Override
public void observeTemporaryRun(
int pass, long entryCount, long encodedBytes) {
maximumTemporaryBytes.accumulateAndGet(encodedBytes, Math::max);
if (pass == 0) {
initialRunCount.incrementAndGet();
initialRunEntries.addAndGet(entryCount);
}
}
@Override
public void observeMergePass(
int pass,
long inputRuns,
long outputRuns,
long inputEntries,
long outputEntries) {
mergePassCount.incrementAndGet();
assertEquals(inputEntries, outputEntries);
assertEquals((inputRuns + 1L) / 2L, outputRuns);
expectedHeadEncodings.addAndGet(inputEntries);
}
@Override
public void observeMergeHeadEncoding() {
actualHeadEncodings.incrementAndGet();
}
};
try (FilesystemRevocationCurrentIndex index = fixture.rebuild()) {
FilesystemRevocationCheckpoint.PublishedGeneration first =
FilesystemRevocationCheckpointBuilder.build(
index, fixture.checkpoints(),
fixture.builderConfiguration(), observer);
FilesystemRevocationCheckpoint.PublishedGeneration second =
FilesystemRevocationCheckpointBuilder.build(
index, fixture.checkpoints(),
fixture.builderConfiguration(), observer);
assertEquals(first.generationId(), second.generationId());
assertEquals(first.path(), second.path());
assertFalse(second.newlyPublished());
}
assertEquals(1L, maximumRunEntries.get());
assertTrue(maximumRunBytes.get() > 0L);
assertTrue(maximumTemporaryBytes.get() > maximumRunBytes.get());
assertEquals(2, maximumOpenInputs.get());
assertEquals(16, initialRunCount.get());
assertEquals(16L, initialRunEntries.get());
assertTrue(mergePassCount.get() >= 6);
assertEquals(expectedHeadEncodings.get(), actualHeadEncodings.get());
assertTrue(directoryEmpty(fixture.workRoot()));
}
System.out.println("...ok");
}
@Test
void currentStateFieldsAndAuthoritativeLocatorsAreExact() throws Exception {
System.out.print("currentStateFieldsAndAuthoritativeLocatorsAreExact ");
try (Fixture fixture = fixture("entry-fields")) {
PkiId clearIdentity = new PkiId("credential:clear");
PkiId revokedIdentity = new PkiId("credential:revoked");
fixture.log().append(clearIdentity, held(1L, 1L));
RevocationTransitionFrameCodec.CompleteRecord clearRecord =
fixture.log().append(clearIdentity, clear(2L, 2L));
RevocationTransitionFrameCodec.CompleteRecord revokedRecord =
fixture.log().append(revokedIdentity, permanentWithAttributes(1L, 3L));
try (FilesystemRevocationCurrentIndex index = fixture.rebuild()) {
FilesystemRevocationCheckpoint.PublishedGeneration generation =
FilesystemRevocationCheckpointBuilder.build(
index, fixture.checkpoints(), fixture.builderConfiguration());
try (FilesystemRevocationCheckpoint checkpoint =
FilesystemRevocationCheckpoint.open(
generation.path(), fixture.logPath(), fixture.log().scan())) {
List<RevocationCheckpointCodec.CurrentStateEntry> entries =
collectEntries(checkpoint);
assertEquals(2, entries.size());
assertEquals(RevocationState.CLEAR, entries.get(0).transition().state());
assertEquals(clearRecord.recordOffset(), entries.get(0).frameStart());
assertEquals(clearRecord.recordEnd(), entries.get(0).frameEnd());
assertEquals(revokedRecord.commitment(), entries.get(1).transitionCommitment());
assertEquals(revokedRecord.recordOffset(), entries.get(1).frameStart());
assertEquals(revokedRecord.recordEnd(), entries.get(1).frameEnd());
assertEquals(1, entries.get(1).transition().attributes().ids().size());
}
}
}
System.out.println("...ok");
}
@Test
void corruptOrTruncatedTemporaryRunFailsClosed() throws Exception {
System.out.print("corruptOrTruncatedTemporaryRunFailsClosed ");
for (boolean truncate : List.of(false, true)) {
try (Fixture fixture = fixture("temporary-corruption-" + truncate)) {
fixture.appendAll(List.of(
new PkiId("credential:a"), new PkiId("credential:b")));
AtomicBoolean changed = new AtomicBoolean();
FilesystemRevocationCheckpointBuilder.FaultInjector faults =
new FilesystemRevocationCheckpointBuilder.FaultInjector() {
@Override
public void fail(
FilesystemRevocationCheckpointBuilder.FaultPoint point) {
// Corruption occurs after the strict run is complete.
}
@Override
public void afterRunWritten(Path run) throws IOException {
if (changed.compareAndSet(false, true)) {
corruptRun(run, truncate);
}
}
};
FilesystemRevocationCheckpointBuilder.Configuration oneRun =
new FilesystemRevocationCheckpointBuilder.Configuration(
Long.MAX_VALUE, 2, 37, fixture.workRoot());
try (FilesystemRevocationCurrentIndex index = fixture.rebuild()) {
assertThrows(IOException.class, () ->
FilesystemRevocationCheckpointBuilder.build(
index, fixture.checkpoints(), oneRun, faults));
}
assertFalse(hasCheckpoint(fixture.checkpoints()));
assertTrue(directoryEmpty(fixture.workRoot()));
}
}
System.out.println("...ok");
}
@Test
void publicationFailureAndCleanupFailureHaveSafeIndependentOutcomes() throws Exception {
System.out.print("publicationFailureAndCleanupFailureHaveSafeIndependentOutcomes ");
try (Fixture fixture = fixture("publication-cleanup")) {
PkiId first = new PkiId("credential:first");
PkiId second = new PkiId("credential:second");
fixture.log().append(first, held(1L, 1L));
try (FilesystemRevocationCurrentIndex index = fixture.rebuild()) {
FilesystemRevocationCheckpoint.PublishedGeneration previous =
FilesystemRevocationCheckpointBuilder.build(
index, fixture.checkpoints(), fixture.builderConfiguration());
index.update(fixture.log().append(second, held(1L, 2L)));
FilesystemRevocationCheckpointBuilder.FaultInjector publicationFailure = point -> {
if (point == FilesystemRevocationCheckpointBuilder.FaultPoint.BEFORE_PUBLICATION) {
throw new IOException("injected publication failure");
}
};
assertThrows(IOException.class, () ->
FilesystemRevocationCheckpointBuilder.build(
index, fixture.checkpoints(), fixture.builderConfiguration(),
publicationFailure));
assertTrue(Files.isRegularFile(previous.path()));
FilesystemRevocationCheckpointBuilder.FaultInjector cleanupFailure = point -> {
if (point == FilesystemRevocationCheckpointBuilder.FaultPoint.CLEANUP) {
throw new IOException("injected cleanup failure");
}
};
FilesystemRevocationCheckpoint.PublishedGeneration current =
FilesystemRevocationCheckpointBuilder.build(
index, fixture.checkpoints(), fixture.builderConfiguration(),
cleanupFailure);
assertTrue(Files.isRegularFile(current.path()));
assertEquals(2L, current.coveredRevision());
}
assertTrue(directoryEmpty(fixture.workRoot()));
}
System.out.println("...ok");
}
@Test
void frozenSnapshotCloseInvalidatesCursorAndDeletesCopy() throws Exception {
System.out.print("frozenSnapshotCloseInvalidatesCursorAndDeletesCopy ");
try (Fixture fixture = fixture("frozen-close")) {
fixture.log().append(new PkiId("credential:first"), held(1L, 1L));
try (FilesystemRevocationCurrentIndex index = fixture.rebuild()) {
Files.createDirectories(fixture.workRoot());
Path copy = fixture.workRoot().resolve("frozen-copy");
FilesystemRevocationCurrentIndex.FrozenSnapshot frozen =
index.freeze(copy, 17);
FilesystemRevocationCurrentIndex.FrozenCursor cursor = frozen.openCursor();
frozen.close();
frozen.close();
assertThrows(IllegalStateException.class, cursor::advance);
assertThrows(IllegalStateException.class, frozen::openCursor);
cursor.close();
cursor.close();
assertFalse(Files.exists(copy));
}
}
System.out.println("...ok");
}
@Test
void frozenCloseFailureCannotReplacePublishedGeneration() throws Exception {
System.out.print("frozenCloseFailureCannotReplacePublishedGeneration ");
try (Fixture fixture = fixture("frozen-close-outcome")) {
fixture.log().append(new PkiId("credential:first"), held(1L, 1L));
FilesystemRevocationCheckpointBuilder.FaultInjector faults = point -> {
if (point == FilesystemRevocationCheckpointBuilder.FaultPoint.FROZEN_CLOSE) {
throw new IOException("injected frozen close failure");
}
};
try (FilesystemRevocationCurrentIndex index = fixture.rebuild()) {
FilesystemRevocationCheckpoint.PublishedGeneration generation =
FilesystemRevocationCheckpointBuilder.build(
index, fixture.checkpoints(), fixture.builderConfiguration(),
faults);
assertTrue(Files.isRegularFile(generation.path()));
}
assertTrue(directoryEmpty(fixture.workRoot()));
}
System.out.println("...ok");
}
@Test
void frozenCloseFailureIsSuppressedBeforePublication() throws Exception {
System.out.print("frozenCloseFailureIsSuppressedBeforePublication ");
try (Fixture fixture = fixture("frozen-close-primary")) {
fixture.log().append(new PkiId("credential:first"), held(1L, 1L));
FilesystemRevocationCheckpointBuilder.FaultInjector faults = point -> {
if (point == FilesystemRevocationCheckpointBuilder.FaultPoint.RUN_WRITE) {
throw new IOException("primary run failure");
}
if (point == FilesystemRevocationCheckpointBuilder.FaultPoint.FROZEN_CLOSE) {
throw new IOException("secondary frozen close failure");
}
};
try (FilesystemRevocationCurrentIndex index = fixture.rebuild()) {
IOException failure = assertThrows(IOException.class, () ->
FilesystemRevocationCheckpointBuilder.build(
index, fixture.checkpoints(), fixture.builderConfiguration(),
faults));
assertEquals("primary run failure", failure.getMessage());
assertEquals(1, failure.getSuppressed().length);
assertEquals("secondary frozen close failure",
failure.getSuppressed()[0].getMessage());
}
assertFalse(hasCheckpoint(fixture.checkpoints()));
assertTrue(directoryEmpty(fixture.workRoot()));
}
System.out.println("...ok");
}
@Test
void malformedFrozenSlotPopulationFailsBeforePublication() throws Exception {
System.out.print("malformedFrozenSlotPopulationFailsBeforePublication ");
try (Fixture fixture = fixture("malformed-frozen")) {
fixture.log().append(new PkiId("credential:first"), held(1L, 1L));
try (FilesystemRevocationCurrentIndex index = fixture.rebuild()) {
clearIndexCells(fixture.indexPath());
assertThrows(IOException.class, () ->
FilesystemRevocationCheckpointBuilder.build(
index, fixture.checkpoints(), fixture.builderConfiguration()));
}
assertFalse(hasCheckpoint(fixture.checkpoints()));
assertTrue(directoryEmpty(fixture.workRoot()));
}
System.out.println("...ok");
}
@Test
void maximumComponentEntryIsOneBoundedSingletonRun() throws Exception {
System.out.print("maximumComponentEntryIsOneBoundedSingletonRun ");
try (Fixture fixture = fixture("maximum-singleton")) {
PkiId identity = new PkiId(
"x".repeat(RevocationTransitionFrameCodec.MAX_COMPONENT_BYTES));
fixture.log().append(identity, held(1L, 1L));
AtomicLong maximumEntries = new AtomicLong();
FilesystemRevocationCheckpointBuilder.FaultInjector observer =
new FilesystemRevocationCheckpointBuilder.FaultInjector() {
@Override
public void fail(FilesystemRevocationCheckpointBuilder.FaultPoint point) {
// Observation only.
}
@Override
public void observeRunBuffer(long entryCount, long encodedBytes) {
maximumEntries.accumulateAndGet(entryCount, Math::max);
}
};
try (FilesystemRevocationCurrentIndex index = fixture.rebuild()) {
FilesystemRevocationCheckpoint.PublishedGeneration generation =
FilesystemRevocationCheckpointBuilder.build(
index, fixture.checkpoints(), fixture.builderConfiguration(),
observer);
try (FilesystemRevocationCheckpoint checkpoint =
FilesystemRevocationCheckpoint.open(
generation.path(), fixture.logPath(), fixture.log().scan())) {
assertEquals(1L, checkpoint.entryCount());
}
}
assertEquals(1L, maximumEntries.get());
}
System.out.println("...ok");
}
@Test
void duplicateIdentityInjectedIntoTemporaryRunFailsClosed() throws Exception {
System.out.print("duplicateIdentityInjectedIntoTemporaryRunFailsClosed ");
try (Fixture fixture = fixture("duplicate-run")) {
fixture.log().append(new PkiId("credential:first"), held(1L, 1L));
AtomicBoolean rewritten = new AtomicBoolean();
FilesystemRevocationCheckpointBuilder.FaultInjector faults =
new FilesystemRevocationCheckpointBuilder.FaultInjector() {
@Override
public void fail(FilesystemRevocationCheckpointBuilder.FaultPoint point) {
// Duplicate injection occurs after strict run creation.
}
@Override
public void afterRunWritten(Path run) throws IOException {
if (rewritten.compareAndSet(false, true)) {
rewriteRunWithDuplicate(run);
}
}
};
FilesystemRevocationCheckpointBuilder.Configuration oneRun =
new FilesystemRevocationCheckpointBuilder.Configuration(
Long.MAX_VALUE, 2, 37, fixture.workRoot());
try (FilesystemRevocationCurrentIndex index = fixture.rebuild()) {
assertThrows(IOException.class, () ->
FilesystemRevocationCheckpointBuilder.build(
index, fixture.checkpoints(), oneRun, faults));
}
assertFalse(hasCheckpoint(fixture.checkpoints()));
assertTrue(directoryEmpty(fixture.workRoot()));
}
System.out.println("...ok");
}
@Test
void authenticSupersededTransitionCannotReplaceFrozenCurrentState() throws Exception {
System.out.print("authenticSupersededTransitionCannotReplaceFrozenCurrentState ");
try (Fixture fixture = fixture("superseded-authentic")) {
PkiId identity = new PkiId("credential:first");
RevocationTransitionFrameCodec.CompleteRecord heldRecord =
fixture.log().append(identity, held(1L, 1L));
fixture.log().append(identity, clear(2L, 2L));
RevocationCheckpointCodec.CurrentStateEntry superseded = entry(heldRecord);
AtomicBoolean rewritten = new AtomicBoolean();
FilesystemRevocationCheckpointBuilder.FaultInjector faults =
new FilesystemRevocationCheckpointBuilder.FaultInjector() {
@Override
public void fail(FilesystemRevocationCheckpointBuilder.FaultPoint point) {
// Authentic replacement occurs after strict run creation.
}
@Override
public void afterRunWritten(Path run) throws IOException {
if (rewritten.compareAndSet(false, true)) {
rewriteRunWithEntry(run, superseded);
}
}
};
try (FilesystemRevocationCurrentIndex index = fixture.rebuild()) {
assertThrows(IOException.class, () ->
FilesystemRevocationCheckpointBuilder.build(
index, fixture.checkpoints(), fixture.builderConfiguration(),
faults));
}
assertFalse(hasCheckpoint(fixture.checkpoints()));
assertTrue(directoryEmpty(fixture.workRoot()));
}
System.out.println("...ok");
}
private Fixture fixture(String name) throws IOException {
Path root = temporaryDirectory.resolve(name);
FsPaths paths = new FsPaths(root);
Path logPath = paths.revocationTransitionLog();
Files.createDirectories(logPath.getParent());
FilesystemRevocationLog log = FilesystemRevocationLog.create(
logPath, STORE_ID, credential -> { });
return new Fixture(
logPath, paths.revocationCurrentIndex(),
paths.revocationCheckpointDirectory(),
paths.revocationCheckpointWorkDirectory(), log);
}
private static RevocationTransition held(long revision, long second) {
return new RevocationTransition(
revision, RevocationState.HELD, Instant.ofEpochSecond(second),
Optional.empty(), new SimpleAttributeSet());
}
private static RevocationTransition clear(long revision, long second) {
return new RevocationTransition(
revision, RevocationState.CLEAR, Instant.ofEpochSecond(second),
Optional.empty(), new SimpleAttributeSet());
}
private static RevocationTransition permanentWithAttributes(long revision, long second) {
SimpleAttributeSet attributes = new SimpleAttributeSet(List.of(
new SimpleAttributeSet.Entry(
new AttributeId("audit.example"),
List.of(new AttributeValue.StringValue("retained")))));
return new RevocationTransition(
revision, RevocationState.PERMANENTLY_REVOKED,
Instant.ofEpochSecond(second), Optional.of(RevocationReason.KEY_COMPROMISE),
attributes);
}
private static RevocationCheckpointCodec.CurrentStateEntry entry(
RevocationTransitionFrameCodec.CompleteRecord record) {
return new RevocationCheckpointCodec.CurrentStateEntry(
record.data().credentialId(), record.data().globalRevision(),
record.data().transition().revision(), record.commitment(),
record.data().transition(), record.recordOffset(), record.recordEnd());
}
private static List<String> collect(FilesystemRevocationCheckpoint checkpoint)
throws Exception {
List<String> values = new ArrayList<>();
try (FilesystemRevocationCheckpoint.Cursor cursor = checkpoint.allCurrentStates()) {
while (cursor.advance(CancellationSignal.NONE)) {
values.add(cursor.current().credentialId().value());
}
}
return values;
}
private static List<RevocationCheckpointCodec.CurrentStateEntry> collectEntries(
FilesystemRevocationCheckpoint checkpoint) throws Exception {
List<RevocationCheckpointCodec.CurrentStateEntry> values = new ArrayList<>();
try (FilesystemRevocationCheckpoint.Cursor cursor = checkpoint.allCurrentStates()) {
while (cursor.advance(CancellationSignal.NONE)) {
values.add(cursor.current());
}
}
return values;
}
private static void corruptRun(Path run, boolean truncate) throws IOException {
try (FileChannel channel = FileChannel.open(
run, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
long last = channel.size() - 1L;
if (truncate) {
channel.truncate(last);
} else {
ByteBuffer byteValue = ByteBuffer.allocate(1);
channel.read(byteValue, last);
byteValue.flip();
byteValue.put(0, (byte) (byteValue.get(0) ^ 0x5a));
channel.write(byteValue, last);
}
}
}
private static void clearIndexCells(Path indexPath) throws IOException {
try (FileChannel channel = FileChannel.open(
indexPath, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
long offset = 2L * FilesystemRevocationCurrentIndex.SUPERBLOCK_BYTES;
ByteBuffer zeros = ByteBuffer.allocate(512);
while (offset < channel.size()) {
zeros.clear();
zeros.limit((int) Math.min((long) zeros.capacity(), channel.size() - offset));
while (zeros.hasRemaining()) {
offset += channel.write(zeros, offset);
}
}
channel.force(true);
}
}
private static void rewriteRunWithDuplicate(Path run) throws IOException {
try (FileChannel channel = FileChannel.open(
run, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
RevocationCheckpointCodec codec = new RevocationCheckpointCodec();
RevocationCheckpointCodec.ValidatedFile validated = codec.validate(channel);
RevocationCheckpointCodec.DecodedRecord decoded = codec.sequentialDecoder().read(
channel, RevocationCheckpointCodec.HEADER_BYTES, validated.trailerOffset());
RevocationCheckpointCodec.HeaderData original = validated.header();
RevocationCheckpointCodec.HeaderData duplicateHeader =
new RevocationCheckpointCodec.HeaderData(
original.storeId(), original.coveredRevision(),
original.finalRecordStart(), original.coveredBoundary(),
original.globalCommitment(), 2L);
RevocationCheckpointCodec.Encoder encoder = codec.encoder(channel, duplicateHeader);
encoder.write(decoded.entry());
encoder.write(decoded.entry());
encoder.finish();
channel.force(true);
}
}
private static void rewriteRunWithEntry(
Path run, RevocationCheckpointCodec.CurrentStateEntry entry) throws IOException {
try (FileChannel channel = FileChannel.open(
run, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
RevocationCheckpointCodec codec = new RevocationCheckpointCodec();
RevocationCheckpointCodec.HeaderData original =
codec.validate(channel).header();
RevocationCheckpointCodec.HeaderData replacementHeader =
new RevocationCheckpointCodec.HeaderData(
original.storeId(), original.coveredRevision(),
original.finalRecordStart(), original.coveredBoundary(),
original.globalCommitment(), 1L);
RevocationCheckpointCodec.Encoder encoder =
codec.encoder(channel, replacementHeader);
encoder.write(entry);
encoder.finish();
channel.force(true);
}
}
private static List<PkiId> collect(
FilesystemRevocationCurrentIndex.FrozenSnapshot frozen) throws Exception {
List<PkiId> values = new ArrayList<>();
try (FilesystemRevocationCurrentIndex.FrozenCursor cursor = frozen.openCursor()) {
while (cursor.advance()) {
values.add(cursor.current().credentialId());
}
}
return values;
}
private static boolean directoryEmpty(Path directory) throws IOException {
if (!Files.isDirectory(directory)) {
return true;
}
try (DirectoryStream<Path> paths = Files.newDirectoryStream(directory)) {
return !paths.iterator().hasNext();
}
}
private static boolean hasCheckpoint(Path directory) throws IOException {
if (!Files.isDirectory(directory)) {
return false;
}
try (DirectoryStream<Path> paths = Files.newDirectoryStream(directory, "*.chk")) {
return paths.iterator().hasNext();
}
}
private record Fixture(
Path logPath,
Path indexPath,
Path checkpoints,
Path workRoot,
FilesystemRevocationLog log) implements AutoCloseable {
private FilesystemRevocationCurrentIndex rebuild() throws IOException {
return FilesystemRevocationCurrentIndex.rebuild(
indexPath, logPath, STORE_ID, INDEX_CONFIGURATION);
}
private FilesystemRevocationCheckpointBuilder.Configuration builderConfiguration() {
return new FilesystemRevocationCheckpointBuilder.Configuration(
1L, 2, 37, workRoot);
}
private void appendAll(List<PkiId> identities) throws IOException {
long globalRevision = 1L;
for (PkiId identity : identities) {
log.append(identity, held(1L, globalRevision));
globalRevision++;
}
}
@Override
public void close() throws IOException {
log.close();
}
}
}

View File

@@ -427,13 +427,13 @@ final class FilesystemRevocationCurrentIndexTest {
}
@Test
void structuralContractHasNoHistoryCollectionOrCheckpointDependency() throws Exception {
System.out.print("structuralContractHasNoHistoryCollectionOrCheckpointDependency ");
void structuralContractHasNoHistoryCollectionOrCheckpointPublication() throws Exception {
System.out.print("structuralContractHasNoHistoryCollectionOrCheckpointPublication ");
String source = Files.readString(Path.of(
"src/main/java/zeroecho/pki/impl/fs/FilesystemRevocationCurrentIndex.java"));
assertFalse(source.contains("List<RevocationTransition>"));
assertFalse(source.contains("readAllBytes()"));
assertFalse(source.contains("FilesystemRevocationCheckpoint"));
assertFalse(source.contains("publishHistorical"));
assertFalse(source.contains("HashMap"));
assertFalse(source.contains("new Thread"));
assertFalse(source.contains("Executors."));