Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ linters:
varnamelen:
ignore-names:
- tt
- ok
ignore-decls:
- mc *minimock.Controller
- t T
Expand Down
23 changes: 22 additions & 1 deletion driver/etcd/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func createTestDriver(t *testing.T) (*etcddriver.Driver, func()) {
func testKey(t *testing.T, prefix string) []byte {
t.Helper()

return []byte("/test/" + prefix + "/" + t.Name())
return []byte("/" + t.Name() + "/" + prefix)
}

func testNestedKey(t *testing.T, prefix, suffix string) []byte {
Expand Down Expand Up @@ -159,6 +159,27 @@ func TestEtcdDriver_Get(t *testing.T) {
assert.Positive(t, retrievedKv.ModRevision, "ModRevision should be greater than 0")
}

func TestEtcdDriver_GetPrefix(t *testing.T) {
ctx := context.Background()

driver, cleanup := createTestDriver(t)
defer cleanup()

key := testKey(t, "get")
value := []byte("get-test-value")

putValue(ctx, t, driver, append(key, []byte("/123")...), value)
putValue(ctx, t, driver, append(key, []byte("/124")...), value)

response, err := driver.Execute(ctx, nil, []operation.Operation{
operation.Get(append(key, []byte("/")...)),
}, nil)
require.NoError(t, err, "Get operation failed")
assert.True(t, response.Succeeded, "Get operation should succeed")
require.Len(t, response.Results, 1, "Get operation should return one result")
require.Len(t, response.Results[0].Values, 2, "Get operation should return one value")
}

func TestEtcdDriver_Delete(t *testing.T) {
ctx := context.Background()

Expand Down
194 changes: 194 additions & 0 deletions integrity/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
// Package integrity provides integrity-protected storage operations.
// It includes generators, validators, and builders for creating typed storage
// with hash and signature verification.
package integrity

import (
"maps"
"slices"

"github.com/tarantool/go-storage"
"github.com/tarantool/go-storage/crypto"
"github.com/tarantool/go-storage/hasher"
"github.com/tarantool/go-storage/marshaller"
"github.com/tarantool/go-storage/namer"
)

type NamerConstructor func(prefix string, hashNames []string, sigNames []string) namer.Namer

// TypedBuilder builds typed storage instances with integrity protection.
type TypedBuilder[T any] struct {
storage storage.Storage
hashers []hasher.Hasher
signers []crypto.Signer
verifiers []crypto.Verifier
marshaller marshaller.TypedYamlMarshaller[T]

prefix string
namerFunc NamerConstructor
}

// NewTypedBuilder creates a new TypedBuilder for the given storage instance.
func NewTypedBuilder[T any](storageInstance storage.Storage) TypedBuilder[T] {
return TypedBuilder[T]{
storage: storageInstance,
hashers: []hasher.Hasher{},
signers: []crypto.Signer{},
verifiers: []crypto.Verifier{},
marshaller: marshaller.NewTypedYamlMarshaller[T](),

prefix: "/",
namerFunc: nil,
}
}

func (s TypedBuilder[T]) copy() TypedBuilder[T] {
return TypedBuilder[T]{
storage: s.storage,
hashers: slices.Clone(s.hashers),
signers: slices.Clone(s.signers),
verifiers: slices.Clone(s.verifiers),
marshaller: s.marshaller,

prefix: s.prefix,
namerFunc: s.namerFunc,
}
}

// WithHasher adds a hasher to the builder.
func (s TypedBuilder[T]) WithHasher(h hasher.Hasher) TypedBuilder[T] {
out := s.copy()

out.hashers = append(out.hashers, h)

return out
}

// WithSignerVerifier adds a signer/verifier to the builder.
func (s TypedBuilder[T]) WithSignerVerifier(sv crypto.SignerVerifier) TypedBuilder[T] {
out := s.copy()

out.signers = append(out.signers, sv)
out.verifiers = append(out.verifiers, sv)

return out
}

// WithSigner adds a signer to the builder.
func (s TypedBuilder[T]) WithSigner(signer crypto.Signer) TypedBuilder[T] {
out := s.copy()

out.signers = append(out.signers, signer)

return out
}

// WithVerifier adds a verifier to the builder.
func (s TypedBuilder[T]) WithVerifier(verifier crypto.Verifier) TypedBuilder[T] {
out := s.copy()

out.verifiers = append(out.verifiers, verifier)

return out
}

// WithMarshaller sets the marshaller for the builder.
func (s TypedBuilder[T]) WithMarshaller(marshaller marshaller.TypedYamlMarshaller[T]) TypedBuilder[T] {
out := s.copy()

out.marshaller = marshaller

return out
}

// WithPrefix sets the key prefix for the builder.
func (s TypedBuilder[T]) WithPrefix(prefix string) TypedBuilder[T] {
out := s.copy()

out.prefix = prefix

return out
}

// WithNamer sets the namer for the builder using a constructor function.
// The constructor function will be called during Build() with the current prefix.
func (s TypedBuilder[T]) WithNamer(namerFunc NamerConstructor) TypedBuilder[T] {
out := s.copy()

out.namerFunc = namerFunc

return out
}

func (s TypedBuilder[T]) getHasherNames() []string {
names := make([]string, 0, len(s.hashers))
for _, hasherInstance := range s.hashers {
names = append(names, hasherInstance.Name())
}

return names
}

func (s TypedBuilder[T]) getSignerNames() []string {
names := make([]string, 0, len(s.signers))
for _, signerInstance := range s.signers {
names = append(names, signerInstance.Name())
}

return names
}

func (s TypedBuilder[T]) getVerifierNames() []string {
names := make([]string, 0, len(s.verifiers))
for _, verifierInstance := range s.verifiers {
names = append(names, verifierInstance.Name())
}

return names
}

func (s TypedBuilder[T]) getSignerVerifierNames() []string {
names := map[string]struct{}{}

for _, name := range s.getSignerNames() {
names[name] = struct{}{}
}

for _, name := range s.getVerifierNames() {
names[name] = struct{}{}
}

return slices.Collect(maps.Keys(names))
}

// Build creates a new Typed storage instance with the configured options.
func (s TypedBuilder[T]) Build() *Typed[T] {
if s.namerFunc == nil {
s.namerFunc = namer.NewDefaultNamer
}

hasherNames := s.getHasherNames()

gen := NewGenerator(
s.namerFunc(s.prefix, hasherNames, s.getSignerNames()),
s.marshaller,
s.hashers,
s.signers,
)

val := NewValidator(
s.namerFunc(s.prefix, hasherNames, s.getVerifierNames()),
s.marshaller,
s.hashers,
s.verifiers,
)

namerInstance := s.namerFunc(s.prefix, hasherNames, s.getSignerVerifierNames())

return &Typed[T]{
base: s.storage,
gen: gen,
val: val,
namer: namerInstance,
}
}
13 changes: 13 additions & 0 deletions integrity/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,16 @@ func (e *FailedToValidateAggregatedError) Finalize() error {
return e
}
}

// InvalidNameError represents an error when a name is invalid.
type InvalidNameError struct {
name string
}

// Error returns a string representation of the invalid name error.
func (e InvalidNameError) Error() string {
return "invalid name: " + e.name
}

// ErrInvalidName is a sentinel error for invalid names.
var ErrInvalidName = InvalidNameError{name: ""}
Comment on lines +251 to +262
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No tests for the new type.

1 change: 0 additions & 1 deletion integrity/error_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// Package integrity provides integrity storage.
package integrity //nolint:testpackage

import (
Expand Down
12 changes: 12 additions & 0 deletions integrity/result.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package integrity

import (
"github.com/tarantool/go-option"
)

// ValidatedResult represents a validated named value.
type ValidatedResult[T any] struct {
Name string
Value option.Generic[T]
Error error
}
Loading
Loading