From 61d5608f1165777f1c0e6c81bc72da2bc7efc71c Mon Sep 17 00:00:00 2001 From: Roy Teeuwen Date: Sat, 21 Mar 2026 22:14:01 +0100 Subject: [PATCH] solves #337: Add an offline mode to be able to read the symbolic name of a jar file without accessing the instance --- cmd/aem/osgi.go | 31 ++++++++++++++++- pkg/osgi/manifest.go | 10 ++++-- pkg/osgi/manifest_test.go | 70 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 pkg/osgi/manifest_test.go diff --git a/cmd/aem/osgi.go b/cmd/aem/osgi.go index 25fe37d0..d59d394e 100644 --- a/cmd/aem/osgi.go +++ b/cmd/aem/osgi.go @@ -8,6 +8,7 @@ import ( "github.com/wttech/aemc/pkg/common/httpx" "github.com/wttech/aemc/pkg/common/mapsx" "github.com/wttech/aemc/pkg/common/pathx" + "github.com/wttech/aemc/pkg/osgi" "strings" ) @@ -204,6 +205,17 @@ func (c *CLI) osgiBundleReadCmd() *cobra.Command { Short: "Read OSGi bundle details", Aliases: []string{"get", "find"}, Run: func(cmd *cobra.Command, args []string) { + offline, _ := cmd.Flags().GetBool("offline") + if offline { + manifest, err := osgiBundleManifestByFlags(cmd) + if err != nil { + c.Error(err) + return + } + c.SetOutput("bundle", manifest) + c.Ok("bundle read") + return + } instance, err := c.aem.InstanceManager().One() if err != nil { c.Error(err) @@ -219,6 +231,7 @@ func (c *CLI) osgiBundleReadCmd() *cobra.Command { }, } osgiBundleDefineFlags(cmd) + cmd.Flags().Bool("offline", false, "Read bundle manifest from local file without connecting to AEM instance") return cmd } @@ -369,6 +382,18 @@ func osgiBundleDefineFlags(cmd *cobra.Command) { cmd.MarkFlagsMutuallyExclusive("symbolic-name", "file") } +func osgiBundleManifestByFlags(cmd *cobra.Command) (*osgi.BundleManifest, error) { + file, _ := cmd.Flags().GetString("file") + if len(file) == 0 { + return nil, fmt.Errorf("flag 'file' is required when using 'offline' mode") + } + fileGlobbed, err := pathx.GlobSome(file) + if err != nil { + return nil, err + } + return osgi.ReadBundleManifest(fileGlobbed) +} + func osgiBundleByFlags(cmd *cobra.Command, i pkg.Instance) (*pkg.OSGiBundle, error) { symbolicName, _ := cmd.Flags().GetString("symbolic-name") if len(symbolicName) > 0 { @@ -377,7 +402,11 @@ func osgiBundleByFlags(cmd *cobra.Command, i pkg.Instance) (*pkg.OSGiBundle, err } file, _ := cmd.Flags().GetString("file") if len(file) > 0 { - bundle, err := i.OSGI().BundleManager().ByFile(file) + fileGlobbed, err := pathx.GlobSome(file) + if err != nil { + return nil, err + } + bundle, err := i.OSGI().BundleManager().ByFile(fileGlobbed) return bundle, err } return nil, fmt.Errorf("flag 'symbolic-name' or 'file' are required") diff --git a/pkg/osgi/manifest.go b/pkg/osgi/manifest.go index 8d5a778a..6ca532ca 100644 --- a/pkg/osgi/manifest.go +++ b/pkg/osgi/manifest.go @@ -8,14 +8,18 @@ import ( func ReadBundleManifest(localPath string) (*BundleManifest, error) { manifest, err := jar.ReadFile(localPath) if err != nil { - return nil, fmt.Errorf("cannot read OSGi bundle manifest from file '%s'", localPath) + return nil, fmt.Errorf("cannot read OSGi bundle manifest from file '%s': %w", localPath, err) } return &BundleManifest{SymbolicName: manifest[AttributeSymbolicName], Version: manifest[AttributeVersion]}, nil } type BundleManifest struct { - SymbolicName string - Version string + SymbolicName string `yaml:"symbolic_name" json:"symbolicName"` + Version string `yaml:"version" json:"version"` +} + +func (m BundleManifest) MarshalText() string { + return fmt.Sprintf("symbolic name '%s'\nversion '%s'\n", m.SymbolicName, m.Version) } const ( diff --git a/pkg/osgi/manifest_test.go b/pkg/osgi/manifest_test.go new file mode 100644 index 00000000..f4c72d7d --- /dev/null +++ b/pkg/osgi/manifest_test.go @@ -0,0 +1,70 @@ +package osgi + +import ( + "archive/zip" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func createTestJAR(t *testing.T, manifest string) string { + t.Helper() + path := filepath.Join(t.TempDir(), "test-bundle.jar") + f, err := os.Create(path) + assert.NoError(t, err) + defer f.Close() + w := zip.NewWriter(f) + defer w.Close() + entry, err := w.Create("META-INF/MANIFEST.MF") + assert.NoError(t, err) + _, err = entry.Write([]byte(manifest)) + assert.NoError(t, err) + return path +} + +func TestReadBundleManifest(t *testing.T) { + t.Parallel() + + jar := createTestJAR(t, "Manifest-Version: 1.0\r\nBundle-SymbolicName: com.example.test\r\nBundle-Version: 1.2.3\r\n") + + manifest, err := ReadBundleManifest(jar) + + assert.NoError(t, err) + assert.Equal(t, "com.example.test", manifest.SymbolicName) + assert.Equal(t, "1.2.3", manifest.Version) +} + +func TestReadBundleManifestMissingFile(t *testing.T) { + t.Parallel() + + _, err := ReadBundleManifest("/nonexistent/bundle.jar") + + assert.Error(t, err) + assert.Contains(t, err.Error(), "cannot read OSGi bundle manifest from file") +} + +func TestReadBundleManifestInvalidJAR(t *testing.T) { + t.Parallel() + + path := filepath.Join(t.TempDir(), "not-a-jar.jar") + err := os.WriteFile(path, []byte("not a jar file"), 0644) + assert.NoError(t, err) + + _, err = ReadBundleManifest(path) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "cannot read OSGi bundle manifest from file") +} + +func TestBundleManifestMarshalText(t *testing.T) { + t.Parallel() + + manifest := BundleManifest{SymbolicName: "com.example.test", Version: "1.0.0"} + + text := manifest.MarshalText() + + assert.Contains(t, text, "com.example.test") + assert.Contains(t, text, "1.0.0") +}