Переглянути джерело

Refact pkg/cwhub (part 3) (#2516)

* removed unused error; comment
* rename loop variables
* happy path
* rename loop variables
* extract function, method
* log.Printf -> log.Infof
* tests -> testdata

from "go help test":

The go tool will ignore a directory named "testdata", making it available
to hold ancillary data needed by the tests.

* align tags
* extract function toEmoji
mmetc 1 рік тому
батько
коміт
89028f17cf

+ 50 - 39
pkg/cwhub/cwhub.go

@@ -47,24 +47,24 @@ type ItemHubStatus struct {
 // Item can be: parsed, scenario, collection
 // Item can be: parsed, scenario, collection
 type Item struct {
 type Item struct {
 	// descriptive info
 	// descriptive info
-	Type                 string   `yaml:"type,omitempty" json:"type,omitempty"`                                     // parser|postoverflows|scenario|collection(|enrich)
-	Stage                string   `json:"stage,omitempty" yaml:"stage,omitempty,omitempty"`                         // Stage for parser|postoverflow: s00-raw/s01-...
+	Type                 string   `json:"type,omitempty"                   yaml:"type,omitempty"`                   // parser|postoverflows|scenario|collection(|enrich)
+	Stage                string   `json:"stage,omitempty"                  yaml:"stage,omitempty"`                  // Stage for parser|postoverflow: s00-raw/s01-...
 	Name                 string   `json:"name,omitempty"`                                                           // as seen in .config.json, usually "author/name"
 	Name                 string   `json:"name,omitempty"`                                                           // as seen in .config.json, usually "author/name"
 	FileName             string   `json:"file_name,omitempty"`                                                      // the filename, ie. apache2-logs.yaml
 	FileName             string   `json:"file_name,omitempty"`                                                      // the filename, ie. apache2-logs.yaml
-	Description          string   `yaml:"description,omitempty" json:"description,omitempty"`                       // as seen in .config.json
+	Description          string   `json:"description,omitempty"            yaml:"description,omitempty"`            // as seen in .config.json
 	Author               string   `json:"author,omitempty"`                                                         // as seen in .config.json
 	Author               string   `json:"author,omitempty"`                                                         // as seen in .config.json
-	References           []string `yaml:"references,omitempty" json:"references,omitempty"`                         // as seen in .config.json
-	BelongsToCollections []string `yaml:"belongs_to_collections,omitempty" json:"belongs_to_collections,omitempty"` // if it's part of collections, track name here
-
-	// remote (hub) infos
-	RemoteURL  string                 `yaml:"remoteURL,omitempty" json:"remoteURL,omitempty"` // the full remote uri of file in http
-	RemotePath string                 `json:"path,omitempty" yaml:"remote_path,omitempty"`    // the path relative to git ie. /parsers/stage/author/file.yaml
-	RemoteHash string                 `yaml:"hash,omitempty" json:"hash,omitempty"`           // the meow
-	Version    string                 `json:"version,omitempty"`                              // the last version
-	Versions   map[string]ItemVersion `json:"versions,omitempty" yaml:"-"`                    // the list of existing versions
-
-	// local (deployed) infos
-	LocalPath string `yaml:"local_path,omitempty" json:"local_path,omitempty"` // the local path relative to ${CFG_DIR}
+	References           []string `json:"references,omitempty"             yaml:"references,omitempty"`             // as seen in .config.json
+	BelongsToCollections []string `json:"belongs_to_collections,omitempty" yaml:"belongs_to_collections,omitempty"` // if it's part of collections, track name here
+
+	// remote (hub) info
+	RemoteURL  string                 `json:"remoteURL,omitempty" yaml:"remoteURL,omitempty"`   // the full remote uri of file in http
+	RemotePath string                 `json:"path,omitempty"      yaml:"remote_path,omitempty"` // the path relative to git ie. /parsers/stage/author/file.yaml
+	RemoteHash string                 `json:"hash,omitempty"      yaml:"hash,omitempty"`        // the meow
+	Version    string                 `json:"version,omitempty"`                                // the last version
+	Versions   map[string]ItemVersion `json:"versions,omitempty"  yaml:"-"`                     // the list of existing versions
+
+	// local (deployed) info
+	LocalPath string `json:"local_path,omitempty" yaml:"local_path,omitempty"` // the local path relative to ${CFG_DIR}
 	// LocalHubPath string
 	// LocalHubPath string
 	LocalVersion string `json:"local_version,omitempty"`
 	LocalVersion string `json:"local_version,omitempty"`
 	LocalHash    string `json:"local_hash,omitempty"` // the local meow
 	LocalHash    string `json:"local_hash,omitempty"` // the local meow
@@ -75,41 +75,52 @@ type Item struct {
 	Local        bool   `json:"local,omitempty"`   // if it's a non versioned control one
 	Local        bool   `json:"local,omitempty"`   // if it's a non versioned control one
 
 
 	// if it's a collection, it not a single file
 	// if it's a collection, it not a single file
-	Parsers       []string `yaml:"parsers,omitempty" json:"parsers,omitempty"`
-	PostOverflows []string `yaml:"postoverflows,omitempty" json:"postoverflows,omitempty"`
-	Scenarios     []string `yaml:"scenarios,omitempty" json:"scenarios,omitempty"`
-	Collections   []string `yaml:"collections,omitempty" json:"collections,omitempty"`
+	Parsers       []string `json:"parsers,omitempty"       yaml:"parsers,omitempty"`
+	PostOverflows []string `json:"postoverflows,omitempty" yaml:"postoverflows,omitempty"`
+	Scenarios     []string `json:"scenarios,omitempty"     yaml:"scenarios,omitempty"`
+	Collections   []string `json:"collections,omitempty"   yaml:"collections,omitempty"`
 }
 }
 
 
-func (i *Item) toHubStatus() ItemHubStatus {
-	hubStatus := ItemHubStatus{}
-	hubStatus.Name = i.Name
-	hubStatus.LocalVersion = i.LocalVersion
-	hubStatus.LocalPath = i.LocalPath
-	hubStatus.Description = i.Description
+func toEmoji(managed bool, installed bool, warning bool, ok bool) emoji.Emoji {
+	if !managed {
+		return emoji.House
+	}
 
 
-	status, ok, warning, managed := ItemStatus(*i)
-	hubStatus.Status = status
+	if !installed {
+		return emoji.Prohibited
+	}
 
 
-	if !managed {
-		hubStatus.UTF8_Status = fmt.Sprintf("%v  %s", emoji.House, status)
-	} else if !i.Installed {
-		hubStatus.UTF8_Status = fmt.Sprintf("%v  %s", emoji.Prohibited, status)
-	} else if warning {
-		hubStatus.UTF8_Status = fmt.Sprintf("%v  %s", emoji.Warning, status)
-	} else if ok {
-		hubStatus.UTF8_Status = fmt.Sprintf("%v  %s", emoji.CheckMark, status)
+	if warning {
+		return emoji.Warning
+	}
+
+	if ok {
+		return emoji.CheckMark
 	}
 	}
 
 
-	return hubStatus
+	// XXX: this is new
+	return emoji.QuestionMark
+}
+
+func (i *Item) toHubStatus() ItemHubStatus {
+	status, ok, warning, managed := ItemStatus(*i)
+
+	return ItemHubStatus{
+		Name:         i.Name,
+		LocalVersion: i.LocalVersion,
+		LocalPath:    i.LocalPath,
+		Description:  i.Description,
+		Status:       status,
+		UTF8_Status:  fmt.Sprintf("%v  %s", toEmoji(managed, i.Installed, warning, ok), status),
+	}
 }
 }
 
 
+// XXX: can we remove these globals?
 var skippedLocal = 0
 var skippedLocal = 0
 var skippedTainted = 0
 var skippedTainted = 0
 
 
 // To be used when reference(s) (is/are) missing in a collection
 // To be used when reference(s) (is/are) missing in a collection
 var ReferenceMissingError = errors.New("Reference(s) missing in collection")
 var ReferenceMissingError = errors.New("Reference(s) missing in collection")
-var MissingHubIndex = errors.New("hub index can't be found")
 
 
 // GetVersionStatus: semver requires 'v' prefix
 // GetVersionStatus: semver requires 'v' prefix
 func GetVersionStatus(v *Item) int {
 func GetVersionStatus(v *Item) int {
@@ -203,11 +214,11 @@ func AddItem(itemType string, item Item) error {
 }
 }
 
 
 func DisplaySummary() {
 func DisplaySummary() {
-	log.Printf("Loaded %d collecs, %d parsers, %d scenarios, %d post-overflow parsers", len(hubIdx[COLLECTIONS]),
+	log.Infof("Loaded %d collecs, %d parsers, %d scenarios, %d post-overflow parsers", len(hubIdx[COLLECTIONS]),
 		len(hubIdx[PARSERS]), len(hubIdx[SCENARIOS]), len(hubIdx[PARSERS_OVFLW]))
 		len(hubIdx[PARSERS]), len(hubIdx[SCENARIOS]), len(hubIdx[PARSERS_OVFLW]))
 
 
 	if skippedLocal > 0 || skippedTainted > 0 {
 	if skippedLocal > 0 || skippedTainted > 0 {
-		log.Printf("unmanaged items: %d local, %d tainted", skippedLocal, skippedTainted)
+		log.Infof("unmanaged items: %d local, %d tainted", skippedLocal, skippedTainted)
 	}
 	}
 }
 }
 
 

+ 6 - 6
pkg/cwhub/cwhub_test.go

@@ -403,7 +403,7 @@ func TestInstallCollection(t *testing.T) {
 
 
 		it = hubIdx[COLLECTIONS][it.Name]
 		it = hubIdx[COLLECTIONS][it.Name]
 		x := GetHubStatusForItemType(COLLECTIONS, it.Name, false)
 		x := GetHubStatusForItemType(COLLECTIONS, it.Name, false)
-		log.Printf("%+v", x)
+		log.Infof("%+v", x)
 
 
 		break
 		break
 	}
 	}
@@ -425,7 +425,7 @@ func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
 	}
 	}
 	response.Header.Set("Content-Type", "application/json")
 	response.Header.Set("Content-Type", "application/json")
 
 
-	log.Printf("---> %s", req.URL.Path)
+	log.Infof("---> %s", req.URL.Path)
 
 
 	// FAKE PARSER
 	// FAKE PARSER
 	resp, ok := responseByPath[req.URL.Path]
 	resp, ok := responseByPath[req.URL.Path]
@@ -455,10 +455,10 @@ func fileToStringX(path string) string {
 
 
 func resetResponseByPath() {
 func resetResponseByPath() {
 	responseByPath = map[string]string{
 	responseByPath = map[string]string{
-		"/master/parsers/s01-parse/crowdsecurity/foobar_parser.yaml":    fileToStringX("./tests/foobar_parser.yaml"),
-		"/master/parsers/s01-parse/crowdsecurity/foobar_subparser.yaml": fileToStringX("./tests/foobar_parser.yaml"),
-		"/master/collections/crowdsecurity/test_collection.yaml":        fileToStringX("./tests/collection_v1.yaml"),
-		"/master/.index.json": fileToStringX("./tests/index1.json"),
+		"/master/parsers/s01-parse/crowdsecurity/foobar_parser.yaml":    fileToStringX("./testdata/foobar_parser.yaml"),
+		"/master/parsers/s01-parse/crowdsecurity/foobar_subparser.yaml": fileToStringX("./testdata/foobar_parser.yaml"),
+		"/master/collections/crowdsecurity/test_collection.yaml":        fileToStringX("./testdata/collection_v1.yaml"),
+		"/master/.index.json": fileToStringX("./testdata/index1.json"),
 		"/master/scenarios/crowdsecurity/foobar_scenario.yaml": `filter: true
 		"/master/scenarios/crowdsecurity/foobar_scenario.yaml": `filter: true
 name: crowdsecurity/foobar_scenario`,
 name: crowdsecurity/foobar_scenario`,
 		"/master/scenarios/crowdsecurity/barfoo_scenario.yaml": `filter: true
 		"/master/scenarios/crowdsecurity/barfoo_scenario.yaml": `filter: true

+ 2 - 2
pkg/cwhub/helpers_test.go

@@ -158,6 +158,6 @@ func assertCollectionDepsInstalled(t *testing.T, collection string) {
 }
 }
 
 
 func pushUpdateToCollectionInHub() {
 func pushUpdateToCollectionInHub() {
-	responseByPath["/master/.index.json"] = fileToStringX("./tests/index2.json")
-	responseByPath["/master/collections/crowdsecurity/test_collection.yaml"] = fileToStringX("./tests/collection_v2.yaml")
+	responseByPath["/master/.index.json"] = fileToStringX("./testdata/index2.json")
+	responseByPath["/master/collections/crowdsecurity/test_collection.yaml"] = fileToStringX("./testdata/collection_v2.yaml")
 }
 }

+ 3 - 3
pkg/cwhub/install.go

@@ -159,7 +159,7 @@ func EnableItem(hub *csconfig.Hub, target Item) (Item, error) {
 	}
 	}
 
 
 	if _, err := os.Stat(parent_dir); os.IsNotExist(err) {
 	if _, err := os.Stat(parent_dir); os.IsNotExist(err) {
-		log.Printf("%s doesn't exist, create", parent_dir)
+		log.Infof("%s doesn't exist, create", parent_dir)
 
 
 		if err := os.MkdirAll(parent_dir, os.ModePerm); err != nil {
 		if err := os.MkdirAll(parent_dir, os.ModePerm); err != nil {
 			return target, fmt.Errorf("while creating directory: %w", err)
 			return target, fmt.Errorf("while creating directory: %w", err)
@@ -187,7 +187,7 @@ func EnableItem(hub *csconfig.Hub, target Item) (Item, error) {
 
 
 	// check if file already exists where it should in configdir (eg /etc/crowdsec/collections/)
 	// check if file already exists where it should in configdir (eg /etc/crowdsec/collections/)
 	if _, err := os.Lstat(parent_dir + "/" + target.FileName); !os.IsNotExist(err) {
 	if _, err := os.Lstat(parent_dir + "/" + target.FileName); !os.IsNotExist(err) {
-		log.Printf("%s already exists.", parent_dir+"/"+target.FileName)
+		log.Infof("%s already exists.", parent_dir+"/"+target.FileName)
 		return target, nil
 		return target, nil
 	}
 	}
 
 
@@ -206,7 +206,7 @@ func EnableItem(hub *csconfig.Hub, target Item) (Item, error) {
 		return target, fmt.Errorf("while creating symlink from %s to %s: %w", srcPath, dstPath, err)
 		return target, fmt.Errorf("while creating symlink from %s to %s: %w", srcPath, dstPath, err)
 	}
 	}
 
 
-	log.Printf("Enabled %s : %s", target.Type, target.Name)
+	log.Infof("Enabled %s : %s", target.Type, target.Name)
 	target.Installed = true
 	target.Installed = true
 	hubIdx[target.Type][target.Name] = target
 	hubIdx[target.Type][target.Name] = target
 
 

+ 131 - 110
pkg/cwhub/loader.go

@@ -14,10 +14,35 @@ import (
 	"github.com/crowdsecurity/crowdsec/pkg/csconfig"
 	"github.com/crowdsecurity/crowdsec/pkg/csconfig"
 )
 )
 
 
+func isYAMLFileName(path string) bool {
+	return strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml")
+}
+
 func validItemFileName(vname string, fauthor string, fname string) bool {
 func validItemFileName(vname string, fauthor string, fname string) bool {
 	return (fauthor+"/"+fname == vname+".yaml") || (fauthor+"/"+fname == vname+".yml")
 	return (fauthor+"/"+fname == vname+".yaml") || (fauthor+"/"+fname == vname+".yml")
 }
 }
 
 
+func handleSymlink(path string) (string, error) {
+	hubpath, err := os.Readlink(path)
+	if err != nil {
+		return "", fmt.Errorf("unable to read symlink of %s", path)
+	}
+	// the symlink target doesn't exist, user might have removed ~/.hub/hub/...yaml without deleting /etc/crowdsec/....yaml
+	_, err = os.Lstat(hubpath)
+	if os.IsNotExist(err) {
+		log.Infof("%s is a symlink to %s that doesn't exist, deleting symlink", path, hubpath)
+		// remove the symlink
+		if err = os.Remove(path); err != nil {
+			return "", fmt.Errorf("failed to unlink %s: %w", path, err)
+		}
+
+		// XXX: is this correct?
+		return "", nil
+	}
+
+	return hubpath, nil
+}
+
 type walker struct {
 type walker struct {
 	// the walk/parserVisit function can't receive extra args
 	// the walk/parserVisit function can't receive extra args
 	hubdir     string
 	hubdir     string
@@ -80,7 +105,7 @@ func (w walker) getItemInfo(path string) (itemFileInfo, bool, error) {
 	}
 	}
 
 
 	log.Tracef("stage:%s ftype:%s", ret.stage, ret.ftype)
 	log.Tracef("stage:%s ftype:%s", ret.stage, ret.ftype)
-	// log.Printf("%s -> name:%s stage:%s", path, fname, stage)
+	// log.Infof("%s -> name:%s stage:%s", path, fname, stage)
 
 
 	if ret.stage == SCENARIOS {
 	if ret.stage == SCENARIOS {
 		ret.ftype = SCENARIOS
 		ret.ftype = SCENARIOS
@@ -120,8 +145,7 @@ func (w walker) parserVisit(path string, f os.DirEntry, err error) error {
 		return nil
 		return nil
 	}
 	}
 
 
-	// we only care about yaml files
-	if !strings.HasSuffix(f.Name(), ".yaml") && !strings.HasSuffix(f.Name(), ".yml") {
+	if !isYAMLFileName(f.Name()) {
 		return nil
 		return nil
 	}
 	}
 
 
@@ -141,28 +165,23 @@ func (w walker) parserVisit(path string, f os.DirEntry, err error) error {
 
 
 		log.Tracef("%s isn't a symlink", path)
 		log.Tracef("%s isn't a symlink", path)
 	} else {
 	} else {
-		hubpath, err = os.Readlink(path)
+		hubpath, err = handleSymlink(path)
 		if err != nil {
 		if err != nil {
-			return fmt.Errorf("unable to read symlink of %s", path)
+			return err
 		}
 		}
-		// the symlink target doesn't exist, user might have removed ~/.hub/hub/...yaml without deleting /etc/crowdsec/....yaml
-		_, err := os.Lstat(hubpath)
-		if os.IsNotExist(err) {
-			log.Infof("%s is a symlink to %s that doesn't exist, deleting symlink", path, hubpath)
-			// remove the symlink
-			if err = os.Remove(path); err != nil {
-				return fmt.Errorf("failed to unlink %s: %w", path, err)
-			}
+		log.Tracef("%s points to %s", path, hubpath)
+
+		if hubpath == "" {
+			// XXX: is this correct?
 			return nil
 			return nil
 		}
 		}
-		log.Tracef("%s points to %s", path, hubpath)
 	}
 	}
 
 
 	// if it's not a symlink and not in hub, it's a local file, don't bother
 	// if it's not a symlink and not in hub, it's a local file, don't bother
 	if local && !inhub {
 	if local && !inhub {
 		log.Tracef("%s is a local file, skip", path)
 		log.Tracef("%s is a local file, skip", path)
 		skippedLocal++
 		skippedLocal++
-		//	log.Printf("local scenario, skip.")
+		//	log.Infof("local scenario, skip.")
 
 
 		_, fileName := filepath.Split(path)
 		_, fileName := filepath.Split(path)
 
 
@@ -185,36 +204,36 @@ func (w walker) parserVisit(path string, f os.DirEntry, err error) error {
 
 
 	match := false
 	match := false
 
 
-	for k, v := range hubIdx[info.ftype] {
-		log.Tracef("check [%s] vs [%s] : %s", info.fname, v.RemotePath, info.ftype+"/"+info.stage+"/"+info.fname+".yaml")
+	for name, item := range hubIdx[info.ftype] {
+		log.Tracef("check [%s] vs [%s] : %s", info.fname, item.RemotePath, info.ftype+"/"+info.stage+"/"+info.fname+".yaml")
 
 
-		if info.fname != v.FileName {
-			log.Tracef("%s != %s (filename)", info.fname, v.FileName)
+		if info.fname != item.FileName {
+			log.Tracef("%s != %s (filename)", info.fname, item.FileName)
 			continue
 			continue
 		}
 		}
 
 
 		// wrong stage
 		// wrong stage
-		if v.Stage != info.stage {
+		if item.Stage != info.stage {
 			continue
 			continue
 		}
 		}
 
 
 		// if we are walking hub dir, just mark present files as downloaded
 		// if we are walking hub dir, just mark present files as downloaded
 		if inhub {
 		if inhub {
 			// wrong author
 			// wrong author
-			if info.fauthor != v.Author {
+			if info.fauthor != item.Author {
 				continue
 				continue
 			}
 			}
 
 
 			// wrong file
 			// wrong file
-			if !validItemFileName(v.Name, info.fauthor, info.fname) {
+			if !validItemFileName(item.Name, info.fauthor, info.fname) {
 				continue
 				continue
 			}
 			}
 
 
-			if path == w.hubdir+"/"+v.RemotePath {
-				log.Tracef("marking %s as downloaded", v.Name)
-				v.Downloaded = true
+			if path == w.hubdir+"/"+item.RemotePath {
+				log.Tracef("marking %s as downloaded", item.Name)
+				item.Downloaded = true
 			}
 			}
-		} else if !hasPathSuffix(hubpath, v.RemotePath) {
+		} else if !hasPathSuffix(hubpath, item.RemotePath) {
 			// wrong file
 			// wrong file
 			// <type>/<stage>/<author>/<name>.yaml
 			// <type>/<stage>/<author>/<name>.yaml
 			continue
 			continue
@@ -226,36 +245,37 @@ func (w walker) parserVisit(path string, f os.DirEntry, err error) error {
 		}
 		}
 
 
 		// let's reverse sort the versions to deal with hash collisions (#154)
 		// let's reverse sort the versions to deal with hash collisions (#154)
-		versions := make([]string, 0, len(v.Versions))
-		for k := range v.Versions {
+		versions := make([]string, 0, len(item.Versions))
+		for k := range item.Versions {
 			versions = append(versions, k)
 			versions = append(versions, k)
 		}
 		}
 
 
 		sort.Sort(sort.Reverse(sort.StringSlice(versions)))
 		sort.Sort(sort.Reverse(sort.StringSlice(versions)))
 
 
 		for _, version := range versions {
 		for _, version := range versions {
-			val := v.Versions[version]
+			val := item.Versions[version]
 			if sha != val.Digest {
 			if sha != val.Digest {
-				// log.Printf("matching filenames, wrong hash %s != %s -- %s", sha, val.Digest, spew.Sdump(v))
+				// log.Infof("matching filenames, wrong hash %s != %s -- %s", sha, val.Digest, spew.Sdump(v))
 				continue
 				continue
 			}
 			}
 
 
-			v.Downloaded = true
-			v.LocalHash = sha
-
 			// we got an exact match, update struct
 			// we got an exact match, update struct
+
+			item.Downloaded = true
+			item.LocalHash = sha
+
 			if !inhub {
 			if !inhub {
-				log.Tracef("found exact match for %s, version is %s, latest is %s", v.Name, version, v.Version)
-				v.LocalPath = path
-				v.LocalVersion = version
-				v.Tainted = false
+				log.Tracef("found exact match for %s, version is %s, latest is %s", item.Name, version, item.Version)
+				item.LocalPath = path
+				item.LocalVersion = version
+				item.Tainted = false
 				// if we're walking the hub, present file doesn't means installed file
 				// if we're walking the hub, present file doesn't means installed file
-				v.Installed = true
+				item.Installed = true
 			}
 			}
 
 
-			if version == v.Version {
-				log.Tracef("%s is up-to-date", v.Name)
-				v.UpToDate = true
+			if version == item.Version {
+				log.Tracef("%s is up-to-date", item.Name)
+				item.UpToDate = true
 			}
 			}
 
 
 			match = true
 			match = true
@@ -264,19 +284,19 @@ func (w walker) parserVisit(path string, f os.DirEntry, err error) error {
 		}
 		}
 
 
 		if !match {
 		if !match {
-			log.Tracef("got tainted match for %s : %s", v.Name, path)
+			log.Tracef("got tainted match for %s: %s", item.Name, path)
 
 
 			skippedTainted++
 			skippedTainted++
 			// the file and the stage is right, but the hash is wrong, it has been tainted by user
 			// the file and the stage is right, but the hash is wrong, it has been tainted by user
 			if !inhub {
 			if !inhub {
-				v.LocalPath = path
-				v.Installed = true
+				item.LocalPath = path
+				item.Installed = true
 			}
 			}
 
 
-			v.UpToDate = false
-			v.LocalVersion = "?"
-			v.Tainted = true
-			v.LocalHash = sha
+			item.UpToDate = false
+			item.LocalVersion = "?"
+			item.Tainted = true
+			item.LocalHash = sha
 		}
 		}
 
 
 		// update the entry if appropriate
 		// update the entry if appropriate
@@ -286,7 +306,7 @@ func (w walker) parserVisit(path string, f os.DirEntry, err error) error {
 		// } else if !inhub {
 		// } else if !inhub {
 
 
 		// } else if
 		// } else if
-		hubIdx[info.ftype][k] = v
+		hubIdx[info.ftype][name] = item
 
 
 		return nil
 		return nil
 	}
 	}
@@ -302,71 +322,72 @@ func CollecDepsCheck(v *Item) error {
 		return nil
 		return nil
 	}
 	}
 
 
-	// if it's a collection, ensure all the items are installed, or tag it as tainted
-	if v.Type == COLLECTIONS {
-		log.Tracef("checking submembers of %s installed:%t", v.Name, v.Installed)
-
-		var tmp = [][]string{v.Parsers, v.PostOverflows, v.Scenarios, v.Collections}
-		for idx, ptr := range tmp {
-			ptrtype := ItemTypes[idx]
-			for _, p := range ptr {
-				val, ok := hubIdx[ptrtype][p]
-				if !ok {
-					log.Fatalf("Referred %s %s in collection %s doesn't exist.", ptrtype, p, v.Name)
-				}
+	if v.Type != COLLECTIONS {
+		return nil
+	}
 
 
-				log.Tracef("check %s installed:%t", val.Name, val.Installed)
+	// if it's a collection, ensure all the items are installed, or tag it as tainted
+	log.Tracef("checking submembers of %s installed:%t", v.Name, v.Installed)
+
+	for idx, itemSlice := range [][]string{v.Parsers, v.PostOverflows, v.Scenarios, v.Collections} {
+		sliceType := ItemTypes[idx]
+		for _, subName := range itemSlice {
+			subItem, ok := hubIdx[sliceType][subName]
+			if !ok {
+				log.Fatalf("Referred %s %s in collection %s doesn't exist.", sliceType, subName, v.Name)
+			}
 
 
-				if !v.Installed {
-					continue
-				}
+			log.Tracef("check %s installed:%t", subItem.Name, subItem.Installed)
 
 
-				if val.Type == COLLECTIONS {
-					log.Tracef("collec, recurse.")
+			if !v.Installed {
+				continue
+			}
 
 
-					if err := CollecDepsCheck(&val); err != nil {
-						if val.Tainted {
-							v.Tainted = true
-						}
+			if subItem.Type == COLLECTIONS {
+				log.Tracef("collec, recurse.")
 
 
-						return fmt.Errorf("sub collection %s is broken: %w", val.Name, err)
+				if err := CollecDepsCheck(&subItem); err != nil {
+					if subItem.Tainted {
+						v.Tainted = true
 					}
 					}
 
 
-					hubIdx[ptrtype][p] = val
+					return fmt.Errorf("sub collection %s is broken: %w", subItem.Name, err)
 				}
 				}
 
 
-				// propagate the state of sub-items to set
-				if val.Tainted {
-					v.Tainted = true
-					return fmt.Errorf("tainted %s %s, tainted", ptrtype, p)
-				}
+				hubIdx[sliceType][subName] = subItem
+			}
 
 
-				if !val.Installed && v.Installed {
-					v.Tainted = true
-					return fmt.Errorf("missing %s %s, tainted", ptrtype, p)
-				}
+			// propagate the state of sub-items to set
+			if subItem.Tainted {
+				v.Tainted = true
+				return fmt.Errorf("tainted %s %s, tainted", sliceType, subName)
+			}
 
 
-				if !val.UpToDate {
-					v.UpToDate = false
-					return fmt.Errorf("outdated %s %s", ptrtype, p)
-				}
+			if !subItem.Installed && v.Installed {
+				v.Tainted = true
+				return fmt.Errorf("missing %s %s, tainted", sliceType, subName)
+			}
 
 
-				skip := false
+			if !subItem.UpToDate {
+				v.UpToDate = false
+				return fmt.Errorf("outdated %s %s", sliceType, subName)
+			}
 
 
-				for idx := range val.BelongsToCollections {
-					if val.BelongsToCollections[idx] == v.Name {
-						skip = true
-					}
-				}
+			skip := false
 
 
-				if !skip {
-					val.BelongsToCollections = append(val.BelongsToCollections, v.Name)
+			for idx := range subItem.BelongsToCollections {
+				if subItem.BelongsToCollections[idx] == v.Name {
+					skip = true
 				}
 				}
+			}
 
 
-				hubIdx[ptrtype][p] = val
-
-				log.Tracef("checking for %s - tainted:%t uptodate:%t", p, v.Tainted, v.UpToDate)
+			if !skip {
+				subItem.BelongsToCollections = append(subItem.BelongsToCollections, v.Name)
 			}
 			}
+
+			hubIdx[sliceType][subName] = subItem
+
+			log.Tracef("checking for %s - tainted:%t uptodate:%t", subName, v.Tainted, v.UpToDate)
 		}
 		}
 	}
 	}
 
 
@@ -389,31 +410,31 @@ func SyncDir(hub *csconfig.Hub, dir string) (error, []string) {
 		}
 		}
 	}
 	}
 
 
-	for k, v := range hubIdx[COLLECTIONS] {
-		if !v.Installed {
+	for name, item := range hubIdx[COLLECTIONS] {
+		if !item.Installed {
 			continue
 			continue
 		}
 		}
 
 
-		versionStatus := GetVersionStatus(&v)
+		versionStatus := GetVersionStatus(&item)
 		switch versionStatus {
 		switch versionStatus {
 		case 0: // latest
 		case 0: // latest
-			if err := CollecDepsCheck(&v); err != nil {
-				warnings = append(warnings, fmt.Sprintf("dependency of %s : %s", v.Name, err))
-				hubIdx[COLLECTIONS][k] = v
+			if err := CollecDepsCheck(&item); err != nil {
+				warnings = append(warnings, fmt.Sprintf("dependency of %s : %s", item.Name, err))
+				hubIdx[COLLECTIONS][name] = item
 			}
 			}
 		case 1: // not up-to-date
 		case 1: // not up-to-date
-			warnings = append(warnings, fmt.Sprintf("update for collection %s available (currently:%s, latest:%s)", v.Name, v.LocalVersion, v.Version))
+			warnings = append(warnings, fmt.Sprintf("update for collection %s available (currently:%s, latest:%s)", item.Name, item.LocalVersion, item.Version))
 		default: // version is higher than the highest available from hub?
 		default: // version is higher than the highest available from hub?
-			warnings = append(warnings, fmt.Sprintf("collection %s is in the future (currently:%s, latest:%s)", v.Name, v.LocalVersion, v.Version))
+			warnings = append(warnings, fmt.Sprintf("collection %s is in the future (currently:%s, latest:%s)", item.Name, item.LocalVersion, item.Version))
 		}
 		}
 
 
-		log.Debugf("installed (%s) - status:%d | installed:%s | latest : %s | full : %+v", v.Name, versionStatus, v.LocalVersion, v.Version, v.Versions)
+		log.Debugf("installed (%s) - status:%d | installed:%s | latest : %s | full : %+v", item.Name, versionStatus, item.LocalVersion, item.Version, item.Versions)
 	}
 	}
 
 
 	return nil, warnings
 	return nil, warnings
 }
 }
 
 
-// Updates the infos from HubInit() with the local state
+// Updates the info from HubInit() with the local state
 func LocalSync(hub *csconfig.Hub) (error, []string) {
 func LocalSync(hub *csconfig.Hub) (error, []string) {
 	skippedLocal = 0
 	skippedLocal = 0
 	skippedTainted = 0
 	skippedTainted = 0
@@ -481,12 +502,12 @@ func LoadPkgIndex(buff []byte) (map[string]map[string]Item, error) {
 		// complete struct
 		// complete struct
 		log.Tracef("%d item", len(RawIndex[itemType]))
 		log.Tracef("%d item", len(RawIndex[itemType]))
 
 
-		for idx, item := range RawIndex[itemType] {
-			item.Name = idx
+		for name, item := range RawIndex[itemType] {
+			item.Name = name
 			item.Type = itemType
 			item.Type = itemType
 			x := strings.Split(item.RemotePath, "/")
 			x := strings.Split(item.RemotePath, "/")
 			item.FileName = x[len(x)-1]
 			item.FileName = x[len(x)-1]
-			RawIndex[itemType][idx] = item
+			RawIndex[itemType][name] = item
 
 
 			if itemType != COLLECTIONS {
 			if itemType != COLLECTIONS {
 				continue
 				continue

+ 0 - 0
pkg/cwhub/tests/collection_v1.yaml → pkg/cwhub/testdata/collection_v1.yaml


+ 0 - 0
pkg/cwhub/tests/collection_v2.yaml → pkg/cwhub/testdata/collection_v2.yaml


+ 0 - 0
pkg/cwhub/tests/foobar_parser.yaml → pkg/cwhub/testdata/foobar_parser.yaml


+ 0 - 0
pkg/cwhub/tests/index1.json → pkg/cwhub/testdata/index1.json


+ 0 - 0
pkg/cwhub/tests/index2.json → pkg/cwhub/testdata/index2.json