Skip to content

Commit

Permalink
Encoded purl components as per the PURL specification. (#23)
Browse files Browse the repository at this point in the history
* Encoded Version, subpath, name, qualifier components of a PURL as per PURL specifation.

* Addressed Pr comments.

* Addressed PR comments.

---------

Co-authored-by: Mounika Rendedla <[email protected]>
  • Loading branch information
morended and Mounika Rendedla authored May 25, 2023
1 parent ef9a9f0 commit a42c0b8
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 12 deletions.
27 changes: 16 additions & 11 deletions src/PackageUrl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public sealed class PackageURL
/// The url encoding of /.
/// </summary>
private const string EncodedSlash = "%2F";
private const string EncodedColon = "%3A";

private static readonly Regex s_typePattern = new Regex("^[a-zA-Z][a-zA-Z0-9.+-]+$", RegexOptions.Compiled);

Expand Down Expand Up @@ -146,27 +147,31 @@ public override string ToString()
}
if (Name != null)
{
purl.Append(Name);
string encodedName = WebUtility.UrlEncode(Name).Replace(EncodedColon, ":");
purl.Append(encodedName);
}
if (Version != null)
{
purl.Append('@').Append(Version);
string encodedVersion = WebUtility.UrlEncode(Version).Replace(EncodedColon, ":");
purl.Append('@').Append(encodedVersion);
}
if (Qualifiers != null && Qualifiers.Count > 0)
{
purl.Append("?");
foreach (var pair in Qualifiers)
{
string encodedValue = WebUtility.UrlEncode(pair.Value).Replace(EncodedSlash, "/");
purl.Append(pair.Key.ToLower());
purl.Append('=');
purl.Append(pair.Value);
purl.Append(encodedValue);
purl.Append('&');
}
purl.Remove(purl.Length - 1, 1);
}
if (Subpath != null)
{
purl.Append("#").Append(Subpath);
string encodedSubpath = WebUtility.UrlEncode(Subpath).Replace(EncodedSlash, "/").Replace(EncodedColon, ":");
purl.Append("#").Append(encodedSubpath);
}
return purl.ToString();
}
Expand Down Expand Up @@ -205,7 +210,7 @@ private void Parse(string purl)
if (remainder.Contains("#"))
{ // subpath is optional - check for existence
int index = remainder.LastIndexOf("#");
Subpath = ValidateSubpath(remainder.Substring(index + 1));
Subpath = ValidateSubpath(WebUtility.UrlDecode(remainder.Substring(index + 1)));
remainder = remainder.Substring(0, index);
}

Expand All @@ -219,7 +224,7 @@ private void Parse(string purl)
if (remainder.Contains("@"))
{ // version is optional - check for existence
int index = remainder.LastIndexOf("@");
Version = remainder.Substring(index + 1);
Version = WebUtility.UrlDecode(remainder.Substring(index + 1));
remainder = remainder.Substring(0, index);
}

Expand All @@ -235,7 +240,7 @@ private void Parse(string purl)
}

Type = ValidateType(firstPartArray[0]);
Name = ValidateName(firstPartArray[firstPartArray.Length - 1]);
Name = ValidateName(WebUtility.UrlDecode(firstPartArray[firstPartArray.Length - 1]));

// Test for namespaces
if (firstPartArray.Length > 2)
Expand All @@ -248,7 +253,7 @@ private void Parse(string purl)
}
@namespace += firstPartArray[i];

Namespace = ValidateNamespace(@namespace);
Namespace = ValidateNamespace(WebUtility.UrlDecode(@namespace));
}
}

Expand All @@ -269,8 +274,8 @@ private string ValidateNamespace(string @namespace)
}
return Type switch
{
"bitbucket" or "github" or "pypi" or "gitlab" => WebUtility.UrlDecode(@namespace.ToLower()),
_ => WebUtility.UrlDecode(@namespace)
"bitbucket" or "github" or "pypi" or "gitlab" => @namespace.ToLower(),
_ => @namespace
};
}

Expand All @@ -297,7 +302,7 @@ private static SortedDictionary<string, string> ValidateQualifiers(string qualif
if (pair.Contains("="))
{
string[] kvpair = pair.Split('=');
list.Add(kvpair[0], kvpair[1]);
list.Add(kvpair[0], WebUtility.UrlDecode(kvpair[1]));
}
}
return list;
Expand Down
1 change: 0 additions & 1 deletion tests/PackageUrl.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ public void TestConstructorParsing(PurlTestData data)

PackageURL purl = new PackageURL(data.Purl);
Assert.Equal(data.CanonicalPurl, purl.ToString());

Assert.Equal("pkg", purl.Scheme);
Assert.Equal(data.Type, purl.Type);
Assert.Equal(data.Namespace, purl.Namespace);
Expand Down
60 changes: 60 additions & 0 deletions tests/TestAssets/test-suite-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -385,5 +385,65 @@
"qualifiers": null,
"subpath": null,
"is_invalid": false
},
{
"description": "valid nuget purl containing a + in version",
"purl": "pkg:nuget/[email protected]%2Bsha.9382aa8e0",
"canonical_purl": "pkg:nuget/[email protected]%2Bsha.9382aa8e0",
"type": "nuget",
"name": "Microsoft.NET.Sdk.MacCatalyst.Manifest-6.0.400",
"version": "15.4.471+sha.9382aa8e0",
"qualifiers": null,
"namespace": null,
"subpath": null,
"is_invalid": false
},
{
"description": "valid maven purl containing a plus in qualifier",
"purl": "pkg:maven/mygroup/[email protected]?mykey=my%2Bvalue",
"canonical_purl": "pkg:maven/mygroup/[email protected]?mykey=my%2Bvalue",
"type": "maven",
"namespace": "mygroup",
"name": "myartifact",
"version": "1.0.0Final",
"qualifiers": { "mykey": "my+value" },
"subpath": null,
"is_invalid": false
},
{
"description": "valid go purl with plus in subpath",
"purl": "pkg:GOLANG/google.golang.org/genproto#/google%2Bapis/api/annotations/",
"canonical_purl": "pkg:golang/google.golang.org/genproto#google%2Bapis/api/annotations",
"type": "golang",
"namespace": "google.golang.org",
"name": "genproto",
"version": null,
"qualifiers": null,
"subpath": "google+apis/api/annotations",
"is_invalid": false
},
{
"description": "valid go purl with colon unencoded in name, version and subpath",
"purl": "pkg:GOLANG/google.golang.org/gen:proto@abc:dedf#/google:apis/api/annotations/",
"canonical_purl": "pkg:golang/google.golang.org/gen:proto@abc:dedf#google:apis/api/annotations",
"type": "golang",
"namespace": "google.golang.org",
"name": "gen:proto",
"version": "abc:dedf",
"qualifiers": null,
"subpath": "google:apis/api/annotations",
"is_invalid": false
},
{
"description": "valid go purl with slash in name, namespace, version and qualifier",
"purl": "pkg:GOLANG/google.golang.org/repo/gen%2Fproto@abc%2Fdedf?repository_url=go.googlesource.com/go",
"canonical_purl": "pkg:golang/google.golang.org/repo/gen%2Fproto@abc%2Fdedf?repository_url=go.googlesource.com/go",
"type": "golang",
"namespace": "google.golang.org/repo",
"name": "gen/proto",
"version": "abc/dedf",
"qualifiers": { "repository_url": "go.googlesource.com/go" },
"subpath": null,
"is_invalid": false
}
]

0 comments on commit a42c0b8

Please sign in to comment.