package busstate import ( "crypto/rand" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "strings" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/buscalls" ) // The put and get sessions of a value larger than one chunk (design 47 G §518, §345, §490, A-21): a bundle sends and asks the // chunks as calls on its channel to the tool runner, which holds the session; nothing of it is on the bus. The capability // word put-sessions names the two calls, so a tool runner that does not state it refuses the session at once. // callID is the bundle's id for one session: random, so two sessions never share one. func callID() string { var b [12]byte _, _ = rand.Read(b[:]) return hex.EncodeToString(b[:]) } // put sends data as a put session: chunks of buscalls.ChunkBytes in order, each answered before the next is sent, the // answer to the last being the put's. func (o Objects) putSession(name string, data []byte, description string) (buscalls.ObjectInfo, error) { if err := stdio.RequireCapability(stdio.CapPutSessions); err != nil { return buscalls.ObjectInfo{}, fmt.Errorf("putting %s, larger than one call carries: %w", name, err) } id := callID() for seq, at := uint32(1), 0; ; seq++ { end := min(at+buscalls.ChunkBytes, len(data)) last := end == len(data) req := buscalls.ObjectsPutChunkRequest{Objects: o.local, Name: name, Call: id, Seq: seq, Last: last, Data: data[at:end]} if seq == 1 { req.Description = description // the first chunk carries it (design 47 G A-25) } raw, err := stdio.BusState(buscalls.ObjectsPutChunk, req) if err != nil { return buscalls.ObjectInfo{}, fmt.Errorf("putting %s, chunk %d: %w", name, seq, err) } var taken buscalls.ObjectsChunkTaken if err := json.Unmarshal(raw, &taken); err != nil { return buscalls.ObjectInfo{}, fmt.Errorf("the runtime's answer to a chunk of %s: %w", name, err) } if taken.Error != "" { return buscalls.ObjectInfo{}, fmt.Errorf("putting %s, chunk %d: %s", name, seq, taken.Error) } if last { var info buscalls.ObjectInfo if err := json.Unmarshal(raw, &info); err != nil { return info, fmt.Errorf("the runtime's answer to the put of %s: %w", name, err) } return info, nil } if taken.Seq != seq { return buscalls.ObjectInfo{}, fmt.Errorf("putting %s: chunk %d was answered as chunk %d", name, seq, taken.Seq) } at = end } } // getSession reads the chunks of the session the runtime opened for name, and checks the whole against the digest and the // size it answered. func getSession(name string, c buscalls.ObjectsChunked) (buscalls.ObjectData, error) { if err := stdio.RequireCapability(stdio.CapPutSessions); err != nil { return buscalls.ObjectData{}, fmt.Errorf("getting %s, larger than one call carries: %w", name, err) } var whole []byte for seq := uint32(1); ; seq++ { raw, err := stdio.BusState(buscalls.ObjectsGetChunk, buscalls.ObjectsGetChunkRequest{Call: c.Call, Seq: seq}) if err != nil { return buscalls.ObjectData{}, fmt.Errorf("getting %s, chunk %d: %w", name, seq, err) } var chunk buscalls.ObjectsChunk if err := json.Unmarshal(raw, &chunk); err != nil { return buscalls.ObjectData{}, fmt.Errorf("the runtime's chunk %d of %s: %w", seq, name, err) } if chunk.Error != "" { return buscalls.ObjectData{}, fmt.Errorf("getting %s, chunk %d: %s", name, seq, chunk.Error) } if chunk.Seq != seq { return buscalls.ObjectData{}, fmt.Errorf("getting %s: chunk %d was answered as chunk %d", name, seq, chunk.Seq) } whole = append(whole, chunk.Data...) if chunk.Last { break } } sum := sha256.Sum256(whole) if c.Digest != "" && !strings.EqualFold(c.Digest, "sha256:"+hex.EncodeToString(sum[:])) { return buscalls.ObjectData{}, fmt.Errorf("getting %s: what was read is not the value the runtime named by %s", name, c.Digest) } if c.Size != 0 && c.Size != uint64(len(whole)) { return buscalls.ObjectData{}, fmt.Errorf("getting %s: %d bytes were read, the runtime said %d", name, len(whole), c.Size) } return buscalls.ObjectData{Info: buscalls.ObjectInfo{Name: name, Size: uint64(len(whole)), Digest: c.Digest}, Data: whole}, nil } // chunkedAnswer is the session an answer to a get opens, if it is not the value itself. func chunkedAnswer(raw json.RawMessage) (buscalls.ObjectsChunked, bool) { var c buscalls.ObjectsChunked if json.Unmarshal(raw, &c) != nil { return c, false } switch strings.TrimSpace(string(c.Chunked)) { case "", "null", "0", "false": return c, false } return c, c.Call != "" }