-
Notifications
You must be signed in to change notification settings - Fork 292
Make the WARC digest algorithm configurable #2110
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f8496c6
09d373a
2a6aec0
727ec1d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,7 @@ | |
| import java.util.Locale; | ||
| import java.util.Map; | ||
| import java.util.Map.Entry; | ||
| import java.util.Objects; | ||
| import java.util.Optional; | ||
| import java.util.UUID; | ||
| import java.util.regex.Pattern; | ||
|
|
@@ -129,23 +130,132 @@ static String sanitizeWarcFieldValue(String value) { | |
| Pattern.compile("(?i)(?:Content-(?:Encoding|Length)|Transfer-Encoding)"); | ||
| protected static final String X_HIDE_HEADER = "X-Crawler-"; | ||
|
|
||
| /** | ||
| * Configuration key setting the algorithm used to compute the WARC-Payload-Digest and | ||
| * WARC-Block-Digest fields. Supported values are {@value #DIGEST_ALGORITHM_SHA1} (the default) | ||
| * and {@value #DIGEST_ALGORITHM_SHA256}. | ||
| * | ||
| * <p>Note: SHA-1 is the convention across the WARC ecosystem and downstream tooling (CDX | ||
| * indexes, revisit record handling) may expect it. Change the default deliberately, not | ||
| * casually. | ||
| */ | ||
| public static final String DIGEST_ALGORITHM_PARAM = "warc.digest.algorithm"; | ||
|
|
||
| public static final String DIGEST_ALGORITHM_SHA1 = "sha1"; | ||
|
|
||
| public static final String DIGEST_ALGORITHM_SHA256 = "sha256"; | ||
|
|
||
| private static final Base32 base32 = new Base32(); | ||
| private static final String digestNoContent = getDigestSha1(new byte[0]); | ||
|
|
||
| protected final String protocolMDprefix; | ||
|
|
||
| /** JCA name of the message digest algorithm, e.g. "SHA-1". */ | ||
| private final String digestJCAName; | ||
|
|
||
| /** Algorithm prefix of the WARC digest fields, e.g. "sha1:". */ | ||
| private final String digestPrefix; | ||
|
|
||
| private final String digestNoContent; | ||
|
|
||
| /** | ||
| * Creates a record format computing the digests with the default algorithm (SHA-1). | ||
| * | ||
| * @param protocolMDprefix prefix of the metadata keys holding the protocol response, as set by | ||
| * {@code protocol.md.prefix}; may be empty | ||
| */ | ||
| public WARCRecordFormat(String protocolMDprefix) { | ||
| this(protocolMDprefix, DIGEST_ALGORITHM_SHA1); | ||
| } | ||
|
|
||
| /** | ||
| * Creates a record format computing the WARC-Payload-Digest and WARC-Block-Digest fields with | ||
| * the given algorithm. | ||
| * | ||
| * @param protocolMDprefix prefix of the metadata keys holding the protocol response, as set by | ||
| * {@code protocol.md.prefix}; may be empty | ||
| * @param digestAlgorithm algorithm for the digest fields, {@value #DIGEST_ALGORITHM_SHA1} (the | ||
| * default) or {@value #DIGEST_ALGORITHM_SHA256}; matched case-insensitively with an | ||
| * optional hyphen, surrounding whitespace is trimmed. A {@code null} or blank value selects | ||
| * the default SHA-1. | ||
| * @throws IllegalArgumentException if the value is not a supported algorithm | ||
| */ | ||
| public WARCRecordFormat(String protocolMDprefix, String digestAlgorithm) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add javadoc
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| this.protocolMDprefix = protocolMDprefix; | ||
| this.digestJCAName = getDigestJCAName(digestAlgorithm); | ||
| this.digestPrefix = digestJCAName.toLowerCase(Locale.ROOT).replace("-", "") + ":"; | ||
| this.digestNoContent = getDigest(new byte[0]); | ||
| } | ||
|
|
||
| public static String getDigestSha1(byte[] bytes) { | ||
| return "sha1:" + base32.encodeAsString(DigestUtils.sha1(bytes)); | ||
| /** | ||
| * Resolve the configured digest algorithm to the JCA name of the message digest. The value is | ||
| * matched case-insensitively and an optional hyphen is ignored, i.e. "sha256", | ||
| * "SHA-256" etc. are all accepted. | ||
| * | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. documentation for the param? what happens if it is null -> return sha1
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 727ec1d: the javadoc now documents the parameter, including the null/blank → SHA-1 behaviour. |
||
| * @param digestAlgorithm algorithm to resolve; {@code null} or blank selects the default SHA-1 | ||
| * @return the JCA name of the message digest, e.g. "SHA-1" | ||
| * @throws IllegalArgumentException if the value is not a supported algorithm | ||
| */ | ||
| private static String getDigestJCAName(String digestAlgorithm) { | ||
| if (StringUtils.isBlank(digestAlgorithm)) { | ||
| return "SHA-1"; | ||
| } | ||
| return switch (digestAlgorithm.trim().toLowerCase(Locale.ROOT).replace("-", "")) { | ||
| case DIGEST_ALGORITHM_SHA1 -> "SHA-1"; | ||
| case DIGEST_ALGORITHM_SHA256 -> "SHA-256"; | ||
| default -> | ||
| throw new IllegalArgumentException( | ||
| "Unsupported value [" | ||
| + digestAlgorithm | ||
| + "] for " | ||
| + DIGEST_ALGORITHM_PARAM | ||
| + ", supported algorithms: " | ||
| + DIGEST_ALGORITHM_SHA1 | ||
| + ", " | ||
| + DIGEST_ALGORITHM_SHA256); | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Compute the digest of the given bytes with the configured algorithm. | ||
| * | ||
| * @param bytes bytes to digest, must not be null | ||
| * @return digest in the form "<algorithm>:<base32>", e.g. | ||
| * "sha1:..." | ||
| * @throws NullPointerException if the input is null | ||
| */ | ||
| public String getDigest(byte[] bytes) { | ||
| Objects.requireNonNull(bytes, "bytes to digest must not be null"); | ||
| MessageDigest md = DigestUtils.getDigest(digestJCAName); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. input validation?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 727ec1d: getDigest(byte[]) now rejects null input via Objects.requireNonNull (it only ever receives arrays here — format() guards the content == null case — but the method is public API). The algorithm itself is validated once at construction time, which keeps the per-call path free of it. |
||
| return digestPrefix + base32Unpadded(md.digest(bytes)); | ||
| } | ||
|
|
||
| /** | ||
| * Compute the digest of the concatenation of the two given byte arrays with the configured | ||
| * algorithm. | ||
| * | ||
| * @param bytes1 first bytes to digest, must not be null | ||
| * @param bytes2 second bytes to digest, must not be null | ||
| * @return digest in the form "<algorithm>:<base32>", e.g. | ||
| * "sha1:..." | ||
| * @throws NullPointerException if one of the inputs is null | ||
| */ | ||
| public String getDigest(byte[] bytes1, byte[] bytes2) { | ||
| Objects.requireNonNull(bytes1, "first bytes to digest must not be null"); | ||
| Objects.requireNonNull(bytes2, "second bytes to digest must not be null"); | ||
| MessageDigest md = DigestUtils.getDigest(digestJCAName); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. input validation?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 727ec1d: same as above, both arrays are null-checked. |
||
| md.update(bytes1); | ||
| return digestPrefix + base32Unpadded(md.digest(bytes2)); | ||
| } | ||
|
|
||
| public static String getDigestSha1(byte[] bytes1, byte[] bytes2) { | ||
| MessageDigest sha1 = DigestUtils.getSha1Digest(); | ||
| sha1.update(bytes1); | ||
| return "sha1:" + base32.encodeAsString(sha1.digest(bytes2)); | ||
| /** | ||
| * Base32-encode a digest value without the trailing "=" padding characters: the WARC | ||
| * digest fields define the digest value as a token, which does not allow the padding character | ||
| * (cf. ISO 28500 WARC 1.1, WARC-Block-Digest / WARC-Payload-Digest). SHA-1 digests are | ||
| * unaffected (32 characters without padding), while e.g. SHA-256 digests would end in | ||
| * "====". | ||
| */ | ||
| private static String base32Unpadded(byte[] digest) { | ||
| return StringUtils.stripEnd(base32.encodeAsString(digest), "="); | ||
| } | ||
|
|
||
| /** Generates a WARC info entry which can be stored at the beginning of each WARC file. */ | ||
|
|
@@ -435,14 +545,14 @@ public byte[] format(Tuple tuple) { | |
| String blockDigest = digestNoContent; | ||
| if (content != null) { | ||
| contentLength = content.length; | ||
| payloadDigest = getDigestSha1(content); | ||
| payloadDigest = getDigest(content); | ||
| if (WARCTypeValue.equals(WARC_TYPE_RESPONSE)) { | ||
| blockDigest = getDigestSha1(httpheaders, content); | ||
| blockDigest = getDigest(httpheaders, content); | ||
| } else { | ||
| blockDigest = payloadDigest; | ||
| } | ||
| } else if (WARCTypeValue.equals(WARC_TYPE_RESPONSE)) { | ||
| blockDigest = getDigestSha1(httpheaders); | ||
| blockDigest = getDigest(httpheaders); | ||
| } | ||
|
|
||
| // add the length of the http header | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,10 +41,25 @@ public class WARCRequestRecordFormat extends WARCRecordFormat { | |
| protected static final Pattern REQUEST_LINE_PATTERN = | ||
| Pattern.compile("^\\S+ \\S+ HTTP/1\\.[01]$"); | ||
|
|
||
| /** Creates a request record format computing the digests with the default algorithm (SHA-1). */ | ||
| public WARCRequestRecordFormat(String protocolMDprefix) { | ||
| super(protocolMDprefix); | ||
| } | ||
|
|
||
| /** | ||
| * Creates a request record format computing the WARC-Block-Digest field with the given | ||
| * algorithm. | ||
| * | ||
| * @param protocolMDprefix prefix of the metadata keys holding the protocol response, as set by | ||
| * {@code protocol.md.prefix}; may be empty | ||
| * @param digestAlgorithm algorithm for the digest fields; see {@link | ||
| * WARCRecordFormat#WARCRecordFormat(String, String)} | ||
| * @throws IllegalArgumentException if the value is not a supported algorithm | ||
| */ | ||
| public WARCRequestRecordFormat(String protocolMDprefix, String digestAlgorithm) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add javadoc
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 727ec1d — javadoc added (and matching ones on MetadataRecordFormat). |
||
| super(protocolMDprefix, digestAlgorithm); | ||
| } | ||
|
|
||
| @Override | ||
| public byte[] format(Tuple tuple) { | ||
|
|
||
|
|
@@ -82,7 +97,7 @@ public byte[] format(Tuple tuple) { | |
| int contentLength = httpheaders.length; | ||
| buffer.append("Content-Length: ").append(Integer.toString(contentLength)).append(CRLF); | ||
|
|
||
| String blockDigest = getDigestSha1(httpheaders); | ||
| String blockDigest = getDigest(httpheaders); | ||
|
|
||
| String captureTime = getCaptureTime(metadata); | ||
| buffer.append("WARC-Date: ").append(captureTime).append(CRLF); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add javadoc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 727ec1d — javadoc added.