Repository: goreleaser/nfpm (go)

Issue:
Invalid RPM version strings when version contains multiple '-'
### What happened?

nfpm can produce RPMs with an invalid verison field.  This happens when the version contains multiple hyphens, e.g. `1.2.3-alpha-beta-gamma`. nfpm will identify `alpha-beta-gamma` as the prerelease and correctly join it with the version using a tilde, producing `1.2.3~alpha-beta-gamma`. However, this version is invalid because hyphens are not allowed in version strings according to the official [RPM documentation](https://rpm-software-management.github.io/rpm/manual/spec.html).

RPM _almost_ handles the version correctly, but since hyphens are used as delimiters for parsing a NEVRA, you hit odd behavior at times:
```bash
$ rpm --dbpath=/tmp/asdf -q ABC --queryformat '%{VERSION}\n'
1.2.3~alpha-beta-gamma
$ rpm --dbpath=/tmp/asdf -q ABC
ABC-1.2.3~alpha-beta-gamma-1.x86_64
$ rpm --dbpath=/tmp/asdf -q ABC-1.2.3~alpha-beta-gamma
package ABC-1.2.3~alpha-beta-gamma is not installed
```

I think nfpm should either
* Transform the pre-release by replacing all hyphens with some other character such as `_`, `.`, or `+`.
* Warn the user in some way, but not try to modify the pre-release. Some ideas:
  * Print a warning if the parsed pre-release contains invalid characters
  * Error out if the  parsed pre-release contains invalid characters
  * Put a note in the documentation somewhere about this pitfall

### How can we reproduce this?

nfpm.yaml
```yaml
name: ABC
version: 1.2.4-alpha-beta-gamma
```
`nfpm p -p rpm`

### nfpm version

```bash
Latest main (41b3349)

       _____ ____  __  __
 _ __ |  ___|  _ \|  \/  |
| '_ \| |_  | |_) | |\/| |
| | | |  _| |  __/| |  | |
|_| |_|_|   |_|   |_|  |_|
nfpm: a simple and 0-dependencies apk, arch linux, deb, ipk, and rpm packager written in Go
https://nfpm.goreleaser.com

GitVersion:    dev
GitCommit:     unknown
GitTreeState:  unknown
BuildDate:     unknown
BuiltBy:       unknown
GoVersion:     go1.22.3
Compiler:      gc
ModuleSum:     h1:RUmeEt8OlEVeSzKRrO5Vl5qVWCtUwx4j9uivGuRo5fw=
Platform:      linux/amd64
```


### Search

- [X] I did search for other open and closed issues before opening this.

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct

### Additional context

_No response_

Pull request description:
fix: rpm doesnt allow dashes in prerelease
closes #835

Patch:
diff --git a/files/files.go b/files/files.go
index 776949d..68fe6a3 100644
--- a/files/files.go
+++ b/files/files.go
@@ -490,6 +490,9 @@ func addTree(
 			c.Destination = NormalizeAbsoluteDirPath(destination)
 			c.FileInfo.Mode = info.Mode() &^ umask
 			c.FileInfo.MTime = info.ModTime()
+			if ownedByFilesystem(c.Destination) {
+				c.Type = TypeImplicitDir
+			}
 		case d.Type()&os.ModeSymlink != 0:
 			linkDestination, err := os.Readlink(path)
 			if err != nil {
diff --git a/files/fs.go b/files/fs.go
index 1e96647..580e99a 100644
--- a/files/fs.go
+++ b/files/fs.go
@@ -4,7 +4,7 @@ import "path/filepath"
 
 func ownedByFilesystem(path string) bool {
 	p := filepath.Clean(path)
-	for _, pp := range fsPaths {
+	for _, pp := range append(fsPaths, logrotatePaths...) {
 		if p == pp {
 			return true
 		}
@@ -12,7 +12,9 @@ func ownedByFilesystem(path string) bool {
 	return false
 }
 
-// from: repoquery --installed -l filesystem | while read -r f; do test -d $f && echo $f; done
+// yum install yum-utils
+
+// repoquery --installed -l filesystem | while read -r f; do test -d "\"$f\"," && echo $f; done
 var fsPaths = []string{
 	"/afs",
 	"/bin",
@@ -213,3 +215,12 @@ var fsPaths = []string{
 	"/var/tmp",
 	"/var/yp",
 }
+
+// repoquery --installed -l logrotate | while read -r f; do test -d "\"$f\"," && echo $f; done
+var logrotatePaths = []string{
+	"/etc/logrotate.d",
+	"/usr/lib/.build-id",
+	"/usr/lib/.build-id/ae",
+	"/usr/share/licenses/logrotate",
+	"/var/lib/logrotate",
+}
diff --git a/files/testdata/issue829/etc/logrotate.d/acme b/files/testdata/issue829/etc/logrotate.d/acme
new file mode 100644
index 0000000..e69de29
diff --git a/files/testdata/issue829/etc/sysconfig/acme b/files/testdata/issue829/etc/sysconfig/acme
new file mode 100644
index 0000000..e69de29
diff --git a/files/testdata/issue829/lib/systemd/system/acme.service b/files/testdata/issue829/lib/systemd/system/acme.service
new file mode 100644
index 0000000..e69de29
diff --git a/files/testdata/issue829/usr/share/doc/acme/readme.md b/files/testdata/issue829/usr/share/doc/acme/readme.md
new file mode 100644
index 0000000..e69de29
diff --git a/files/testdata/issue829/usr/share/man/man1/acme.1.gz b/files/testdata/issue829/usr/share/man/man1/acme.1.gz
new file mode 100644
index 0000000..e69de29
diff --git a/files/testdata/issue829/var/log/acme/.gitkeep b/files/testdata/issue829/var/log/acme/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/rpm/rpm.go b/rpm/rpm.go
index 5ec04d1..3e98b70 100644
--- a/rpm/rpm.go
+++ b/rpm/rpm.go
@@ -271,7 +271,7 @@ func formatVersion(info *nfpm.Info) string {
 	version := info.Version
 
 	if info.Prerelease != "" {
-		version += "~" + info.Prerelease
+		version += "~" + strings.ReplaceAll(info.Prerelease, "-", "_")
 	}
 
 	if info.VersionMetadata != "" {


Test patch:
diff --git a/files/files_test.go b/files/files_test.go
index 16a4182..6474687 100644
--- a/files/files_test.go
+++ b/files/files_test.go
@@ -974,3 +974,68 @@ func TestAsExplicitRelativePath(t *testing.T) {
 		assert.Equal(t, expected, files.AsExplicitRelativePath(input))
 	}
 }
+
+func TestIssue829(t *testing.T) {
+	var config testStruct
+	dec := yaml.NewDecoder(strings.NewReader(`---
+contents:
+  # Implementation.
+  - src: ./testdata/issue829/lib/
+    dst: /usr/lib/
+    type: tree
+  - src: ./testdata/issue829/usr/
+    dst: /usr/
+    type: tree
+  - src: ./testdata/issue829/var/
+    dst: /var/
+    type: tree
+
+  # Configuration.
+  - src: ./testdata/issue829/etc/logrotate.d/acme
+    dst: /etc/logrotate.d/acme
+    type: config|noreplace
+  - src: ./testdata/issue829/etc/sysconfig/acme
+    dst: /etc/sysconfig/acme
+    type: config|noreplace
+`))
+	dec.KnownFields(true)
+	require.NoError(t, dec.Decode(&config))
+	files, err := files.PrepareForPackager(
+		config.Contents,
+		0,
+		"",
+		false,
+		mtime,
+	)
+	require.NoError(t, err)
+
+	expect := map[string]string{
+		"/etc/":                                "implicit dir",
+		"/etc/logrotate.d/":                    "implicit dir",
+		"/etc/logrotate.d/acme":                "config|noreplace",
+		"/etc/sysconfig/":                      "implicit dir",
+		"/etc/sysconfig/acme":                  "config|noreplace",
+		"/usr/lib/":                            "implicit dir",
+		"/usr/lib/systemd/":                    "implicit dir",
+		"/usr/lib/systemd/system/":             "implicit dir",
+		"/usr/lib/systemd/system/acme.service": "file",
+		"/usr/":                                "implicit dir",
+		"/usr/bin/":                            "implicit dir",
+		"/usr/bin/acme":                        "file",
+		"/usr/share/":                          "implicit dir",
+		"/usr/share/doc/":                      "implicit dir",
+		"/usr/share/doc/acme/":                 "dir",
+		"/usr/share/doc/acme/readme.md":        "file",
+		"/usr/share/man/":                      "implicit dir",
+		"/usr/share/man/man1/":                 "implicit dir",
+		"/usr/share/man/man1/acme.1.gz":        "file",
+		"/var/":                                "implicit dir",
+		"/var/log/":                            "implicit dir",
+		"/var/log/acme/":                       "dir",
+		"/var/log/acme/.gitkeep":               "file",
+	}
+
+	for _, file := range files {
+		require.Equal(t, expect[file.Destination], file.Type, "invalid type for %s", file.Destination)
+	}
+}
diff --git a/rpm/rpm_test.go b/rpm/rpm_test.go
index 62ab016..7569871 100644
--- a/rpm/rpm_test.go
+++ b/rpm/rpm_test.go
@@ -420,6 +420,16 @@ func TestRPMVersionWithPrerelease(t *testing.T) {
 	require.Equal(t, "1", meta.Release)
 }
 
+func TestRPMVersionWithPrereleaseWithDashes(t *testing.T) {
+	info := exampleInfo()
+	info.Version = "1.0.0"
+	info.Prerelease = "rc1-alpha-omega" // nolint:goconst
+	meta, err := buildRPMMeta(info)
+	require.NoError(t, err)
+	require.Equal(t, "1.0.0~rc1_alpha_omega", meta.Version)
+	require.Equal(t, "1", meta.Release)
+}
+
 func TestRPMVersionWithReleaseAndPrerelease(t *testing.T) {
 	// https://fedoraproject.org/wiki/Package_Versioning_Examples#Complex_versioning_examples
 	info := exampleInfo()