ManifestWriter.java

1
// SPDX-License-Identifier: Apache-2.0
2
// Copyright 2026 Egothor
3
// Copyright 2026 Accenture
4
package org.egothor.methodatlas.evidence;
5
6
import java.io.BufferedWriter;
7
import java.io.IOException;
8
import java.io.InputStream;
9
import java.nio.charset.StandardCharsets;
10
import java.nio.file.Files;
11
import java.nio.file.Path;
12
import java.security.MessageDigest;
13
import java.security.NoSuchAlgorithmException;
14
import java.util.ArrayList;
15
import java.util.Collections;
16
import java.util.HexFormat;
17
import java.util.List;
18
import java.util.stream.Stream;
19
20
/**
21
 * Builds the {@code manifest.sha256} file that anchors an evidence pack.
22
 *
23
 * <p>
24
 * The manifest enumerates every file in the pack directory other than
25
 * itself and any {@code .signed} sibling, listing one entry per line in the
26
 * format {@code <hex-digest>  <filename>}. Files are sorted lexicographically
27
 * by filename so the manifest is byte-stable between runs that produce the
28
 * same artefacts.
29
 * </p>
30
 *
31
 * <p>
32
 * Package-private because nothing outside the evidence module needs to call
33
 * this writer.
34
 * </p>
35
 */
36
final class ManifestWriter {
37
38
    /** Java standard digest algorithm name for SHA-256. */
39
    private static final String DIGEST_ALGORITHM = "SHA-256";
40
41
    /** Buffer size used when streaming files through the digest. */
42
    private static final int BUFFER_SIZE = 8192;
43
44
    /** Filename suffix marking a signed-envelope sibling that must be excluded. */
45
    private static final String SIGNED_SUFFIX = ".signed";
46
47
    /** Two-space separator required by the shasum/coreutils manifest format. */
48
    private static final String SEPARATOR = "  ";
49
50
    private ManifestWriter() {
51
        // Utility class — instantiation makes no sense.
52
    }
53
54
    /**
55
     * Writes a SHA-256 manifest of every regular file in {@code dir}, except
56
     * {@code manifestFile} itself and any file whose name ends with
57
     * {@code .signed}.
58
     *
59
     * @param dir          directory to enumerate; must exist
60
     * @param manifestFile output path; overwritten if it already exists
61
     * @throws IOException if directory listing, digest computation, or
62
     *                     writing fails
63
     */
64
    /* default */ static void write(Path dir, Path manifestFile) throws IOException {
65
        List<Path> files = collectFiles(dir, manifestFile);
66
        try (BufferedWriter writer = Files.newBufferedWriter(manifestFile, StandardCharsets.UTF_8)) {
67
            for (Path file : files) {
68
                String digest = sha256Hex(file);
69 1 1. write : removed call to java/io/BufferedWriter::write → KILLED
                writer.write(digest);
70 1 1. write : removed call to java/io/BufferedWriter::write → KILLED
                writer.write(SEPARATOR);
71 1 1. write : removed call to java/io/BufferedWriter::write → KILLED
                writer.write(filenameOf(file));
72 1 1. write : removed call to java/io/BufferedWriter::write → KILLED
                writer.write('\n');
73
            }
74
        }
75
    }
76
77
    /**
78
     * Returns the filename portion of {@code p} as a non-null string. Used
79
     * defensively because {@link Path#getFileName()} can theoretically
80
     * return {@code null} (root paths); inside {@link Files#list(Path)}
81
     * results that case is unreachable, but SpotBugs flags the chained
82
     * {@code toString()} on a possibly-null return.
83
     *
84
     * @param p path to inspect
85
     * @return filename as string; never {@code null}
86
     */
87
    private static String filenameOf(Path p) {
88
        Path name = p.getFileName();
89 3 1. filenameOf : removed conditional - replaced equality check with false → SURVIVED
2. filenameOf : replaced return value with "" for org/egothor/methodatlas/evidence/ManifestWriter::filenameOf → KILLED
3. filenameOf : removed conditional - replaced equality check with true → KILLED
        return name == null ? "" : name.toString();
90
    }
91
92
    /**
93
     * Lists candidate files in lexicographic order.
94
     *
95
     * @param dir          directory to enumerate
96
     * @param manifestFile manifest path to exclude
97
     * @return sorted list of files eligible for inclusion
98
     * @throws IOException if the directory cannot be listed
99
     */
100
    private static List<Path> collectFiles(Path dir, Path manifestFile) throws IOException {
101
        List<Path> files = new ArrayList<>();
102
        try (Stream<Path> stream = Files.list(dir)) {
103 2 1. lambda$collectFiles$0 : replaced boolean return with true for org/egothor/methodatlas/evidence/ManifestWriter::lambda$collectFiles$0 → SURVIVED
2. lambda$collectFiles$0 : replaced boolean return with false for org/egothor/methodatlas/evidence/ManifestWriter::lambda$collectFiles$0 → KILLED
            stream.filter(Files::isRegularFile)
104 3 1. lambda$collectFiles$1 : removed conditional - replaced equality check with false → KILLED
2. lambda$collectFiles$1 : replaced boolean return with true for org/egothor/methodatlas/evidence/ManifestWriter::lambda$collectFiles$1 → KILLED
3. lambda$collectFiles$1 : removed conditional - replaced equality check with true → KILLED
                    .filter(p -> !p.equals(manifestFile))
105 3 1. lambda$collectFiles$2 : removed conditional - replaced equality check with true → KILLED
2. lambda$collectFiles$2 : replaced boolean return with true for org/egothor/methodatlas/evidence/ManifestWriter::lambda$collectFiles$2 → KILLED
3. lambda$collectFiles$2 : removed conditional - replaced equality check with false → KILLED
                    .filter(p -> !filenameOf(p).endsWith(SIGNED_SUFFIX))
106 1 1. collectFiles : removed call to java/util/stream/Stream::forEach → KILLED
                    .forEach(files::add);
107
        }
108 2 1. collectFiles : removed call to java/util/Collections::sort → SURVIVED
2. lambda$collectFiles$3 : replaced int return with 0 for org/egothor/methodatlas/evidence/ManifestWriter::lambda$collectFiles$3 → SURVIVED
        Collections.sort(files, (a, b) -> filenameOf(a).compareTo(filenameOf(b)));
109 1 1. collectFiles : replaced return value with Collections.emptyList for org/egothor/methodatlas/evidence/ManifestWriter::collectFiles → KILLED
        return files;
110
    }
111
112
    /**
113
     * Computes a lowercase-hex SHA-256 digest of the supplied file.
114
     *
115
     * @param file path to digest
116
     * @return 64-character lowercase hex string
117
     * @throws IOException if reading fails or SHA-256 is unavailable
118
     */
119
    private static String sha256Hex(Path file) throws IOException {
120
        MessageDigest digest = newDigest();
121
        byte[] buffer = new byte[BUFFER_SIZE];
122
        try (InputStream in = Files.newInputStream(file)) {
123
            int read;
124 3 1. sha256Hex : changed conditional boundary → SURVIVED
2. sha256Hex : removed conditional - replaced comparison check with false → KILLED
3. sha256Hex : removed conditional - replaced comparison check with true → KILLED
            while ((read = in.read(buffer)) >= 0) {
125 1 1. sha256Hex : removed call to java/security/MessageDigest::update → KILLED
                digest.update(buffer, 0, read);
126
            }
127
        }
128 1 1. sha256Hex : replaced return value with "" for org/egothor/methodatlas/evidence/ManifestWriter::sha256Hex → KILLED
        return HexFormat.of().formatHex(digest.digest());
129
    }
130
131
    /**
132
     * Creates a fresh SHA-256 digest, wrapping the checked exception into an
133
     * {@link IOException} so callers do not need a separate catch clause.
134
     *
135
     * @return new {@link MessageDigest} instance
136
     * @throws IOException when SHA-256 is unexpectedly unavailable (never on a
137
     *                     standard JDK)
138
     */
139
    private static MessageDigest newDigest() throws IOException {
140
        try {
141 1 1. newDigest : replaced return value with null for org/egothor/methodatlas/evidence/ManifestWriter::newDigest → KILLED
            return MessageDigest.getInstance(DIGEST_ALGORITHM);
142
        } catch (NoSuchAlgorithmException e) {
143
            throw new IOException("SHA-256 not available", e);
144
        }
145
    }
146
}

Mutations

69

1.1
Location : write
Killed by : org.egothor.methodatlas.evidence.ManifestWriterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.ManifestWriterTest]/[method:writeListsFilesLexicographicallyAndComputesSha256(java.nio.file.Path)]
removed call to java/io/BufferedWriter::write → KILLED

70

1.1
Location : write
Killed by : org.egothor.methodatlas.evidence.ManifestWriterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.ManifestWriterTest]/[method:writeExcludesManifestAndSignedSiblings(java.nio.file.Path)]
removed call to java/io/BufferedWriter::write → KILLED

71

1.1
Location : write
Killed by : org.egothor.methodatlas.evidence.ManifestWriterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.ManifestWriterTest]/[method:writeExcludesManifestAndSignedSiblings(java.nio.file.Path)]
removed call to java/io/BufferedWriter::write → KILLED

72

1.1
Location : write
Killed by : org.egothor.methodatlas.evidence.ManifestWriterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.ManifestWriterTest]/[method:writeListsFilesLexicographicallyAndComputesSha256(java.nio.file.Path)]
removed call to java/io/BufferedWriter::write → KILLED

89

1.1
Location : filenameOf
Killed by : org.egothor.methodatlas.evidence.ManifestWriterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.ManifestWriterTest]/[method:writeExcludesManifestAndSignedSiblings(java.nio.file.Path)]
replaced return value with "" for org/egothor/methodatlas/evidence/ManifestWriter::filenameOf → KILLED

2.2
Location : filenameOf
Killed by : org.egothor.methodatlas.evidence.ManifestWriterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.ManifestWriterTest]/[method:writeExcludesManifestAndSignedSiblings(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

3.3
Location : filenameOf
Killed by : none
removed conditional - replaced equality check with false → SURVIVED
Covering tests

103

1.1
Location : lambda$collectFiles$0
Killed by : org.egothor.methodatlas.evidence.ManifestWriterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.ManifestWriterTest]/[method:writeExcludesManifestAndSignedSiblings(java.nio.file.Path)]
replaced boolean return with false for org/egothor/methodatlas/evidence/ManifestWriter::lambda$collectFiles$0 → KILLED

2.2
Location : lambda$collectFiles$0
Killed by : none
replaced boolean return with true for org/egothor/methodatlas/evidence/ManifestWriter::lambda$collectFiles$0 → SURVIVED
Covering tests

104

1.1
Location : lambda$collectFiles$1
Killed by : org.egothor.methodatlas.evidence.ManifestWriterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.ManifestWriterTest]/[method:writeExcludesManifestAndSignedSiblings(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

2.2
Location : lambda$collectFiles$1
Killed by : org.egothor.methodatlas.evidence.ManifestWriterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.ManifestWriterTest]/[method:writeExcludesManifestAndSignedSiblings(java.nio.file.Path)]
replaced boolean return with true for org/egothor/methodatlas/evidence/ManifestWriter::lambda$collectFiles$1 → KILLED

3.3
Location : lambda$collectFiles$1
Killed by : org.egothor.methodatlas.evidence.ManifestWriterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.ManifestWriterTest]/[method:writeExcludesManifestAndSignedSiblings(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

105

1.1
Location : lambda$collectFiles$2
Killed by : org.egothor.methodatlas.evidence.ManifestWriterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.ManifestWriterTest]/[method:writeExcludesManifestAndSignedSiblings(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

2.2
Location : lambda$collectFiles$2
Killed by : org.egothor.methodatlas.evidence.ManifestWriterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.ManifestWriterTest]/[method:writeExcludesManifestAndSignedSiblings(java.nio.file.Path)]
replaced boolean return with true for org/egothor/methodatlas/evidence/ManifestWriter::lambda$collectFiles$2 → KILLED

3.3
Location : lambda$collectFiles$2
Killed by : org.egothor.methodatlas.evidence.ManifestWriterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.ManifestWriterTest]/[method:writeExcludesManifestAndSignedSiblings(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

106

1.1
Location : collectFiles
Killed by : org.egothor.methodatlas.evidence.ManifestWriterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.ManifestWriterTest]/[method:writeExcludesManifestAndSignedSiblings(java.nio.file.Path)]
removed call to java/util/stream/Stream::forEach → KILLED

108

1.1
Location : collectFiles
Killed by : none
removed call to java/util/Collections::sort → SURVIVED
Covering tests

2.2
Location : lambda$collectFiles$3
Killed by : none
replaced int return with 0 for org/egothor/methodatlas/evidence/ManifestWriter::lambda$collectFiles$3 → SURVIVED Covering tests

109

1.1
Location : collectFiles
Killed by : org.egothor.methodatlas.evidence.ManifestWriterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.ManifestWriterTest]/[method:writeExcludesManifestAndSignedSiblings(java.nio.file.Path)]
replaced return value with Collections.emptyList for org/egothor/methodatlas/evidence/ManifestWriter::collectFiles → KILLED

124

1.1
Location : sha256Hex
Killed by : org.egothor.methodatlas.evidence.ManifestWriterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.ManifestWriterTest]/[method:writeListsFilesLexicographicallyAndComputesSha256(java.nio.file.Path)]
removed conditional - replaced comparison check with false → KILLED

2.2
Location : sha256Hex
Killed by : none
changed conditional boundary → SURVIVED
Covering tests

3.3
Location : sha256Hex
Killed by : org.egothor.methodatlas.evidence.ManifestWriterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.ManifestWriterTest]/[method:writeExcludesManifestAndSignedSiblings(java.nio.file.Path)]
removed conditional - replaced comparison check with true → KILLED

125

1.1
Location : sha256Hex
Killed by : org.egothor.methodatlas.evidence.ManifestWriterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.ManifestWriterTest]/[method:writeListsFilesLexicographicallyAndComputesSha256(java.nio.file.Path)]
removed call to java/security/MessageDigest::update → KILLED

128

1.1
Location : sha256Hex
Killed by : org.egothor.methodatlas.evidence.ManifestWriterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.ManifestWriterTest]/[method:writeListsFilesLexicographicallyAndComputesSha256(java.nio.file.Path)]
replaced return value with "" for org/egothor/methodatlas/evidence/ManifestWriter::sha256Hex → KILLED

141

1.1
Location : newDigest
Killed by : org.egothor.methodatlas.evidence.ManifestWriterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.evidence.ManifestWriterTest]/[method:writeExcludesManifestAndSignedSiblings(java.nio.file.Path)]
replaced return value with null for org/egothor/methodatlas/evidence/ManifestWriter::newDigest → KILLED

Active mutators

Tests examined


Report generated by PIT 1.22.1