44package packages
55
66import (
7+ "bufio"
78 "fmt"
89 "io"
910 "os"
10- "regexp"
11+ "path"
12+ "sort"
13+ "strconv"
14+ "strings"
1115
1216 "github.com/rs/zerolog/log"
1317 "github.com/spf13/afero"
@@ -26,6 +30,12 @@ type SnapPkgManager struct {
2630 platform * inventory.Platform
2731}
2832
33+ type snapListEntry struct {
34+ name string
35+ version string
36+ rev string
37+ }
38+
2939func (spm * SnapPkgManager ) Name () string {
3040 return "Snap Package Manager"
3141}
@@ -35,50 +45,195 @@ func (spm *SnapPkgManager) Format() string {
3545}
3646
3747func (spm * SnapPkgManager ) List () ([]Package , error ) {
38- fs := spm .conn .FileSystem ()
39- snapDir := "/snap"
40- afs := & afero. Afero { Fs : fs }
41- _ , dErr := afs . Stat ( snapDir )
42- if dErr != nil {
43- log . Debug (). Str ( "path" , snapDir ). Msg ( "cannot find snap dir" )
44- return [] Package {}, nil
48+ if spm .conn .Capabilities (). Has ( shared . Capability_RunCommand ) {
49+ packages , err := spm . listFromCLI ()
50+ if err == nil {
51+ return packages , nil
52+ }
53+
54+ log . Debug (). Err ( err ). Msg ( "mql[snap]> could not enumerate snaps via cli, falling back to filesystem" )
4555 }
4656
47- // e.g. /snap/firefox/6103/meta/snap.yaml
48- // https://snapcraft.io/docs/the-snap-format#p-3326-setup-files
49- snapRegEx := regexp .MustCompile (`/snap/[^/]+/\d+/meta/snap\.yaml` )
50- files := []string {}
51- err := afs .Walk (snapDir , func (path string , info os.FileInfo , err error ) error {
52- if info == nil || info .IsDir () {
53- return nil
54- }
55- if ! snapRegEx .MatchString (path ) {
56- return nil
57+ return spm .listFromFS ()
58+ }
59+
60+ func (spm * SnapPkgManager ) listFromCLI () ([]Package , error ) {
61+ cmdResult , err := spm .conn .RunCommand ("snap list" )
62+ if err != nil {
63+ return nil , err
64+ }
65+
66+ if cmdResult .ExitStatus != 0 {
67+ stderr := "unknown error"
68+ if cmdResult .Stderr != nil {
69+ stderrBytes , readErr := io .ReadAll (cmdResult .Stderr )
70+ if readErr == nil {
71+ stderr = strings .TrimSpace (string (stderrBytes ))
72+ if stderr == "" {
73+ stderr = "unknown error"
74+ }
75+ }
5776 }
5877
59- files = append (files , path )
60- return nil
61- })
78+ return nil , fmt .Errorf ("snap list failed: %s" , stderr )
79+ }
80+
81+ if cmdResult .Stdout == nil {
82+ return []Package {}, nil
83+ }
84+
85+ entries , err := parseSnapListOutput (cmdResult .Stdout )
6286 if err != nil {
6387 return nil , err
6488 }
6589
66- pkgList := []Package {}
67- for _ , file := range files {
68- manifest , err := afs .Open (file )
90+ afs := & afero.Afero {Fs : spm .conn .FileSystem ()}
91+ pkgList := make ([]Package , 0 , len (entries ))
92+
93+ for _ , entry := range entries {
94+ manifestPath := path .Join ("/snap" , entry .name , entry .rev , "meta" , "snap.yaml" )
95+ manifest , err := afs .Open (manifestPath )
6996 if err != nil {
70- log .Error ().Err (err ).Str ("file " , file ).Msg ("could not open manifest file " )
97+ log .Debug ().Err (err ).Str ("path " , manifestPath ).Msg ("mql[snap]> could not open snap manifest from cli revision " )
7198 continue
7299 }
100+
73101 pkg , err := spm .parseSnapManifest (manifest )
102+ manifest .Close ()
74103 if err != nil {
75- log .Error ().Err (err ).Str ("file" , file ).Msg ("could not parse manifest file" )
76- manifest .Close ()
104+ log .Debug ().Err (err ).Str ("path" , manifestPath ).Msg ("mql[snap]> could not parse snap manifest from cli revision" )
77105 continue
78106 }
107+
79108 pkgList = append (pkgList , pkg )
80- manifest .Close ()
81109 }
110+
111+ return pkgList , nil
112+ }
113+
114+ func parseSnapListOutput (input io.Reader ) ([]snapListEntry , error ) {
115+ scanner := bufio .NewScanner (input )
116+ entries := []snapListEntry {}
117+ firstNonEmptyLine := true
118+
119+ for scanner .Scan () {
120+ line := strings .TrimSpace (scanner .Text ())
121+ if line == "" {
122+ continue
123+ }
124+
125+ fields := strings .Fields (line )
126+ if len (fields ) == 0 {
127+ continue
128+ }
129+
130+ if firstNonEmptyLine {
131+ firstNonEmptyLine = false
132+ if len (fields ) >= 3 && fields [0 ] == "Name" && fields [2 ] == "Rev" {
133+ continue
134+ }
135+ }
136+
137+ if len (fields ) < 3 {
138+ continue
139+ }
140+
141+ entries = append (entries , snapListEntry {
142+ name : fields [0 ],
143+ version : fields [1 ],
144+ rev : fields [2 ],
145+ })
146+ }
147+
148+ if err := scanner .Err (); err != nil {
149+ return nil , err
150+ }
151+
152+ return entries , nil
153+ }
154+
155+ func (spm * SnapPkgManager ) listFromFS () ([]Package , error ) {
156+ afs := & afero.Afero {Fs : spm .conn .FileSystem ()}
157+ const snapDir = "/snap"
158+
159+ dirEntries , err := afs .ReadDir (snapDir )
160+ if err != nil {
161+ if os .IsNotExist (err ) {
162+ log .Debug ().Str ("path" , snapDir ).Msg ("cannot find snap dir" )
163+ return []Package {}, nil
164+ }
165+
166+ return nil , err
167+ }
168+
169+ pkgList := []Package {}
170+ for _ , entry := range dirEntries {
171+ name := entry .Name ()
172+ currentManifestPath := path .Join (snapDir , name , "current" , "meta" , "snap.yaml" )
173+ manifest , err := afs .Open (currentManifestPath )
174+ if err == nil {
175+ pkg , err := spm .parseSnapManifest (manifest )
176+ manifest .Close ()
177+ if err != nil {
178+ log .Debug ().Err (err ).Str ("path" , currentManifestPath ).Msg ("mql[snap]> could not parse current snap manifest" )
179+ continue
180+ }
181+
182+ pkgList = append (pkgList , pkg )
183+ continue
184+ }
185+
186+ if ! os .IsNotExist (err ) {
187+ log .Debug ().Err (err ).Str ("path" , currentManifestPath ).Msg ("mql[snap]> could not open current snap manifest" )
188+ continue
189+ }
190+
191+ revisionDir := path .Join (snapDir , name )
192+ revisionEntries , err := afs .ReadDir (revisionDir )
193+ if err != nil {
194+ if ! os .IsNotExist (err ) {
195+ log .Debug ().Err (err ).Str ("path" , revisionDir ).Msg ("mql[snap]> could not inspect snap revisions" )
196+ }
197+ continue
198+ }
199+
200+ revisions := make ([]int , 0 , len (revisionEntries ))
201+ for _ , revisionEntry := range revisionEntries {
202+ if ! revisionEntry .IsDir () {
203+ continue
204+ }
205+
206+ revision , err := strconv .Atoi (revisionEntry .Name ())
207+ if err != nil {
208+ continue
209+ }
210+
211+ revisions = append (revisions , revision )
212+ }
213+
214+ sort .Sort (sort .Reverse (sort .IntSlice (revisions )))
215+ for _ , revision := range revisions {
216+ manifestPath := path .Join (snapDir , name , strconv .Itoa (revision ), "meta" , "snap.yaml" )
217+ manifest , err := afs .Open (manifestPath )
218+ if err != nil {
219+ if ! os .IsNotExist (err ) {
220+ log .Debug ().Err (err ).Str ("path" , manifestPath ).Msg ("mql[snap]> could not open snap manifest from revision fallback" )
221+ }
222+ continue
223+ }
224+
225+ pkg , err := spm .parseSnapManifest (manifest )
226+ manifest .Close ()
227+ if err != nil {
228+ log .Debug ().Err (err ).Str ("path" , manifestPath ).Msg ("mql[snap]> could not parse snap manifest from revision fallback" )
229+ continue
230+ }
231+
232+ pkgList = append (pkgList , pkg )
233+ break
234+ }
235+ }
236+
82237 return pkgList , nil
83238}
84239
0 commit comments