package declaration import ( "bytes" "encoding/base64" "encoding/json" "reflect" "strings" "testing" ) func refFile(extra string) string { return `{"declaration":1,"resources":[{"id":"m.f","type":"file","path":"/etc/m","digest":"sha256:` + strings.Repeat("a", 64) + `","size":70000` + extra + `}]}` } // 1694/AC1: a reference round trips, and an invalid one is refused naming the field. func TestAFileReferenceRoundTripsAndABadOneNamesItsField(t *testing.T) { d, err := Parse([]byte(refFile(""))) if err != nil { t.Fatal(err) } f := d.Resources[0].(*File) if !f.IsReference() || f.Size != 70000 || f.Digest != "sha256:"+strings.Repeat("a", 64) { t.Fatalf("read: %+v", f) } raw, _ := json.Marshal(d.Resources[0]) d2, err := Parse([]byte(`{"declaration":1,"resources":[` + string(raw) + `]}`)) if err != nil || !reflect.DeepEqual(d2.Resources[0], d.Resources[0]) { t.Fatalf("round trip: %v %s", err, raw) } for want, raw := range map[string]string{ "digest": strings.Replace(refFile(""), strings.Repeat("a", 64), "abc", 1), "size": strings.Replace(refFile(""), `"size":70000`, `"size":0`, 1), "lower-case": strings.Replace(refFile(""), strings.Repeat("a", 64), strings.Repeat("A", 64), 1), "content": refFile(`,"content":"x"`), "bytes": refFile(`,"bytes":"eA=="`), "whole": refFile(`,"into":"block"`), } { _, err := Parse([]byte(raw)) if err == nil || !strings.Contains(err.Error(), want) { t.Errorf("want a refusal naming %q, got %v", want, err) } } // a size alone is not a reference if _, err := Parse([]byte(`{"declaration":1,"resources":[{"id":"m.f","type":"file","path":"/e","size":5}]}`)); err == nil { t.Error("a size without a digest was accepted") } } // 1694/AC2: a declaration as today's SDK writes it reads unchanged and its bytes carry no new key. func TestAnInlineFileIsReadAndWrittenAsBefore(t *testing.T) { const in = `{"id":"m.f","type":"file","path":"/e","content":"hello"}` d, err := Parse([]byte(`{"declaration":1,"resources":[` + in + `]}`)) if err != nil { t.Fatal(err) } out, _ := json.Marshal(d.Resources[0]) if string(out) != in { t.Fatalf("signed bytes changed: %s", out) } if d.Resources[0].(*File).IsReference() { t.Fatal("inline read as a reference") } } // 1694/AC3: the controller's helper makes a reference of a big file and the engine's check refuses a changed byte. func TestABigFileBecomesAReferenceAndAChangedByteFailsTheCheck(t *testing.T) { big := strings.Repeat("x", FileReferenceThreshold+1) f := &File{ID: "m.f", Type: TypeFile, Path: "/e", Content: big} if !f.TravelsByDigest() { t.Fatal("a file over the threshold stays inline") } ref, body, err := f.AsReference() if err != nil || ref.Content != "" || ref.Size != int64(len(big)) || string(body) != big { t.Fatalf("%v %+v", err, ref) } if p := ref.validate("f", false); len(p) > 0 { t.Fatalf("the reference is refused: %v", p) } if name, err := ObjectName(ref.Digest); err != nil || len(name) != 64 { t.Fatalf("object name %q %v", name, err) } if err := ref.Verify(body); err != nil { t.Fatal(err) } bad := bytes.Clone(body) bad[7] ^= 1 if err := ref.Verify(bad); err == nil || !strings.Contains(err.Error(), "hashes to") { t.Fatalf("a changed byte: %v", err) } if err := ref.Verify(body[1:]); err == nil || !strings.Contains(err.Error(), "bytes") { t.Fatalf("a short object: %v", err) } } // 1694/AC4: what stays inline, and bytes files travel decoded. func TestWhichFilesTravelByDigest(t *testing.T) { small := strings.Repeat("x", FileReferenceThreshold) big := strings.Repeat("x", FileReferenceThreshold+1) for name, f := range map[string]File{ "at the threshold": {Content: small}, "sealed": {Sealed: "s"}, "secrets": {Content: big, Secrets: map[string]string{"a": "b"}}, "into": {Content: big, Into: IntoBlock}, } { if f.TravelsByDigest() { t.Errorf("%s travels by digest", name) } if _, _, err := f.AsReference(); err == nil { t.Errorf("%s was made a reference", name) } } exact := File{ID: "m.e", Bytes: base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{7}, FileReferenceThreshold))} if exact.TravelsByDigest() { t.Error("a bytes file of exactly the threshold travels by digest") } if _, _, err := exact.AsReference(); err == nil { t.Error("a bytes file of exactly the threshold was made a reference") } raw := bytes.Repeat([]byte{0, 1, 2}, FileReferenceThreshold) f := File{ID: "m.b", Bytes: base64.StdEncoding.EncodeToString(raw)} ref, body, err := f.AsReference() if err != nil || !bytes.Equal(body, raw) || ref.Size != int64(len(raw)) || ref.Digest != DigestOf(raw) { t.Fatalf("bytes file: %v", err) } } func TestTheSharedNames(t *testing.T) { if FileBucket != "declaration-files" || FileReferencesCapability != "file-references" || FileReferenceThreshold != 65536 { t.Fatal("a name changed") } if _, err := ObjectName("sha256:zz"); err == nil { t.Fatal("bad digest named an object") } }