package delivery import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "path" "regexp" "strings" ) // **A run ask has a program built from a change and run in the build seat's sandbox** (novox/hq ADR 0644, ADR 0656; // design 47, *What ADR 0656 leaves to this design*: the build seat's ask `run`; issue 1921). It goes on the Go seat's // check stream, a whole job pulled where there is capacity, like a compose: the build module fetches Program at its // commit and builds it once per RunKey, in a step that runs nothing but the build; fetches every head of Heads and lays // each read-only under the directory RunHeadsEnv names, at its repository's path (`/`); reads the input // Input names, checks its digest and writes it to the file RunInputEnv names; then runs the program with Args, no // network and no bus account (ADR 0448 §2, §4), and answers RunAnswer on its step key. // // The input is carried by reference, as a compose carries its manifests: the asker keeps it in a state of its own by its // digest (RunInput), measured against RunInputBound before it asks, so a message on the bus never carries it (issue 1930). // RunJob is a run ask's job. type RunJob struct { // Program is where the program is built: the repository and full commit fetched, the Go module's directory in it and // the program's import path. The ask's Packages are the mesh package versions it builds against (ADR 0573 §2, §4). Program RunProgram `json:"program"` // Heads are the commits the program reads, each laid read-only at its repository's path; a repository at most once. Heads []RunHead `json:"heads"` // Input names the program's input, kept by the asker by its digest. Input RunInput `json:"input"` // Args are the program's arguments (a checking program's `-seats` or `-verdict`), each a word or a flag. Args []string `json:"args,omitempty"` // Reader names, for a reader of the ask, what the run is for ("checking program of mesh-delivery head"). Reader string `json:"reader,omitempty"` } // RunProgram is where a run's program is built from. type RunProgram struct { Repository string `json:"repository"` Commit string `json:"commit"` Dir string `json:"dir"` Package string `json:"package"` } // RunHead is one commit a run's program reads. type RunHead struct { Repository string `json:"repository"` Commit string `json:"commit"` } // RunInput is a run's input by reference: the state it is kept in, named as a manifest's `reads` names a state // (`.`), its key (the sha256 of its bytes, 64 lowercase hex digits) and its size in bytes. type RunInput struct { State string `json:"state"` Digest string `json:"digest"` Size int `json:"size"` } // RunInputBound is the most bytes a run's input may hold: under the bus's message limit, so the asker can keep it as // one value. const RunInputBound = 768 << 10 // RunHeadsEnv names, for the program, the directory its heads are laid in; RunInputEnv the file its input is written to. const ( RunHeadsEnv = "MESH_RUN_HEADS" RunInputEnv = "MESH_RUN_INPUT" ) // RunInputOf is the reference to body kept in state, refusing a body over RunInputBound, naming its size, so an asker // refuses it before asking. func RunInputOf(state string, body []byte) (RunInput, error) { if len(body) > RunInputBound { return RunInput{}, fmt.Errorf("delivery: a run's input of %d bytes is over its bound of %d bytes", len(body), RunInputBound) } sum := sha256.Sum256(body) in := RunInput{State: state, Digest: hex.EncodeToString(sum[:]), Size: len(body)} return in, in.check("") } // Verify refuses body when it is not the input i names: another size or another digest. func (i RunInput) Verify(body []byte) error { sum := sha256.Sum256(body) if len(body) != i.Size || hex.EncodeToString(sum[:]) != i.Digest { return fmt.Errorf("delivery: the input read from %s under %s is not the one the ask names (%d bytes, %d named)", i.State, i.Digest, len(body), i.Size) } return nil } var ( runStateOK = regexp.MustCompile(`^[a-z0-9][-a-z0-9]*\.[a-z0-9][-a-z0-9]*$`) runDigestOK = regexp.MustCompile(`^[0-9a-f]{64}$`) runArgOK = regexp.MustCompile(`^[-=_./a-zA-Z0-9]+$`) ) func (i RunInput) check(id string) error { switch { case !runStateOK.MatchString(i.State): return fmt.Errorf("delivery: the run ask %s keeps its input in %q, which is no .", id, i.State) case !runDigestOK.MatchString(i.Digest): return fmt.Errorf("delivery: the run ask %s names its input by %q, which is no sha256 digest", id, i.Digest) case i.Size <= 0 || i.Size > RunInputBound: return fmt.Errorf("delivery: the run ask %s names an input of %d bytes, outside 1 to %d", id, i.Size, RunInputBound) } return nil } func (r RunJob) check(id string) error { p := r.Program if err := checkRepository(p.Repository); err != nil { return fmt.Errorf("the run ask %s's program: %w", id, err) } if !askCommitOK.MatchString(p.Commit) { return fmt.Errorf("delivery: the run ask %s builds its program at no full commit", id) } if p.Dir == "" || p.Dir != path.Clean(p.Dir) || strings.HasPrefix(p.Dir, "/") || p.Dir == ".." || strings.HasPrefix(p.Dir, "../") { return fmt.Errorf("delivery: the run ask %s names no clean directory inside the repository (%q)", id, p.Dir) } if p.Package == "" || strings.ContainsAny(p.Package, " \t\n") { return fmt.Errorf("delivery: the run ask %s names no package to build (%q)", id, p.Package) } if len(r.Heads) == 0 { return fmt.Errorf("delivery: the run ask %s names no head", id) } seen := map[string]bool{} for _, h := range r.Heads { if err := checkRepository(h.Repository); err != nil { return fmt.Errorf("the run ask %s's head: %w", id, err) } if !askCommitOK.MatchString(h.Commit) { return fmt.Errorf("delivery: the run ask %s names the head of %s at no full commit", id, h.Repository) } if seen[h.Repository] { return fmt.Errorf("delivery: the run ask %s names %s twice", id, h.Repository) } seen[h.Repository] = true } for _, a := range r.Args { if !runArgOK.MatchString(a) { return fmt.Errorf("delivery: the run ask %s names the argument %q, which is no word or flag", id, a) } } return r.Input.check(id) } // RunAnswer is a run ask's step result: whether this run built the program or took the one kept under its key, the // key, the program's exit, the last line of its standard output (Output, raw when it is JSON, else as a JSON string), // and Said: the first line of its standard error, or why the step could not run it. type RunAnswer struct { Built bool `json:"built"` Key string `json:"key"` Exit int `json:"exit"` Output json.RawMessage `json:"output,omitempty"` Said string `json:"said,omitempty"` } // RunKey is the key a build module keeps a built program under, as ComposerKey keeps a composer: what it was built // from, the toolchain and the asked package versions, hashed (`program-` and 16 hex digits). func RunKey(p RunProgram, toolchain ToolchainUsed, versions PackageVersions) string { return "program-" + strings.TrimPrefix(ComposerKey(p.Repository, p.Commit, p.Dir, p.Package, toolchain, versions), "composer-") }