Repository: canonical/lxd (Go)

Issue:
Storage pool used by is double url encoded
# Required information

 * Distribution: Ubuntu snap 
 * Distribution version: 5.18-8eef740
   * Storage backend in use: directory

# Issue description

In directory storage pools, non-ascii characters are allowed in custom volume names. The storage pools used_by section url encodes those names twice, resulting in broken urls.

# Steps to reproduce

 1. Create a storage pool called `dir` with driver directory
 2. Create a custom storage volume in that pool and name it `tüdeldü`.
 3. Query the storage pool via API and look at the used_by section `GET 1.0/storage-pools/dir` (see screenshot below)

![Screenshot from 2023-10-17 15-15-33](https://github.com/canonical/lxd/assets/1155472/1b699e66-6318-4436-af01-04b316194de3)

The returned url is
* /1.0/storage-pools/dir/volumes/custom/t%25C3%25BCdeld%25C3%25BC

But the correct url is
* 1.0/storage-pools/dir/volumes/custom/t%C3%BCdeld%C3%BC

Appearently the volume name is url encoded twice. It should only be encoded once.


Discussion:

Patch:
diff --git a/shared/api/url.go b/shared/api/url.go
index f28e272ce1c4..6b79694dc208 100644
--- a/shared/api/url.go
+++ b/shared/api/url.go
@@ -32,14 +32,20 @@ func (u *URL) Host(host string) *URL {
 // Path sets the path of the URL from one or more path parts.
 // It appends each of the pathParts (escaped using url.PathEscape) prefixed with "/" to the URL path.
 func (u *URL) Path(pathParts ...string) *URL {
-	var b strings.Builder
+	var path, rawPath strings.Builder
 
 	for _, pathPart := range pathParts {
-		b.WriteString("/") // Build an absolute URL.
-		b.WriteString(url.PathEscape(pathPart))
+		// Generate unencoded path.
+		path.WriteString("/") // Build an absolute URL.
+		path.WriteString(pathPart)
+
+		// Generate encoded path hint (this will be used by u.URL.EncodedPath() to decide its methodology).
+		rawPath.WriteString("/") // Build an absolute URL.
+		rawPath.WriteString(url.PathEscape(pathPart))
 	}
 
-	u.URL.Path = b.String()
+	u.URL.Path = path.String()
+	u.URL.RawPath = rawPath.String()
 
 	return u
 }


Test patch:
diff --git a/shared/api/url_test.go b/shared/api/url_test.go
index 769bd7c73a7c..3ea4783cf7a0 100644
--- a/shared/api/url_test.go
+++ b/shared/api/url_test.go
@@ -14,11 +14,11 @@ func ExampleURL() {
 	fmt.Println(u.Host("example.com"))
 	fmt.Println(u.Scheme("https"))
 
-	// Output: /1.0/networks/name-with-%252F-in-it
-	// /1.0/networks/name-with-%252F-in-it
-	// /1.0/networks/name-with-%252F-in-it?project=project-with-%25-in-it
-	// /1.0/networks/name-with-%252F-in-it?project=project-with-%25-in-it
-	// /1.0/networks/name-with-%252F-in-it?project=project-with-%25-in-it&target=member-with-%25-in-it
-	// //example.com/1.0/networks/name-with-%252F-in-it?project=project-with-%25-in-it&target=member-with-%25-in-it
-	// https://example.com/1.0/networks/name-with-%252F-in-it?project=project-with-%25-in-it&target=member-with-%25-in-it
+	// Output: /1.0/networks/name-with-%2F-in-it
+	// /1.0/networks/name-with-%2F-in-it
+	// /1.0/networks/name-with-%2F-in-it?project=project-with-%25-in-it
+	// /1.0/networks/name-with-%2F-in-it?project=project-with-%25-in-it
+	// /1.0/networks/name-with-%2F-in-it?project=project-with-%25-in-it&target=member-with-%25-in-it
+	// //example.com/1.0/networks/name-with-%2F-in-it?project=project-with-%25-in-it&target=member-with-%25-in-it
+	// https://example.com/1.0/networks/name-with-%2F-in-it?project=project-with-%25-in-it&target=member-with-%25-in-it
 }