Repository navigation
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
There was a problem hiding this comment.
Code Review
This pull request updates the CRC32C validation logic for Google Cloud Storage reads, ensuring that resumed reads correctly skip full-object checksum validation. It also refines transcoding detection by comparing stored content encoding against actual content encoding and normalizes encoding strings when checking for client-side decompression. Feedback suggests trimming both the stored and actual encoding strings in isTranscoded to prevent comparison failures caused by leading or trailing whitespaces.
| return (storedEncoding != null | ||
| && !storedEncoding.equalsIgnoreCase(encoding == null ? "identity" : encoding)) | ||
| || isDecompressedByClient(response); |
There was a problem hiding this comment.
If the Content-Encoding header contains leading or trailing whitespace (e.g., " GZIP " or "gzip "), the case-insensitive comparison storedEncoding.equalsIgnoreCase(...) will fail because storedEncoding (e.g., "gzip") is not trimmed. This causes isTranscoded to incorrectly return true even when the client requested raw bytes (returnRaw = true) and the data is not transcoded. As a result, checksum validation will be skipped. Trimming both storedEncoding and encoding before comparison ensures robust and correct validation behavior.
| return (storedEncoding != null | |
| && !storedEncoding.equalsIgnoreCase(encoding == null ? "identity" : encoding)) | |
| || isDecompressedByClient(response); | |
| return (storedEncoding != null | |
| && !storedEncoding.trim().equalsIgnoreCase(encoding == null ? "identity" : encoding.trim())) | |
| || isDecompressedByClient(response); |
Validating normal downloads exposed two cases that rejected intact data: - A resumed `reader()` kept the hash from the interrupted response and compared it to the full-object crc32c when GCS included `x-goog-hash` on the resumed 206 response. Clear the hash when a response cannot be validated. - `isDecompressedByClient` matched only lowercase `gzip`, while google-http-client also decompresses `GZIP` and `x-gzip`. Use the same normalization as the HTTP client.
19ba5b3 to
5198dd6
Compare
Ignore surrounding spaces and tabs when comparing stored and response encodings, so unchanged raw downloads still get checksum validation. Add tests for both headers and client decompression.
The problem
Fixes #14773
CRC32C validation is effectively disabled for ALL normal HTTP downloads through
reader(),readAllBytes(), anddownloadTo(). Damaged data can be returned without a checksum error.GCS sends
x-goog-stored-content-encodingandx-goog-stored-content-lengthto describe how a file is stored. The client treats either header as a sign that GCS changed the file before sending it, so it skips the checksum check even when the bytes are unchanged.How to reproduce
Use
com.google.cloud:google-cloud-storage:2.75.0to download this public Landsat metadata file. It is 8,454 bytes and needs no credentials.Set a breakpoint in
HttpStorageRpcHasherHelper.shouldValidate()and run:The full download returns HTTP 200 with these headers:
There is no
Content-Encodingheader. GCS sends the file unchanged and provides its checksum, butshouldValidate()returnsfalse. It should returntrueso the client checks the downloaded bytes.I confirmed this against the live Landsat file with version 2.75.0. All three download methods returned the same 8,454 bytes, and a separately calculated CRC32C matched the checksum from GCS. This reproduction shows the skipped check; the damaged-data tests below show the consequence.
What this PR changes
The client now runs the check on normal downloads. It skips the check only when the data it receives is not the stored file:
gzip,GZIPorx-gzip. The client now uses the same rule, so it never checks unzipped data against the checksum of the zipped file.reader()used to keep the checksum from the first, incomplete attempt and compare it with the checksum for the whole file. That failed even though the data was fine. It now drops that checksum when it resumes.How it was tested
Unit tests. The tests cover normal downloads, unzipped downloads, partial downloads, and resumed downloads with and without a checksum from GCS on the second request. They also cover uppercase
GZIPand spaces or tabs around encoding values, with and without client decompression. All 42 tests in the three test classes pass.mvn -pl java-storage/google-cloud-storage \ -Dtest=HttpStorageRpcHasherHelperTest,HttpStorageRpcChecksumTest,ApiaryUnbufferedReadableByteChannelTest \ testReal GCS downloads. I downloaded these public files with the client, through
readAllBytes(),downloadTo()andreader(), both with and without the client's unzip option:cloud-samples-data/storage/static-hosting/index.htmlchromium-clang-format/76267ebd69846fb255d926fb71e2f70a538a9e22apache-beam-samples/gcs/apache-beam-samples/gcs/bigfile.txt.gzapache-beam-samples/gcs/bigfile_with_encoding_plain.txt.gzapache-beam-samples/gcs/bigfile_with_encoding.txt.gzistio-prow/api-postsubmit/1/build-log.txtreader()downloads succeeded with the right data.GZIP: the proxy changedgziptoGZIPin the headers. With this change, every download method succeeded.The file that GCS always unzips still can't be checked. GCS sends the checksum of the zipped file but delivers the unzipped data, so there is nothing to compare against. This is the same as before.
I have not seen GCS send
GZIPin uppercase on its own. Handling it keeps the client in step with the HTTP library.