You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/capsule/open3_test.go

399 lines
16 KiB

Format 3, step 3: the reader Open reads format 3 (spec 29.2 to 29.7, 63 steps 17 and 18): the PRELUDE accepts VERSION 3, and the files go to a Sink. - Sink: Begin with the validated head, Create for each file in the order of the head, and Commit only after every check of step 17; after any failure that follows a successful Begin, Abort, once. A format 3 capsule without a Sink fails right after step 2 with ErrSinkRequired, a caller error with no code, no failed step and no request; a capsule of format 1 or 2 without dst fails there too. - Step 17 in its substeps: the frame and the area, security and its verdicts, which never fail, the head, the files filling CONTENT, the SHA-256 of each file and the padding. A failure of age or a plaintext whose length is not P prevails; otherwise the first substep that fails decides, and a code other than ERR_INTEGRITY is reported only after reading PAYLOAD_AGE to its end. - The reads of BODY grow with the bytes received, never with AREA_LEN, HEAD_LEN or a declared size (spec 57); a test measures it. - A failure of the Sink is the caller's own error with ERR_INTEGRITY, as one of dst is in formats 1 and 2. - Opened gains Head, Verdicts, AreaLen and UnusableHeadExtensions. - Test data: "version changed" sets VERSION 4, and the format 2 list gains "format 2 time_only relabeled format 3", which fails at step 14, as section 64 of spec v0.10 lists: 126 cases, 89 of the spec. The randomly built capsules keep their recorded bytes. - testkit: Build writes format 3 and can edit the padded plaintext; Head3, Body3, DiscardSink and MemorySink build and open BODY. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
package capsule_test
import (
"bytes"
"context"
"encoding/binary"
"errors"
"io"
"runtime"
"testing"
"filippo.io/age"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/extension"
"g.activething.com/go/DateKeys/internal/testkit"
)
// The tests of this file cover the reader of format 3 (spec v0.10): step 17,
// its substeps and their precedence, and step 18 (spec §29.2 to §29.7, §63).
// capsule3 describes a format 3 capsule of round 1000 to build, with edits
// at each level: the head, HEAD_CBOR, BODY, the plaintext with its padding,
// and PAYLOAD_AGE.
type capsule3 struct {
paths []string
contents [][]byte
comment, author string
security []byte // nil: the empty security that writers write
head func(h *capsule.Head)
headCBOR func(b []byte) []byte
body func(b []byte) []byte
plain func(p []byte) []byte
payload func(t *testing.T, p []byte) []byte
structure capsule.Policy
recipients []age.Recipient
}
// build returns the .dkc and its BODY, before any edit of the plaintext.
func (c capsule3) build(t *testing.T) ([]byte, []byte) {
t.Helper()
h := testkit.Head3(c.comment, c.author, c.paths, c.contents)
if c.head != nil {
c.head(h)
}
hb, err := capsule.EncodeHead(h)
if err != nil {
t.Fatal(err)
}
if c.headCBOR != nil {
hb = c.headCBOR(hb)
}
sec := c.security
if sec == nil {
sec = capsule.EncodeSecurity()
}
body := testkit.Body3(capsule.AreaUnit, sec, hb, c.contents...)
Format 3, step 3: the reader Open reads format 3 (spec 29.2 to 29.7, 63 steps 17 and 18): the PRELUDE accepts VERSION 3, and the files go to a Sink. - Sink: Begin with the validated head, Create for each file in the order of the head, and Commit only after every check of step 17; after any failure that follows a successful Begin, Abort, once. A format 3 capsule without a Sink fails right after step 2 with ErrSinkRequired, a caller error with no code, no failed step and no request; a capsule of format 1 or 2 without dst fails there too. - Step 17 in its substeps: the frame and the area, security and its verdicts, which never fail, the head, the files filling CONTENT, the SHA-256 of each file and the padding. A failure of age or a plaintext whose length is not P prevails; otherwise the first substep that fails decides, and a code other than ERR_INTEGRITY is reported only after reading PAYLOAD_AGE to its end. - The reads of BODY grow with the bytes received, never with AREA_LEN, HEAD_LEN or a declared size (spec 57); a test measures it. - A failure of the Sink is the caller's own error with ERR_INTEGRITY, as one of dst is in formats 1 and 2. - Opened gains Head, Verdicts, AreaLen and UnusableHeadExtensions. - Test data: "version changed" sets VERSION 4, and the format 2 list gains "format 2 time_only relabeled format 3", which fails at step 14, as section 64 of spec v0.10 lists: 126 cases, 89 of the spec. The randomly built capsules keep their recorded bytes. - testkit: Build writes format 3 and can edit the padded plaintext; Head3, Body3, DiscardSink and MemorySink build and open BODY. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
if c.body != nil {
body = c.body(body)
}
b, err := testkit.Build{Format: capsule.Format3, Declared: c.structure, Structure: c.structure,
AccessRecipients: c.recipients, Plaintext: body, EditPlaintext: c.plain}.Make()
if err != nil {
t.Fatal(err)
}
if c.payload == nil {
return b.DKC, body
}
return testkit.Join(b.Prelude[:], b.PublicHeader, b.Sealed, c.payload(t, bytes.Clone(b.Payload))), body
}
type result3 struct {
opened *capsule.Opened
sink *testkit.MemorySink
err error
unread int // bytes of the .dkc that Open did not read
}
// open3 opens a capsule of round 1000 into sink.
func open3(t *testing.T, dkc []byte, sink capsule.Sink, ids ...age.Identity) result3 {
t.Helper()
o := defaultOpen(1000)
o.Identities, o.Sink = ids, sink
r := bytes.NewReader(dkc)
opened, err := capsule.Open(context.Background(), nil, r, o)
res := result3{opened: opened, err: err, unread: r.Len()}
if m, ok := sink.(*testkit.MemorySink); ok {
res.sink = m
}
return res
}
// chunk returns the offset in the age file p of its STREAM chunk i: after
// the header come a 16-byte nonce and chunks of 64 KiB, each with a 16-byte
// tag (C2SP age.md).
func chunk(t *testing.T, p []byte, i int) int {
t.Helper()
n, err := testkit.HeaderLen(p)
if err != nil {
t.Fatal(err)
}
return n + 16 + i*(64<<10+16)
}
// Spec §29.2 to §29.7, §63 steps 17 and 18: a format 3 capsule opens, its
// files reach the Sink in the order of the head, and Commit comes last.
func TestOpen3(t *testing.T) {
photo := bytes.Repeat([]byte("playa"), 30000) // three STREAM chunks
c := capsule3{
paths: []string{"fotos/playa.jpg", "nota.txt", "vacío.txt"},
contents: [][]byte{photo, []byte("Hola.\n"), {}},
comment: "Para ti ❤\ufe0f", author: "Ana López",
Format 3, step 3: the reader Open reads format 3 (spec 29.2 to 29.7, 63 steps 17 and 18): the PRELUDE accepts VERSION 3, and the files go to a Sink. - Sink: Begin with the validated head, Create for each file in the order of the head, and Commit only after every check of step 17; after any failure that follows a successful Begin, Abort, once. A format 3 capsule without a Sink fails right after step 2 with ErrSinkRequired, a caller error with no code, no failed step and no request; a capsule of format 1 or 2 without dst fails there too. - Step 17 in its substeps: the frame and the area, security and its verdicts, which never fail, the head, the files filling CONTENT, the SHA-256 of each file and the padding. A failure of age or a plaintext whose length is not P prevails; otherwise the first substep that fails decides, and a code other than ERR_INTEGRITY is reported only after reading PAYLOAD_AGE to its end. - The reads of BODY grow with the bytes received, never with AREA_LEN, HEAD_LEN or a declared size (spec 57); a test measures it. - A failure of the Sink is the caller's own error with ERR_INTEGRITY, as one of dst is in formats 1 and 2. - Opened gains Head, Verdicts, AreaLen and UnusableHeadExtensions. - Test data: "version changed" sets VERSION 4, and the format 2 list gains "format 2 time_only relabeled format 3", which fails at step 14, as section 64 of spec v0.10 lists: 126 cases, 89 of the spec. The randomly built capsules keep their recorded bytes. - testkit: Build writes format 3 and can edit the padded plaintext; Head3, Body3, DiscardSink and MemorySink build and open BODY. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
}
dkc, body := c.build(t)
// Steps 1 to 8 need no Sink.
in, err := capsule.Inspect(bytes.NewReader(dkc), capsule.InspectOptions{Registry: testkit.Registry()})
if err != nil || in.Prelude.Format != capsule.Format3 {
t.Fatalf("Inspect: format %d, %v", in.Prelude.Format, err)
}
res := open3(t, dkc, &testkit.MemorySink{})
if res.err != nil {
t.Fatal(res.err)
}
o, s := res.opened, res.sink
if !s.Committed || s.Aborted {
t.Errorf("committed %v, aborted %v", s.Committed, s.Aborted)
}
for i, want := range c.contents {
if !bytes.Equal(s.Files[i], want) {
t.Errorf("file %d: %d bytes, want %d", i+1, len(s.Files[i]), len(want))
}
}
p, _ := capsule.PaddedLength(uint64(len(body)), capsule.Reforzado)
switch {
case o.Format != capsule.Format3 || o.AreaLen != capsule.AreaUnit:
Format 3, step 3: the reader Open reads format 3 (spec 29.2 to 29.7, 63 steps 17 and 18): the PRELUDE accepts VERSION 3, and the files go to a Sink. - Sink: Begin with the validated head, Create for each file in the order of the head, and Commit only after every check of step 17; after any failure that follows a successful Begin, Abort, once. A format 3 capsule without a Sink fails right after step 2 with ErrSinkRequired, a caller error with no code, no failed step and no request; a capsule of format 1 or 2 without dst fails there too. - Step 17 in its substeps: the frame and the area, security and its verdicts, which never fail, the head, the files filling CONTENT, the SHA-256 of each file and the padding. A failure of age or a plaintext whose length is not P prevails; otherwise the first substep that fails decides, and a code other than ERR_INTEGRITY is reported only after reading PAYLOAD_AGE to its end. - The reads of BODY grow with the bytes received, never with AREA_LEN, HEAD_LEN or a declared size (spec 57); a test measures it. - A failure of the Sink is the caller's own error with ERR_INTEGRITY, as one of dst is in formats 1 and 2. - Opened gains Head, Verdicts, AreaLen and UnusableHeadExtensions. - Test data: "version changed" sets VERSION 4, and the format 2 list gains "format 2 time_only relabeled format 3", which fails at step 14, as section 64 of spec v0.10 lists: 126 cases, 89 of the spec. The randomly built capsules keep their recorded bytes. - testkit: Build writes format 3 and can edit the padded plaintext; Head3, Body3, DiscardSink and MemorySink build and open BODY. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
t.Errorf("format %d, area %d", o.Format, o.AreaLen)
case o.PayloadLength != uint64(len(body)) || o.PaddedLength != p || o.Padding != capsule.Reforzado:
t.Errorf("L = %d, P = %d, padding %s; want %d, %d", o.PayloadLength, o.PaddedLength, o.Padding, len(body), p)
case o.Head == nil || o.Head != s.Head || o.Head.Comment != c.comment || o.Head.Author != c.author || len(o.Head.Files) != 3:
t.Errorf("head %+v", o.Head)
case o.Verdicts != capsule.Verdicts{Signature: capsule.VerdictNoSignature, Seal: capsule.VerdictNoSeal}:
t.Errorf("verdicts %+v", o.Verdicts)
}
checks := o.Inspection.Checks
if n := len(checks); n < 2 || checks[n-2].Step != 17 || !checks[n-2].OK || checks[n-1].Step != 18 || !checks[n-1].OK {
t.Errorf("last checks %+v", checks[len(checks)-2:])
}
// The examples of spec §29.2: L and P of three small capsules.
for _, tc := range []struct {
name string
c capsule3
l, p uint64
}{
{"no files and no comment", capsule3{}, 577, 768},
{"a comment of one byte", capsule3{comment: "a"}, 580, 768},
{"nota.txt of 1000 bytes, with mtime", capsule3{paths: []string{"nota.txt"}, contents: [][]byte{make([]byte, 1000)},
head: func(h *capsule.Head) { h.Files[0].MTime, h.Files[0].HasMTime = 1790000000, true }}, 1641, 1792},
} {
dkc, _ := tc.c.build(t)
res := open3(t, dkc, &testkit.MemorySink{})
if res.err != nil || res.opened.PayloadLength != tc.l || res.opened.PaddedLength != tc.p || !res.sink.Committed {
t.Errorf("%s: L = %d, P = %d, %v; want %d, %d", tc.name, res.opened.PayloadLength, res.opened.PaddedLength, res.err, tc.l, tc.p)
}
}
// time_and_key: format 3 has the 16 stanzas of format 2 (spec §29.1, §39).
id, _ := age.GenerateX25519Identity()
dkc, _ = capsule3{paths: []string{"a"}, contents: [][]byte{[]byte("x")},
structure: capsule.TimeAndKey, recipients: []age.Recipient{id.Recipient()}}.build(t)
if res := open3(t, dkc, &testkit.MemorySink{}, id); res.err != nil || string(res.sink.Files[0]) != "x" {
t.Errorf("time_and_key: %v", res.err)
}
}
// Spec §63 step 17: a failure of age, or a plaintext whose length is not P,
// prevails; otherwise the first substep that fails decides. A code other
// than ERR_INTEGRITY is reported only after reading PAYLOAD_AGE to its end.
// Nothing reaches Commit, and a Sink that got Begin gets Abort.
func TestOpen3Substeps(t *testing.T) {
two := capsule3{paths: []string{"a.txt", "b.txt"}, contents: [][]byte{[]byte("uno"), []byte("dos")}}
// With a big file the head is in STREAM chunk 0, and two chunks follow.
big := capsule3{paths: []string{"a.bin"}, contents: [][]byte{bytes.Repeat([]byte{0x5a}, 100000)}}
with := func(c capsule3, edit func(c *capsule3)) capsule3 { edit(&c); return c }
u32 := func(at int, v uint32) func(b []byte) []byte {
return func(b []byte) []byte { binary.BigEndian.PutUint32(b[at:], v); return b }
}
dotdot := func(h *capsule.Head) { h.Files[0].Path = ".." }
lastPadding := func(p []byte) []byte { p[len(p)-1] = 1; return p }
short := func(p []byte) []byte { return p[:len(p)-1] }
long := func(p []byte) []byte { return append(p, make([]byte, 256)...) }
areaByte := func(b []byte) []byte { b[capsule.BodyFrameSize+capsule.AreaUnit-1] = 1; return b }
Format 3, step 3: the reader Open reads format 3 (spec 29.2 to 29.7, 63 steps 17 and 18): the PRELUDE accepts VERSION 3, and the files go to a Sink. - Sink: Begin with the validated head, Create for each file in the order of the head, and Commit only after every check of step 17; after any failure that follows a successful Begin, Abort, once. A format 3 capsule without a Sink fails right after step 2 with ErrSinkRequired, a caller error with no code, no failed step and no request; a capsule of format 1 or 2 without dst fails there too. - Step 17 in its substeps: the frame and the area, security and its verdicts, which never fail, the head, the files filling CONTENT, the SHA-256 of each file and the padding. A failure of age or a plaintext whose length is not P prevails; otherwise the first substep that fails decides, and a code other than ERR_INTEGRITY is reported only after reading PAYLOAD_AGE to its end. - The reads of BODY grow with the bytes received, never with AREA_LEN, HEAD_LEN or a declared size (spec 57); a test measures it. - A failure of the Sink is the caller's own error with ERR_INTEGRITY, as one of dst is in formats 1 and 2. - Opened gains Head, Verdicts, AreaLen and UnusableHeadExtensions. - Test data: "version changed" sets VERSION 4, and the format 2 list gains "format 2 time_only relabeled format 3", which fails at step 14, as section 64 of spec v0.10 lists: 126 cases, 89 of the spec. The randomly built capsules keep their recorded bytes. - testkit: Build writes format 3 and can edit the padded plaintext; Head3, Body3, DiscardSink and MemorySink build and open BODY. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
// HEAD_CBOR starts with the map, key 0, the text header and the 13 bytes
// of "datekeys-head", then key 1 and the version.
const headTag, headVersion = 3, 17
for _, tc := range []struct {
name string
c capsule3
want error // nil: opens
begun bool // the Sink got Begin, and so Abort
eof bool // Open read the .dkc to its end
}{
// 17.2: the frame and the area.
{"AREA_LEN 511", with(two, func(c *capsule3) { c.body = u32(0, 511) }), datekeys.ErrIntegrity, false, false},
{"AREA_LEN 513", with(two, func(c *capsule3) { c.body = u32(0, 513) }), datekeys.ErrIntegrity, false, false},
{"AREA_LEN 66048", with(two, func(c *capsule3) { c.body = u32(0, 66048) }), datekeys.ErrIntegrity, false, false},
{"SECURITY_LEN 0", with(two, func(c *capsule3) { c.body = u32(4, 0) }), datekeys.ErrIntegrity, false, false},
{"SECURITY_LEN 513", with(two, func(c *capsule3) { c.body = u32(4, 513) }), datekeys.ErrIntegrity, false, false},
{"HEAD_LEN 0", with(two, func(c *capsule3) { c.body = u32(8, 0) }), datekeys.ErrIntegrity, false, false},
{"HEAD_LEN 2^24 + 1", with(two, func(c *capsule3) { c.body = u32(8, 1<<24+1) }), datekeys.ErrIntegrity, false, false},
{"12 + AREA_LEN + HEAD_LEN = L + 1", with(capsule3{}, func(c *capsule3) { c.body = short }), datekeys.ErrIntegrity, false, false},
{"L < 12", with(two, func(c *capsule3) { c.body = func(b []byte) []byte { return b[:11] } }), datekeys.ErrIntegrity, false, false},
{"a byte of the area not zero", with(two, func(c *capsule3) { c.body = areaByte }), datekeys.ErrIntegrity, false, false},
{"a byte of the area not zero, and path ..", with(two, func(c *capsule3) { c.head, c.body = dotdot, areaByte }), datekeys.ErrIntegrity, false, false},
// 17.3 and 17.6: security never fails.
{"security unreadable", with(two, func(c *capsule3) { c.security = []byte{0xa0} }), nil, false, true},
// 17.4: the head, whose codes wait for the end of PAYLOAD_AGE.
{"head of version 2", with(two, func(c *capsule3) {
c.headCBOR = func(b []byte) []byte { b[headVersion] = 2; return b }
}), datekeys.ErrUnsupportedVersion, false, true},
{"head of another type tag", with(two, func(c *capsule3) {
c.headCBOR = func(b []byte) []byte { b[headTag] = 'D'; return b }
}), datekeys.ErrNonCanonicalCBOR, false, true},
{"head with one byte more within HEAD_LEN", with(two, func(c *capsule3) {
c.headCBOR = func(b []byte) []byte { return append(b, 0) }
}), datekeys.ErrNonCanonicalCBOR, false, true},
{"paths b and a, in that order", with(two, func(c *capsule3) { c.paths = []string{"b.txt", "a.txt"} }), datekeys.ErrNonCanonicalCBOR, false, true},
{"path ..", with(two, func(c *capsule3) { c.head = dotdot }), datekeys.ErrHeadInvalid, false, true},
{"paths A.txt and a.txt", with(two, func(c *capsule3) { c.paths = []string{"A.txt", "a.txt"} }), datekeys.ErrHeadInvalid, false, true},
Format 3, step 4: the writer EncryptFiles writes format 3 (spec 29.2 to 29.6, 61, 62, 62.1): the files of a list of Sources, each read twice, with the comment and the declared author. - Before anything is written: the paths and the texts are checked with the rules of the reader, in the words of a writer, naming the rule and the character, and the two paths of an R7 collision (rule 15); the comment has its CR LF and lone CR turned into LF (29.6); L is measured with a head whose salt and SHA-256 are zero, as long as the final one, and the first reading hashes each file, which must have exactly its Size. - The files go in the byte order of their paths (R8), whatever the order of the Sources; the mtime is kept only from 1970 to 9999, never clipped (rule 16); at least one file or a comment (rule 14). - The head, with a fresh salt, the control and the security area are decoded with the rules of the reader before sealing (rule 17), and the frame is checked against L. The area is 512 bytes with the empty security, whatever the options (rule 13). - The second reading writes each file into PAYLOAD_AGE and fails if its size or SHA-256 changed (rule 18). - Encrypt and EncryptFiles share the sealing; Encrypt writes format 2 only with the new TestVectors option (rule 1), and takes no head. The test data generators set it, and so does the CLI until step 5 moves it to EncryptFiles. - Result.Head is the head written. DecodeHead keeps the check of the critical extensions apart, so that the self-check decodes the head as the one of the control does. - The examples and the live test write with EncryptFiles. - The reader tests had a literal U+202E, now escaped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
{"comment with U+202E", with(two, func(c *capsule3) { c.comment = "a\u202eb" }), datekeys.ErrHeadInvalid, false, true},
Format 3, step 3: the reader Open reads format 3 (spec 29.2 to 29.7, 63 steps 17 and 18): the PRELUDE accepts VERSION 3, and the files go to a Sink. - Sink: Begin with the validated head, Create for each file in the order of the head, and Commit only after every check of step 17; after any failure that follows a successful Begin, Abort, once. A format 3 capsule without a Sink fails right after step 2 with ErrSinkRequired, a caller error with no code, no failed step and no request; a capsule of format 1 or 2 without dst fails there too. - Step 17 in its substeps: the frame and the area, security and its verdicts, which never fail, the head, the files filling CONTENT, the SHA-256 of each file and the padding. A failure of age or a plaintext whose length is not P prevails; otherwise the first substep that fails decides, and a code other than ERR_INTEGRITY is reported only after reading PAYLOAD_AGE to its end. - The reads of BODY grow with the bytes received, never with AREA_LEN, HEAD_LEN or a declared size (spec 57); a test measures it. - A failure of the Sink is the caller's own error with ERR_INTEGRITY, as one of dst is in formats 1 and 2. - Opened gains Head, Verdicts, AreaLen and UnusableHeadExtensions. - Test data: "version changed" sets VERSION 4, and the format 2 list gains "format 2 time_only relabeled format 3", which fails at step 14, as section 64 of spec v0.10 lists: 126 cases, 89 of the spec. The randomly built capsules keep their recorded bytes. - testkit: Build writes format 3 and can edit the padded plaintext; Head3, Body3, DiscardSink and MemorySink build and open BODY. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
{"start of an entry not the end of the one before", with(two, func(c *capsule3) {
c.head = func(h *capsule.Head) { h.Files[1].Start, h.Files[1].End = 2, 5 }
}), datekeys.ErrHeadInvalid, false, true},
{"an unknown critical extension", with(two, func(c *capsule3) {
c.head = func(h *capsule.Head) { h.Critical = []extension.Extension{{ID: "x.head", Version: 1}} }
}), datekeys.ErrExtensionCriticalUnknown, false, true},
// 17.5: the files fill CONTENT.
{"end of the last file not C", with(two, func(c *capsule3) {
c.body = func(b []byte) []byte { return append(b, 'x') }
}), datekeys.ErrIntegrity, false, false},
// 17.7: the SHA-256 of each file, and 17.8: the padding, up to P.
{"a byte of a file changed", with(two, func(c *capsule3) {
c.body = func(b []byte) []byte { b[len(b)-1] ^= 1; return b }
}), datekeys.ErrIntegrity, true, false},
{"a padding byte not zero", with(two, func(c *capsule3) { c.plain = lastPadding }), datekeys.ErrIntegrity, true, false},
{"plaintext of P - 1 bytes", with(two, func(c *capsule3) { c.plain = short }), datekeys.ErrIntegrity, true, true},
{"plaintext of P + 256 bytes", with(two, func(c *capsule3) { c.plain = long }), datekeys.ErrIntegrity, true, false},
// Precedence: the head fails first, and what follows decides only when
// it is a failure of age or of the length.
{"path .. and a padding byte not zero", with(two, func(c *capsule3) { c.head, c.plain = dotdot, lastPadding }), datekeys.ErrHeadInvalid, false, true},
{"path .. and a plaintext of P - 1 bytes", with(two, func(c *capsule3) { c.head, c.plain = dotdot, short }), datekeys.ErrIntegrity, false, true},
{"path .. and a plaintext of P + 256 bytes", with(two, func(c *capsule3) { c.head, c.plain = dotdot, long }), datekeys.ErrIntegrity, false, true},
{"path .. and the next STREAM chunk corrupt", with(big, func(c *capsule3) {
c.head = dotdot
c.payload = func(t *testing.T, p []byte) []byte { p[chunk(t, p, 1)+100] ^= 1; return p }
}), datekeys.ErrIntegrity, false, false},
{"path .. and a cut right after its chunk", with(big, func(c *capsule3) {
c.head = dotdot
c.payload = func(t *testing.T, p []byte) []byte { return p[:chunk(t, p, 1)] }
}), datekeys.ErrIntegrity, false, true},
{"path .. and a big file", with(big, func(c *capsule3) { c.head = dotdot }), datekeys.ErrHeadInvalid, false, true},
} {
dkc, _ := tc.c.build(t)
res := open3(t, dkc, &testkit.MemorySink{})
o, s := res.opened, res.sink
if tc.want == nil {
if res.err != nil || !s.Committed || o.Verdicts.Signature != capsule.VerdictUnreadable || len(o.Verdicts.Lines()) != 1 {
t.Errorf("%s: %v, verdicts %+v", tc.name, res.err, o.Verdicts)
}
continue
}
step := failedStep(t, o.Inspection.Checks, res.err)
expectStep(t, tc.name, step, res.err, tc.want, 17)
switch {
case s.Committed || o.Head != nil:
t.Errorf("%s: committed %v, head %v", tc.name, s.Committed, o.Head)
case (s.Head != nil) != tc.begun || s.Aborted != tc.begun:
t.Errorf("%s: begun %v, aborted %v, want %v", tc.name, s.Head != nil, s.Aborted, tc.begun)
case tc.eof && res.unread != 0:
t.Errorf("%s: %d bytes of the .dkc not read", tc.name, res.unread)
}
}
}
// failSink fails at one point: "begin", "create", "write", "close" or
// "commit", at file 2 for "create", "write" and "close".
type failSink struct {
testkit.MemorySink
at string
aborts int
}
var errDiskFull = errors.New("disk full")
func (s *failSink) Begin(h *capsule.Head) error {
if s.at == "begin" {
return errDiskFull
}
return s.MemorySink.Begin(h)
}
func (s *failSink) Create(i int) (io.WriteCloser, error) {
if s.at == "create" && i == 1 {
return nil, errDiskFull
}
w, err := s.MemorySink.Create(i)
return &failWriter{WriteCloser: w, fail: i == 1, at: s.at}, err
}
func (s *failSink) Commit() error {
if s.at == "commit" {
return errDiskFull
}
return s.MemorySink.Commit()
}
func (s *failSink) Abort() { s.aborts++; s.MemorySink.Abort() }
type failWriter struct {
io.WriteCloser
fail bool
at string
}
func (w *failWriter) Write(b []byte) (int, error) {
if w.fail && w.at == "write" {
return 0, errDiskFull
}
return w.WriteCloser.Write(b)
}
func (w *failWriter) Close() error {
if w.fail && w.at == "close" {
return errDiskFull
}
return w.WriteCloser.Close()
}
// A failure of the Sink is the caller's own error with ERR_INTEGRITY, as a
// failure of dst is in formats 1 and 2, and after Begin it gets Abort, once.
func TestOpen3SinkFailures(t *testing.T) {
dkc, _ := capsule3{paths: []string{"a.txt", "b.txt"}, contents: [][]byte{[]byte("uno"), []byte("dos")}}.build(t)
for _, at := range []string{"begin", "create", "write", "close", "commit"} {
s := &failSink{at: at}
res := open3(t, dkc, s)
wantAborts := 1
if at == "begin" {
wantAborts = 0
}
switch {
case !errors.Is(res.err, errDiskFull) || datekeys.Code(res.err) != datekeys.Code(datekeys.ErrIntegrity):
t.Errorf("%s: %v", at, res.err)
case s.aborts != wantAborts || s.Committed || res.opened.Head != nil:
t.Errorf("%s: %d aborts, committed %v", at, s.aborts, s.Committed)
}
}
if res := open3(t, dkc, &failSink{}); res.err != nil {
t.Fatal(res.err)
}
}
// Spec §57: a reader reserves no memory by what BODY declares before it
// receives the bytes. Each capsule declares 16 MiB, in HEAD_LEN or in the
// size of a file, and holds a plaintext of 2000 bytes; Open fails, and
// allocates about what a small capsule makes it allocate.
func TestOpen3DeclaredLengths(t *testing.T) {
const declared = 16 << 20
cut := func(p []byte) []byte { return p[:2000] }
for _, tc := range []struct {
name string
c capsule3
}{
{"HEAD_LEN", capsule3{plain: cut, body: func(b []byte) []byte {
binary.BigEndian.PutUint32(b[8:], declared)
return append(b, make([]byte, declared)...)
}}},
{"size", capsule3{plain: cut, paths: []string{"a.bin"}, contents: [][]byte{make([]byte, declared)}}},
} {
dkc, _ := tc.c.build(t)
var before, after runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&before)
res := open3(t, dkc, &testkit.MemorySink{})
runtime.ReadMemStats(&after)
if n := after.TotalAlloc - before.TotalAlloc; n > 4<<20 || !errors.Is(res.err, datekeys.ErrIntegrity) {
t.Errorf("%s: %d bytes allocated, %v", tc.name, n, res.err)
}
}
}

Powered by TurnKey Linux.