From 5f837efc15f3d1e0d7fbed7fc569251143266584 Mon Sep 17 00:00:00 2001
From: Michael Kriese <michael.kriese@visualon.de>
Date: Tue, 23 Jan 2024 22:42:46 +0100
Subject: [PATCH] feat(nuget): basic manifest download

---
 modules/context/base.go             |  10 +++
 modules/packages/nuget/metadata.go  | 100 ++++++++++++++++++++--------
 routers/api/packages/nuget/nuget.go |  63 ++++++++++++------
 3 files changed, 127 insertions(+), 46 deletions(-)

diff --git a/modules/context/base.go b/modules/context/base.go
index 8df1dde866..625fd2680c 100644
--- a/modules/context/base.go
+++ b/modules/context/base.go
@@ -5,6 +5,7 @@ package context
 
 import (
 	"context"
+	"encoding/xml"
 	"fmt"
 	"io"
 	"net/http"
@@ -136,6 +137,15 @@ func (b *Base) JSON(status int, content any) {
 	}
 }
 
+// XML render content as XML
+func (b *Base) XML(status int, content any) {
+	b.Resp.Header().Set("Content-Type", "application/xml;charset=utf-8")
+	b.Resp.WriteHeader(status)
+	if err := xml.NewEncoder(b.Resp).Encode(content); err != nil {
+		log.Error("Render XML failed: %v", err)
+	}
+}
+
 // RemoteAddr returns the client machine ip address
 func (b *Base) RemoteAddr() string {
 	return b.Req.RemoteAddr
diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go
index 3c478b1c02..e7a4d2c0ed 100644
--- a/modules/packages/nuget/metadata.go
+++ b/modules/packages/nuget/metadata.go
@@ -71,34 +71,47 @@ type Dependency struct {
 	Version string `json:"version"`
 }
 
+type nuspecPackageType struct {
+	Name string `xml:"name,attr"`
+}
+
+type nuspecPackageTypes struct {
+	PackageType []nuspecPackageType `xml:"packageType"`
+}
+
+type nuspecRepository struct {
+	URL string `xml:"url,attr"`
+}
+type nuspecDependency struct {
+	ID      string `xml:"id,attr"`
+	Version string `xml:"version,attr"`
+	Exclude string `xml:"exclude,attr"`
+}
+
+type nuspecGroup struct {
+	TargetFramework string             `xml:"targetFramework,attr"`
+	Dependency      []nuspecDependency `xml:"dependency"`
+}
+
+type nuspecDependencies struct {
+	Group []nuspecGroup `xml:"group"`
+}
+
+type nuspeceMetadata struct {
+	ID                       string             `xml:"id"`
+	Version                  string             `xml:"version"`
+	Authors                  string             `xml:"authors"`
+	RequireLicenseAcceptance bool               `xml:"requireLicenseAcceptance"`
+	ProjectURL               string             `xml:"projectUrl"`
+	Description              string             `xml:"description"`
+	ReleaseNotes             string             `xml:"releaseNotes"`
+	PackageTypes             nuspecPackageTypes `xml:"packageTypes"`
+	Repository               nuspecRepository   `xml:"repository"`
+	Dependencies             nuspecDependencies `xml:"dependencies"`
+}
+
 type nuspecPackage struct {
-	Metadata struct {
-		ID                       string `xml:"id"`
-		Version                  string `xml:"version"`
-		Authors                  string `xml:"authors"`
-		RequireLicenseAcceptance bool   `xml:"requireLicenseAcceptance"`
-		ProjectURL               string `xml:"projectUrl"`
-		Description              string `xml:"description"`
-		ReleaseNotes             string `xml:"releaseNotes"`
-		PackageTypes             struct {
-			PackageType []struct {
-				Name string `xml:"name,attr"`
-			} `xml:"packageType"`
-		} `xml:"packageTypes"`
-		Repository struct {
-			URL string `xml:"url,attr"`
-		} `xml:"repository"`
-		Dependencies struct {
-			Group []struct {
-				TargetFramework string `xml:"targetFramework,attr"`
-				Dependency      []struct {
-					ID      string `xml:"id,attr"`
-					Version string `xml:"version,attr"`
-					Exclude string `xml:"exclude,attr"`
-				} `xml:"dependency"`
-			} `xml:"group"`
-		} `xml:"dependencies"`
-	} `xml:"metadata"`
+	Metadata nuspeceMetadata `xml:"metadata"`
 }
 
 // ParsePackageMetaData parses the metadata of a Nuget package file
@@ -204,3 +217,36 @@ func toNormalizedVersion(v *version.Version) string {
 	}
 	return buf.String()
 }
+
+// returning any here because we use a private type and we don't need the type for xml marshalling
+func GenerateNuspec(pd *Package) any {
+	m := nuspeceMetadata{
+		ID:                       pd.ID,
+		Version:                  pd.Version,
+		Authors:                  pd.Metadata.Authors,
+		ProjectURL:               pd.Metadata.ProjectURL,
+		Repository:               nuspecRepository{URL: pd.Metadata.RepositoryURL},
+		RequireLicenseAcceptance: pd.Metadata.RequireLicenseAcceptance,
+		Dependencies: nuspecDependencies{
+			Group: make([]nuspecGroup, len(pd.Metadata.Dependencies)),
+		},
+	}
+
+	for tgf, deps := range pd.Metadata.Dependencies {
+		gDeps := make([]nuspecDependency, len(deps))
+		for i, dep := range deps {
+			gDeps[i] = nuspecDependency{
+				ID:      dep.ID,
+				Version: dep.Version,
+			}
+		}
+		m.Dependencies.Group = append(m.Dependencies.Group, nuspecGroup{
+			TargetFramework: tgf,
+			Dependency:      gDeps,
+		})
+	}
+
+	return &nuspecPackage{
+		Metadata: m,
+	}
+}
diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go
index 769c4c1824..1ba102bfff 100644
--- a/routers/api/packages/nuget/nuget.go
+++ b/routers/api/packages/nuget/nuget.go
@@ -387,34 +387,59 @@ func EnumeratePackageVersionsV3(ctx *context.Context) {
 	ctx.JSON(http.StatusOK, resp)
 }
 
-// https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg
+// https://learn.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-manifest-nuspec
+// https://learn.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg
 func DownloadPackageFile(ctx *context.Context) {
 	packageName := ctx.Params("id")
 	packageVersion := ctx.Params("version")
 	filename := ctx.Params("filename")
 
-	s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
-		ctx,
-		&packages_service.PackageInfo{
-			Owner:       ctx.Package.Owner,
-			PackageType: packages_model.TypeNuGet,
-			Name:        packageName,
-			Version:     packageVersion,
-		},
-		&packages_service.PackageFileInfo{
-			Filename: filename,
-		},
-	)
-	if err != nil {
-		if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
+	if filename == fmt.Sprintf("%s.nuspec", packageName) {
+		pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName, packageVersion)
+		if err != nil {
 			apiError(ctx, http.StatusNotFound, err)
 			return
 		}
-		apiError(ctx, http.StatusInternalServerError, err)
-		return
-	}
 
-	helper.ServePackageFile(ctx, s, u, pf)
+		pd, err := packages_model.GetPackageDescriptor(ctx, pv)
+		if err != nil {
+			apiError(ctx, http.StatusInternalServerError, err)
+			return
+		}
+		pkg := &nuget_module.Package{
+			ID:       pd.Package.Name,
+			Version:  packageVersion,
+			Metadata: pd.Metadata.(*nuget_module.Metadata),
+		}
+
+		ctx.XML(http.StatusOK, nuget_module.GenerateNuspec(pkg))
+	} else if filename == fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion) {
+
+		s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
+			ctx,
+			&packages_service.PackageInfo{
+				Owner:       ctx.Package.Owner,
+				PackageType: packages_model.TypeNuGet,
+				Name:        packageName,
+				Version:     packageVersion,
+			},
+			&packages_service.PackageFileInfo{
+				Filename: filename,
+			},
+		)
+		if err != nil {
+			if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
+				apiError(ctx, http.StatusNotFound, err)
+				return
+			}
+			apiError(ctx, http.StatusInternalServerError, err)
+			return
+		}
+
+		helper.ServePackageFile(ctx, s, u, pf)
+	} else {
+		apiError(ctx, http.StatusInternalServerError, "Invalid filename")
+	}
 }
 
 // UploadPackage creates a new package with the metadata contained in the uploaded nupgk file