Resolves #48448. These should help with reviewing the XAR and BOM implementations: - https://claude.ai/code/artifact/60a78c1d-2fc9-45da-9471-1517fe77adb4. - https://claude.ai/code/artifact/1c759a32-02f7-4a41-8611-04d7358367d7. The darwin only tests (bom_darwin_test.go) have been executed on my workstation. Goal is to make sure to run the tests on macOS Github runners in https://github.com/fleetdm/fleet/issues/33371. - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] QA'd all new/changed functionality manually ## fleetd/orbit/Fleet Desktop - [x] Verified compatibility with the latest released version of Fleet (see [Must rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)) - [x] Verified auto-update works from the released version of component to the new version (see [tools/tuf/test](../tools/tuf/test/README.md)) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * macOS package builds now use an internal, built-in implementation instead of external packaging tools. * `.pkg` installer creation no longer depends on Docker for macOS packaging. * **Bug Fixes** * Improved packaging reliability by reducing platform-specific build steps. * Packaging test coverage was streamlined to better match the supported build environment. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
131 lines
4.9 KiB
Go
131 lines
4.9 KiB
Go
package packaging
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/fleetdm/fleet/v4/pkg/file"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// These tests exercise the pure-Go xar writer (writeXar) by feeding its output
|
|
// to Fleet's own xar decoder in pkg/file. A xar that round-trips through the
|
|
// decoder proves the encoder produces a well-formed header, a valid zlib TOC,
|
|
// and correct heap offsets/lengths (the decoder reads members back by those
|
|
// offsets). The writer is platform-independent, so these run everywhere.
|
|
|
|
const testDistribution = `<?xml version="1.0" encoding="utf-8"?>
|
|
<installer-gui-script minSpecVersion="2">
|
|
<title>Fleet osquery</title>
|
|
<product id="com.fleetdm.orbit" version="1.2.3"/>
|
|
<pkg-ref id="com.fleetdm.orbit.base.pkg" version="1.2.3" packageIdentifier="com.fleetdm.orbit"/>
|
|
</installer-gui-script>
|
|
`
|
|
|
|
const testPackageInfo = `<pkg-info format-version="2" identifier="com.fleetdm.orbit" version="9.9.9" install-location="/" auth="root"/>
|
|
`
|
|
|
|
// writeXarTree writes the given name->contents map (names may contain "/" to
|
|
// create nested directories) into a fresh temp dir and returns its path.
|
|
func writeXarTree(t *testing.T, files map[string][]byte) string {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
for name, data := range files {
|
|
p := filepath.Join(root, name)
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(p), 0o755))
|
|
require.NoError(t, os.WriteFile(p, data, 0o644))
|
|
}
|
|
return root
|
|
}
|
|
|
|
// buildXar runs writeXar over root and returns the archive bytes.
|
|
func buildXar(t *testing.T, root string) []byte {
|
|
t.Helper()
|
|
out := filepath.Join(t.TempDir(), "out.pkg")
|
|
require.NoError(t, writeXar(root, out))
|
|
b, err := os.ReadFile(out)
|
|
require.NoError(t, err)
|
|
return b
|
|
}
|
|
|
|
func extractXARMetadata(t *testing.T, xarBytes []byte) *file.InstallerMetadata {
|
|
t.Helper()
|
|
tfr, err := fleet.NewTempFileReader(bytes.NewReader(xarBytes), t.TempDir)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = tfr.Close() })
|
|
meta, err := file.ExtractXARMetadata(tfr)
|
|
require.NoError(t, err)
|
|
return meta
|
|
}
|
|
|
|
// TestWriteXarReadableByDecoder builds a distribution-style .pkg tree and
|
|
// verifies the pure-Go writer's output is a valid xar: parseable header + TOC,
|
|
// a discoverable Distribution member, and metadata read back correctly from the
|
|
// heap.
|
|
func TestWriteXarReadableByDecoder(t *testing.T) {
|
|
root := writeXarTree(t, map[string][]byte{
|
|
"Distribution": []byte(testDistribution),
|
|
"base.pkg/PackageInfo": []byte(testPackageInfo),
|
|
"base.pkg/Payload": bytes.Repeat([]byte("orbit-payload-bytes\n"), 1000),
|
|
})
|
|
xarBytes := buildXar(t, root)
|
|
|
|
// Valid, unsigned xar: exercises magic-byte check, SHA-1 hash-type mapping,
|
|
// zlib TOC decompression, and TOC XML parsing.
|
|
require.ErrorIs(t, file.CheckPKGSignature(bytes.NewReader(xarBytes)), file.ErrNotSigned)
|
|
|
|
// The TOC lists the top-level Distribution file.
|
|
hasDist, err := file.XARHasDistribution(bytes.NewReader(xarBytes))
|
|
require.NoError(t, err)
|
|
require.True(t, hasDist)
|
|
|
|
// Reading the Distribution member back (via its <offset>/<length> within the
|
|
// heap that begins with the 20-byte TOC checksum) yields exactly the bytes we
|
|
// wrote; if any offset/length were off the XML parse would fail.
|
|
meta := extractXARMetadata(t, xarBytes)
|
|
require.Equal(t, "Fleet osquery", meta.Name)
|
|
require.Equal(t, "1.2.3", meta.Version)
|
|
require.Equal(t, "com.fleetdm.orbit", meta.BundleIdentifier)
|
|
require.Contains(t, meta.PackageIDs, "com.fleetdm.orbit")
|
|
}
|
|
|
|
// TestWriteXarPackageInfoFallback verifies a component-style .pkg (top-level
|
|
// PackageInfo, no Distribution) also round-trips: the decoder falls back to
|
|
// PackageInfo, which again requires the writer's heap offsets to be correct.
|
|
func TestWriteXarPackageInfoFallback(t *testing.T) {
|
|
root := writeXarTree(t, map[string][]byte{
|
|
"PackageInfo": []byte(testPackageInfo),
|
|
})
|
|
xarBytes := buildXar(t, root)
|
|
|
|
hasDist, err := file.XARHasDistribution(bytes.NewReader(xarBytes))
|
|
require.NoError(t, err)
|
|
require.False(t, hasDist)
|
|
|
|
meta := extractXARMetadata(t, xarBytes)
|
|
require.Equal(t, "9.9.9", meta.Version)
|
|
require.Equal(t, "com.fleetdm.orbit", meta.BundleIdentifier)
|
|
}
|
|
|
|
// TestWriteXarEmptyAndNestedDirs ensures the writer handles empty files and
|
|
// nested directories without corrupting the archive (the decoder still parses
|
|
// the header/TOC and finds the Distribution).
|
|
func TestWriteXarEmptyAndNestedDirs(t *testing.T) {
|
|
root := writeXarTree(t, map[string][]byte{
|
|
"Distribution": []byte(testDistribution),
|
|
"base.pkg/empty": {},
|
|
"base.pkg/nested/deep/Info.plist": []byte("<plist/>"),
|
|
})
|
|
// An empty directory too (map above only creates dirs with files).
|
|
require.NoError(t, os.MkdirAll(filepath.Join(root, "Resources"), 0o755))
|
|
|
|
xarBytes := buildXar(t, root)
|
|
require.ErrorIs(t, file.CheckPKGSignature(bytes.NewReader(xarBytes)), file.ErrNotSigned)
|
|
|
|
meta := extractXARMetadata(t, xarBytes)
|
|
require.Equal(t, "Fleet osquery", meta.Name)
|
|
}
|