diff --git a/.sdkharness/tests/conformance/LargeResume.java b/.sdkharness/tests/conformance/LargeResume.java
index 53be57139..bd14fa83d 100644
--- a/.sdkharness/tests/conformance/LargeResume.java
+++ b/.sdkharness/tests/conformance/LargeResume.java
@@ -38,11 +38,11 @@
*
Two legs, because the SDK treats the two content shapes differently. Leg "no sha1": the content source does
* not know its sha1, so the large file carries no large_file_sha1 and the rerun is the same request as the first
* run. Leg "sha1": the content source knows its sha1 (the SDK records it as the large_file_sha1 file info); the
- * rerun is again the same request as the first run. If the SDK refuses it with a "mismatch" error, the scenario
- * FAILS with a FINDING as the reason: the check detects, it does not work around the refusal. The finding is about
- * the SDK, not about B2: B2LargeFileUploader removes the key
- * {@code X-Bz-Info-large_file_sha1} (B2Headers.LARGE_FILE_SHA1) from the file info before comparing it with the
- * request's, but the info map is keyed {@code large_file_sha1} (B2Headers.LARGE_FILE_SHA1_INFO_NAME).
+ * rerun is again the same request as the first run, and the SDK must accept it. An earlier SDK version refused it
+ * with a "mismatch" error, because B2LargeFileUploader removed the key {@code X-Bz-Info-large_file_sha1}
+ * (B2Headers.LARGE_FILE_SHA1) from the stored file info before comparing it with the request's, while the info map
+ * is keyed {@code large_file_sha1} (B2Headers.LARGE_FILE_SHA1_INFO_NAME). This leg fails with a FINDING as the
+ * reason if that refusal comes back; the check detects, it does not work around the refusal.
*/
public class LargeResume {
private static final int PARTS = 4;
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1c43cf915..a831222eb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,9 @@
A missing or empty signature header throws `B2SignatureVerificationException`; a null or empty signing secret throws `IllegalArgumentException`.
* Signature comparison is now constant-time.
+### Fixed
+* `finishUploadingLargeFile` no longer throws a local `mismatch` error when resuming a large file whose content source knows its sha1.
+
### Added
* Added `B2EventNotification.parseUnverified`, which parses without checking a signature. The one-argument `parse` overloads are deprecated in its favor.
diff --git a/core/src/main/java/com/backblaze/b2/client/B2LargeFileUploader.java b/core/src/main/java/com/backblaze/b2/client/B2LargeFileUploader.java
index ffeaa4590..f3c1c8785 100644
--- a/core/src/main/java/com/backblaze/b2/client/B2LargeFileUploader.java
+++ b/core/src/main/java/com/backblaze/b2/client/B2LargeFileUploader.java
@@ -138,7 +138,8 @@ B2FileVersion finishUploadingLargeFile(B2FileVersion largeFileVersion,
final Map infos = new TreeMap<>();
//noinspection CollectionAddAllCanBeReplacedWithConstructor
infos.putAll(largeFileVersion.getFileInfo());
- infos.remove(B2Headers.LARGE_FILE_SHA1);
+ // the stored file info is keyed by the bare name, "large_file_sha1", not by the "X-Bz-Info-" header name.
+ infos.remove(B2Headers.LARGE_FILE_SHA1_INFO_NAME);
throwIfMismatch("fileInfo", toString(request.getFileInfo()), toString(infos));
}
diff --git a/core/src/test/java/com/backblaze/b2/client/B2LargeFileUploaderTest.java b/core/src/test/java/com/backblaze/b2/client/B2LargeFileUploaderTest.java
index feaabf482..481ba17ad 100644
--- a/core/src/test/java/com/backblaze/b2/client/B2LargeFileUploaderTest.java
+++ b/core/src/test/java/com/backblaze/b2/client/B2LargeFileUploaderTest.java
@@ -370,6 +370,59 @@ public void testThrowIfLargeFileVersionDoesntSeemToMatchRequest_mismatchingSha1(
B2LargeFileUploader.throwIfLargeFileVersionDoesntSeemToMatchRequest(largeFileVersion, contentSource.getContentLength(), request);
}
+ @Test
+ public void testThrowIfLargeFileVersionDoesntSeemToMatchRequest_matchingSha1IsNotAFileInfoMismatch() throws IOException, B2Exception {
+ // the SDK stores the sha1 of a large file as the file info "large_file_sha1" when it starts the file.
+ // that entry is checked as the sha1, so it must not also count as an unexpected file info entry.
+ when(contentSource.getSha1OrNull()).thenReturn(makeSha1(1));
+ final B2UploadFileRequest request = B2UploadFileRequest
+ .builder(bucketId(1), fileName(2), B2ContentTypes.APPLICATION_OCTET, contentSource)
+ .build();
+ final B2FileVersion largeFileVersion = new B2FileVersion(
+ fileId(1),
+ fileName(2),
+ contentSource.getContentLength(),
+ B2ContentTypes.APPLICATION_OCTET,
+ null,
+ null,
+ B2Collections.mapOf(B2Headers.LARGE_FILE_SHA1_INFO_NAME, makeSha1(1)),
+ "action",
+ 1234,
+ null,
+ null,
+ null,
+ null);
+
+ // does not throw.
+ B2LargeFileUploader.throwIfLargeFileVersionDoesntSeemToMatchRequest(largeFileVersion, contentSource.getContentLength(), request);
+ }
+
+ @Test
+ public void testThrowIfLargeFileVersionDoesntSeemToMatchRequest_otherFileInfoStillMismatches() throws IOException, B2Exception {
+ when(contentSource.getSha1OrNull()).thenReturn(makeSha1(1));
+ final B2UploadFileRequest request = B2UploadFileRequest
+ .builder(bucketId(1), fileName(2), B2ContentTypes.APPLICATION_OCTET, contentSource)
+ .build();
+ final B2FileVersion largeFileVersion = new B2FileVersion(
+ fileId(1),
+ fileName(2),
+ contentSource.getContentLength(),
+ B2ContentTypes.APPLICATION_OCTET,
+ null,
+ null,
+ B2Collections.mapOf(B2Headers.LARGE_FILE_SHA1_INFO_NAME, makeSha1(1), "color", "blue"),
+ "action",
+ 1234,
+ null,
+ null,
+ null,
+ null);
+
+ thrown.expect(B2LocalException.class);
+ thrown.expectMessage("contentSource has fileInfo");
+ B2LargeFileUploader.throwIfLargeFileVersionDoesntSeemToMatchRequest(largeFileVersion, contentSource.getContentLength(), request);
+ }
+
@Test
public void testThrowIfLargeFileVersionDoesntSeemToMatchRequest_mismatchingContentType() throws IOException, B2Exception {
final B2UploadFileRequest request = B2UploadFileRequest