diff --git a/pkg/sysinfo/sysinfo_darwin.go b/pkg/sysinfo/sysinfo_darwin.go
index b89cd031..e8182f89 100644
--- a/pkg/sysinfo/sysinfo_darwin.go
+++ b/pkg/sysinfo/sysinfo_darwin.go
@@ -2,13 +2,75 @@ package sysinfo
// SPDX-License-Identifier: GPL-3.0-or-later
-// canSymlink always returns true, as symlinking on non-Windows platforms is not
-// hard.
+import (
+ "encoding/xml"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+)
+
+type PlistData struct {
+ XMLName xml.Name `xml:"plist"`
+ Dict Dict `xml:"dict"`
+}
+
+type Dict struct {
+ Keys []string `xml:"key"`
+ Strings []string `xml:"string"`
+}
+
+// canSymlink always returns true, as symlinking on non-Windows platforms is not hard.
func canSymlink() (bool, error) {
return true, nil
}
func description() (string, error) {
- // TODO: figure out how to get more info on macOS.
- return "macOS", nil
+ plistFile := "/System/Library/CoreServices/SystemVersion.plist"
+ info, err := getSystemInfo(plistFile)
+ if err != nil {
+ log.Warn().Err(err).Msg("Could not retrieve system information")
+ return "macOS", nil
+ }
+ return info, nil
+}
+
+func getSystemInfo(plistFile string) (string, error) {
+ data, err := os.ReadFile(plistFile)
+ if err != nil {
+ return "", fmt.Errorf("could not read system info file %s: %w", plistFile, err)
+ }
+
+ var plist PlistData
+ if err := xml.Unmarshal(data, &plist); err != nil {
+ return "", fmt.Errorf("failed to read system info from %s: %w", plistFile, err)
+ }
+
+ productName := "macOS"
+ var productVersion, buildVersion string
+
+ for i, key := range plist.Dict.Keys {
+ if i >= len(plist.Dict.Strings) {
+ break
+ }
+ switch key {
+ case "ProductName":
+ productName = plist.Dict.Strings[i]
+ case "ProductVersion":
+ productVersion = plist.Dict.Strings[i]
+ case "ProductBuildVersion":
+ buildVersion = plist.Dict.Strings[i]
+ }
+ }
+
+ parts := []string{productName}
+ if productVersion != "" {
+ parts = append(parts, productVersion)
+ }
+ if buildVersion != "" {
+ parts = append(parts, fmt.Sprintf("(Build %s)", buildVersion))
+ }
+
+ return strings.Join(parts, " "), nil
}
diff --git a/pkg/sysinfo/sysinfo_darwin_test.go b/pkg/sysinfo/sysinfo_darwin_test.go
new file mode 100644
index 00000000..43f34035
--- /dev/null
+++ b/pkg/sysinfo/sysinfo_darwin_test.go
@@ -0,0 +1,121 @@
+package sysinfo
+
+import (
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetSystemInfo_ValidPlist(t *testing.T) {
+ plistContent := `
+
+
+ ProductName
+ macOS
+ ProductVersion
+ 15.3.1
+ ProductBuildVersion
+ 24D70
+
+ `
+
+ tempFile, cleanup := createTempPlist(t, plistContent)
+ defer cleanup()
+
+ expected := "macOS 15.3.1 (Build 24D70)"
+ result, err := getSystemInfo(tempFile)
+
+ assert.NoError(t, err)
+ assert.Equal(t, expected, result)
+}
+
+func TestGetSystemInfo_NoProductName(t *testing.T) {
+ plistContent := `
+
+
+ ProductVersion
+ 15.3.1
+ ProductBuildVersion
+ 24D70
+
+ `
+
+ tempFile, cleanup := createTempPlist(t, plistContent)
+ defer cleanup()
+
+ expected := "macOS 15.3.1 (Build 24D70)"
+ result, err := getSystemInfo(tempFile)
+
+ assert.NoError(t, err)
+ assert.Equal(t, expected, result)
+}
+
+func TestGetSystemInfo_OnlyProductName(t *testing.T) {
+ plistContent := `
+
+
+ ProductName
+ macOS Custom
+
+ `
+
+ tempFile, cleanup := createTempPlist(t, plistContent)
+ defer cleanup()
+
+ expected := "macOS Custom"
+ result, err := getSystemInfo(tempFile)
+
+ assert.NoError(t, err)
+ assert.Equal(t, expected, result)
+}
+
+func TestGetSystemInfo_EmptyDict(t *testing.T) {
+ plistContent := `
+
+
+ `
+
+ tempFile, cleanup := createTempPlist(t, plistContent)
+ defer cleanup()
+
+ expected := "macOS"
+ result, err := getSystemInfo(tempFile)
+
+ assert.NoError(t, err)
+ assert.Equal(t, expected, result)
+}
+
+func TestGetSystemInfo_InvalidXML(t *testing.T) {
+ plistContent := `INVALID_XML_DATA`
+
+ tempFile, cleanup := createTempPlist(t, plistContent)
+ defer cleanup()
+
+ _, err := getSystemInfo(tempFile)
+
+ assert.Error(t, err)
+}
+
+func TestGetSystemInfo_FileNotFound(t *testing.T) {
+ _, err := getSystemInfo("/path/to/nonexistent.plist")
+
+ assert.Error(t, err)
+}
+
+func createTempPlist(t *testing.T, content string) (string, func()) {
+ tempFile, err := os.CreateTemp("", "test_plist_*.plist")
+ assert.NoError(t, err)
+
+ _, err = tempFile.WriteString(content)
+ assert.NoError(t, err)
+
+ err = tempFile.Close()
+ assert.NoError(t, err)
+
+ cleanup := func() {
+ os.Remove(tempFile.Name())
+ }
+
+ return tempFile.Name(), cleanup
+}