package buildstep import ( "fmt" "path" "regexp" "strings" ) var ( requestCommitOK = regexp.MustCompile(`^[0-9a-f]{40}$`) requestRepositoryOK = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*$`) ) // FetchRequest is what a build module's fetch tool is asked: the repository and the full commit, the pull request's // ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, and, for a build of a // module whose every artifact names its paths, that module's directory and its named paths (novox/hq ADR 0499 clause 4). // // **The paths come from the builder, not from a manifest** (ADR 0495 clause 1; the lead's decision L8 of R4e): the // builder fills Dir and Paths from the indexer's Module value (Path, and KeptInputs2.Paths' keys), and the build module // gives the build and test steps a workspace holding those paths, the manifest and its lock files. A request without // them builds from the module's directory, as before. type FetchRequest struct { Repository string `json:"repository"` Commit string `json:"commit"` Ref string `json:"ref,omitempty"` Base string `json:"base,omitempty"` Dir string `json:"dir,omitempty"` Paths []string `json:"paths,omitempty"` } // FetchRequestProperties are the JSON schema properties of FetchRequest's dir and paths, for a fetch tool's Input. func FetchRequestProperties() map[string]any { return map[string]any{ "dir": map[string]any{"type": "string", "description": "the module's directory in the repository (\".\" for its root), with paths"}, "paths": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "the paths the module names, relative to dir, from the indexer's kept inputs: the build sees only these, the manifest and its lock files"}, } } // Args is r as a tool call's arguments. func (r FetchRequest) Args() map[string]any { args := map[string]any{"repository": r.Repository, "commit": r.Commit} for k, v := range map[string]string{"ref": r.Ref, "base": r.Base, "dir": r.Dir} { if v != "" { args[k] = v } } if len(r.Paths) > 0 { args["paths"] = append([]string(nil), r.Paths...) } return args } // ParseFetchRequest reads a fetch tool's arguments; arguments it does not know (a build module's own) are left to the // caller. It refuses a repository that is not /, a commit or a base that is not 40 hex digits, paths with no // module directory or a directory with no path, a directory leaving the repository and a path leaving the directory, // naming each. func ParseFetchRequest(args map[string]any) (FetchRequest, error) { var r FetchRequest r.Repository, _ = args["repository"].(string) r.Commit, _ = args["commit"].(string) r.Ref, _ = args["ref"].(string) r.Base, _ = args["base"].(string) r.Dir, _ = args["dir"].(string) switch v := args["paths"].(type) { case nil: case []string: r.Paths = append([]string(nil), v...) case []any: for _, p := range v { s, ok := p.(string) if !ok { return FetchRequest{}, fmt.Errorf("paths is a list of strings") } r.Paths = append(r.Paths, s) } default: return FetchRequest{}, fmt.Errorf("paths is a list of strings") } return r, r.Check() } // Check is what ParseFetchRequest refuses. func (r FetchRequest) Check() error { if !requestRepositoryOK.MatchString(r.Repository) || strings.Contains(r.Repository, "..") { return fmt.Errorf("%q is not a repository, /", r.Repository) } if !requestCommitOK.MatchString(r.Commit) { return fmt.Errorf("%q is not a full commit (40 hex digits)", r.Commit) } if r.Base != "" && !requestCommitOK.MatchString(r.Base) { return fmt.Errorf("%q is not a trunk commit (40 hex digits)", r.Base) } switch { case len(r.Paths) > 0 && r.Dir == "": return fmt.Errorf("the request names paths and names no module directory (dir) they are relative to") case len(r.Paths) == 0 && r.Dir != "": return fmt.Errorf("the request names a module directory and no path") } if r.Dir != "" && (outside(r.Dir) || strings.HasPrefix(r.Dir, "/")) { return fmt.Errorf("the module directory %q leaves the repository", r.Dir) } for _, p := range r.Paths { if p == "" || strings.HasPrefix(p, "/") || outside(p) || path.Clean(p) == "." { return fmt.Errorf("the path %q leaves the module's directory or is not a path in it", p) } } return nil } func outside(p string) bool { c := path.Clean(p) return c == ".." || strings.HasPrefix(c, "../") }