package models import ( "fmt" "regexp" "strings" ) type CacheEntry struct { Key string `yaml:"key"` Hash []string `yaml:"hash"` Paths []string `yaml:"paths"` // 1 is fastest, 19 is smallest, 0 is the zstd default CompressionLevel int `yaml:"compression-level"` // on-success is the default, always also saves on failed runs When string `yaml:"when"` } // keys become storage paths, so no slashes var cacheKeyRe = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`) func (c CacheEntry) Validate() error { if !cacheKeyRe.MatchString(c.Key) { return fmt.Errorf("cache: invalid key %q (allowed: letters, digits, '.', '_', '-')", c.Key) } for _, f := range c.Hash { // rev-parse needs plain repo-relative paths, not pathspecs if f == "" || strings.HasPrefix(f, "/") || strings.HasPrefix(f, "..") { return fmt.Errorf("cache %q: hash path %q is not repo-relative", c.Key, f) } if strings.ContainsAny(f, ": \t\n\"'`$\\*?[") || strings.Contains(f, "/../") || strings.HasSuffix(f, "/..") { return fmt.Errorf("cache %q: hash path %q contains unsupported characters", c.Key, f) } } if len(c.Paths) == 0 { return fmt.Errorf("cache %q: no paths", c.Key) } if c.CompressionLevel < 0 || c.CompressionLevel > 19 { return fmt.Errorf("cache %q: compression-level %d out of range (1-19)", c.Key, c.CompressionLevel) } switch c.When { case "", "on-success", "always": default: return fmt.Errorf("cache %q: when %q is not one of on-success, always", c.Key, c.When) } seen := make(map[string]bool, len(c.Paths)) for _, p := range c.Paths { if p == "" { return fmt.Errorf("cache %q: empty path", c.Key) } if strings.ContainsAny(p, " \t\n\"'`$\\") { return fmt.Errorf("cache %q: path %q contains unsupported characters", c.Key, p) } if strings.Contains(p, "..") { return fmt.Errorf("cache %q: path %q must not contain '..'", c.Key, p) } if seen[p] { return fmt.Errorf("cache %q: duplicate path %q", c.Key, p) } seen[p] = true } return nil }