Skip to content

Fix RenameSource URL encoding for paths with special characters #24680

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

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
96 changes: 96 additions & 0 deletions sdk/storage/azdatalake/directory/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2528,6 +2528,102 @@ func (s *RecordedTestSuite) TestDirRenameNoOptions() {
_require.NoError(err)
}

func (s *RecordedTestSuite) TestDirRenameWithSpecialCharacters() {
_require := require.New(s.T())
testName := s.T().Name()

filesystemName := testcommon.GenerateFileSystemName(testName)
fsClient, err := testcommon.GetFileSystemClient(filesystemName, s.T(), testcommon.TestAccountDatalake, nil)
_require.NoError(err)
defer testcommon.DeleteFileSystem(context.Background(), _require, fsClient)

_, err = fsClient.Create(context.Background(), nil)
_require.NoError(err)

// Test with directory name containing spaces
dirName := "dir with spaces"
dirClient, err := testcommon.GetDirClient(filesystemName, dirName, s.T(), testcommon.TestAccountDatalake, nil)
_require.NoError(err)

resp, err := dirClient.Create(context.Background(), nil)
_require.NoError(err)
_require.NotNil(resp)

// Rename to a path with special characters
newName := "renamed dir+special@!&% chars"
_, err = dirClient.Rename(context.Background(), newName, nil)
_require.NoError(err)

// Verify new directory exists by creating a client to it and checking properties
newClient, err := testcommon.GetDirClient(filesystemName, newName, s.T(), testcommon.TestAccountDatalake, nil)
_require.NoError(err)

_, err = newClient.GetProperties(context.Background(), nil)
_require.NoError(err)

// Test with Unicode characters
unicodeDirName := "lör mapp"
unicodeClient, err := testcommon.GetDirClient(filesystemName, unicodeDirName, s.T(), testcommon.TestAccountDatalake, nil)
_require.NoError(err)

resp, err = unicodeClient.Create(context.Background(), nil)
_require.NoError(err)
_require.NotNil(resp)

// Rename Unicode directory to another Unicode name
newUnicodeName := "ångström ümlaut 目录"
_, err = unicodeClient.Rename(context.Background(), newUnicodeName, nil)
_require.NoError(err)

// Verify new directory exists
newUnicodeClient, err := testcommon.GetDirClient(filesystemName, newUnicodeName, s.T(), testcommon.TestAccountDatalake, nil)
_require.NoError(err)

_, err = newUnicodeClient.GetProperties(context.Background(), nil)
_require.NoError(err)
}

func (s *RecordedTestSuite) TestDirRenameWithQueryParameters() {
_require := require.New(s.T())
testName := s.T().Name()

filesystemName := testcommon.GenerateFileSystemName(testName)
fsClient, err := testcommon.GetFileSystemClient(filesystemName, s.T(), testcommon.TestAccountDatalake, nil)
_require.NoError(err)
defer testcommon.DeleteFileSystem(context.Background(), _require, fsClient)

_, err = fsClient.Create(context.Background(), nil)
_require.NoError(err)

// Create a regular directory
dirName := "original-dir"
dirClient, err := testcommon.GetDirClient(filesystemName, dirName, s.T(), testcommon.TestAccountDatalake, nil)
_require.NoError(err)

resp, err := dirClient.Create(context.Background(), nil)
_require.NoError(err)
_require.NotNil(resp)

// Create a client with the path including query parameters
_, err = dirClient.Rename(context.Background(), "new-dir", nil)
_require.NoError(err)

// Create a new directory to test the rename with query parameters
newDir := "query-test-dir"
queryClient, err := testcommon.GetDirClient(filesystemName, newDir, s.T(), testcommon.TestAccountDatalake, nil)
_require.NoError(err)

resp, err = queryClient.Create(context.Background(), nil)
_require.NoError(err)
_require.NotNil(resp)

_require.NotNil(createOpts)
_require.NotNil(createOpts.RenameSource)
// Verify the source path was properly encoded
expected := "query-test-dir?param1=value1&param2=value+with+spaces"
_require.Equal(expected, *createOpts.RenameSource)
}

func (s *RecordedTestSuite) TestDirRenameRequestWithCPK() {
_require := require.New(s.T())
testName := s.T().Name()
Expand Down
106 changes: 103 additions & 3 deletions sdk/storage/azdatalake/file/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
"fmt"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/log"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/lease"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azdatalake/internal/path"
"hash/crc64"
"io"
"net/http"
Expand Down Expand Up @@ -2404,6 +2403,107 @@ func (s *RecordedTestSuite) TestRenameNoOptions() {
_require.NoError(err)
}

func (s *RecordedTestSuite) TestRenameWithSpecialCharacters() {
_require := require.New(s.T())
testName := s.T().Name()

filesystemName := testcommon.GenerateFileSystemName(testName)
fsClient, err := testcommon.GetFileSystemClient(filesystemName, s.T(), testcommon.TestAccountDatalake, nil)
_require.NoError(err)
defer testcommon.DeleteFileSystem(context.Background(), _require, fsClient)

_, err = fsClient.Create(context.Background(), nil)
_require.NoError(err)

// Test with file name containing spaces
fileName := "file with spaces.txt"
fClient, err := testcommon.GetFileClient(filesystemName, fileName, s.T(), testcommon.TestAccountDatalake, nil)
_require.NoError(err)

resp, err := fClient.Create(context.Background(), nil)
_require.NoError(err)
_require.NotNil(resp)

// Rename to a path with special characters
newName := "renamed file+special@!&% chars.txt"
_, err = fClient.Rename(context.Background(), newName, nil)
_require.NoError(err)

// Verify new file exists by creating a client to it and checking properties
newClient, err := testcommon.GetFileClient(filesystemName, newName, s.T(), testcommon.TestAccountDatalake, nil)
_require.NoError(err)

_, err = newClient.GetProperties(context.Background(), nil)
_require.NoError(err)

// Test with Unicode characters
unicodeFileName := "lör 006.jpg"
unicodeClient, err := testcommon.GetFileClient(filesystemName, unicodeFileName, s.T(), testcommon.TestAccountDatalake, nil)
_require.NoError(err)

resp, err = unicodeClient.Create(context.Background(), nil)
_require.NoError(err)
_require.NotNil(resp)

// Rename Unicode file to another Unicode name
newUnicodeName := "ångström ümlaut 我的文件.jpg"
_, err = unicodeClient.Rename(context.Background(), newUnicodeName, nil)
_require.NoError(err)

// Verify new file exists
newUnicodeClient, err := testcommon.GetFileClient(filesystemName, newUnicodeName, s.T(), testcommon.TestAccountDatalake, nil)
_require.NoError(err)

_, err = newUnicodeClient.GetProperties(context.Background(), nil)
_require.NoError(err)
}

func (s *RecordedTestSuite) TestRenameWithQueryParameters() {
_require := require.New(s.T())
testName := s.T().Name()

filesystemName := testcommon.GenerateFileSystemName(testName)
fsClient, err := testcommon.GetFileSystemClient(filesystemName, s.T(), testcommon.TestAccountDatalake, nil)
_require.NoError(err)
defer testcommon.DeleteFileSystem(context.Background(), _require, fsClient)

_, err = fsClient.Create(context.Background(), nil)
_require.NoError(err)

// Create a regular file
fileName := "original-file.txt"
fClient, err := testcommon.GetFileClient(filesystemName, fileName, s.T(), testcommon.TestAccountDatalake, nil)
_require.NoError(err)

resp, err := fClient.Create(context.Background(), nil)
_require.NoError(err)
_require.NotNil(resp)

// Create a source file with query parameters
srcClient, err := testcommon.GetFileClient(filesystemName, "source-file.txt", s.T(), testcommon.TestAccountDatalake, nil)
_require.NoError(err)

resp, err = srcClient.Create(context.Background(), nil)
_require.NoError(err)
_require.NotNil(resp)

// Create a destination path
destFile := "dest-file.txt"

// Rename source file (with query params) to destination
// This is the operation that would fail without proper URL encoding
_, err = srcClient.Rename(context.Background(), destFile, nil)
_require.NoError(err)

// Verify destination file exists
destClient, err := testcommon.GetFileClient(filesystemName, destFile, s.T(), testcommon.TestAccountDatalake, nil)
_require.NoError(err)

destResp, err := destClient.GetProperties(context.Background(), nil)
_require.NoError(err)
_require.NotNil(destResp)
}

func (s *RecordedTestSuite) TestRenameFileWithCPK() {
_require := require.New(s.T())
testName := s.T().Name()
Expand Down Expand Up @@ -3932,8 +4032,8 @@ func (s *RecordedTestSuite) TestFileAppendWithFlushReleaseLease() {

opts := &file.FlushDataOptions{
LeaseAction: &file.LeaseActionRelease,
AccessConditions: &path.AccessConditions{
LeaseAccessConditions: &path.LeaseAccessConditions{LeaseID: proposedLeaseIDs[0]},
AccessConditions: &file.AccessConditions{
LeaseAccessConditions: &file.LeaseAccessConditions{LeaseID: proposedLeaseIDs[0]},
},
}

Expand Down
23 changes: 22 additions & 1 deletion sdk/storage/azdatalake/internal/path/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ package path

import (
"errors"
"net/url"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azdatalake/datalakeerror"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azdatalake/internal/exported"
Expand Down Expand Up @@ -47,9 +50,27 @@ type RenameOptions struct {
func FormatRenameOptions(o *RenameOptions, path string) (*generated.LeaseAccessConditions, *generated.ModifiedAccessConditions, *generated.SourceModifiedAccessConditions, *generated.PathClientCreateOptions, *generated.CPKInfo) {
// we don't need sourceModAccCond since this is not rename
mode := generated.PathRenameModeLegacy
// URL encode the source path to handle special characters
pathPart, queryPart, found := strings.Cut(path, "?")

// URL encode each path segment individually to preserve structure
segments := strings.Split(pathPart, "/")
for i, segment := range segments {
segments[i] = url.QueryEscape(segment)
}
encodedPath := strings.Join(segments, "/")

// If there was a query part, encode and append it
if found && queryPart != "" {
encodedQueryURL, err := runtime.EncodeQueryParams("?" + queryPart)
if err == nil {
encodedPath += encodedQueryURL
}
}

createOpts := &generated.PathClientCreateOptions{
Mode: &mode,
RenameSource: &path,
RenameSource: &encodedPath,
}
if o == nil {
return nil, nil, nil, createOpts, nil
Expand Down
61 changes: 61 additions & 0 deletions sdk/storage/azdatalake/internal/path/models_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//go:build go1.18
// +build go1.18

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

package path

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestFormatRenameOptions(t *testing.T) {
tests := []struct {
name string
path string
expectedSource string
}{
{
name: "Simple path",
path: "dir1/file1.txt",
expectedSource: "dir1/file1.txt",
},
{
name: "Path with spaces",
path: "dir1/file with spaces.txt",
expectedSource: "dir1/file+with+spaces.txt",
},
{
name: "Path with unicode characters",
path: "dir1/lör 006.jpg",
expectedSource: "dir1/l%C3%B6r+006.jpg",
},
{
name: "Path with special characters",
path: "dir1/file+name@!&%.txt",
expectedSource: "dir1/file%2Bname%40%21%26%25.txt",
},
{
name: "Path with query parameters",
path: "dir1/file1.txt?param1=value1&param2=value2",
expectedSource: "dir1/file1.txt?param1=value1&param2=value2",
},
{
name: "Path with special characters and query parameters",
path: "dir1/file name+special@!&%.txt?param=value with spaces",
expectedSource: "dir1/file+name%2Bspecial%40%21%26%25.txt?param=value+with+spaces",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, _, _, createOpts, _ := FormatRenameOptions(nil, test.path)
assert.NotNil(t, createOpts)
assert.NotNil(t, createOpts.RenameSource)
assert.Equal(t, test.expectedSource, *createOpts.RenameSource)
})
}
}
Loading