Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
55 changes: 49 additions & 6 deletions internal/providers/sourceproviders/fedorasource/fedorasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"errors"
"fmt"
"log/slog"
"net/url"
"path/filepath"
"regexp"
"strings"
Expand Down Expand Up @@ -314,13 +315,18 @@ const (
// [PlaceholderFilename], [PlaceholderHashType], and [PlaceholderHash].
// Placeholders not present in the template are simply ignored.
//
// Substituted values are URL path-escaped via [url.PathEscape] so that reserved
// characters such as /, ?, #, and % do not alter the URL structure.
//
// Returns an error if any of the provided values contain a placeholder string, as this
// would cause ambiguous substitution results depending on replacement order.
// would cause ambiguous substitution results depending on replacement order, or if the
// resulting URL is not valid.
func BuildLookasideURL(template, packageName, fileName, hashType, hash string) (string, error) {
// allPlaceholders lists all supported lookaside URI template placeholders.
allPlaceholders := []string{PlaceholderPkg, PlaceholderFilename, PlaceholderHashType, PlaceholderHash}

for _, v := range []string{packageName, fileName, hashType, hash} {
// Check the normalized (lowercased) hashType since that is the form actually substituted.
for _, v := range []string{packageName, fileName, strings.ToLower(hashType), hash} {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit-pick: we're doing strings.ToLower(hashType) twice inside BuildLookasideURL. What do you think about doing hashType = strings.ToLower(hashType) at the beginning of the function to avoid repetition?

for _, p := range allPlaceholders {
if strings.Contains(v, p) {
return "", fmt.Errorf("value %#q contains placeholder %s, which would cause ambiguous substitution", v, p)
Expand All @@ -329,10 +335,47 @@ func BuildLookasideURL(template, packageName, fileName, hashType, hash string) (
}

uri := template
uri = strings.ReplaceAll(uri, PlaceholderPkg, packageName)
uri = strings.ReplaceAll(uri, PlaceholderFilename, fileName)
uri = strings.ReplaceAll(uri, PlaceholderHashType, strings.ToLower(hashType))
uri = strings.ReplaceAll(uri, PlaceholderHash, hash)
uri = strings.ReplaceAll(uri, PlaceholderPkg, url.PathEscape(packageName))
uri = strings.ReplaceAll(uri, PlaceholderFilename, url.PathEscape(fileName))
uri = strings.ReplaceAll(uri, PlaceholderHashType, url.PathEscape(strings.ToLower(hashType)))
Comment thread
dmcilvaney marked this conversation as resolved.
Outdated
uri = strings.ReplaceAll(uri, PlaceholderHash, url.PathEscape(hash))

u, err := url.Parse(uri)
if err != nil {
return "", fmt.Errorf("resulting lookaside URL is not valid:\n%w", err)
}

if u.Scheme == "" || u.Host == "" {
return "", fmt.Errorf("resulting lookaside URL %#q is missing scheme or host", uri)
}

return uri, nil
}

// BuildDistGitURL constructs a dist-git repository URL by substituting the
// [PlaceholderPkg] placeholder in the URI template with the provided package name.
//
// The package name is URL path-escaped via [url.PathEscape] so that reserved
// characters such as /, ?, #, and % do not alter the URL structure.
//
// Returns an error if the package name contains a placeholder string, or if the
// resulting URL is not valid.
func BuildDistGitURL(template, packageName string) (string, error) {
if strings.Contains(packageName, PlaceholderPkg) {
return "", fmt.Errorf("package name %#q contains placeholder %s, which would cause ambiguous substitution",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return "", fmt.Errorf("package name %#q contains placeholder %s, which would cause ambiguous substitution",
return "", fmt.Errorf("package name %#q contains placeholder %#q, which would cause ambiguous substitution",

packageName, PlaceholderPkg)
}

uri := strings.ReplaceAll(template, PlaceholderPkg, url.PathEscape(packageName))
Comment thread
dmcilvaney marked this conversation as resolved.

u, err := url.Parse(uri)
if err != nil {
return "", fmt.Errorf("resulting dist-git URL is not valid:\n%w", err)
}

if u.Scheme == "" || u.Host == "" {
return "", fmt.Errorf("resulting dist-git URL %#q is missing scheme or host", uri)
}

return uri, nil
}
137 changes: 137 additions & 0 deletions internal/providers/sourceproviders/fedorasource/fedorasource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,78 @@ func TestBuildLookasideURL(t *testing.T) {
hash: "abc123",
expectedError: "ambiguous substitution",
},
{
name: "filename with slash is path-escaped",
template: "https://example.com/$pkg/$filename/$hashtype/$hash",
pkg: "my-pkg",
filename: "foo/bar",
hashType: "SHA512",
hash: "abc123",
expected: "https://example.com/my-pkg/foo%2Fbar/sha512/abc123",
},
{
name: "filename with question mark is path-escaped",
template: "https://example.com/$pkg/$filename/$hashtype/$hash",
pkg: "my-pkg",
filename: "file?x=1",
hashType: "SHA512",
hash: "abc123",
expected: "https://example.com/my-pkg/file%3Fx=1/sha512/abc123",
},
{
name: "filename with hash is path-escaped",
template: "https://example.com/$pkg/$filename/$hashtype/$hash",
pkg: "my-pkg",
filename: "file#frag",
hashType: "SHA512",
hash: "abc123",
expected: "https://example.com/my-pkg/file%23frag/sha512/abc123",
},
{
name: "filename with malformed percent is path-escaped",
template: "https://example.com/$pkg/$filename/$hashtype/$hash",
pkg: "my-pkg",
filename: "file%zz",
hashType: "SHA512",
hash: "abc123",
expected: "https://example.com/my-pkg/file%25zz/sha512/abc123",
},
{
name: "packageName with slash is path-escaped",
template: "https://example.com/$pkg/$filename/$hashtype/$hash",
pkg: "foo/bar",
filename: "source.tar.gz",
hashType: "SHA512",
hash: "abc123",
expected: "https://example.com/foo%2Fbar/source.tar.gz/sha512/abc123",
},
{
name: "packageName with hash is path-escaped",
template: "https://example.com/$pkg/$filename/$hashtype/$hash",
pkg: "foo#bar",
filename: "source.tar.gz",
hashType: "SHA512",
hash: "abc123",
expected: "https://example.com/foo%23bar/source.tar.gz/sha512/abc123",
},
{
name: "hashType containing uppercase placeholder is caught after lowercasing",
template: "https://example.com/$pkg/$filename/$hashtype/$hash",
pkg: "my-pkg",
filename: "source.tar.gz",
hashType: "$PKG",
hash: "abc123",
expectedError: "ambiguous substitution",
},
{
name: "template without scheme is rejected",
template: "example.com/$pkg/$filename/$hashtype/$hash",
pkg: "my-pkg",
filename: "source.tar.gz",
hashType: "SHA512",
hash: "abc123",
expectedError: "missing scheme or host",
},
}

for _, testCase := range tests {
Expand All @@ -342,3 +414,68 @@ func TestBuildLookasideURL(t *testing.T) {
})
}
}

func TestBuildDistGitURL(t *testing.T) {
tests := []struct {
name string
template string
pkg string
expected string
expectedError string
}{
{
name: "standard template",
template: "https://src.example.com/rpms/$pkg.git",
pkg: "curl",
expected: "https://src.example.com/rpms/curl.git",
},
{
name: "packageName containing $pkg placeholder",
template: "https://src.example.com/rpms/$pkg.git",
pkg: "evil-$pkg-name",
expectedError: "ambiguous substitution",
},
{
name: "packageName with slash is path-escaped",
template: "https://src.example.com/rpms/$pkg.git",
pkg: "foo/bar",
expected: "https://src.example.com/rpms/foo%2Fbar.git",
},
{
name: "packageName with hash is path-escaped",
template: "https://src.example.com/rpms/$pkg.git",
pkg: "foo#bar",
expected: "https://src.example.com/rpms/foo%23bar.git",
},
{
name: "packageName with question mark is path-escaped",
template: "https://src.example.com/rpms/$pkg.git",
pkg: "foo?bar",
expected: "https://src.example.com/rpms/foo%3Fbar.git",
},
{
name: "packageName with malformed percent is path-escaped",
template: "https://src.example.com/rpms/$pkg.git",
pkg: "foo%zz",
expected: "https://src.example.com/rpms/foo%25zz.git",
},
{
name: "template without scheme is rejected",
template: "example.com/rpms/$pkg.git",
pkg: "curl",
expectedError: "missing scheme or host",
},
}

for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
result, err := BuildDistGitURL(testCase.template, testCase.pkg)
if testCase.expectedError != "" {
assert.ErrorContains(t, err, testCase.expectedError)
} else {
require.NoError(t, err)
assert.Equal(t, testCase.expected, result)
}
})
}
}
11 changes: 8 additions & 3 deletions internal/providers/sourceproviders/fedorasourceprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"fmt"
"log/slog"
"path/filepath"
"strings"
"time"

"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/components"
Expand Down Expand Up @@ -106,7 +105,10 @@ func (g *FedoraSourcesProviderImpl) GetComponent(
return errors.New("destination path cannot be empty")
}

gitRepoURL := strings.ReplaceAll(g.distroGitBaseURI, "$pkg", upstreamNameToUse)
gitRepoURL, err := fedorasource.BuildDistGitURL(g.distroGitBaseURI, upstreamNameToUse)
if err != nil {
return fmt.Errorf("failed to build dist-git URL for %#q:\n%w", upstreamNameToUse, err)
}
Comment thread
dmcilvaney marked this conversation as resolved.

slog.Info("Getting component from git repo",
"component", componentName,
Expand Down Expand Up @@ -259,7 +261,10 @@ func (g *FedoraSourcesProviderImpl) ResolveIdentity(
upstreamName = component.GetName()
}

gitRepoURL := strings.ReplaceAll(g.distroGitBaseURI, "$pkg", upstreamName)
gitRepoURL, err := fedorasource.BuildDistGitURL(g.distroGitBaseURI, upstreamName)
if err != nil {
return "", fmt.Errorf("failed to build dist-git URL for %#q:\n%w", upstreamName, err)
}
Comment thread
dmcilvaney marked this conversation as resolved.

return g.resolveCommit(ctx, gitRepoURL, upstreamName, component.GetConfig().Spec.UpstreamCommit)
}
Expand Down
Loading