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 +}