Skip to content

Commit dda496d

Browse files
authored
Merge branch 'main' into fix-securitycenter-object
2 parents 25b724d + d2b2f3c commit dda496d

4 files changed

Lines changed: 227 additions & 0 deletions

File tree

Storage/src/Bucket.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,11 @@ public function exists(array $options = [])
247247
* validation hash will be sent. Choose either `md5` or `crc32` to
248248
* force a hash method regardless of performance implications.
249249
* **Defaults to** `true`.
250+
* @type string $crc32c The base64 encoded CRC32C checksum of the object
251+
* data. If provided, this hash will be used for server-side
252+
* validation.
253+
* @type string $md5 The base64 encoded MD5 hash of the object data. If
254+
* provided, this hash will be used for server-side validation.
250255
* @type int $chunkSize If provided the upload will be done in chunks.
251256
* The size must be in multiples of 262144 bytes. With chunking
252257
* you have increased reliability at the risk of higher overhead.
@@ -379,6 +384,11 @@ public function upload($data, array $options = [])
379384
* validation hash will be sent. Choose either `md5` or `crc32` to
380385
* force a hash method regardless of performance implications.
381386
* **Defaults to** `true`.
387+
* @type string $crc32c The base64 encoded CRC32C checksum of the object
388+
* data. If provided, this hash will be used for server-side
389+
* validation.
390+
* @type string $md5 The base64 encoded MD5 hash of the object data. If
391+
* provided, this hash will be used for server-side validation.
382392
* @type string $predefinedAcl Predefined ACL to apply to the object.
383393
* Acceptable values include, `"authenticatedRead"`,
384394
* `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`,

Storage/src/Connection/Rest.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,36 @@ private function resolveUploadOptions(array $args)
499499
$args['name'] = basename($args['data']->getMetadata('uri'));
500500
}
501501

502+
if (isset($args['crc32c'])) {
503+
$args['metadata']['crc32c'] = $args['crc32c'];
504+
$userCrc32c = $args['crc32c'];
505+
unset($args['crc32c']);
506+
}
507+
if (isset($args['md5'])) {
508+
$args['metadata']['md5Hash'] = $args['md5'];
509+
$userMd5 = $args['md5'];
510+
unset($args['md5']);
511+
}
512+
if (isset($userCrc32c) || isset($userMd5)) {
513+
// Disable auto-validation to prevent redundant calculations
514+
$args['validate'] = false;
515+
516+
$xGoogHash = [];
517+
if (isset($userMd5)) {
518+
$xGoogHash[] = 'md5=' . $userMd5;
519+
}
520+
if (isset($userCrc32c)) {
521+
$xGoogHash[] = 'crc32c=' . $userCrc32c;
522+
}
523+
524+
// Append to existing X-Goog-Hash if present
525+
if (isset($args['headers']['X-Goog-Hash'])) {
526+
$args['headers']['X-Goog-Hash'] .= ',' . implode(',', $xGoogHash);
527+
} else {
528+
$args['headers']['X-Goog-Hash'] = implode(',', $xGoogHash);
529+
}
530+
}
531+
502532
$validate = $this->chooseValidationMethod($args);
503533
$xGoogHashHeader = '';
504534
if ($validate !== false) {

Storage/tests/System/UploadObjectsTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,57 @@ public function testCrc32cChecksumFails()
142142
]
143143
]);
144144
}
145+
146+
public function testCrc32cChecksumFailsWithTopLevelOption()
147+
{
148+
$this->expectException(BadRequestException::class);
149+
150+
$data = 'somedata';
151+
$badChecksum = base64_encode(hash('crc32c', 'bad-data', true));
152+
153+
self::$bucket->upload($data, [
154+
'name' => uniqid(self::TESTING_PREFIX),
155+
'crc32c' => $badChecksum
156+
]);
157+
}
158+
159+
public function testCrc32cChecksumSucceedsWithTopLevelOption()
160+
{
161+
$data = 'somedata';
162+
$goodChecksum = base64_encode(hash('crc32c', $data, true));
163+
164+
$object = self::$bucket->upload($data, [
165+
'name' => uniqid(self::TESTING_PREFIX),
166+
'crc32c' => $goodChecksum
167+
]);
168+
$this->assertEquals(strlen($data), $object->info()['size']);
169+
$object->delete();
170+
}
171+
172+
public function testMd5ChecksumFailsWithTopLevelOption()
173+
{
174+
$this->expectException(BadRequestException::class);
175+
176+
$data = 'somedata';
177+
$badChecksum = base64_encode(hash('md5', 'bad-data', true));
178+
179+
self::$bucket->upload($data, [
180+
'name' => uniqid(self::TESTING_PREFIX),
181+
'md5' => $badChecksum
182+
]);
183+
}
184+
185+
public function testMd5ChecksumSucceedsWithTopLevelOption()
186+
{
187+
$data = 'somedata';
188+
$goodChecksum = base64_encode(hash('md5', $data, true));
189+
190+
$object = self::$bucket->upload($data, [
191+
'name' => uniqid(self::TESTING_PREFIX),
192+
'md5' => $goodChecksum
193+
]);
194+
195+
$this->assertEquals(strlen($data), $object->info()['size']);
196+
$object->delete();
197+
}
145198
}

Storage/tests/Unit/Connection/RestTest.php

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,140 @@ function ($args) use (&$actualRequest, $response) {
581581
$this->assertArrayNotHasKey('crc32c', $metadata);
582582
}
583583

584+
public function testInsertObjectWithUserProvidedHashes()
585+
{
586+
$rest = new Rest();
587+
$testData = 'some test data';
588+
$testStream = Utils::streamFor($testData);
589+
$userCrc32c = 'user-crc';
590+
$userMd5 = 'user-md5';
591+
$expectedHashHeader = 'md5=' . $userMd5 . ',crc32c=' . $userCrc32c;
592+
593+
$actualRequest = null;
594+
$response = new Response(200, ['Location' => 'http://www.mordor.com'], $this->successBody);
595+
596+
$this->requestWrapper->send(
597+
Argument::type(RequestInterface::class),
598+
Argument::type('array')
599+
)->will(
600+
function ($args) use (&$actualRequest, $response) {
601+
$actualRequest = $args[0];
602+
return $response;
603+
}
604+
);
605+
606+
$rest->setRequestWrapper($this->requestWrapper->reveal());
607+
608+
$options = [
609+
'bucket' => 'my-test-bucket',
610+
'name' => 'test-user-hash-file.txt',
611+
'data' => $testStream,
612+
'crc32c' => $userCrc32c,
613+
'md5' => $userMd5,
614+
'validate' => true
615+
];
616+
617+
$uploader = $rest->insertObject($options);
618+
$this->assertInstanceOf(MultipartUploader::class, $uploader);
619+
$uploader->upload();
620+
621+
$this->assertNotNull($actualRequest);
622+
$this->assertTrue($actualRequest->hasHeader('X-Goog-Hash'));
623+
$this->assertEquals([$expectedHashHeader], $actualRequest->getHeader('X-Goog-Hash'));
624+
625+
list($contentType, $metadata) = $this->getContentTypeAndMetadata($actualRequest);
626+
$this->assertEquals($userMd5, $metadata['md5Hash']);
627+
$this->assertEquals($userCrc32c, $metadata['crc32c']);
628+
}
629+
630+
public function testInsertObjectWithUserProvidedCrc32cOnly()
631+
{
632+
$rest = new Rest();
633+
$testData = 'some test data';
634+
$testStream = Utils::streamFor($testData);
635+
$userCrc32c = 'user-crc';
636+
$expectedHashHeader = 'crc32c=' . $userCrc32c;
637+
638+
$actualRequest = null;
639+
$response = new Response(200, ['Location' => 'http://www.mordor.com'], $this->successBody);
640+
641+
$this->requestWrapper->send(
642+
Argument::type(RequestInterface::class),
643+
Argument::type('array')
644+
)->will(
645+
function ($args) use (&$actualRequest, $response) {
646+
$actualRequest = $args[0];
647+
return $response;
648+
}
649+
);
650+
651+
$rest->setRequestWrapper($this->requestWrapper->reveal());
652+
653+
$options = [
654+
'bucket' => 'my-test-bucket',
655+
'name' => 'test-user-hash-file.txt',
656+
'data' => $testStream,
657+
'crc32c' => $userCrc32c,
658+
'validate' => true
659+
];
660+
661+
$uploader = $rest->insertObject($options);
662+
$this->assertInstanceOf(MultipartUploader::class, $uploader);
663+
$uploader->upload();
664+
665+
$this->assertNotNull($actualRequest);
666+
$this->assertTrue($actualRequest->hasHeader('X-Goog-Hash'));
667+
$this->assertEquals([$expectedHashHeader], $actualRequest->getHeader('X-Goog-Hash'));
668+
669+
list($contentType, $metadata) = $this->getContentTypeAndMetadata($actualRequest);
670+
$this->assertEquals($userCrc32c, $metadata['crc32c']);
671+
$this->assertArrayNotHasKey('md5Hash', $metadata);
672+
}
673+
674+
public function testInsertObjectWithUserProvidedMd5Only()
675+
{
676+
$rest = new Rest();
677+
$testData = 'some test data';
678+
$testStream = Utils::streamFor($testData);
679+
$userMd5 = 'user-md5';
680+
$expectedHashHeader = 'md5=' . $userMd5;
681+
682+
$actualRequest = null;
683+
$response = new Response(200, ['Location' => 'http://www.mordor.com'], $this->successBody);
684+
685+
$this->requestWrapper->send(
686+
Argument::type(RequestInterface::class),
687+
Argument::type('array')
688+
)->will(
689+
function ($args) use (&$actualRequest, $response) {
690+
$actualRequest = $args[0];
691+
return $response;
692+
}
693+
);
694+
695+
$rest->setRequestWrapper($this->requestWrapper->reveal());
696+
697+
$options = [
698+
'bucket' => 'my-test-bucket',
699+
'name' => 'test-user-hash-file.txt',
700+
'data' => $testStream,
701+
'md5' => $userMd5,
702+
'validate' => true
703+
];
704+
705+
$uploader = $rest->insertObject($options);
706+
$this->assertInstanceOf(MultipartUploader::class, $uploader);
707+
$uploader->upload();
708+
709+
$this->assertNotNull($actualRequest);
710+
$this->assertTrue($actualRequest->hasHeader('X-Goog-Hash'));
711+
$this->assertEquals([$expectedHashHeader], $actualRequest->getHeader('X-Goog-Hash'));
712+
713+
list($contentType, $metadata) = $this->getContentTypeAndMetadata($actualRequest);
714+
$this->assertEquals($userMd5, $metadata['md5Hash']);
715+
$this->assertArrayNotHasKey('crc32c', $metadata);
716+
}
717+
584718
/**
585719
* @dataProvider validationMethod
586720
*/

0 commit comments

Comments
 (0)