Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -0,0 +1,424 @@
// monorepo/native/desktop/maplefile-cli/pkg/crypto/crypto.go
package crypto
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"io"
"log"
"github.com/tyler-smith/go-bip39"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/nacl/box"
)
const (
// Key sizes
MasterKeySize = 32 // 256-bit
KeyEncryptionKeySize = 32
RecoveryKeySize = 32
CollectionKeySize = 32
FileKeySize = 32
// ChaCha20-Poly1305 (symmetric encryption) constants
ChaCha20Poly1305KeySize = 32 // ChaCha20 key size
ChaCha20Poly1305NonceSize = 12 // ChaCha20-Poly1305 nonce size
ChaCha20Poly1305Overhead = 16 // Poly1305 authentication tag size
// Legacy naming for backward compatibility
SecretBoxKeySize = ChaCha20Poly1305KeySize
SecretBoxNonceSize = ChaCha20Poly1305NonceSize
SecretBoxOverhead = ChaCha20Poly1305Overhead
// Box (asymmetric encryption) constants
BoxPublicKeySize = 32
BoxSecretKeySize = 32
BoxNonceSize = 24
BoxOverhead = box.Overhead
BoxSealOverhead = BoxPublicKeySize + BoxOverhead
// Argon2 parameters - must match between platforms
Argon2IDAlgorithm = "argon2id"
Argon2MemLimit = 4 * 1024 * 1024 // 4 MB (matching your internal/common/crypto settings)
Argon2OpsLimit = 1 // 1 iteration (matching your settings)
Argon2Parallelism = 1
Argon2KeySize = 32
Argon2SaltSize = 16
// Encryption algorithm identifiers
ChaCha20Poly1305Algorithm = "chacha20poly1305"
BoxSealAlgorithm = "box_seal"
)
// EncryptedData represents encrypted data with its nonce
type EncryptedData struct {
Ciphertext []byte
Nonce []byte
}
// GenerateRandomBytes generates cryptographically secure random bytes
func GenerateRandomBytes(size int) ([]byte, error) {
if size <= 0 {
return nil, errors.New("size must be positive")
}
buf := make([]byte, size)
_, err := io.ReadFull(rand.Reader, buf)
if err != nil {
return nil, fmt.Errorf("failed to generate random bytes: %w", err)
}
return buf, nil
}
// GenerateVerificationID creates a human-readable representation of a public key
// JavaScript equivalent: The same BIP39 mnemonic implementation
// Generate VerificationID from public key (deterministic)
func GenerateVerificationID(publicKey []byte) (string, error) {
if publicKey == nil {
err := fmt.Errorf("no public key entered")
log.Printf("pkg.crypto.VerifyVerificationID - Failed to generate verification ID with error: %v\n", err)
return "", fmt.Errorf("failed to generate verification ID: %w", err)
}
// 1. Hash the public key with SHA256
hash := sha256.Sum256(publicKey[:])
// 2. Use the hash as entropy for BIP39
mnemonic, err := bip39.NewMnemonic(hash[:])
if err != nil {
log.Printf("pkg.crypto.VerifyVerificationID - Failed to generate verification ID with error: %v\n", err)
return "", fmt.Errorf("failed to generate verification ID: %w", err)
}
return mnemonic, nil
}
// Verify VerificationID matches public key
func VerifyVerificationID(publicKey []byte, verificationID string) bool {
expectedID, err := GenerateVerificationID(publicKey)
if err != nil {
log.Printf("pkg.crypto.VerifyVerificationID - Failed to generate verification ID with error: %v\n", err)
return false
}
return expectedID == verificationID
}
// GenerateKeyPair generates a NaCl box keypair for asymmetric encryption
func GenerateKeyPair() (publicKey []byte, privateKey []byte, verificationID string, err error) {
pubKey, privKey, err := box.GenerateKey(rand.Reader)
if err != nil {
return nil, nil, "", fmt.Errorf("failed to generate key pair: %w", err)
}
if pubKey == nil {
return nil, nil, "", fmt.Errorf("public key is empty")
}
// Generate deterministic verification ID
verificationID, err = GenerateVerificationID(pubKey[:])
if err != nil {
return nil, nil, "", err
}
return pubKey[:], privKey[:], verificationID, nil
}
// DeriveKeyFromPassword derives a key from a password using Argon2id
// This matches the parameters used in your registration and login flows
func DeriveKeyFromPassword(password string, salt []byte) ([]byte, error) {
if len(salt) != Argon2SaltSize {
return nil, fmt.Errorf("invalid salt size: expected %d, got %d", Argon2SaltSize, len(salt))
}
key := argon2.IDKey(
[]byte(password),
salt,
Argon2OpsLimit,
Argon2MemLimit,
Argon2Parallelism,
Argon2KeySize,
)
return key, nil
}
// EncryptWithSecretBox encrypts data with a symmetric key using ChaCha20-Poly1305
func EncryptWithSecretBox(data, key []byte) (*EncryptedData, error) {
if len(key) != ChaCha20Poly1305KeySize {
return nil, fmt.Errorf("invalid key size: expected %d, got %d", ChaCha20Poly1305KeySize, len(key))
}
// Create ChaCha20-Poly1305 cipher
cipher, err := chacha20poly1305.New(key)
if err != nil {
return nil, fmt.Errorf("failed to create cipher: %w", err)
}
// Generate nonce
nonce, err := GenerateRandomBytes(ChaCha20Poly1305NonceSize)
if err != nil {
return nil, fmt.Errorf("failed to generate nonce: %w", err)
}
// Encrypt
ciphertext := cipher.Seal(nil, nonce, data, nil)
return &EncryptedData{
Ciphertext: ciphertext,
Nonce: nonce,
}, nil
}
// EncryptDataWithKey is a helper that encrypts data and returns ciphertext and nonce separately
// This is for backward compatibility with existing code
func EncryptDataWithKey(data, key []byte) (ciphertext []byte, nonce []byte, err error) {
encData, err := EncryptWithSecretBox(data, key)
if err != nil {
return nil, nil, err
}
return encData.Ciphertext, encData.Nonce, nil
}
// DecryptWithSecretBox decrypts data with a symmetric key using ChaCha20-Poly1305
func DecryptWithSecretBox(ciphertext, nonce, key []byte) ([]byte, error) {
if len(key) != ChaCha20Poly1305KeySize {
return nil, fmt.Errorf("invalid key size: expected %d, got %d", ChaCha20Poly1305KeySize, len(key))
}
if len(nonce) != ChaCha20Poly1305NonceSize {
return nil, fmt.Errorf("invalid nonce size: expected %d, got %d", ChaCha20Poly1305NonceSize, len(nonce))
}
// Create ChaCha20-Poly1305 cipher
cipher, err := chacha20poly1305.New(key)
if err != nil {
return nil, fmt.Errorf("failed to create cipher: %w", err)
}
// Decrypt
plaintext, err := cipher.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, fmt.Errorf("failed to decrypt: %w", err)
}
return plaintext, nil
}
// EncryptWithBox encrypts data using NaCl box with the recipient's public key and sender's private key
func EncryptWithBox(message []byte, recipientPublicKey, senderPrivateKey []byte) (*EncryptedData, error) {
if len(recipientPublicKey) != BoxPublicKeySize {
return nil, fmt.Errorf("recipient public key must be %d bytes", BoxPublicKeySize)
}
if len(senderPrivateKey) != BoxSecretKeySize {
return nil, fmt.Errorf("sender private key must be %d bytes", BoxSecretKeySize)
}
// Generate nonce
nonce, err := GenerateRandomBytes(BoxNonceSize)
if err != nil {
return nil, fmt.Errorf("failed to generate nonce: %w", err)
}
// Create fixed-size arrays
var recipientPubKey [32]byte
var senderPrivKey [32]byte
var nonceArray [24]byte
copy(recipientPubKey[:], recipientPublicKey)
copy(senderPrivKey[:], senderPrivateKey)
copy(nonceArray[:], nonce)
// Encrypt
ciphertext := box.Seal(nil, message, &nonceArray, &recipientPubKey, &senderPrivKey)
return &EncryptedData{
Ciphertext: ciphertext,
Nonce: nonce,
}, nil
}
// DecryptWithBox decrypts data using NaCl box with the sender's public key and recipient's private key
func DecryptWithBox(ciphertext, nonce []byte, senderPublicKey, recipientPrivateKey []byte) ([]byte, error) {
if len(senderPublicKey) != BoxPublicKeySize {
return nil, fmt.Errorf("sender public key must be %d bytes", BoxPublicKeySize)
}
if len(recipientPrivateKey) != BoxSecretKeySize {
return nil, fmt.Errorf("recipient private key must be %d bytes", BoxSecretKeySize)
}
if len(nonce) != BoxNonceSize {
return nil, fmt.Errorf("nonce must be %d bytes", BoxNonceSize)
}
// Create fixed-size arrays
var senderPubKey [32]byte
var recipientPrivKey [32]byte
var nonceArray [24]byte
copy(senderPubKey[:], senderPublicKey)
copy(recipientPrivKey[:], recipientPrivateKey)
copy(nonceArray[:], nonce)
// Decrypt
plaintext, ok := box.Open(nil, ciphertext, &nonceArray, &senderPubKey, &recipientPrivKey)
if !ok {
return nil, errors.New("failed to decrypt: invalid keys, nonce, or corrupted ciphertext")
}
return plaintext, nil
}
// EncryptWithBoxSeal encrypts data with a recipient's public key using anonymous sender (sealed box)
// This is used for encrypting data where the sender doesn't need to be authenticated
func EncryptWithBoxSeal(message []byte, recipientPublicKey []byte) ([]byte, error) {
if len(recipientPublicKey) != BoxPublicKeySize {
return nil, fmt.Errorf("recipient public key must be %d bytes", BoxPublicKeySize)
}
// Create a fixed-size array for the recipient's public key
var recipientPubKey [32]byte
copy(recipientPubKey[:], recipientPublicKey)
// Generate an ephemeral keypair
ephemeralPubKey, ephemeralPrivKey, err := box.GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf("failed to generate ephemeral keypair: %w", err)
}
// Generate a random nonce
nonce, err := GenerateRandomBytes(BoxNonceSize)
if err != nil {
return nil, fmt.Errorf("failed to generate nonce: %w", err)
}
var nonceArray [24]byte
copy(nonceArray[:], nonce)
// Encrypt the message
ciphertext := box.Seal(nil, message, &nonceArray, &recipientPubKey, ephemeralPrivKey)
// Result format: ephemeral_public_key || nonce || ciphertext
result := make([]byte, BoxPublicKeySize+BoxNonceSize+len(ciphertext))
copy(result[:BoxPublicKeySize], ephemeralPubKey[:])
copy(result[BoxPublicKeySize:BoxPublicKeySize+BoxNonceSize], nonce)
copy(result[BoxPublicKeySize+BoxNonceSize:], ciphertext)
return result, nil
}
// DecryptWithBoxSeal decrypts data that was encrypted with EncryptWithBoxSeal
func DecryptWithBoxSeal(sealedData []byte, recipientPublicKey, recipientPrivateKey []byte) ([]byte, error) {
if len(recipientPublicKey) != BoxPublicKeySize {
return nil, fmt.Errorf("recipient public key must be %d bytes", BoxPublicKeySize)
}
if len(recipientPrivateKey) != BoxSecretKeySize {
return nil, fmt.Errorf("recipient private key must be %d bytes", BoxSecretKeySize)
}
if len(sealedData) < BoxPublicKeySize+BoxNonceSize+box.Overhead {
return nil, errors.New("sealed data too short")
}
// Extract components
ephemeralPublicKey := sealedData[:BoxPublicKeySize]
nonce := sealedData[BoxPublicKeySize : BoxPublicKeySize+BoxNonceSize]
ciphertext := sealedData[BoxPublicKeySize+BoxNonceSize:]
// Create fixed-size arrays
var ephemeralPubKey [32]byte
var recipientPrivKey [32]byte
var nonceArray [24]byte
copy(ephemeralPubKey[:], ephemeralPublicKey)
copy(recipientPrivKey[:], recipientPrivateKey)
copy(nonceArray[:], nonce)
// Decrypt
plaintext, ok := box.Open(nil, ciphertext, &nonceArray, &ephemeralPubKey, &recipientPrivKey)
if !ok {
return nil, errors.New("failed to decrypt sealed box: invalid keys or corrupted ciphertext")
}
return plaintext, nil
}
// DecryptWithBoxAnonymous decrypts data that was encrypted anonymously (without nonce in the data)
// This is used in the login flow for decrypting challenges
func DecryptWithBoxAnonymous(encryptedData []byte, recipientPublicKey, recipientPrivateKey []byte) ([]byte, error) {
if len(recipientPublicKey) != BoxPublicKeySize {
return nil, fmt.Errorf("recipient public key must be %d bytes", BoxPublicKeySize)
}
if len(recipientPrivateKey) != BoxSecretKeySize {
return nil, fmt.Errorf("recipient private key must be %d bytes", BoxSecretKeySize)
}
// Create fixed-size arrays
var pubKeyArray, privKeyArray [32]byte
copy(pubKeyArray[:], recipientPublicKey)
copy(privKeyArray[:], recipientPrivateKey)
// Decrypt the sealed box challenge
decryptedData, ok := box.OpenAnonymous(nil, encryptedData, &pubKeyArray, &privKeyArray)
if !ok {
return nil, errors.New("failed to decrypt anonymous box: invalid keys or corrupted data")
}
return decryptedData, nil
}
// EncodeToBase64 encodes bytes to base64 standard encoding
func EncodeToBase64(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}
// EncodeToBase64URL encodes bytes to base64 URL-safe encoding without padding
func EncodeToBase64URL(data []byte) string {
return base64.RawURLEncoding.EncodeToString(data)
}
// DecodeFromBase64 decodes a base64 standard encoded string to bytes
func DecodeFromBase64(s string) ([]byte, error) {
return base64.StdEncoding.DecodeString(s)
}
// DecodeFromBase64URL decodes a base64 URL-safe encoded string without padding to bytes
func DecodeFromBase64URL(s string) ([]byte, error) {
return base64.RawURLEncoding.DecodeString(s)
}
// CombineNonceAndCiphertext combines nonce and ciphertext into a single byte slice
// This is useful for storing encrypted data as a single blob
func CombineNonceAndCiphertext(nonce, ciphertext []byte) []byte {
combined := make([]byte, len(nonce)+len(ciphertext))
copy(combined[:len(nonce)], nonce)
copy(combined[len(nonce):], ciphertext)
return combined
}
// SplitNonceAndCiphertext splits a combined byte slice into nonce and ciphertext
// For ChaCha20-Poly1305, the nonce size is 12 bytes
func SplitNonceAndCiphertext(combined []byte, nonceSize int) (nonce []byte, ciphertext []byte, err error) {
if len(combined) < nonceSize {
return nil, nil, fmt.Errorf("combined data too short: expected at least %d bytes, got %d", nonceSize, len(combined))
}
nonce = combined[:nonceSize]
ciphertext = combined[nonceSize:]
return nonce, ciphertext, nil
}
// Helper function to convert EncryptedData to separate slices (for backward compatibility)
func (ed *EncryptedData) Separate() (ciphertext []byte, nonce []byte) {
return ed.Ciphertext, ed.Nonce
}
// ClearBytes overwrites a byte slice with zeros
// This should be called on sensitive data like keys when they're no longer needed
func ClearBytes(b []byte) {
for i := range b {
b[i] = 0
}
}
func HashSHA256(proofData []byte) []byte {
hash := sha256.Sum256(proofData)
return hash[:]
}

View file

@ -0,0 +1,146 @@
package httperror
// This package introduces a new `error` type that combines an HTTP status code and a message.
import (
"encoding/json"
"errors"
"net/http"
)
// HTTPError represents an http error that occurred while handling a request
type HTTPError struct {
Code int `json:"-"` // HTTP Status code. We use `-` to skip json marshaling.
Errors *map[string]string `json:"-"` // The original error. Same reason as above.
}
// New creates a new HTTPError instance with a multi-field errors.
func New(statusCode int, errorsMap *map[string]string) error {
return HTTPError{
Code: statusCode,
Errors: errorsMap,
}
}
// NewForSingleField create a new HTTPError instance for a single field. This is a convinience constructor.
func NewForSingleField(statusCode int, field string, message string) error {
return HTTPError{
Code: statusCode,
Errors: &map[string]string{field: message},
}
}
// NewForBadRequest create a new HTTPError instance pertaining to 403 bad requests with the multi-errors. This is a convinience constructor.
func NewForBadRequest(err *map[string]string) error {
return HTTPError{
Code: http.StatusBadRequest,
Errors: err,
}
}
// NewForBadRequestWithSingleField create a new HTTPError instance pertaining to 403 bad requests for a single field. This is a convinience constructor.
func NewForBadRequestWithSingleField(field string, message string) error {
return HTTPError{
Code: http.StatusBadRequest,
Errors: &map[string]string{field: message},
}
}
func NewForInternalServerErrorWithSingleField(field string, message string) error {
return HTTPError{
Code: http.StatusInternalServerError,
Errors: &map[string]string{field: message},
}
}
// NewForNotFoundWithSingleField create a new HTTPError instance pertaining to 404 not found for a single field. This is a convinience constructor.
func NewForNotFoundWithSingleField(field string, message string) error {
return HTTPError{
Code: http.StatusNotFound,
Errors: &map[string]string{field: message},
}
}
// NewForServiceUnavailableWithSingleField create a new HTTPError instance pertaining service unavailable for a single field. This is a convinience constructor.
func NewForServiceUnavailableWithSingleField(field string, message string) error {
return HTTPError{
Code: http.StatusServiceUnavailable,
Errors: &map[string]string{field: message},
}
}
// NewForLockedWithSingleField create a new HTTPError instance pertaining to 424 locked for a single field. This is a convinience constructor.
func NewForLockedWithSingleField(field string, message string) error {
return HTTPError{
Code: http.StatusLocked,
Errors: &map[string]string{field: message},
}
}
// NewForForbiddenWithSingleField create a new HTTPError instance pertaining to 403 bad requests for a single field. This is a convinience constructor.
func NewForForbiddenWithSingleField(field string, message string) error {
return HTTPError{
Code: http.StatusForbidden,
Errors: &map[string]string{field: message},
}
}
// NewForUnauthorizedWithSingleField create a new HTTPError instance pertaining to 401 unauthorized for a single field. This is a convinience constructor.
func NewForUnauthorizedWithSingleField(field string, message string) error {
return HTTPError{
Code: http.StatusUnauthorized,
Errors: &map[string]string{field: message},
}
}
// NewForGoneWithSingleField create a new HTTPError instance pertaining to 410 gone for a single field. This is a convinience constructor.
func NewForGoneWithSingleField(field string, message string) error {
return HTTPError{
Code: http.StatusGone,
Errors: &map[string]string{field: message},
}
}
// Error function used to implement the `error` interface for returning errors.
func (err HTTPError) Error() string {
b, e := json.Marshal(err.Errors)
if e != nil { // Defensive code
return e.Error()
}
return string(b)
}
// ResponseError function returns the HTTP error response based on the httpcode used.
func ResponseError(rw http.ResponseWriter, err error) {
// Copied from:
// https://dev.to/tigorlazuardi/go-creating-custom-error-wrapper-and-do-proper-error-equality-check-11k7
rw.Header().Set("Content-Type", "Application/json")
//
// CASE 1 OF 2: Handle API Errors.
//
var ew HTTPError
if errors.As(err, &ew) {
rw.WriteHeader(ew.Code)
_ = json.NewEncoder(rw).Encode(ew.Errors)
return
}
//
// CASE 2 OF 2: Handle non ErrorWrapper types.
//
rw.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(rw).Encode(err.Error())
}
// NewForInternalServerError create a new HTTPError instance pertaining to 500 internal server error with the multi-errors. This is a convinience constructor.
func NewForInternalServerError(err string) error {
return HTTPError{
Code: http.StatusInternalServerError,
Errors: &map[string]string{"message": err},
}
}

View file

@ -0,0 +1,327 @@
package httperror
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
func TestNew(t *testing.T) {
tests := []struct {
name string
code int
errors map[string]string
wantCode int
}{
{
name: "basic error",
code: http.StatusBadRequest,
errors: map[string]string{"field": "error message"},
wantCode: http.StatusBadRequest,
},
{
name: "empty errors map",
code: http.StatusNotFound,
errors: map[string]string{},
wantCode: http.StatusNotFound,
},
{
name: "multiple errors",
code: http.StatusBadRequest,
errors: map[string]string{"field1": "error1", "field2": "error2"},
wantCode: http.StatusBadRequest,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := New(tt.code, &tt.errors)
httpErr, ok := err.(HTTPError)
if !ok {
t.Fatal("expected HTTPError type")
}
if httpErr.Code != tt.wantCode {
t.Errorf("Code = %v, want %v", httpErr.Code, tt.wantCode)
}
for k, v := range tt.errors {
if (*httpErr.Errors)[k] != v {
t.Errorf("Errors[%s] = %v, want %v", k, (*httpErr.Errors)[k], v)
}
}
})
}
}
func TestNewForBadRequest(t *testing.T) {
tests := []struct {
name string
errors map[string]string
}{
{
name: "single error",
errors: map[string]string{"field": "error"},
},
{
name: "multiple errors",
errors: map[string]string{"field1": "error1", "field2": "error2"},
},
{
name: "empty errors",
errors: map[string]string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := NewForBadRequest(&tt.errors)
httpErr, ok := err.(HTTPError)
if !ok {
t.Fatal("expected HTTPError type")
}
if httpErr.Code != http.StatusBadRequest {
t.Errorf("Code = %v, want %v", httpErr.Code, http.StatusBadRequest)
}
for k, v := range tt.errors {
if (*httpErr.Errors)[k] != v {
t.Errorf("Errors[%s] = %v, want %v", k, (*httpErr.Errors)[k], v)
}
}
})
}
}
func TestNewForSingleField(t *testing.T) {
tests := []struct {
name string
code int
field string
message string
}{
{
name: "basic error",
code: http.StatusBadRequest,
field: "test",
message: "error",
},
{
name: "empty field",
code: http.StatusNotFound,
field: "",
message: "error",
},
{
name: "empty message",
code: http.StatusBadRequest,
field: "field",
message: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := NewForSingleField(tt.code, tt.field, tt.message)
httpErr, ok := err.(HTTPError)
if !ok {
t.Fatal("expected HTTPError type")
}
if httpErr.Code != tt.code {
t.Errorf("Code = %v, want %v", httpErr.Code, tt.code)
}
if (*httpErr.Errors)[tt.field] != tt.message {
t.Errorf("Errors[%s] = %v, want %v", tt.field, (*httpErr.Errors)[tt.field], tt.message)
}
})
}
}
func TestError(t *testing.T) {
tests := []struct {
name string
errors map[string]string
wantErr bool
}{
{
name: "valid json",
errors: map[string]string{"field": "error"},
wantErr: false,
},
{
name: "empty map",
errors: map[string]string{},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := HTTPError{
Code: http.StatusBadRequest,
Errors: &tt.errors,
}
errStr := err.Error()
var jsonMap map[string]string
if jsonErr := json.Unmarshal([]byte(errStr), &jsonMap); (jsonErr != nil) != tt.wantErr {
t.Errorf("Error() json.Unmarshal error = %v, wantErr %v", jsonErr, tt.wantErr)
return
}
if !tt.wantErr {
for k, v := range tt.errors {
if jsonMap[k] != v {
t.Errorf("Error() jsonMap[%s] = %v, want %v", k, jsonMap[k], v)
}
}
}
})
}
}
func TestResponseError(t *testing.T) {
tests := []struct {
name string
err error
wantCode int
wantContent string
}{
{
name: "http error",
err: NewForBadRequestWithSingleField("field", "invalid"),
wantCode: http.StatusBadRequest,
wantContent: `{"field":"invalid"}`,
},
{
name: "standard error",
err: fmt.Errorf("standard error"),
wantCode: http.StatusInternalServerError,
wantContent: `"standard error"`,
},
{
name: "nil error",
err: errors.New("<nil>"),
wantCode: http.StatusInternalServerError,
wantContent: `"\u003cnil\u003e"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rr := httptest.NewRecorder()
ResponseError(rr, tt.err)
// Check status code
if rr.Code != tt.wantCode {
t.Errorf("ResponseError() code = %v, want %v", rr.Code, tt.wantCode)
}
// Check content type
if ct := rr.Header().Get("Content-Type"); ct != "Application/json" {
t.Errorf("ResponseError() Content-Type = %v, want Application/json", ct)
}
// Trim newline from response for comparison
got := rr.Body.String()
got = got[:len(got)-1] // Remove trailing newline added by json.Encoder
if got != tt.wantContent {
t.Errorf("ResponseError() content = %v, want %v", got, tt.wantContent)
}
})
}
}
func TestErrorWrapping(t *testing.T) {
originalErr := errors.New("original error")
wrappedErr := fmt.Errorf("wrapped: %w", originalErr)
httpErr := NewForBadRequestWithSingleField("field", wrappedErr.Error())
// Test error unwrapping
if !errors.Is(httpErr, httpErr) {
t.Error("errors.Is failed for same error")
}
var targetErr HTTPError
if !errors.As(httpErr, &targetErr) {
t.Error("errors.As failed to get HTTPError")
}
}
// Test all convenience constructors
func TestConvenienceConstructors(t *testing.T) {
tests := []struct {
name string
create func() error
wantCode int
}{
{
name: "NewForBadRequestWithSingleField",
create: func() error {
return NewForBadRequestWithSingleField("field", "message")
},
wantCode: http.StatusBadRequest,
},
{
name: "NewForNotFoundWithSingleField",
create: func() error {
return NewForNotFoundWithSingleField("field", "message")
},
wantCode: http.StatusNotFound,
},
{
name: "NewForServiceUnavailableWithSingleField",
create: func() error {
return NewForServiceUnavailableWithSingleField("field", "message")
},
wantCode: http.StatusServiceUnavailable,
},
{
name: "NewForLockedWithSingleField",
create: func() error {
return NewForLockedWithSingleField("field", "message")
},
wantCode: http.StatusLocked,
},
{
name: "NewForForbiddenWithSingleField",
create: func() error {
return NewForForbiddenWithSingleField("field", "message")
},
wantCode: http.StatusForbidden,
},
{
name: "NewForUnauthorizedWithSingleField",
create: func() error {
return NewForUnauthorizedWithSingleField("field", "message")
},
wantCode: http.StatusUnauthorized,
},
{
name: "NewForGoneWithSingleField",
create: func() error {
return NewForGoneWithSingleField("field", "message")
},
wantCode: http.StatusGone,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.create()
httpErr, ok := err.(HTTPError)
if !ok {
t.Fatal("expected HTTPError type")
}
if httpErr.Code != tt.wantCode {
t.Errorf("Code = %v, want %v", httpErr.Code, tt.wantCode)
}
if (*httpErr.Errors)["field"] != "message" {
t.Errorf("Error message = %v, want 'message'", (*httpErr.Errors)["field"])
}
})
}
}

View file

@ -0,0 +1,29 @@
package storage
// Storage interface defines the methods that can be used to interact with a key-value database.
type Storage interface {
// Get returns the value associated with the specified key, or an error if the key is not found.
Get(key string) ([]byte, error)
// Set sets the value associated with the specified key.
// If the key already exists, its value is updated.
Set(key string, val []byte) error
// Delete removes the value associated with the specified key from the database.
Delete(key string) error
// Iterate is similar to View, but allows the iteration to start from a specific key prefix.
// The seekThenIterateKey parameter can be used to specify a key to seek to before starting the iteration.
Iterate(processFunc func(key, value []byte) error) error
IterateWithFilterByKeys(ks []string, processFunc func(key, value []byte) error) error
// Close closes the database, releasing any system resources it holds.
Close() error
OpenTransaction() error
CommitTransaction() error
DiscardTransaction()
}

View file

@ -0,0 +1,26 @@
package leveldb
type LevelDBConfigurationProvider interface {
GetDBPath() string
GetDBName() string
}
type LevelDBConfigurationProviderImpl struct {
dbPath string
dbName string
}
func NewLevelDBConfigurationProvider(dbPath string, dbName string) LevelDBConfigurationProvider {
return &LevelDBConfigurationProviderImpl{
dbPath: dbPath,
dbName: dbName,
}
}
func (me *LevelDBConfigurationProviderImpl) GetDBPath() string {
return me.dbPath
}
func (me *LevelDBConfigurationProviderImpl) GetDBName() string {
return me.dbName
}

View file

@ -0,0 +1,218 @@
package leveldb
import (
"log"
"strings"
"github.com/syndtr/goleveldb/leveldb"
dberr "github.com/syndtr/goleveldb/leveldb/errors"
"github.com/syndtr/goleveldb/leveldb/filter"
"github.com/syndtr/goleveldb/leveldb/opt"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/pkg/storage"
)
// storageImpl implements the db.Database interface.
// It uses a LevelDB database to store key-value pairs.
type storageImpl struct {
// The LevelDB database instance.
db *leveldb.DB
transaction *leveldb.Transaction
}
// NewDiskStorage creates a new instance of the storageImpl.
// It opens the database file at the specified path and returns an error if it fails.
func NewDiskStorage(provider LevelDBConfigurationProvider, logger *zap.Logger) storage.Storage {
logger = logger.Named("leveldb")
if provider == nil {
log.Fatal("NewDiskStorage: missing LevelDB configuration provider\n")
}
if provider.GetDBPath() == "" {
log.Fatal("NewDiskStorage: cannot have empty filepath for the database\n")
}
if provider.GetDBName() == "" {
log.Fatal("NewDiskStorage: cannot have empty db name for the database\n")
}
o := &opt.Options{
Filter: filter.NewBloomFilter(10),
}
filePath := provider.GetDBPath() + "/" + provider.GetDBName()
db, err := leveldb.OpenFile(filePath, o)
if err != nil {
log.Fatalf("NewDiskStorage: failed loading up key value storer adapter at %v with error: %v\n", filePath, err)
}
return &storageImpl{
db: db,
}
}
// Get retrieves a value from the database by its key.
// It returns an error if the key is not found.
func (impl *storageImpl) Get(k string) ([]byte, error) {
if impl.transaction == nil {
bin, err := impl.db.Get([]byte(k), nil)
if err == dberr.ErrNotFound {
return nil, nil
}
return bin, nil
}
bin, err := impl.transaction.Get([]byte(k), nil)
if err == dberr.ErrNotFound {
return nil, nil
}
return bin, nil
}
// Set sets a value in the database by its key.
// It returns an error if the operation fails.
func (impl *storageImpl) Set(k string, val []byte) error {
if impl.transaction == nil {
impl.db.Delete([]byte(k), nil)
err := impl.db.Put([]byte(k), val, nil)
if err == dberr.ErrNotFound {
return nil
}
return err
}
impl.transaction.Delete([]byte(k), nil)
err := impl.transaction.Put([]byte(k), val, nil)
if err == dberr.ErrNotFound {
return nil
}
return err
}
// Delete deletes a value from the database by its key.
// It returns an error if the operation fails.
func (impl *storageImpl) Delete(k string) error {
if impl.transaction == nil {
err := impl.db.Delete([]byte(k), nil)
if err == dberr.ErrNotFound {
return nil
}
return err
}
err := impl.transaction.Delete([]byte(k), nil)
if err == dberr.ErrNotFound {
return nil
}
return err
}
// Iterate iterates over the key-value pairs in the database, starting from the specified key prefix.
// It calls the provided function for each pair.
// It returns an error if the iteration fails.
func (impl *storageImpl) Iterate(processFunc func(key, value []byte) error) error {
if impl.transaction == nil {
iter := impl.db.NewIterator(nil, nil)
for ok := iter.First(); ok; ok = iter.Next() {
// Call the passed function for each key-value pair.
err := processFunc(iter.Key(), iter.Value())
if err == dberr.ErrNotFound {
return nil
}
if err != nil {
return err // Exit early if the processing function returns an error.
}
}
iter.Release()
return iter.Error()
}
iter := impl.transaction.NewIterator(nil, nil)
for ok := iter.First(); ok; ok = iter.Next() {
// Call the passed function for each key-value pair.
err := processFunc(iter.Key(), iter.Value())
if err == dberr.ErrNotFound {
return nil
}
if err != nil {
return err // Exit early if the processing function returns an error.
}
}
iter.Release()
return iter.Error()
}
func (impl *storageImpl) IterateWithFilterByKeys(ks []string, processFunc func(key, value []byte) error) error {
if impl.transaction == nil {
iter := impl.db.NewIterator(nil, nil)
for ok := iter.First(); ok; ok = iter.Next() {
// Iterate over our keys to search by.
for _, k := range ks {
searchKey := strings.ToLower(k)
targetKey := strings.ToLower(string(iter.Key()))
// If the item we currently have matches our keys then execute.
if searchKey == targetKey {
// Call the passed function for each key-value pair.
err := processFunc(iter.Key(), iter.Value())
if err == dberr.ErrNotFound {
return nil
}
if err != nil {
return err // Exit early if the processing function returns an error.
}
}
}
}
iter.Release()
return iter.Error()
}
iter := impl.transaction.NewIterator(nil, nil)
for ok := iter.First(); ok; ok = iter.Next() {
// Call the passed function for each key-value pair.
err := processFunc(iter.Key(), iter.Value())
if err == dberr.ErrNotFound {
return nil
}
if err != nil {
return err // Exit early if the processing function returns an error.
}
}
iter.Release()
return iter.Error()
}
// Close closes the database.
// It returns an error if the operation fails.
func (impl *storageImpl) Close() error {
if impl.transaction != nil {
impl.transaction.Discard()
}
return impl.db.Close()
}
func (impl *storageImpl) OpenTransaction() error {
transaction, err := impl.db.OpenTransaction()
if err != nil {
return nil
}
impl.transaction = transaction
return nil
}
func (impl *storageImpl) CommitTransaction() error {
defer func() {
impl.transaction = nil
}()
// Commit the snapshot to the database
return impl.transaction.Commit()
}
func (impl *storageImpl) DiscardTransaction() {
defer func() {
impl.transaction = nil
}()
impl.transaction.Discard()
}

View file

@ -0,0 +1,479 @@
package leveldb
import (
"os"
"path/filepath"
"reflect"
"testing"
"go.uber.org/zap"
)
// MockLevelDBConfigProvider is a mock implementation of a presumed LevelDBConfigurationProvider interface.
// This is created to satisfy the changed signature of NewDiskStorage.
type MockLevelDBConfigProvider struct {
Path string
// Add other fields here if the actual LevelDBConfigurationProvider interface
// and NewDiskStorage function require more configuration options (e.g., LevelDB options).
}
// GetDBPath returns the database path. This method is assumed to be part of the
// LevelDBConfigurationProvider interface.
func (m *MockLevelDBConfigProvider) GetDBPath() string {
return m.Path
}
// If LevelDBConfigurationProvider requires other methods (e.g., GetOptions()),
// they would need to be implemented here as well. For this repair, we assume
// GetDBPath is sufficient based on the original parameters of NewDiskStorage.
// testDir creates a temporary directory for testing
func testDir(t *testing.T) string {
dir, err := os.MkdirTemp("", "leveldb-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
return dir
}
// cleanup removes the test directory and its contents
func cleanup(t *testing.T, dir string) {
err := os.RemoveAll(dir)
if err != nil {
t.Errorf("Failed to cleanup test dir: %v", err)
}
}
// TestNewDiskStorage tests the creation of a new storage instance
func TestNewDiskStorage(t *testing.T) {
dir := testDir(t)
defer cleanup(t, dir)
dbName := "test.db"
config := &MockLevelDBConfigProvider{
Path: filepath.Join(dir, dbName),
}
logger := zap.NewExample()
storage := NewDiskStorage(config, logger)
if storage == nil {
t.Fatal("Expected non-nil storage instance")
}
// Type assertion to verify we get the correct implementation
impl, ok := storage.(*storageImpl)
if !ok {
t.Fatal("Expected storageImpl instance")
}
if impl.db == nil {
t.Fatal("Expected non-nil leveldb instance")
}
// It's generally better to call Close on the interface, but if impl.Close() has
// specific behavior being tested or is necessary for some reason, it can stay.
// However, a defer storage.Close() would be more idiomatic if not for specific impl testing.
err := storage.Close() // Changed from impl.Close() to storage.Close() for consistency
if err != nil {
t.Fatalf("Failed to close storage: %v", err)
}
}
// TestBasicOperations tests the basic Set/Get/Delete operations
func TestBasicOperations(t *testing.T) {
dir := testDir(t)
defer cleanup(t, dir)
dbName := "test.db"
config := &MockLevelDBConfigProvider{
Path: filepath.Join(dir, dbName),
}
storage := NewDiskStorage(config, zap.NewExample())
defer storage.Close()
// Test Set and Get
t.Run("Set and Get", func(t *testing.T) {
key := "test-key"
value := []byte("test-value")
err := storage.Set(key, value)
if err != nil {
t.Fatalf("Set failed: %v", err)
}
retrieved, err := storage.Get(key)
if err != nil {
t.Fatalf("Get failed: %v", err)
}
if !reflect.DeepEqual(retrieved, value) {
t.Errorf("Retrieved value doesn't match: got %v, want %v", retrieved, value)
}
})
// Test Get with non-existent key
t.Run("Get Non-existent", func(t *testing.T) {
val, err := storage.Get("non-existent")
// LevelDB's Get typically returns leveldb.ErrNotFound for non-existent keys.
// If the wrapper converts this to (nil, nil), the original check is fine.
// If it propagates ErrNotFound, then err should be checked for that specific error.
// Assuming the wrapper intends (nil, nil) for not found.
if err != nil {
t.Fatalf("Expected nil error or specific 'not found' error for non-existent key, got: %v", err)
}
if val != nil {
t.Errorf("Expected nil value for non-existent key, got: %v", val)
}
})
// Test Delete
t.Run("Delete", func(t *testing.T) {
key := "delete-test"
value := []byte("delete-value")
// First set a value
err := storage.Set(key, value)
if err != nil {
t.Fatalf("Set failed: %v", err)
}
// Delete it
err = storage.Delete(key)
if err != nil {
t.Fatalf("Delete failed: %v", err)
}
// Verify it's gone
val, err := storage.Get(key)
if err != nil {
t.Fatalf("Get after delete failed: %v", err) // Similar to "Get Non-existent"
}
if val != nil {
t.Error("Expected nil value after deletion")
}
})
}
// TestIteration tests the iteration functionality
func TestIteration(t *testing.T) {
dir := testDir(t)
defer cleanup(t, dir)
dbName := "test.db"
config := &MockLevelDBConfigProvider{
Path: filepath.Join(dir, dbName),
}
storage := NewDiskStorage(config, zap.NewExample())
defer storage.Close()
// Prepare test data
testData := map[string][]byte{
"key1": []byte("value1"),
"key2": []byte("value2"),
"key3": []byte("value3"),
}
// Insert test data
for k, v := range testData {
if err := storage.Set(k, v); err != nil {
t.Fatalf("Failed to set test data: %v", err)
}
}
// Test basic iteration
t.Run("Basic Iteration", func(t *testing.T) {
found := make(map[string][]byte)
err := storage.Iterate(func(key, value []byte) error {
valueCopy := make([]byte, len(value))
copy(valueCopy, value)
found[string(key)] = valueCopy
return nil
})
if err != nil {
t.Fatalf("Iteration failed: %v", err)
}
if len(found) != len(testData) {
t.Errorf("Iteration found %d items, expected %d", len(found), len(testData))
}
for k, expectedValue := range testData {
actualValue, exists := found[k]
if !exists {
t.Errorf("Key %q not found in iteration results", k)
continue
}
if !reflect.DeepEqual(actualValue, expectedValue) {
t.Errorf("Value mismatch for key %q: got %q, want %q",
k, string(actualValue), string(expectedValue))
}
}
})
// Test filtered iteration
t.Run("Filtered Iteration", func(t *testing.T) {
filterKeys := []string{"key1", "key3"}
expectedFilteredData := make(map[string][]byte)
for _, k := range filterKeys {
if v, ok := testData[k]; ok {
expectedFilteredData[k] = v
}
}
found := make(map[string][]byte)
err := storage.IterateWithFilterByKeys(filterKeys, func(key, value []byte) error {
valueCopy := make([]byte, len(value))
copy(valueCopy, value)
found[string(key)] = valueCopy
return nil
})
if err != nil {
t.Fatalf("Filtered iteration failed: %v", err)
}
if len(found) != len(expectedFilteredData) {
t.Errorf("Filtered iteration found %d items, expected %d", len(found), len(expectedFilteredData))
}
for _, k := range filterKeys {
expectedValue, dataExists := testData[k]
if !dataExists { // Should not happen if filterKeys are from testData
t.Errorf("Test data sanity check: key %q from filterKeys not in testData", k)
continue
}
actualValue, exists := found[k]
if !exists {
t.Errorf("Key %q not found in filtered results", k)
continue
}
if !reflect.DeepEqual(actualValue, expectedValue) {
t.Errorf("Value mismatch for key %q: got %q, want %q",
k, string(actualValue), string(expectedValue))
}
}
})
}
// TestTransactions tests the transaction functionality
func TestTransactions(t *testing.T) {
dir := testDir(t)
defer cleanup(t, dir)
dbName := "test.db"
config := &MockLevelDBConfigProvider{
Path: filepath.Join(dir, dbName),
}
storage := NewDiskStorage(config, zap.NewExample())
defer storage.Close()
t.Run("Transaction Commit", func(t *testing.T) {
err := storage.OpenTransaction()
if err != nil {
t.Fatalf("Failed to open transaction: %v", err)
}
key := "tx-test"
value := []byte("tx-value")
err = storage.Set(key, value) // Inside transaction
if err != nil {
storage.DiscardTransaction() // Ensure transaction is cleaned up on failure
t.Fatalf("Failed to set in transaction: %v", err)
}
err = storage.CommitTransaction()
if err != nil {
t.Fatalf("Failed to commit transaction: %v", err)
}
retrieved, err := storage.Get(key)
if err != nil {
t.Fatalf("Failed to get after commit: %v", err)
}
if !reflect.DeepEqual(retrieved, value) {
t.Errorf("Retrieved value doesn't match after commit: got %v, want %v", retrieved, value)
}
})
t.Run("Transaction Discard", func(t *testing.T) {
err := storage.OpenTransaction()
if err != nil {
t.Fatalf("Failed to open transaction: %v", err)
}
key := "discard-test"
value := []byte("discard-value")
// Set a value outside transaction first to ensure Get behavior is clear
outerKey := "outer-key-discard"
outerValue := []byte("outer-value-discard")
if err := storage.Set(outerKey, outerValue); err != nil {
storage.DiscardTransaction()
t.Fatalf("Setup: Failed to set outer key: %v", err)
}
err = storage.Set(key, value) // Inside transaction
if err != nil {
storage.DiscardTransaction()
t.Fatalf("Failed to set in transaction: %v", err)
}
storage.DiscardTransaction()
// Verify changes were not persisted for 'key'
val, err := storage.Get(key)
if err != nil { // Assuming (nil, nil) for not found, or check for specific not-found error
t.Fatalf("Get after discard failed for transactional key: %v", err)
}
if val != nil {
t.Error("Expected nil value for transactional key after discarding transaction")
}
// Verify outer key is still accessible (if transactions don't block normal ops)
// This depends on the implementation detail of how transactions interact with non-transactional ops.
// If OpenTransaction acquires a lock, this Get might behave differently.
// For now, assuming Get outside transaction should work.
retrievedOuter, err := storage.Get(outerKey)
if err != nil {
t.Fatalf("Get after discard failed for outer key: %v", err)
}
if !reflect.DeepEqual(retrievedOuter, outerValue) {
t.Errorf("Outer key value changed or became inaccessible after discard: got %v, want %v", retrievedOuter, outerValue)
}
})
}
// TestPersistence verifies that data persists after closing and reopening the database
func TestPersistence(t *testing.T) {
dir := testDir(t)
defer cleanup(t, dir)
dbName := "persist.db"
key := "persist-key"
value := []byte("persist-value")
// First session: write data
func() {
config := &MockLevelDBConfigProvider{
Path: filepath.Join(dir, dbName),
}
storage := NewDiskStorage(config, zap.NewExample())
defer storage.Close()
err := storage.Set(key, value)
if err != nil {
t.Fatalf("Failed to set value: %v", err)
}
}() // storage is closed here
// Second session: verify data
func() {
config := &MockLevelDBConfigProvider{
Path: filepath.Join(dir, dbName),
}
storage := NewDiskStorage(config, zap.NewExample())
defer storage.Close()
retrieved, err := storage.Get(key)
if err != nil {
t.Fatalf("Failed to get value: %v", err)
}
if !reflect.DeepEqual(retrieved, value) {
t.Errorf("Retrieved value doesn't match after reopen: got %v, want %v", retrieved, value)
}
}() // storage is closed here
}
// TestDatabaseError tests error handling for invalid database operations
func TestDatabaseError(t *testing.T) {
dir := testDir(t)
defer cleanup(t, dir)
dbName := "test.db"
config := &MockLevelDBConfigProvider{
Path: filepath.Join(dir, dbName),
}
storage := NewDiskStorage(config, zap.NewExample())
// Test argument validation (should ideally be tested on an open DB,
// but original test implies testing on closed DB or combined effects)
// If these are meant to be pure argument validation, they should be done before Close().
// For now, keeping original structure.
// Test Set with empty key (on an open DB first, then closed)
// Let's assume the Set method itself validates this, regardless of DB state.
// If Set returns ErrClosed when DB is closed, it might mask empty key error.
// To test empty key specifically:
if err := storage.Set("", []byte("value")); err == nil { // This might be an error from the underlying DB for empty key
t.Error("Expected error setting empty key (on open DB or for argument validation)")
}
// Test Set with nil value
if err := storage.Set("key", nil); err == nil { // This might be an error from underlying DB for nil value
t.Error("Expected error setting nil value (on open DB or for argument validation)")
}
// Now, close the database to force errors for subsequent operations
errClose := storage.Close()
if errClose != nil {
t.Fatalf("Failed to close storage for error testing: %v", errClose)
}
// Test database operations on a closed DB
t.Run("Operations on Closed DB", func(t *testing.T) {
if err := storage.Set("another-key", []byte("value")); err == nil {
t.Error("Expected error setting on a closed database")
}
if _, err := storage.Get("another-key"); err == nil {
t.Error("Expected error getting from a closed database")
}
if err := storage.Delete("another-key"); err == nil {
t.Error("Expected error deleting from a closed database")
}
if err := storage.Iterate(func(k, v []byte) error { return nil }); err == nil {
t.Error("Expected error iterating on a closed database")
}
// Test OpenTransaction on a closed DB
// Original comment: "OpenTransaction returns nil even on error per implementation"
// This behavior is unusual. A robust implementation should return an error.
err := storage.OpenTransaction()
// If the contract is that it returns nil error even if DB is closed:
if err != nil {
t.Errorf("OpenTransaction returned an error (%v), expected nil error even when db is closed based on original test comment", err)
}
// Even if OpenTransaction returns nil error, it shouldn't have actually created a transaction.
if impl, ok := storage.(*storageImpl); ok {
if impl.transaction != nil {
t.Error("Transaction should be nil after OpenTransaction on a closed DB, even if no error was returned by OpenTransaction")
}
} else {
t.Fatal("Expected storageImpl instance for checking transaction state")
}
// CommitTransaction on a closed DB (and no active transaction)
if err := storage.CommitTransaction(); err == nil {
t.Error("Expected error committing transaction on a closed database / no active transaction")
}
// DiscardTransaction on a closed DB (and no active transaction)
// Discard is often idempotent, so it might not error.
storage.DiscardTransaction() // Assuming this is safe to call.
// Closing an already closed DB
if err := storage.Close(); err == nil {
// This depends on the desired behavior of Close(). Some are idempotent, others error.
// t.Error("Expected error when closing an already closed database, if that's the contract")
}
})
}