Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 37 additions & 4 deletions src/main/java/org/gaul/s3proxy/azureblob/AzureBlobStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@
public final class AzureBlobStore extends BaseBlobStore {
private static final String STUB_BLOB_PREFIX = ".s3proxy/stubs/";
private static final String TARGET_BLOB_NAME_TAG = "s3proxy_target_blob_name";
private static final String BASE64URL_PREFIX = "base64url:";
private static final String TARGET_BLOB_NAME_MARKER = "s3proxy-v1:";
private static final HashFunction MD5 = Hashing.md5();
// Disable retries since client should retry on errors.
private static final RequestRetryOptions NO_RETRY_OPTIONS = new RequestRetryOptions(
Expand Down Expand Up @@ -644,7 +646,8 @@ public MultipartUpload initiateMultipartUpload(String container,
stubBlobClient.uploadWithResponse(uploadOptions, null, null);

var tags = new java.util.HashMap<String, String>();
tags.put(TARGET_BLOB_NAME_TAG, targetBlobName);
tags.put(TARGET_BLOB_NAME_TAG,
encodeTargetBlobNameTagValue(targetBlobName));
stubBlobClient.setTags(tags);

return MultipartUpload.create(container, targetBlobName,
Expand Down Expand Up @@ -713,7 +716,8 @@ public String completeMultipartUpload(MultipartUpload mpu,
throw bse;
}

String targetBlobName = stubTags.get(TARGET_BLOB_NAME_TAG);
String targetBlobName = decodeTargetBlobNameTagValue(
stubTags.get(TARGET_BLOB_NAME_TAG));
if (targetBlobName == null) {
throw new IllegalArgumentException(
"Stub blob missing target name tag: uploadId=" + uploadKey);
Expand Down Expand Up @@ -977,7 +981,8 @@ public List<MultipartPart> listMultipartUpload(MultipartUpload mpu) {
String targetBlobName;
try {
var stubTags = stubBlobClient.getTags();
targetBlobName = stubTags.get(TARGET_BLOB_NAME_TAG);
targetBlobName = decodeTargetBlobNameTagValue(
stubTags.get(TARGET_BLOB_NAME_TAG));
} catch (BlobStorageException bse) {
if (bse.getErrorCode().equals(BlobErrorCode.BLOB_NOT_FOUND)) {
throw new IllegalArgumentException(
Expand Down Expand Up @@ -1055,7 +1060,8 @@ public List<MultipartUpload> listMultipartUploads(String container) {
continue;
}

String targetBlobName = tags.get(TARGET_BLOB_NAME_TAG);
String targetBlobName = decodeTargetBlobNameTagValue(
tags.get(TARGET_BLOB_NAME_TAG));
builder.add(MultipartUpload.create(container, targetBlobName,
uploadKey, null, null));
}
Expand Down Expand Up @@ -1151,6 +1157,33 @@ private static String makeBlockId(String nonce, int partNumber) {
rawId.getBytes(StandardCharsets.UTF_8));
}

static String encodeTargetBlobNameTagValue(String blobName) {
String raw = TARGET_BLOB_NAME_MARKER + blobName;
return BASE64URL_PREFIX + Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(raw.getBytes(StandardCharsets.UTF_8));
}

static String decodeTargetBlobNameTagValue(@Nullable String tagValue) {
if (tagValue == null || !tagValue.startsWith(BASE64URL_PREFIX)) {
return tagValue;
}
String decodedValue;
try {
decodedValue = new String(Base64.getUrlDecoder().decode(
tagValue.substring(BASE64URL_PREFIX.length())),
StandardCharsets.UTF_8);
} catch (IllegalArgumentException iae) {
// Allow legacy/plain values that happen to start with the prefix.
return tagValue;
}
if (!decodedValue.startsWith(TARGET_BLOB_NAME_MARKER)) {
// Allow legacy/plain values that decode to arbitrary strings.
return tagValue;
}
return decodedValue.substring(TARGET_BLOB_NAME_MARKER.length());
}

/**
* Translate BlobStorageException to a jclouds exception. Throws if
* translated otherwise returns.
Expand Down
50 changes: 50 additions & 0 deletions src/test/java/org/gaul/s3proxy/azureblob/AzureBlobStoreTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2014-2026 Andrew Gaul <andrew@gaul.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.gaul.s3proxy.azureblob;

import static org.assertj.core.api.Assertions.assertThat;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

import org.junit.Test;

public final class AzureBlobStoreTest {
@Test
public void testTargetBlobNameTagEncodingIsBackwardCompatible() {
String blobName = "folder/with spaces/+-_=.txt";
String encoded = AzureBlobStore.encodeTargetBlobNameTagValue(blobName);

assertThat(encoded).isNotEqualTo(blobName);
assertThat(AzureBlobStore.decodeTargetBlobNameTagValue(encoded))
.isEqualTo(blobName);

String legacyPlain = "legacy/blob/name";
assertThat(AzureBlobStore.decodeTargetBlobNameTagValue(legacyPlain))
.isEqualTo(legacyPlain);

String legacyPrefix = "base64url:legacy-not-base64-*";
assertThat(AzureBlobStore.decodeTargetBlobNameTagValue(legacyPrefix))
.isEqualTo(legacyPrefix);

String legacyBase64 = "base64url:" + Base64.getUrlEncoder()
.withoutPadding()
.encodeToString("legacy-markerless".getBytes(StandardCharsets.UTF_8));
assertThat(AzureBlobStore.decodeTargetBlobNameTagValue(legacyBase64))
.isEqualTo(legacyBase64);
}
}
Loading