package meshcatalogclient import ( "encoding/json" "fmt" "path" "strings" seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-sdk/go/keys" ) // RootPath is a `modules` value's path when the module's manifest is its repository's root. const RootPath = "." // SourcePrefix1 opens the fingerprint of the old form a `modules` value may still carry in `fp`. const SourcePrefix1 = "src1:" // ModulesKey is the key of a `modules` value: the module's name. func ModulesKey(module string) (string, error) { if err := keys.CheckName("module", module); err != nil { return "", err } return seat.ModulesByModuleKey(module), nil } // ParseModulesKey is the inverse of ModulesKey. func ParseModulesKey(key string) (string, error) { module, err := ModulesKey(key) if err != nil { return "", fmt.Errorf("meshcatalogclient: %q is not a modules key: %w", key, err) } return module, nil } // CheckModule refuses a `modules` value that contradicts itself: both or neither of a fingerprint and the reason for none, // of either form, an old fingerprint not of the form `src1:`, kept inputs without a manifest digest, a verdict the indexer // does not write, problems without a refusal (or a refusal without problems), a path that is not clean inside the module's // directory, and a fingerprint or a digest not of its form. func CheckModule(m seat.ModulesByModuleValue) error { switch { case m.Fp != "" && m.None != "": return fmt.Errorf("meshcatalogclient: a module has both a fingerprint and a reason for none") case m.Fp == "" && m.None == "" && m.Fp2 == "" && m.None2 == "": return fmt.Errorf("meshcatalogclient: a module has neither a fingerprint nor a reason for none, of either form") case m.Fp != "" && !strings.HasPrefix(m.Fp, SourcePrefix1): return fmt.Errorf("meshcatalogclient: the module's fingerprint %q does not start with %s", m.Fp, SourcePrefix1) case m.Fp2 != "" && m.None2 != "": return fmt.Errorf("meshcatalogclient: a module has both fp2 and none2") } for language, t := range m.Toolchains { if t.Version == "" || t.Recipe == "" { return fmt.Errorf("meshcatalogclient: the module's toolchain of %q needs its version and its recipe version", language) } } if m.Inputs2 != nil && m.Manifest == "" { return fmt.Errorf("meshcatalogclient: a module keeps new-form inputs but has no manifest digest") } for artifact, paths := range m.Paths { if artifact == "" || len(paths) == 0 { return fmt.Errorf("meshcatalogclient: the module's named paths of the artifact %q are empty or name no artifact", artifact) } for _, p := range paths { if !cleanInside(p) { return fmt.Errorf("meshcatalogclient: the artifact %q names the path %q, not a clean path inside the module's directory", artifact, p) } } } if in := m.Inputs2; in != nil { if f := in.ImportForm; f != "" && (len(in.Imports) == 0 || !strings.HasPrefix(f, SourcePrefix2) || !keys.IsHex64(f[len(SourcePrefix2):])) { return fmt.Errorf("meshcatalogclient: the kept import form %q is set only with imports, and is %s and 64 hex digits", f, SourcePrefix2) } for p, object := range in.Paths { if !cleanInside(p) { return fmt.Errorf("meshcatalogclient: the named path %q is not a clean path inside the module's directory", p) } if object == "" { return fmt.Errorf("meshcatalogclient: the named path %q names no git object", p) } } } switch { case m.Verdict != "" && m.Verdict != VerdictAccepted && m.Verdict != VerdictRefused: return fmt.Errorf("meshcatalogclient: the module's verdict %q is neither %s nor %s", m.Verdict, VerdictAccepted, VerdictRefused) case len(m.Unknown) > 0 && m.Verdict != VerdictRefused: return fmt.Errorf("meshcatalogclient: a module's unknown fields are named only with the refusal a wait is written as") case m.Verdict != "" && m.Manifest == "": return fmt.Errorf("meshcatalogclient: a module carries a verdict and no manifest digest") case (m.Verdict == VerdictRefused) != (len(m.Problems) > 0): return fmt.Errorf("meshcatalogclient: a module's problems are named only with a refusal, and a refusal names them") case m.Verdict == VerdictRefused && len(m.Warnings) > 0: return fmt.Errorf("meshcatalogclient: a refused module warns: a refusal names problems") } if m.Fp2 != "" { if _, err := KeyPart(m.Fp2); err != nil { return err } } if m.Manifest != "" { if _, err := ManifestsKey(m.Manifest); err != nil { return err } } return nil } func cleanInside(p string) bool { return p != "" && p == path.Clean(p) && !strings.HasPrefix(p, "/") && p != ".." && !strings.HasPrefix(p, "../") } // EncodeModule is m as JSON at the kept values' version; it refuses what CheckModule refuses. func EncodeModule(m seat.ModulesByModuleValue) ([]byte, error) { if err := CheckModule(m); err != nil { return nil, err } m.V = keys.Version return json.Marshal(m) } // DecodeModule reads a `modules` value; it refuses an unknown version (keys.ErrUnknownVersion) and what EncodeModule refuses. func DecodeModule(data []byte) (seat.ModulesByModuleValue, error) { if err := keys.CheckVersion(data); err != nil { return seat.ModulesByModuleValue{}, err } var m seat.ModulesByModuleValue if err := json.Unmarshal(data, &m); err != nil { return seat.ModulesByModuleValue{}, fmt.Errorf("meshcatalogclient: value does not fit its shape: %w", err) } if err := CheckModule(m); err != nil { return seat.ModulesByModuleValue{}, err } return m, nil }