Repository: minio/minio (language: go)

Issue:
Duplicated common prefixes for list-objects(-v2)
## Expected Behavior
`list-objects` should list all keys and all common prefixes (since we are far from the 1000 keys limit):
```
$ aws s3api --profile=minio --endpoint-url=http://localhost:9100 list-objects --bucket bucket --delimiter _ --page-size 2 | jq
{
  "Contents": [
    {
      "Key": "aaa",
      "LastModified": "2022-11-22T11:27:29.316000+00:00",
      "ETag": "\"e5f99328ead74d67230cac6b38d834f3\"",
      "Size": 16,
      "StorageClass": "STANDARD",
      "Owner": {
        "DisplayName": "minio",
        "ID": "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4"
      }
    },
    {
      "Key": "ccc",
      "LastModified": "2022-11-22T11:27:44.146000+00:00",
      "ETag": "\"e5f99328ead74d67230cac6b38d834f3\"",
      "Size": 16,
      "StorageClass": "STANDARD",
      "Owner": {
        "DisplayName": "minio",
        "ID": "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4"
      }
    }
  ],
  "CommonPrefixes": [
    {
      "Prefix": "bbb_"
    }
  ]
}

```

## Current Behavior
`CommonPrefixes` contains `bbb_` twice:
```
 aws s3api --profile=minio --endpoint-url=http://localhost:9100 list-objects --bucket bucket --delimiter _ --page-size 2 | jq
{
  "Contents": [
    {
      "Key": "aaa",
      "LastModified": "2022-11-22T11:27:29.316000+00:00",
      "ETag": "\"e5f99328ead74d67230cac6b38d834f3\"",
      "Size": 16,
      "StorageClass": "STANDARD",
      "Owner": {
        "DisplayName": "minio",
        "ID": "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4"
      }
    },
    {
      "Key": "ccc",
      "LastModified": "2022-11-22T11:27:44.146000+00:00",
      "ETag": "\"e5f99328ead74d67230cac6b38d834f3\"",
      "Size": 16,
      "StorageClass": "STANDARD",
      "Owner": {
        "DisplayName": "minio",
        "ID": "02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4"
      }
    }
  ],
  "CommonPrefixes": [
    {
      "Prefix": "bbb_"
    },
    {
      "Prefix": "bbb_"
    }
  ]
}
```

## Possible Solution
It seems to work for `/` as a delimiter, so the special logic (if there is such a thing) for `/` might solve the issue.

## Steps to Reproduce (for bugs)
In freshly started environment (see below for simple docker-compose), run (`foo.txt` is a 16 byte text file):
```
$ mc cp foo.txt local/bucket/aaa
...
$ mc cp foo.txt local/bucket/bbb_aaa
...
$ mc cp foo.txt local/bucket/bbb_bbb
...
$ mc cp foo.txt local/bucket/ccc    
...
$ mc ls local/bucket
[2022-11-22 12:27:29 CET]    16B STANDARD aaa
[2022-11-22 12:27:36 CET]    16B STANDARD bbb_aaa
[2022-11-22 12:27:39 CET]    16B STANDARD bbb_bbb
[2022-11-22 12:27:44 CET]    16B STANDARD ccc
```

Then run the `aws s3api` command: 
```
$ aws s3api --profile=minio --endpoint-url=http://localhost:9100 list-objects --bucket bucket --delimiter _ --page-size 2 | jq
...
```

Note: same result using `list-objects-v2`

## Context
Test for list-objects(-v2) are failing

## Regression
Not sure.

## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->
* Version used (`minio --version`):
`RELEASE.2022-11-17T23-20-09Z`
* Server setup and configuration:
Setup using docker-compose:
```
  minio-server:
    image: quay.io/minio/minio:RELEASE.2022-11-17T23-20-09Z
    environment:
      MINIO_ROOT_USER: minioadmin00
      MINIO_ROOT_PASSWORD: minioadmin00
    command:
      - server
      - /data
      - --address
      - ':9100'
      - --console-address
      - ':9190'
    ports:
      - '9100:9100'
      - '9190:9190'
```
* Operating System and version (`uname -a`):
`Darwin Hans-MBPro2021.local 21.6.0 Darwin Kernel Version 21.6.0: Wed Aug 10 14:28:23 PDT 2022; root:xnu-8020.141.5~2/RELEASE_ARM64_T6000 arm64`



Discussion / hints:
Thanks for the detailed reproducer.

I will take a look.

Fix patch:
diff --git a/cmd/erasure-server-pool.go b/cmd/erasure-server-pool.go
index 7b2ac71f8faef..41cb258270c72 100644
--- a/cmd/erasure-server-pool.go
+++ b/cmd/erasure-server-pool.go
@@ -1254,7 +1254,20 @@ func (z *erasureServerPools) ListObjectVersions(ctx context.Context, bucket, pre
 	}
 	for _, obj := range objects {
 		if obj.IsDir && obj.ModTime.IsZero() && delimiter != "" {
-			loi.Prefixes = append(loi.Prefixes, obj.Name)
+			// Only add each once.
+			// With slash delimiter we only get the directory once.
+			found := false
+			if delimiter != slashSeparator {
+				for _, p := range loi.Prefixes {
+					if found {
+						break
+					}
+					found = p == obj.Name
+				}
+			}
+			if !found {
+				loi.Prefixes = append(loi.Prefixes, obj.Name)
+			}
 		} else {
 			loi.Objects = append(loi.Objects, obj)
 		}
@@ -1334,7 +1347,20 @@ func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, ma
 	}
 	for _, obj := range objects {
 		if obj.IsDir && obj.ModTime.IsZero() && delimiter != "" {
-			loi.Prefixes = append(loi.Prefixes, obj.Name)
+			// Only add each once.
+			// With slash delimiter we only get the directory once.
+			found := false
+			if delimiter != slashSeparator {
+				for _, p := range loi.Prefixes {
+					if found {
+						break
+					}
+					found = p == obj.Name
+				}
+			}
+			if !found {
+				loi.Prefixes = append(loi.Prefixes, obj.Name)
+			}
 		} else {
 			loi.Objects = append(loi.Objects, obj)
 		}


Test patch:
diff --git a/cmd/object-api-listobjects_test.go b/cmd/object-api-listobjects_test.go
index f48b7c634ef40..625b1b68877ad 100644
--- a/cmd/object-api-listobjects_test.go
+++ b/cmd/object-api-listobjects_test.go
@@ -315,18 +315,20 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
 	t, _ := t1.(*testing.T)
 	testBuckets := []string{
 		// This bucket is used for testing ListObject operations.
-		"test-bucket-list-object",
+		0: "test-bucket-list-object",
 		// This bucket will be tested with empty directories
-		"test-bucket-empty-dir",
+		1: "test-bucket-empty-dir",
 		// Will not store any objects in this bucket,
 		// Its to test ListObjects on an empty bucket.
-		"empty-bucket",
+		2: "empty-bucket",
 		// Listing the case where the marker > last object.
-		"test-bucket-single-object",
+		3: "test-bucket-single-object",
 		// Listing uncommon delimiter.
-		"test-bucket-delimiter",
+		4: "test-bucket-delimiter",
 		// Listing prefixes > maxKeys
-		"test-bucket-max-keys-prefixes",
+		5: "test-bucket-max-keys-prefixes",
+		// Listing custom delimiters
+		6: "test-bucket-custom-delimiter",
 	}
 	for _, bucket := range testBuckets {
 		err := obj.MakeBucketWithLocation(context.Background(), bucket, MakeBucketOptions{
@@ -366,6 +368,10 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
 		{testBuckets[5], "foo/201910/2112", "content", nil},
 		{testBuckets[5], "foo/201910_txt", "content", nil},
 		{testBuckets[5], "201910/foo/bar/xl.meta/1.txt", "content", nil},
+		{testBuckets[6], "aaa", "content", nil},
+		{testBuckets[6], "bbb_aaa", "content", nil},
+		{testBuckets[6], "bbb_aaa", "content", nil},
+		{testBuckets[6], "ccc", "content", nil},
 	}
 	for _, object := range testObjects {
 		md5Bytes := md5.Sum([]byte(object.content))
@@ -788,6 +794,15 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
 				{Name: "201910/foo/bar/xl.meta/1.txt"},
 			},
 		},
+		// ListObjectsResult-40
+		{
+			IsTruncated: false,
+			Objects: []ObjectInfo{
+				{Name: "aaa"},
+				{Name: "ccc"},
+			},
+			Prefixes: []string{"bbb_"},
+		},
 	}
 
 	testCases := []struct {
@@ -918,6 +933,8 @@ func _testListObjects(obj ObjectLayer, instanceType string, t1 TestErrHandler, v
 		{testBuckets[5], "foo/201910", "", "", 1000, resultCases[38], nil, true},
 		// Test listing with prefix match with 'xl.meta'
 		{testBuckets[5], "201910/foo/bar", "", "", 1000, resultCases[39], nil, true},
+		// Test listing with custom prefix
+		{testBuckets[6], "", "", "_", 1000, resultCases[40], nil, true},
 	}
 
 	for i, testCase := range testCases {