From 8498e67309478bd0a65a7b1c6f3c8e6ce62c0956 Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Sun, 7 Apr 2024 18:46:59 +0200
Subject: [PATCH] Some NuGet package enhancements (#30280)

Fixes #30265

1. Read second type of dependencies
2. Render `Description` and `ReleaseNotes`

old:

![grafik](https://github.com/go-gitea/gitea/assets/1666336/abac057c-11cd-4d25-b196-01ff899d948e)

new:

![grafik](https://github.com/go-gitea/gitea/assets/1666336/35302273-740c-481a-a031-1f80d2d7d336)

The NuGet spec does not specify what kind of text can be stored in the
description but we can best guess markdown. The official NuGet registry
just [converts the newlines to html
lines](https://www.nuget.org/packages/rb.Firefox#readme-body-tab).

3. Extract and render the readme. This is the new and better place to
store larger text than in the description. The content is markdown.

![grafik](https://github.com/go-gitea/gitea/assets/1666336/f442264e-3735-4b55-92c4-3b89a8ebafb0)

---------

Co-authored-by: Benjamin Heemann <benjamin.heemann@raith.de>
---
 modules/packages/nuget/metadata.go      | 34 +++++++++++++-
 modules/packages/nuget/metadata_test.go | 62 +++++++++++++++----------
 templates/package/content/nuget.tmpl    |  9 ++--
 3 files changed, 73 insertions(+), 32 deletions(-)

diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go
index 3c478b1c02..6769c514cc 100644
--- a/modules/packages/nuget/metadata.go
+++ b/modules/packages/nuget/metadata.go
@@ -58,6 +58,7 @@ type Package struct {
 type Metadata struct {
 	Description              string                  `json:"description,omitempty"`
 	ReleaseNotes             string                  `json:"release_notes,omitempty"`
+	Readme                   string                  `json:"readme,omitempty"`
 	Authors                  string                  `json:"authors,omitempty"`
 	ProjectURL               string                  `json:"project_url,omitempty"`
 	RepositoryURL            string                  `json:"repository_url,omitempty"`
@@ -71,6 +72,7 @@ type Dependency struct {
 	Version string `json:"version"`
 }
 
+// https://learn.microsoft.com/en-us/nuget/reference/nuspec
 type nuspecPackage struct {
 	Metadata struct {
 		ID                       string `xml:"id"`
@@ -80,6 +82,7 @@ type nuspecPackage struct {
 		ProjectURL               string `xml:"projectUrl"`
 		Description              string `xml:"description"`
 		ReleaseNotes             string `xml:"releaseNotes"`
+		Readme                   string `xml:"readme"`
 		PackageTypes             struct {
 			PackageType []struct {
 				Name string `xml:"name,attr"`
@@ -89,6 +92,11 @@ type nuspecPackage struct {
 			URL string `xml:"url,attr"`
 		} `xml:"repository"`
 		Dependencies struct {
+			Dependency []struct {
+				ID      string `xml:"id,attr"`
+				Version string `xml:"version,attr"`
+				Exclude string `xml:"exclude,attr"`
+			} `xml:"dependency"`
 			Group []struct {
 				TargetFramework string `xml:"targetFramework,attr"`
 				Dependency      []struct {
@@ -122,14 +130,14 @@ func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) {
 			}
 			defer f.Close()
 
-			return ParseNuspecMetaData(f)
+			return ParseNuspecMetaData(archive, f)
 		}
 	}
 	return nil, ErrMissingNuspecFile
 }
 
 // ParseNuspecMetaData parses a Nuspec file to retrieve the metadata of a Nuget package
-func ParseNuspecMetaData(r io.Reader) (*Package, error) {
+func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
 	var p nuspecPackage
 	if err := xml.NewDecoder(r).Decode(&p); err != nil {
 		return nil, err
@@ -166,6 +174,28 @@ func ParseNuspecMetaData(r io.Reader) (*Package, error) {
 		Dependencies:             make(map[string][]Dependency),
 	}
 
+	if p.Metadata.Readme != "" {
+		f, err := archive.Open(p.Metadata.Readme)
+		if err == nil {
+			buf, _ := io.ReadAll(f)
+			m.Readme = string(buf)
+			_ = f.Close()
+		}
+	}
+
+	if len(p.Metadata.Dependencies.Dependency) > 0 {
+		deps := make([]Dependency, 0, len(p.Metadata.Dependencies.Dependency))
+		for _, dep := range p.Metadata.Dependencies.Dependency {
+			if dep.ID == "" || dep.Version == "" {
+				continue
+			}
+			deps = append(deps, Dependency{
+				ID:      dep.ID,
+				Version: dep.Version,
+			})
+		}
+		m.Dependencies[""] = deps
+	}
 	for _, group := range p.Metadata.Dependencies.Group {
 		deps := make([]Dependency, 0, len(group.Dependency))
 		for _, dep := range group.Dependency {
diff --git a/modules/packages/nuget/metadata_test.go b/modules/packages/nuget/metadata_test.go
index bba2bff4a5..f466492f8a 100644
--- a/modules/packages/nuget/metadata_test.go
+++ b/modules/packages/nuget/metadata_test.go
@@ -6,7 +6,6 @@ package nuget
 import (
 	"archive/zip"
 	"bytes"
-	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -19,6 +18,7 @@ const (
 	projectURL        = "https://gitea.io"
 	description       = "Package Description"
 	releaseNotes      = "Package Release Notes"
+	readme            = "Readme"
 	repositoryURL     = "https://gitea.io/gitea/gitea"
 	targetFramework   = ".NETStandard2.1"
 	dependencyID      = "System.Text.Json"
@@ -36,6 +36,7 @@ const nuspecContent = `<?xml version="1.0" encoding="utf-8"?>
     <description>` + description + `</description>
     <releaseNotes>` + releaseNotes + `</releaseNotes>
     <repository url="` + repositoryURL + `" />
+    <readme>README.md</readme>
     <dependencies>
       <group targetFramework="` + targetFramework + `">
         <dependency id="` + dependencyID + `" version="` + dependencyVersion + `" exclude="Build,Analyzers" />
@@ -60,17 +61,19 @@ const symbolsNuspecContent = `<?xml version="1.0" encoding="utf-8"?>
 </package>`
 
 func TestParsePackageMetaData(t *testing.T) {
-	createArchive := func(name, content string) []byte {
+	createArchive := func(files map[string]string) []byte {
 		var buf bytes.Buffer
 		archive := zip.NewWriter(&buf)
-		w, _ := archive.Create(name)
-		w.Write([]byte(content))
+		for name, content := range files {
+			w, _ := archive.Create(name)
+			w.Write([]byte(content))
+		}
 		archive.Close()
 		return buf.Bytes()
 	}
 
 	t.Run("MissingNuspecFile", func(t *testing.T) {
-		data := createArchive("dummy.txt", "")
+		data := createArchive(map[string]string{"dummy.txt": ""})
 
 		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.Nil(t, np)
@@ -78,7 +81,7 @@ func TestParsePackageMetaData(t *testing.T) {
 	})
 
 	t.Run("MissingNuspecFileInRoot", func(t *testing.T) {
-		data := createArchive("sub/package.nuspec", "")
+		data := createArchive(map[string]string{"sub/package.nuspec": ""})
 
 		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.Nil(t, np)
@@ -86,7 +89,7 @@ func TestParsePackageMetaData(t *testing.T) {
 	})
 
 	t.Run("InvalidNuspecFile", func(t *testing.T) {
-		data := createArchive("package.nuspec", "")
+		data := createArchive(map[string]string{"package.nuspec": ""})
 
 		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.Nil(t, np)
@@ -94,10 +97,10 @@ func TestParsePackageMetaData(t *testing.T) {
 	})
 
 	t.Run("InvalidPackageId", func(t *testing.T) {
-		data := createArchive("package.nuspec", `<?xml version="1.0" encoding="utf-8"?>
+		data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?>
 		<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
 		  <metadata></metadata>
-		</package>`)
+		</package>`})
 
 		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.Nil(t, np)
@@ -105,30 +108,34 @@ func TestParsePackageMetaData(t *testing.T) {
 	})
 
 	t.Run("InvalidPackageVersion", func(t *testing.T) {
-		data := createArchive("package.nuspec", `<?xml version="1.0" encoding="utf-8"?>
+		data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?>
 		<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
 		  <metadata>
-			<id>`+id+`</id>
+			<id>` + id + `</id>
 		  </metadata>
-		</package>`)
+		</package>`})
 
 		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.Nil(t, np)
 		assert.ErrorIs(t, err, ErrNuspecInvalidVersion)
 	})
 
-	t.Run("Valid", func(t *testing.T) {
-		data := createArchive("package.nuspec", nuspecContent)
+	t.Run("MissingReadme", func(t *testing.T) {
+		data := createArchive(map[string]string{"package.nuspec": nuspecContent})
 
 		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.NoError(t, err)
 		assert.NotNil(t, np)
+		assert.Empty(t, np.Metadata.Readme)
 	})
-}
 
-func TestParseNuspecMetaData(t *testing.T) {
 	t.Run("Dependency Package", func(t *testing.T) {
-		np, err := ParseNuspecMetaData(strings.NewReader(nuspecContent))
+		data := createArchive(map[string]string{
+			"package.nuspec": nuspecContent,
+			"README.md":      readme,
+		})
+
+		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.NoError(t, err)
 		assert.NotNil(t, np)
 		assert.Equal(t, DependencyPackage, np.PackageType)
@@ -139,6 +146,7 @@ func TestParseNuspecMetaData(t *testing.T) {
 		assert.Equal(t, projectURL, np.Metadata.ProjectURL)
 		assert.Equal(t, description, np.Metadata.Description)
 		assert.Equal(t, releaseNotes, np.Metadata.ReleaseNotes)
+		assert.Equal(t, readme, np.Metadata.Readme)
 		assert.Equal(t, repositoryURL, np.Metadata.RepositoryURL)
 		assert.Len(t, np.Metadata.Dependencies, 1)
 		assert.Contains(t, np.Metadata.Dependencies, targetFramework)
@@ -148,13 +156,15 @@ func TestParseNuspecMetaData(t *testing.T) {
 		assert.Equal(t, dependencyVersion, deps[0].Version)
 
 		t.Run("NormalizedVersion", func(t *testing.T) {
-			np, err := ParseNuspecMetaData(strings.NewReader(`<?xml version="1.0" encoding="utf-8"?>
-<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
-  <metadata>
-	<id>test</id>
-	<version>1.04.5.2.5-rc.1+metadata</version>
-  </metadata>
-</package>`))
+			data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?>
+				<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
+				  <metadata>
+					<id>test</id>
+					<version>1.04.5.2.5-rc.1+metadata</version>
+				  </metadata>
+				</package>`})
+
+			np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 			assert.NoError(t, err)
 			assert.NotNil(t, np)
 			assert.Equal(t, "1.4.5.2-rc.1", np.Version)
@@ -162,7 +172,9 @@ func TestParseNuspecMetaData(t *testing.T) {
 	})
 
 	t.Run("Symbols Package", func(t *testing.T) {
-		np, err := ParseNuspecMetaData(strings.NewReader(symbolsNuspecContent))
+		data := createArchive(map[string]string{"package.nuspec": symbolsNuspecContent})
+
+		np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
 		assert.NoError(t, err)
 		assert.NotNil(t, np)
 		assert.Equal(t, SymbolsPackage, np.PackageType)
diff --git a/templates/package/content/nuget.tmpl b/templates/package/content/nuget.tmpl
index 0911260fba..f1fe420c0b 100644
--- a/templates/package/content/nuget.tmpl
+++ b/templates/package/content/nuget.tmpl
@@ -16,12 +16,11 @@
 		</div>
 	</div>
 
-	{{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.ReleaseNotes}}
+	{{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.ReleaseNotes .PackageDescriptor.Metadata.Readme}}
 		<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.about"}}</h4>
-		<div class="ui attached segment">
-			{{if .PackageDescriptor.Metadata.Description}}{{.PackageDescriptor.Metadata.Description}}{{end}}
-			{{if .PackageDescriptor.Metadata.ReleaseNotes}}{{.PackageDescriptor.Metadata.ReleaseNotes}}{{end}}
-		</div>
+		{{if .PackageDescriptor.Metadata.Description}}<div class="ui attached segment">{{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.Description}}</div>{{end}}
+		{{if .PackageDescriptor.Metadata.Readme}}<div class="ui attached segment markup markdown">{{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.Readme}}</div>{{end}}
+		{{if .PackageDescriptor.Metadata.ReleaseNotes}}<div class="ui attached segment">{{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.ReleaseNotes}}</div>{{end}}
 	{{end}}
 
 	{{if .PackageDescriptor.Metadata.Dependencies}}