publish.Publish opens the transfer naming every part's digest and sends only the parts the holder answers it lacks, in chunks of at most 760 KiB (a message under the bus's 1 MiB once base64), in sequence with the last marked; a part whose bytes are not its digest is refused before any of it travels. The seat's messages are mesh-contracts'; the call is the caller's own. Its test takes the place of the vault's get test, merged into the vault's table. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
123 lines
4.5 KiB
Go
123 lines
4.5 KiB
Go
package publish
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-contracts/go/parts/registry"
|
|
)
|
|
|
|
// A file travels to a registry seat in parts (ADR 0611, design 51 *Publishing*; SDK-1): only the parts the holder says
|
|
// it lacks are read and sent, each in chunks of at most MaxChunk in sequence with the last marked, so a cut transfer
|
|
// opened again goes on from what the holder holds; a part whose bytes are not its digest is refused before any of it
|
|
// travels.
|
|
func TestAFileTravelsInTheParts(t *testing.T) {
|
|
big := bytes.Repeat([]byte("x"), MaxChunk+10)
|
|
small, empty := []byte("config"), []byte{}
|
|
part := func(b []byte, opened *int) Part {
|
|
sum := sha256.Sum256(b)
|
|
return Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)),
|
|
Open: func() (io.ReadCloser, error) { *opened++; return io.NopCloser(bytes.NewReader(b)), nil }}
|
|
}
|
|
type chunk struct {
|
|
part string
|
|
seq int64
|
|
last bool
|
|
data []byte
|
|
}
|
|
holder := func(lacks []string, got *[]chunk, asked *[]string) Call {
|
|
return func(key string, body any) (json.RawMessage, error) {
|
|
*asked = append(*asked, key)
|
|
raw, _ := json.Marshal(body)
|
|
switch key {
|
|
case "seat:oci-artifact-registry.publish":
|
|
return json.Marshal(registry.PublishAnswer{Transfer: "t1", Lacks: lacks, Held: len(lacks) == 0})
|
|
case "seat:oci-artifact-registry.publish-part":
|
|
var in registry.PublishPartInput
|
|
_ = json.Unmarshal(raw, &in)
|
|
data, _ := base64.StdEncoding.DecodeString(in.Data)
|
|
if len(raw) > 1<<20 {
|
|
t.Errorf("a chunk's message is %d bytes, over the bus's largest", len(raw))
|
|
}
|
|
*got = append(*got, chunk{in.Part, in.Sequence, in.Last, data})
|
|
return json.Marshal(registry.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part})
|
|
}
|
|
t.Fatalf("asked %s", key)
|
|
return nil, nil
|
|
}
|
|
}
|
|
in := registry.PublishInput{Transfer: "t1", Name: "files", Kind: "archive", Digest: "sha256:" + strings.Repeat("a", 64)}
|
|
var opens [3]int
|
|
parts := []Part{part(big, &opens[0]), part(small, &opens[1]), part(empty, &opens[2])}
|
|
|
|
for _, c := range []struct {
|
|
name string
|
|
lacks []int
|
|
chunks int
|
|
err string
|
|
}{
|
|
{"a fresh transfer sends every part, the big one in two chunks and the empty one as one", []int{0, 1, 2}, 4, ""},
|
|
{"a cut transfer opened again sends only what the holder lacks", []int{2}, 1, ""},
|
|
{"a digest the holder holds sends nothing", nil, 0, ""},
|
|
} {
|
|
opens = [3]int{}
|
|
var lacks []string
|
|
for _, i := range c.lacks {
|
|
lacks = append(lacks, parts[i].Digest)
|
|
}
|
|
var got []chunk
|
|
var asked []string
|
|
sent, err := Publish(context.Background(), holder(lacks, &got, &asked), "oci-artifact-registry", in, parts)
|
|
if err != nil || sent.Chunks != c.chunks || len(got) != c.chunks || len(sent.Parts) != len(c.lacks) {
|
|
t.Errorf("%s: sent %+v, %d chunks heard, %v", c.name, sent, len(got), err)
|
|
continue
|
|
}
|
|
for i := range parts {
|
|
want := 0
|
|
for _, l := range c.lacks {
|
|
if l == i {
|
|
want = 2 // read once to check its digest, once to send it
|
|
}
|
|
}
|
|
if opens[i] != want {
|
|
t.Errorf("%s: part %d read %d times, want %d", c.name, i, opens[i], want)
|
|
}
|
|
}
|
|
whole := map[string][]byte{}
|
|
for i, ch := range got {
|
|
whole[ch.part] = append(whole[ch.part], ch.data...)
|
|
lastOfPart := i == len(got)-1 || got[i+1].part != ch.part
|
|
if ch.last != lastOfPart || (i > 0 && got[i-1].part == ch.part && got[i-1].seq+1 != ch.seq) {
|
|
t.Errorf("%s: chunk %d of %s is %d, last %v", c.name, i, ch.part[:12], ch.seq, ch.last)
|
|
}
|
|
}
|
|
for _, l := range c.lacks {
|
|
if p := parts[l]; int64(len(whole[p.Digest])) != p.Size {
|
|
t.Errorf("%s: part %d arrived as %d bytes of %d", c.name, l, len(whole[p.Digest]), p.Size)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A part whose bytes are not its digest travels not at all; a part the holder names that the transfer does not is
|
|
// refused, naming it.
|
|
liar := part(small, &opens[1])
|
|
liar.Digest = parts[0].Digest
|
|
var got []chunk
|
|
var asked []string
|
|
if _, err := Publish(context.Background(), holder([]string{liar.Digest}, &got, &asked), "oci-artifact-registry", in,
|
|
[]Part{liar}); err == nil || len(got) != 0 || !strings.Contains(err.Error(), "nothing of it is sent") {
|
|
t.Errorf("a part not its digest: %v, %d chunks sent", err, len(got))
|
|
}
|
|
if _, err := Publish(context.Background(), holder([]string{"sha256:" + strings.Repeat("f", 64)}, &got, &asked),
|
|
"oci-artifact-registry", in, parts); err == nil || !strings.Contains(err.Error(), "does not name") {
|
|
t.Errorf("a part the transfer does not name: %v", err)
|
|
}
|
|
}
|