From 84ca397e65c7c6626a8e71ae7f75d46a9652509f Mon Sep 17 00:00:00 2001 From: Adam Hughes <9903835+tri-adam@users.noreply.github.com> Date: Tue, 18 Jun 2024 19:04:11 +0000 Subject: [PATCH] refactor: split add/del/set code into files --- pkg/sif/add.go | 81 +++++++ pkg/sif/add_test.go | 155 +++++++++++++ pkg/sif/create.go | 376 ------------------------------ pkg/sif/create_test.go | 507 ----------------------------------------- pkg/sif/delete.go | 168 ++++++++++++++ pkg/sif/delete_test.go | 197 ++++++++++++++++ pkg/sif/set.go | 169 ++++++++++++++ pkg/sif/set_test.go | 197 ++++++++++++++++ 8 files changed, 967 insertions(+), 883 deletions(-) create mode 100644 pkg/sif/add.go create mode 100644 pkg/sif/add_test.go create mode 100644 pkg/sif/delete.go create mode 100644 pkg/sif/delete_test.go create mode 100644 pkg/sif/set.go create mode 100644 pkg/sif/set_test.go diff --git a/pkg/sif/add.go b/pkg/sif/add.go new file mode 100644 index 00000000..db1bd6b4 --- /dev/null +++ b/pkg/sif/add.go @@ -0,0 +1,81 @@ +// Copyright (c) 2018-2023, Sylabs Inc. All rights reserved. +// Copyright (c) 2017, SingularityWare, LLC. All rights reserved. +// Copyright (c) 2017, Yannick Cote All rights reserved. +// This software is licensed under a 3-clause BSD license. Please consult the +// LICENSE file distributed with the sources of this project regarding your +// rights to use or distribute this software. + +package sif + +import ( + "fmt" + "time" +) + +// addOpts accumulates object add options. +type addOpts struct { + t time.Time +} + +// AddOpt are used to specify object add options. +type AddOpt func(*addOpts) error + +// OptAddDeterministic sets header/descriptor fields to values that support deterministic +// modification of images. +func OptAddDeterministic() AddOpt { + return func(ao *addOpts) error { + ao.t = time.Time{} + return nil + } +} + +// OptAddWithTime specifies t as the image modification time. +func OptAddWithTime(t time.Time) AddOpt { + return func(ao *addOpts) error { + ao.t = t + return nil + } +} + +// AddObject adds a new data object and its descriptor into the specified SIF file. +// +// By default, the image modification time is set to the current time for non-deterministic images, +// and unset otherwise. To override this, consider using OptAddDeterministic or OptAddWithTime. +func (f *FileImage) AddObject(di DescriptorInput, opts ...AddOpt) error { + ao := addOpts{} + + if !f.isDeterministic() { + ao.t = time.Now() + } + + for _, opt := range opts { + if err := opt(&ao); err != nil { + return fmt.Errorf("%w", err) + } + } + + // Find an unused descriptor. + i := 0 + for _, rd := range f.rds { + if !rd.Used { + break + } + i++ + } + + if err := f.writeDataObject(i, di, ao.t); err != nil { + return fmt.Errorf("%w", err) + } + + if err := f.writeDescriptors(); err != nil { + return fmt.Errorf("%w", err) + } + + f.h.ModifiedAt = ao.t.Unix() + + if err := f.writeHeader(); err != nil { + return fmt.Errorf("%w", err) + } + + return nil +} diff --git a/pkg/sif/add_test.go b/pkg/sif/add_test.go new file mode 100644 index 00000000..4f88ae1a --- /dev/null +++ b/pkg/sif/add_test.go @@ -0,0 +1,155 @@ +// Copyright (c) 2018-2023, Sylabs Inc. All rights reserved. +// This software is licensed under a 3-clause BSD license. Please consult the +// LICENSE file distributed with the sources of this project regarding your +// rights to use or distribute this software. + +package sif + +import ( + "errors" + "testing" + "time" + + "github.com/sebdah/goldie/v2" +) + +func TestAddObject(t *testing.T) { + tests := []struct { + name string + createOpts []CreateOpt + di DescriptorInput + opts []AddOpt + wantErr error + }{ + { + name: "ErrInsufficientCapacity", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + OptCreateWithDescriptorCapacity(0), + }, + di: getDescriptorInput(t, DataGeneric, []byte{0xfe, 0xed}), + wantErr: errInsufficientCapacity, + }, + { + name: "ErrPrimaryPartition", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + OptCreateWithDescriptors( + getDescriptorInput(t, DataPartition, []byte{0xfa, 0xce}, + OptPartitionMetadata(FsSquash, PartPrimSys, "386"), + ), + ), + }, + di: getDescriptorInput(t, DataPartition, []byte{0xfe, 0xed}, + OptPartitionMetadata(FsSquash, PartPrimSys, "amd64"), + ), + wantErr: errPrimaryPartition, + }, + { + name: "Deterministic", + createOpts: []CreateOpt{ + OptCreateWithID("de170c43-36ab-44a8-bca9-1ea1a070a274"), + OptCreateWithTime(time.Unix(946702800, 0)), + }, + di: getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), + opts: []AddOpt{ + OptAddDeterministic(), + }, + }, + { + name: "WithTime", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + }, + di: getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), + opts: []AddOpt{ + OptAddWithTime(time.Unix(946702800, 0)), + }, + }, + { + name: "Empty", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + }, + di: getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), + }, + { + name: "EmptyNotAligned", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + }, + di: getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}, + OptObjectAlignment(0), + ), + }, + { + name: "EmptyAligned", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + }, + di: getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}, + OptObjectAlignment(128), + ), + }, + { + name: "NotEmpty", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + OptCreateWithDescriptors( + getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), + ), + }, + di: getDescriptorInput(t, DataPartition, []byte{0xfe, 0xed}, + OptPartitionMetadata(FsSquash, PartPrimSys, "386"), + ), + }, + { + name: "NotEmptyNotAligned", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + OptCreateWithDescriptors( + getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), + ), + }, + di: getDescriptorInput(t, DataPartition, []byte{0xfe, 0xed}, + OptPartitionMetadata(FsSquash, PartPrimSys, "386"), + OptObjectAlignment(0), + ), + }, + { + name: "NotEmptyAligned", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + OptCreateWithDescriptors( + getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), + ), + }, + di: getDescriptorInput(t, DataPartition, []byte{0xfe, 0xed}, + OptPartitionMetadata(FsSquash, PartPrimSys, "386"), + OptObjectAlignment(128), + ), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var b Buffer + + f, err := CreateContainer(&b, tt.createOpts...) + if err != nil { + t.Fatal(err) + } + + if got, want := f.AddObject(tt.di, tt.opts...), tt.wantErr; !errors.Is(got, want) { + t.Errorf("got error %v, want %v", got, want) + } + + if err := f.UnloadContainer(); err != nil { + t.Error(err) + } + + g := goldie.New(t, goldie.WithTestNameForDir(true)) + g.Assert(t, tt.name, b.Bytes()) + }) + } +} diff --git a/pkg/sif/create.go b/pkg/sif/create.go index be3f6dc1..0f41e0a9 100644 --- a/pkg/sif/create.go +++ b/pkg/sif/create.go @@ -8,7 +8,6 @@ package sif import ( - "encoding" "encoding/binary" "errors" "fmt" @@ -321,378 +320,3 @@ func CreateContainerAtPath(path string, opts ...CreateOpt) (*FileImage, error) { f.closeOnUnload = true return f, nil } - -// addOpts accumulates object add options. -type addOpts struct { - t time.Time -} - -// AddOpt are used to specify object add options. -type AddOpt func(*addOpts) error - -// OptAddDeterministic sets header/descriptor fields to values that support deterministic -// modification of images. -func OptAddDeterministic() AddOpt { - return func(ao *addOpts) error { - ao.t = time.Time{} - return nil - } -} - -// OptAddWithTime specifies t as the image modification time. -func OptAddWithTime(t time.Time) AddOpt { - return func(ao *addOpts) error { - ao.t = t - return nil - } -} - -// AddObject adds a new data object and its descriptor into the specified SIF file. -// -// By default, the image modification time is set to the current time for non-deterministic images, -// and unset otherwise. To override this, consider using OptAddDeterministic or OptAddWithTime. -func (f *FileImage) AddObject(di DescriptorInput, opts ...AddOpt) error { - ao := addOpts{} - - if !f.isDeterministic() { - ao.t = time.Now() - } - - for _, opt := range opts { - if err := opt(&ao); err != nil { - return fmt.Errorf("%w", err) - } - } - - // Find an unused descriptor. - i := 0 - for _, rd := range f.rds { - if !rd.Used { - break - } - i++ - } - - if err := f.writeDataObject(i, di, ao.t); err != nil { - return fmt.Errorf("%w", err) - } - - if err := f.writeDescriptors(); err != nil { - return fmt.Errorf("%w", err) - } - - f.h.ModifiedAt = ao.t.Unix() - - if err := f.writeHeader(); err != nil { - return fmt.Errorf("%w", err) - } - - return nil -} - -// isLast return true if the data object associated with d is the last in f. -func (f *FileImage) isLast(d *rawDescriptor) bool { - isLast := true - - end := d.Offset + d.Size - f.WithDescriptors(func(d Descriptor) bool { - isLast = d.Offset()+d.Size() <= end - return !isLast - }) - - return isLast -} - -// zeroReader is an io.Reader that returns a stream of zero-bytes. -type zeroReader struct{} - -func (zeroReader) Read(b []byte) (int, error) { - for i := range b { - b[i] = 0 - } - return len(b), nil -} - -// zero overwrites the data object described by d with a stream of zero bytes. -func (f *FileImage) zero(d *rawDescriptor) error { - if _, err := f.rw.Seek(d.Offset, io.SeekStart); err != nil { - return err - } - - _, err := io.CopyN(f.rw, zeroReader{}, d.Size) - return err -} - -// truncateAt truncates f at the start of the padded data object described by d. -func (f *FileImage) truncateAt(d *rawDescriptor) error { - start := d.Offset + d.Size - d.SizeWithPadding - - return f.rw.Truncate(start) -} - -// deleteOpts accumulates object deletion options. -type deleteOpts struct { - zero bool - compact bool - t time.Time -} - -// DeleteOpt are used to specify object deletion options. -type DeleteOpt func(*deleteOpts) error - -// OptDeleteZero specifies whether the deleted object should be zeroed. -func OptDeleteZero(b bool) DeleteOpt { - return func(do *deleteOpts) error { - do.zero = b - return nil - } -} - -// OptDeleteCompact specifies whether the image should be compacted following object deletion. -func OptDeleteCompact(b bool) DeleteOpt { - return func(do *deleteOpts) error { - do.compact = b - return nil - } -} - -// OptDeleteDeterministic sets header/descriptor fields to values that support deterministic -// modification of images. -func OptDeleteDeterministic() DeleteOpt { - return func(do *deleteOpts) error { - do.t = time.Time{} - return nil - } -} - -// OptDeleteWithTime specifies t as the image modification time. -func OptDeleteWithTime(t time.Time) DeleteOpt { - return func(do *deleteOpts) error { - do.t = t - return nil - } -} - -var errCompactNotImplemented = errors.New("compact not implemented for non-last object") - -// DeleteObject deletes the data object with id, according to opts. -// -// To zero the data region of the deleted object, use OptDeleteZero. To compact the file following -// object deletion, use OptDeleteCompact. -// -// By default, the image modification time is set to the current time for non-deterministic images, -// and unset otherwise. To override this, consider using OptDeleteDeterministic or -// OptDeleteWithTime. -func (f *FileImage) DeleteObject(id uint32, opts ...DeleteOpt) error { - do := deleteOpts{} - - if !f.isDeterministic() { - do.t = time.Now() - } - - for _, opt := range opts { - if err := opt(&do); err != nil { - return fmt.Errorf("%w", err) - } - } - - d, err := f.getDescriptor(WithID(id)) - if err != nil { - return fmt.Errorf("%w", err) - } - - if do.compact && !f.isLast(d) { - return fmt.Errorf("%w", errCompactNotImplemented) - } - - if do.zero { - if err := f.zero(d); err != nil { - return fmt.Errorf("%w", err) - } - } - - if do.compact { - if err := f.truncateAt(d); err != nil { - return fmt.Errorf("%w", err) - } - - f.h.DataSize -= d.SizeWithPadding - } - - f.h.DescriptorsFree++ - f.h.ModifiedAt = do.t.Unix() - - // If we remove the primary partition, set the global header Arch field to HdrArchUnknown - // to indicate that the SIF file doesn't include a primary partition and no dependency - // on any architecture exists. - if d.isPartitionOfType(PartPrimSys) { - f.h.Arch = hdrArchUnknown - } - - // Reset rawDescripter with empty struct - *d = rawDescriptor{} - - if err := f.writeDescriptors(); err != nil { - return fmt.Errorf("%w", err) - } - - if err := f.writeHeader(); err != nil { - return fmt.Errorf("%w", err) - } - - return nil -} - -// setOpts accumulates object set options. -type setOpts struct { - t time.Time -} - -// SetOpt are used to specify object set options. -type SetOpt func(*setOpts) error - -// OptSetDeterministic sets header/descriptor fields to values that support deterministic -// modification of images. -func OptSetDeterministic() SetOpt { - return func(so *setOpts) error { - so.t = time.Time{} - return nil - } -} - -// OptSetWithTime specifies t as the image/object modification time. -func OptSetWithTime(t time.Time) SetOpt { - return func(so *setOpts) error { - so.t = t - return nil - } -} - -var ( - errNotPartition = errors.New("data object not a partition") - errNotSystem = errors.New("data object not a system partition") -) - -// SetPrimPart sets the specified system partition to be the primary one. -// -// By default, the image/object modification times are set to the current time for -// non-deterministic images, and unset otherwise. To override this, consider using -// OptSetDeterministic or OptSetWithTime. -func (f *FileImage) SetPrimPart(id uint32, opts ...SetOpt) error { - so := setOpts{} - - if !f.isDeterministic() { - so.t = time.Now() - } - - for _, opt := range opts { - if err := opt(&so); err != nil { - return fmt.Errorf("%w", err) - } - } - - descr, err := f.getDescriptor(WithID(id)) - if err != nil { - return fmt.Errorf("%w", err) - } - - if descr.DataType != DataPartition { - return fmt.Errorf("%w", errNotPartition) - } - - var p partition - if err := descr.getExtra(binaryUnmarshaler{&p}); err != nil { - return fmt.Errorf("%w", err) - } - - // if already primary system partition, nothing to do - if p.Parttype == PartPrimSys { - return nil - } - - if p.Parttype != PartSystem { - return fmt.Errorf("%w", errNotSystem) - } - - // If there is currently a primary system partition, update it. - if d, err := f.getDescriptor(WithPartitionType(PartPrimSys)); err == nil { - var p partition - if err := d.getExtra(binaryUnmarshaler{&p}); err != nil { - return fmt.Errorf("%w", err) - } - - p.Parttype = PartSystem - - if err := d.setExtra(p); err != nil { - return fmt.Errorf("%w", err) - } - - d.ModifiedAt = so.t.Unix() - } else if !errors.Is(err, ErrObjectNotFound) { - return fmt.Errorf("%w", err) - } - - // Update the descriptor of the new primary system partition. - p.Parttype = PartPrimSys - - if err := descr.setExtra(p); err != nil { - return fmt.Errorf("%w", err) - } - - descr.ModifiedAt = so.t.Unix() - - if err := f.writeDescriptors(); err != nil { - return fmt.Errorf("%w", err) - } - - f.h.Arch = p.Arch - f.h.ModifiedAt = so.t.Unix() - - if err := f.writeHeader(); err != nil { - return fmt.Errorf("%w", err) - } - - return nil -} - -// SetMetadata sets the metadata of the data object with id to md, according to opts. -// -// By default, the image/object modification times are set to the current time for -// non-deterministic images, and unset otherwise. To override this, consider using -// OptSetDeterministic or OptSetWithTime. -func (f *FileImage) SetMetadata(id uint32, md encoding.BinaryMarshaler, opts ...SetOpt) error { - so := setOpts{} - - if !f.isDeterministic() { - so.t = time.Now() - } - - for _, opt := range opts { - if err := opt(&so); err != nil { - return fmt.Errorf("%w", err) - } - } - - rd, err := f.getDescriptor(WithID(id)) - if err != nil { - return fmt.Errorf("%w", err) - } - - if err := rd.setExtra(md); err != nil { - return fmt.Errorf("%w", err) - } - - rd.ModifiedAt = so.t.Unix() - - if err := f.writeDescriptors(); err != nil { - return fmt.Errorf("%w", err) - } - - f.h.ModifiedAt = so.t.Unix() - - if err := f.writeHeader(); err != nil { - return fmt.Errorf("%w", err) - } - - return nil -} diff --git a/pkg/sif/create_test.go b/pkg/sif/create_test.go index f12e2b8d..fec085f0 100644 --- a/pkg/sif/create_test.go +++ b/pkg/sif/create_test.go @@ -286,510 +286,3 @@ func TestCreateContainerAtPath(t *testing.T) { }) } } - -func TestAddObject(t *testing.T) { - tests := []struct { - name string - createOpts []CreateOpt - di DescriptorInput - opts []AddOpt - wantErr error - }{ - { - name: "ErrInsufficientCapacity", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - OptCreateWithDescriptorCapacity(0), - }, - di: getDescriptorInput(t, DataGeneric, []byte{0xfe, 0xed}), - wantErr: errInsufficientCapacity, - }, - { - name: "ErrPrimaryPartition", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - OptCreateWithDescriptors( - getDescriptorInput(t, DataPartition, []byte{0xfa, 0xce}, - OptPartitionMetadata(FsSquash, PartPrimSys, "386"), - ), - ), - }, - di: getDescriptorInput(t, DataPartition, []byte{0xfe, 0xed}, - OptPartitionMetadata(FsSquash, PartPrimSys, "amd64"), - ), - wantErr: errPrimaryPartition, - }, - { - name: "Deterministic", - createOpts: []CreateOpt{ - OptCreateWithID("de170c43-36ab-44a8-bca9-1ea1a070a274"), - OptCreateWithTime(time.Unix(946702800, 0)), - }, - di: getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), - opts: []AddOpt{ - OptAddDeterministic(), - }, - }, - { - name: "WithTime", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - }, - di: getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), - opts: []AddOpt{ - OptAddWithTime(time.Unix(946702800, 0)), - }, - }, - { - name: "Empty", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - }, - di: getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), - }, - { - name: "EmptyNotAligned", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - }, - di: getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}, - OptObjectAlignment(0), - ), - }, - { - name: "EmptyAligned", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - }, - di: getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}, - OptObjectAlignment(128), - ), - }, - { - name: "NotEmpty", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - OptCreateWithDescriptors( - getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), - ), - }, - di: getDescriptorInput(t, DataPartition, []byte{0xfe, 0xed}, - OptPartitionMetadata(FsSquash, PartPrimSys, "386"), - ), - }, - { - name: "NotEmptyNotAligned", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - OptCreateWithDescriptors( - getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), - ), - }, - di: getDescriptorInput(t, DataPartition, []byte{0xfe, 0xed}, - OptPartitionMetadata(FsSquash, PartPrimSys, "386"), - OptObjectAlignment(0), - ), - }, - { - name: "NotEmptyAligned", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - OptCreateWithDescriptors( - getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), - ), - }, - di: getDescriptorInput(t, DataPartition, []byte{0xfe, 0xed}, - OptPartitionMetadata(FsSquash, PartPrimSys, "386"), - OptObjectAlignment(128), - ), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var b Buffer - - f, err := CreateContainer(&b, tt.createOpts...) - if err != nil { - t.Fatal(err) - } - - if got, want := f.AddObject(tt.di, tt.opts...), tt.wantErr; !errors.Is(got, want) { - t.Errorf("got error %v, want %v", got, want) - } - - if err := f.UnloadContainer(); err != nil { - t.Error(err) - } - - g := goldie.New(t, goldie.WithTestNameForDir(true)) - g.Assert(t, tt.name, b.Bytes()) - }) - } -} - -func TestDeleteObject(t *testing.T) { - tests := []struct { - name string - createOpts []CreateOpt - id uint32 - opts []DeleteOpt - wantErr error - }{ - { - name: "ErrObjectNotFound", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - }, - id: 1, - wantErr: ErrObjectNotFound, - }, - { - name: "Zero", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - OptCreateWithDescriptors( - getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), - ), - }, - id: 1, - opts: []DeleteOpt{ - OptDeleteZero(true), - }, - }, - { - name: "Compact", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - OptCreateWithDescriptors( - getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), - ), - }, - id: 1, - opts: []DeleteOpt{ - OptDeleteCompact(true), - }, - }, - { - name: "ZeroCompact", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - OptCreateWithDescriptors( - getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), - ), - }, - id: 1, - opts: []DeleteOpt{ - OptDeleteZero(true), - OptDeleteCompact(true), - }, - }, - { - name: "Deterministic", - createOpts: []CreateOpt{ - OptCreateWithID("de170c43-36ab-44a8-bca9-1ea1a070a274"), - OptCreateWithDescriptors( - getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), - ), - OptCreateWithTime(time.Unix(946702800, 0)), - }, - id: 1, - opts: []DeleteOpt{ - OptDeleteDeterministic(), - }, - }, - { - name: "WithTime", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - OptCreateWithDescriptors( - getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), - ), - }, - id: 1, - opts: []DeleteOpt{ - OptDeleteWithTime(time.Unix(946702800, 0)), - }, - }, - { - name: "PrimaryPartition", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - OptCreateWithDescriptors( - getDescriptorInput(t, DataPartition, []byte{0xfa, 0xce}, - OptPartitionMetadata(FsSquash, PartPrimSys, "386"), - ), - ), - }, - id: 1, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var b Buffer - - f, err := CreateContainer(&b, tt.createOpts...) - if err != nil { - t.Fatal(err) - } - - if got, want := f.DeleteObject(tt.id, tt.opts...), tt.wantErr; !errors.Is(got, want) { - t.Errorf("got error %v, want %v", got, want) - } - - if err := f.UnloadContainer(); err != nil { - t.Error(err) - } - - g := goldie.New(t, goldie.WithTestNameForDir(true)) - g.Assert(t, tt.name, b.Bytes()) - }) - } -} - -func TestDeleteObjectAndAddObject(t *testing.T) { - tests := []struct { - name string - id uint32 - opts []DeleteOpt - }{ - { - name: "Compact", - id: 2, - opts: []DeleteOpt{ - OptDeleteCompact(true), - }, - }, - { - name: "NoCompact", - id: 2, - }, - { - name: "Zero", - id: 2, - opts: []DeleteOpt{ - OptDeleteZero(true), - }, - }, - { - name: "ZeroCompact", - id: 2, - opts: []DeleteOpt{ - OptDeleteZero(true), - OptDeleteCompact(true), - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var b Buffer - - f, err := CreateContainer(&b, - OptCreateDeterministic(), - OptCreateWithDescriptors( - getDescriptorInput(t, DataGeneric, []byte("abc")), - getDescriptorInput(t, DataGeneric, []byte("def")), - ), - ) - if err != nil { - t.Fatal(err) - } - - if err := f.DeleteObject(tt.id, tt.opts...); err != nil { - t.Fatal(err) - } - - if err := f.AddObject(getDescriptorInput(t, DataGeneric, []byte("ghi"))); err != nil { - t.Fatal(err) - } - - g := goldie.New(t, goldie.WithTestNameForDir(true)) - g.Assert(t, tt.name, b.Bytes()) - }) - } -} - -func TestSetPrimPart(t *testing.T) { - tests := []struct { - name string - createOpts []CreateOpt - id uint32 - opts []SetOpt - wantErr error - }{ - { - name: "ErrObjectNotFound", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - }, - id: 1, - wantErr: ErrObjectNotFound, - }, - { - name: "Deterministic", - createOpts: []CreateOpt{ - OptCreateWithID("de170c43-36ab-44a8-bca9-1ea1a070a274"), - OptCreateWithDescriptors( - getDescriptorInput(t, DataPartition, []byte{0xfa, 0xce}, - OptPartitionMetadata(FsRaw, PartSystem, "386"), - ), - ), - OptCreateWithTime(time.Unix(946702800, 0)), - }, - id: 1, - opts: []SetOpt{ - OptSetDeterministic(), - }, - }, - { - name: "WithTime", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - OptCreateWithDescriptors( - getDescriptorInput(t, DataPartition, []byte{0xfa, 0xce}, - OptPartitionMetadata(FsRaw, PartPrimSys, "386"), - ), - getDescriptorInput(t, DataPartition, []byte{0xfe, 0xed}, - OptPartitionMetadata(FsRaw, PartSystem, "amd64"), - ), - ), - }, - id: 2, - opts: []SetOpt{ - OptSetWithTime(time.Unix(946702800, 0)), - }, - }, - { - name: "One", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - OptCreateWithDescriptors( - getDescriptorInput(t, DataPartition, []byte{0xfa, 0xce}, - OptPartitionMetadata(FsRaw, PartSystem, "386"), - ), - ), - }, - id: 1, - }, - { - name: "Two", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - OptCreateWithDescriptors( - getDescriptorInput(t, DataPartition, []byte{0xfa, 0xce}, - OptPartitionMetadata(FsRaw, PartPrimSys, "386"), - ), - getDescriptorInput(t, DataPartition, []byte{0xfe, 0xed}, - OptPartitionMetadata(FsRaw, PartSystem, "amd64"), - ), - ), - }, - id: 2, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var b Buffer - - f, err := CreateContainer(&b, tt.createOpts...) - if err != nil { - t.Fatal(err) - } - - if got, want := f.SetPrimPart(tt.id, tt.opts...), tt.wantErr; !errors.Is(got, want) { - t.Errorf("got error %v, want %v", got, want) - } - - if err := f.UnloadContainer(); err != nil { - t.Error(err) - } - - g := goldie.New(t, goldie.WithTestNameForDir(true)) - g.Assert(t, tt.name, b.Bytes()) - }) - } -} - -func TestSetMetadata(t *testing.T) { - tests := []struct { - name string - createOpts []CreateOpt - id uint32 - opts []SetOpt - wantErr error - }{ - { - name: "Deterministic", - createOpts: []CreateOpt{ - OptCreateWithID("de170c43-36ab-44a8-bca9-1ea1a070a274"), - OptCreateWithDescriptors( - getDescriptorInput(t, DataOCIBlob, []byte{0xfa, 0xce}), - ), - OptCreateWithTime(time.Unix(946702800, 0)), - }, - id: 1, - opts: []SetOpt{ - OptSetDeterministic(), - }, - }, - { - name: "WithTime", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - OptCreateWithDescriptors( - getDescriptorInput(t, DataOCIBlob, []byte{0xfa, 0xce}), - ), - }, - id: 1, - opts: []SetOpt{ - OptSetWithTime(time.Unix(946702800, 0)), - }, - }, - { - name: "One", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - OptCreateWithDescriptors( - getDescriptorInput(t, DataOCIBlob, []byte{0xfa, 0xce}), - ), - }, - id: 1, - }, - { - name: "Two", - createOpts: []CreateOpt{ - OptCreateDeterministic(), - OptCreateWithDescriptors( - getDescriptorInput(t, DataOCIBlob, []byte{0xfa, 0xce}), - getDescriptorInput(t, DataOCIBlob, []byte{0xfe, 0xed}), - ), - }, - id: 2, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var b Buffer - - f, err := CreateContainer(&b, tt.createOpts...) - if err != nil { - t.Fatal(err) - } - - if got, want := f.SetMetadata(tt.id, newOCIBlobDigest(), tt.opts...), tt.wantErr; !errors.Is(got, want) { - t.Errorf("got error %v, want %v", got, want) - } - - if err := f.UnloadContainer(); err != nil { - t.Error(err) - } - - g := goldie.New(t, goldie.WithTestNameForDir(true)) - g.Assert(t, tt.name, b.Bytes()) - }) - } -} diff --git a/pkg/sif/delete.go b/pkg/sif/delete.go new file mode 100644 index 00000000..c0f0aba3 --- /dev/null +++ b/pkg/sif/delete.go @@ -0,0 +1,168 @@ +// Copyright (c) 2018-2023, Sylabs Inc. All rights reserved. +// Copyright (c) 2017, SingularityWare, LLC. All rights reserved. +// Copyright (c) 2017, Yannick Cote All rights reserved. +// This software is licensed under a 3-clause BSD license. Please consult the +// LICENSE file distributed with the sources of this project regarding your +// rights to use or distribute this software. + +package sif + +import ( + "errors" + "fmt" + "io" + "time" +) + +// isLast return true if the data object associated with d is the last in f. +func (f *FileImage) isLast(d *rawDescriptor) bool { + isLast := true + + end := d.Offset + d.Size + f.WithDescriptors(func(d Descriptor) bool { + isLast = d.Offset()+d.Size() <= end + return !isLast + }) + + return isLast +} + +// zeroReader is an io.Reader that returns a stream of zero-bytes. +type zeroReader struct{} + +func (zeroReader) Read(b []byte) (int, error) { + for i := range b { + b[i] = 0 + } + return len(b), nil +} + +// zero overwrites the data object described by d with a stream of zero bytes. +func (f *FileImage) zero(d *rawDescriptor) error { + if _, err := f.rw.Seek(d.Offset, io.SeekStart); err != nil { + return err + } + + _, err := io.CopyN(f.rw, zeroReader{}, d.Size) + return err +} + +// truncateAt truncates f at the start of the padded data object described by d. +func (f *FileImage) truncateAt(d *rawDescriptor) error { + start := d.Offset + d.Size - d.SizeWithPadding + + return f.rw.Truncate(start) +} + +// deleteOpts accumulates object deletion options. +type deleteOpts struct { + zero bool + compact bool + t time.Time +} + +// DeleteOpt are used to specify object deletion options. +type DeleteOpt func(*deleteOpts) error + +// OptDeleteZero specifies whether the deleted object should be zeroed. +func OptDeleteZero(b bool) DeleteOpt { + return func(do *deleteOpts) error { + do.zero = b + return nil + } +} + +// OptDeleteCompact specifies whether the image should be compacted following object deletion. +func OptDeleteCompact(b bool) DeleteOpt { + return func(do *deleteOpts) error { + do.compact = b + return nil + } +} + +// OptDeleteDeterministic sets header/descriptor fields to values that support deterministic +// modification of images. +func OptDeleteDeterministic() DeleteOpt { + return func(do *deleteOpts) error { + do.t = time.Time{} + return nil + } +} + +// OptDeleteWithTime specifies t as the image modification time. +func OptDeleteWithTime(t time.Time) DeleteOpt { + return func(do *deleteOpts) error { + do.t = t + return nil + } +} + +var errCompactNotImplemented = errors.New("compact not implemented for non-last object") + +// DeleteObject deletes the data object with id, according to opts. +// +// To zero the data region of the deleted object, use OptDeleteZero. To compact the file following +// object deletion, use OptDeleteCompact. +// +// By default, the image modification time is set to the current time for non-deterministic images, +// and unset otherwise. To override this, consider using OptDeleteDeterministic or +// OptDeleteWithTime. +func (f *FileImage) DeleteObject(id uint32, opts ...DeleteOpt) error { + do := deleteOpts{} + + if !f.isDeterministic() { + do.t = time.Now() + } + + for _, opt := range opts { + if err := opt(&do); err != nil { + return fmt.Errorf("%w", err) + } + } + + d, err := f.getDescriptor(WithID(id)) + if err != nil { + return fmt.Errorf("%w", err) + } + + if do.compact && !f.isLast(d) { + return fmt.Errorf("%w", errCompactNotImplemented) + } + + if do.zero { + if err := f.zero(d); err != nil { + return fmt.Errorf("%w", err) + } + } + + if do.compact { + if err := f.truncateAt(d); err != nil { + return fmt.Errorf("%w", err) + } + + f.h.DataSize -= d.SizeWithPadding + } + + f.h.DescriptorsFree++ + f.h.ModifiedAt = do.t.Unix() + + // If we remove the primary partition, set the global header Arch field to HdrArchUnknown + // to indicate that the SIF file doesn't include a primary partition and no dependency + // on any architecture exists. + if d.isPartitionOfType(PartPrimSys) { + f.h.Arch = hdrArchUnknown + } + + // Reset rawDescripter with empty struct + *d = rawDescriptor{} + + if err := f.writeDescriptors(); err != nil { + return fmt.Errorf("%w", err) + } + + if err := f.writeHeader(); err != nil { + return fmt.Errorf("%w", err) + } + + return nil +} diff --git a/pkg/sif/delete_test.go b/pkg/sif/delete_test.go new file mode 100644 index 00000000..683e00dd --- /dev/null +++ b/pkg/sif/delete_test.go @@ -0,0 +1,197 @@ +// Copyright (c) 2018-2023, Sylabs Inc. All rights reserved. +// This software is licensed under a 3-clause BSD license. Please consult the +// LICENSE file distributed with the sources of this project regarding your +// rights to use or distribute this software. + +package sif + +import ( + "errors" + "testing" + "time" + + "github.com/sebdah/goldie/v2" +) + +func TestDeleteObject(t *testing.T) { + tests := []struct { + name string + createOpts []CreateOpt + id uint32 + opts []DeleteOpt + wantErr error + }{ + { + name: "ErrObjectNotFound", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + }, + id: 1, + wantErr: ErrObjectNotFound, + }, + { + name: "Zero", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + OptCreateWithDescriptors( + getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), + ), + }, + id: 1, + opts: []DeleteOpt{ + OptDeleteZero(true), + }, + }, + { + name: "Compact", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + OptCreateWithDescriptors( + getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), + ), + }, + id: 1, + opts: []DeleteOpt{ + OptDeleteCompact(true), + }, + }, + { + name: "ZeroCompact", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + OptCreateWithDescriptors( + getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), + ), + }, + id: 1, + opts: []DeleteOpt{ + OptDeleteZero(true), + OptDeleteCompact(true), + }, + }, + { + name: "Deterministic", + createOpts: []CreateOpt{ + OptCreateWithID("de170c43-36ab-44a8-bca9-1ea1a070a274"), + OptCreateWithDescriptors( + getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), + ), + OptCreateWithTime(time.Unix(946702800, 0)), + }, + id: 1, + opts: []DeleteOpt{ + OptDeleteDeterministic(), + }, + }, + { + name: "WithTime", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + OptCreateWithDescriptors( + getDescriptorInput(t, DataGeneric, []byte{0xfa, 0xce}), + ), + }, + id: 1, + opts: []DeleteOpt{ + OptDeleteWithTime(time.Unix(946702800, 0)), + }, + }, + { + name: "PrimaryPartition", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + OptCreateWithDescriptors( + getDescriptorInput(t, DataPartition, []byte{0xfa, 0xce}, + OptPartitionMetadata(FsSquash, PartPrimSys, "386"), + ), + ), + }, + id: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var b Buffer + + f, err := CreateContainer(&b, tt.createOpts...) + if err != nil { + t.Fatal(err) + } + + if got, want := f.DeleteObject(tt.id, tt.opts...), tt.wantErr; !errors.Is(got, want) { + t.Errorf("got error %v, want %v", got, want) + } + + if err := f.UnloadContainer(); err != nil { + t.Error(err) + } + + g := goldie.New(t, goldie.WithTestNameForDir(true)) + g.Assert(t, tt.name, b.Bytes()) + }) + } +} + +func TestDeleteObjectAndAddObject(t *testing.T) { + tests := []struct { + name string + id uint32 + opts []DeleteOpt + }{ + { + name: "Compact", + id: 2, + opts: []DeleteOpt{ + OptDeleteCompact(true), + }, + }, + { + name: "NoCompact", + id: 2, + }, + { + name: "Zero", + id: 2, + opts: []DeleteOpt{ + OptDeleteZero(true), + }, + }, + { + name: "ZeroCompact", + id: 2, + opts: []DeleteOpt{ + OptDeleteZero(true), + OptDeleteCompact(true), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var b Buffer + + f, err := CreateContainer(&b, + OptCreateDeterministic(), + OptCreateWithDescriptors( + getDescriptorInput(t, DataGeneric, []byte("abc")), + getDescriptorInput(t, DataGeneric, []byte("def")), + ), + ) + if err != nil { + t.Fatal(err) + } + + if err := f.DeleteObject(tt.id, tt.opts...); err != nil { + t.Fatal(err) + } + + if err := f.AddObject(getDescriptorInput(t, DataGeneric, []byte("ghi"))); err != nil { + t.Fatal(err) + } + + g := goldie.New(t, goldie.WithTestNameForDir(true)) + g.Assert(t, tt.name, b.Bytes()) + }) + } +} diff --git a/pkg/sif/set.go b/pkg/sif/set.go new file mode 100644 index 00000000..45a28efe --- /dev/null +++ b/pkg/sif/set.go @@ -0,0 +1,169 @@ +// Copyright (c) 2018-2023, Sylabs Inc. All rights reserved. +// Copyright (c) 2017, SingularityWare, LLC. All rights reserved. +// Copyright (c) 2017, Yannick Cote All rights reserved. +// This software is licensed under a 3-clause BSD license. Please consult the +// LICENSE file distributed with the sources of this project regarding your +// rights to use or distribute this software. + +package sif + +import ( + "encoding" + "errors" + "fmt" + "time" +) + +// setOpts accumulates object set options. +type setOpts struct { + t time.Time +} + +// SetOpt are used to specify object set options. +type SetOpt func(*setOpts) error + +// OptSetDeterministic sets header/descriptor fields to values that support deterministic +// modification of images. +func OptSetDeterministic() SetOpt { + return func(so *setOpts) error { + so.t = time.Time{} + return nil + } +} + +// OptSetWithTime specifies t as the image/object modification time. +func OptSetWithTime(t time.Time) SetOpt { + return func(so *setOpts) error { + so.t = t + return nil + } +} + +var ( + errNotPartition = errors.New("data object not a partition") + errNotSystem = errors.New("data object not a system partition") +) + +// SetPrimPart sets the specified system partition to be the primary one. +// +// By default, the image/object modification times are set to the current time for +// non-deterministic images, and unset otherwise. To override this, consider using +// OptSetDeterministic or OptSetWithTime. +func (f *FileImage) SetPrimPart(id uint32, opts ...SetOpt) error { + so := setOpts{} + + if !f.isDeterministic() { + so.t = time.Now() + } + + for _, opt := range opts { + if err := opt(&so); err != nil { + return fmt.Errorf("%w", err) + } + } + + descr, err := f.getDescriptor(WithID(id)) + if err != nil { + return fmt.Errorf("%w", err) + } + + if descr.DataType != DataPartition { + return fmt.Errorf("%w", errNotPartition) + } + + var p partition + if err := descr.getExtra(binaryUnmarshaler{&p}); err != nil { + return fmt.Errorf("%w", err) + } + + // if already primary system partition, nothing to do + if p.Parttype == PartPrimSys { + return nil + } + + if p.Parttype != PartSystem { + return fmt.Errorf("%w", errNotSystem) + } + + // If there is currently a primary system partition, update it. + if d, err := f.getDescriptor(WithPartitionType(PartPrimSys)); err == nil { + var p partition + if err := d.getExtra(binaryUnmarshaler{&p}); err != nil { + return fmt.Errorf("%w", err) + } + + p.Parttype = PartSystem + + if err := d.setExtra(p); err != nil { + return fmt.Errorf("%w", err) + } + + d.ModifiedAt = so.t.Unix() + } else if !errors.Is(err, ErrObjectNotFound) { + return fmt.Errorf("%w", err) + } + + // Update the descriptor of the new primary system partition. + p.Parttype = PartPrimSys + + if err := descr.setExtra(p); err != nil { + return fmt.Errorf("%w", err) + } + + descr.ModifiedAt = so.t.Unix() + + if err := f.writeDescriptors(); err != nil { + return fmt.Errorf("%w", err) + } + + f.h.Arch = p.Arch + f.h.ModifiedAt = so.t.Unix() + + if err := f.writeHeader(); err != nil { + return fmt.Errorf("%w", err) + } + + return nil +} + +// SetMetadata sets the metadata of the data object with id to md, according to opts. +// +// By default, the image/object modification times are set to the current time for +// non-deterministic images, and unset otherwise. To override this, consider using +// OptSetDeterministic or OptSetWithTime. +func (f *FileImage) SetMetadata(id uint32, md encoding.BinaryMarshaler, opts ...SetOpt) error { + so := setOpts{} + + if !f.isDeterministic() { + so.t = time.Now() + } + + for _, opt := range opts { + if err := opt(&so); err != nil { + return fmt.Errorf("%w", err) + } + } + + rd, err := f.getDescriptor(WithID(id)) + if err != nil { + return fmt.Errorf("%w", err) + } + + if err := rd.setExtra(md); err != nil { + return fmt.Errorf("%w", err) + } + + rd.ModifiedAt = so.t.Unix() + + if err := f.writeDescriptors(); err != nil { + return fmt.Errorf("%w", err) + } + + f.h.ModifiedAt = so.t.Unix() + + if err := f.writeHeader(); err != nil { + return fmt.Errorf("%w", err) + } + + return nil +} diff --git a/pkg/sif/set_test.go b/pkg/sif/set_test.go new file mode 100644 index 00000000..edf0a530 --- /dev/null +++ b/pkg/sif/set_test.go @@ -0,0 +1,197 @@ +// Copyright (c) 2018-2023, Sylabs Inc. All rights reserved. +// This software is licensed under a 3-clause BSD license. Please consult the +// LICENSE file distributed with the sources of this project regarding your +// rights to use or distribute this software. + +package sif + +import ( + "errors" + "testing" + "time" + + "github.com/sebdah/goldie/v2" +) + +func TestSetPrimPart(t *testing.T) { + tests := []struct { + name string + createOpts []CreateOpt + id uint32 + opts []SetOpt + wantErr error + }{ + { + name: "ErrObjectNotFound", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + }, + id: 1, + wantErr: ErrObjectNotFound, + }, + { + name: "Deterministic", + createOpts: []CreateOpt{ + OptCreateWithID("de170c43-36ab-44a8-bca9-1ea1a070a274"), + OptCreateWithDescriptors( + getDescriptorInput(t, DataPartition, []byte{0xfa, 0xce}, + OptPartitionMetadata(FsRaw, PartSystem, "386"), + ), + ), + OptCreateWithTime(time.Unix(946702800, 0)), + }, + id: 1, + opts: []SetOpt{ + OptSetDeterministic(), + }, + }, + { + name: "WithTime", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + OptCreateWithDescriptors( + getDescriptorInput(t, DataPartition, []byte{0xfa, 0xce}, + OptPartitionMetadata(FsRaw, PartPrimSys, "386"), + ), + getDescriptorInput(t, DataPartition, []byte{0xfe, 0xed}, + OptPartitionMetadata(FsRaw, PartSystem, "amd64"), + ), + ), + }, + id: 2, + opts: []SetOpt{ + OptSetWithTime(time.Unix(946702800, 0)), + }, + }, + { + name: "One", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + OptCreateWithDescriptors( + getDescriptorInput(t, DataPartition, []byte{0xfa, 0xce}, + OptPartitionMetadata(FsRaw, PartSystem, "386"), + ), + ), + }, + id: 1, + }, + { + name: "Two", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + OptCreateWithDescriptors( + getDescriptorInput(t, DataPartition, []byte{0xfa, 0xce}, + OptPartitionMetadata(FsRaw, PartPrimSys, "386"), + ), + getDescriptorInput(t, DataPartition, []byte{0xfe, 0xed}, + OptPartitionMetadata(FsRaw, PartSystem, "amd64"), + ), + ), + }, + id: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var b Buffer + + f, err := CreateContainer(&b, tt.createOpts...) + if err != nil { + t.Fatal(err) + } + + if got, want := f.SetPrimPart(tt.id, tt.opts...), tt.wantErr; !errors.Is(got, want) { + t.Errorf("got error %v, want %v", got, want) + } + + if err := f.UnloadContainer(); err != nil { + t.Error(err) + } + + g := goldie.New(t, goldie.WithTestNameForDir(true)) + g.Assert(t, tt.name, b.Bytes()) + }) + } +} + +func TestSetMetadata(t *testing.T) { + tests := []struct { + name string + createOpts []CreateOpt + id uint32 + opts []SetOpt + wantErr error + }{ + { + name: "Deterministic", + createOpts: []CreateOpt{ + OptCreateWithID("de170c43-36ab-44a8-bca9-1ea1a070a274"), + OptCreateWithDescriptors( + getDescriptorInput(t, DataOCIBlob, []byte{0xfa, 0xce}), + ), + OptCreateWithTime(time.Unix(946702800, 0)), + }, + id: 1, + opts: []SetOpt{ + OptSetDeterministic(), + }, + }, + { + name: "WithTime", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + OptCreateWithDescriptors( + getDescriptorInput(t, DataOCIBlob, []byte{0xfa, 0xce}), + ), + }, + id: 1, + opts: []SetOpt{ + OptSetWithTime(time.Unix(946702800, 0)), + }, + }, + { + name: "One", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + OptCreateWithDescriptors( + getDescriptorInput(t, DataOCIBlob, []byte{0xfa, 0xce}), + ), + }, + id: 1, + }, + { + name: "Two", + createOpts: []CreateOpt{ + OptCreateDeterministic(), + OptCreateWithDescriptors( + getDescriptorInput(t, DataOCIBlob, []byte{0xfa, 0xce}), + getDescriptorInput(t, DataOCIBlob, []byte{0xfe, 0xed}), + ), + }, + id: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var b Buffer + + f, err := CreateContainer(&b, tt.createOpts...) + if err != nil { + t.Fatal(err) + } + + if got, want := f.SetMetadata(tt.id, newOCIBlobDigest(), tt.opts...), tt.wantErr; !errors.Is(got, want) { + t.Errorf("got error %v, want %v", got, want) + } + + if err := f.UnloadContainer(); err != nil { + t.Error(err) + } + + g := goldie.New(t, goldie.WithTestNameForDir(true)) + g.Assert(t, tt.name, b.Bytes()) + }) + } +}