Main reverted G's half merge (#189, reverted by8afc3ebin #191) and then took the reader release's S1-S5, which copied G's code. G's form holds where they differ (process_bus.go, seats.go, state.go and their tests); the release's kept names stay so main's callers keep building: SeatViewPrefix beside SeatReadPrefix, and QueueInvoke's empty strings on not-ok (with its test TestQueueInvoke). The tree differs from G's headf30dee54only by those and the release's two tests (objects cap option, seat states and stream kinds). Builds: design 47 G (the milestone's sdk part, as atf30dee54) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
114 lines
4.4 KiB
Go
114 lines
4.4 KiB
Go
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 != ""
|
|
}
|