refactor: Remove unused packages and flatten project structure (#27)

* refactor: Remove unused packages and flatten project structure

Removes the following unused packages:
- pkg/crypt
- pkg/workspace
- pkg/io

Moves the remaining packages (core, e, runtime) to the top level of the project.

Updates all import paths to reflect the new structure.

* refactor: Remove unused packages and flatten project structure

Removes the following unused packages:
- pkg/crypt
- pkg/workspace
- pkg/io

Moves the remaining packages (core, e, runtime) to the top level of the project.

Updates all import paths to reflect the new structure.

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
This commit is contained in:
Snider 2025-11-13 17:26:38 +00:00 committed by GitHub
parent 614aed51ba
commit 67a38acc7c
48 changed files with 75 additions and 2886 deletions

View file

@ -4,7 +4,7 @@ import (
"embed" "embed"
"log" "log"
"github.com/Snider/Core/pkg/runtime" "github.com/Snider/Core/runtime"
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
) )

View file

@ -101,10 +101,10 @@ package {{.ServiceName}}
import ( import (
// Import the internal implementation with an alias. // Import the internal implementation with an alias.
impl "github.com/Snider/Core/pkg/{{.ServiceName}}" impl "github.com/Snider/Core/{{.ServiceName}}"
// Import the core contracts to re-export the interface. // Import the core contracts to re-export the interface.
"github.com/Snider/Core/pkg/core" "github.com/Snider/Core/core"
) )
{{range .Symbols}} {{range .Symbols}}

View file

@ -4,7 +4,7 @@ import (
"embed" "embed"
"log" "log"
"github.com/Snider/Core/pkg/runtime" "github.com/Snider/Core/runtime"
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
) )

View file

@ -4,7 +4,7 @@ import (
"embed" "embed"
"log" "log"
"github.com/Snider/Core/pkg/runtime" "github.com/Snider/Core/runtime"
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
) )

View file

@ -21,7 +21,7 @@ import (
"embed" "embed"
"log" "log"
"github.com/Snider/Core/pkg/runtime" "github.com/Snider/Core/runtime"
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
) )

View file

8
go.mod
View file

@ -3,17 +3,14 @@ module github.com/Snider/Core
go 1.25 go 1.25
require ( require (
github.com/ProtonMail/go-crypto v1.3.0
github.com/pkg/sftp v1.13.10
github.com/skeema/knownhosts v1.3.2
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/wailsapp/wails/v3 v3.0.0-alpha.37 github.com/wailsapp/wails/v3 v3.0.0-alpha.37
golang.org/x/crypto v0.43.0
) )
require ( require (
dario.cat/mergo v1.0.2 // indirect dario.cat/mergo v1.0.2 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/adrg/xdg v0.5.3 // indirect github.com/adrg/xdg v0.5.3 // indirect
github.com/bep/debounce v1.2.1 // indirect github.com/bep/debounce v1.2.1 // indirect
github.com/cloudflare/circl v1.6.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect
@ -32,7 +29,6 @@ require (
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect github.com/kevinburke/ssh_config v1.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/u v1.1.1 // indirect github.com/leaanthony/u v1.1.1 // indirect
github.com/lmittmann/tint v1.1.2 // indirect github.com/lmittmann/tint v1.1.2 // indirect
@ -45,9 +41,11 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.52.0 // indirect github.com/samber/lo v1.52.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect
github.com/skeema/knownhosts v1.3.2 // indirect
github.com/wailsapp/go-webview2 v1.0.22 // indirect github.com/wailsapp/go-webview2 v1.0.22 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/net v0.46.0 // indirect golang.org/x/net v0.46.0 // indirect
golang.org/x/sys v0.37.0 // indirect golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect golang.org/x/text v0.30.0 // indirect

4
go.sum
View file

@ -54,8 +54,6 @@ github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PW
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@ -84,8 +82,6 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=

View file

@ -1,33 +0,0 @@
package crypt
import (
"github.com/Snider/Core/pkg/core"
"github.com/Snider/Core/pkg/crypt/internal"
)
// Options holds configuration for the crypt service.
type Options = internal.Options
// Service provides cryptographic functions to the application.
type Service = internal.Service
// HashType defines the supported hashing algorithms.
type HashType = internal.HashType
const (
LTHN = internal.LTHN
SHA512 = internal.SHA512
SHA256 = internal.SHA256
SHA1 = internal.SHA1
MD5 = internal.MD5
)
// New is the constructor for static dependency injection.
func New() (*Service, error) {
return internal.New()
}
// Register is the constructor for dynamic dependency injection.
func Register(c *core.Core) (any, error) {
return internal.Register(c)
}

View file

@ -1,22 +0,0 @@
package crypt
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestHash(t *testing.T) {
s, err := New()
assert.NoError(t, err)
payload := "hello"
hash := s.Hash(LTHN, payload)
assert.NotEmpty(t, hash)
}
func TestLuhn(t *testing.T) {
s, err := New()
assert.NoError(t, err)
assert.True(t, s.Luhn("79927398713"))
assert.False(t, s.Luhn("79927398714"))
}

View file

@ -1,181 +0,0 @@
package internal
import (
"bytes"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/binary"
"encoding/hex"
"io"
"strconv"
"strings"
"github.com/Snider/Core/pkg/core"
"github.com/Snider/Core/pkg/crypt/lthn"
"github.com/Snider/Core/pkg/crypt/openpgp"
"github.com/Snider/Core/pkg/e"
)
// Options holds configuration for the crypt service.
type Options struct{}
// Service provides cryptographic functions to the application.
type Service struct {
*core.Runtime[Options]
}
// HashType defines the supported hashing algorithms.
type HashType string
const (
LTHN HashType = "lthn"
SHA512 HashType = "sha512"
SHA256 HashType = "sha256"
SHA1 HashType = "sha1"
MD5 HashType = "md5"
)
// newCryptService contains the common logic for initializing a Service struct.
func newCryptService() (*Service, error) {
return &Service{}, nil
}
// New is the constructor for static dependency injection.
// It creates a Service instance without initializing the core.Runtime field.
func New() (*Service, error) {
return newCryptService()
}
// Register is the constructor for dynamic dependency injection (used with core.WithService).
// It creates a Service instance and initializes its core.Runtime field.
func Register(c *core.Core) (any, error) {
s, err := newCryptService()
if err != nil {
return nil, e.E("crypt.Register", "failed to create new crypt service", err)
}
s.Runtime = core.NewRuntime(c, Options{})
return s, nil
}
// --- Hashing ---
// Hash computes a hash of the payload using the specified algorithm.
func (s *Service) Hash(lib HashType, payload string) string {
switch lib {
case LTHN:
return lthn.Hash(payload)
case SHA512:
hash := sha512.Sum512([]byte(payload))
return hex.EncodeToString(hash[:])
case SHA1:
hash := sha1.Sum([]byte(payload))
return hex.EncodeToString(hash[:])
case MD5:
hash := md5.Sum([]byte(payload))
return hex.EncodeToString(hash[:])
case SHA256:
fallthrough
default:
hash := sha256.Sum256([]byte(payload))
return hex.EncodeToString(hash[:])
}
}
// --- Checksums ---
// Luhn validates a number using the Luhn algorithm.
func (s *Service) Luhn(payload string) bool {
payload = strings.ReplaceAll(payload, " ", "")
sum := 0
isSecond := false
for i := len(payload) - 1; i >= 0; i-- {
digit, err := strconv.Atoi(string(payload[i]))
if err != nil {
return false // Contains non-digit
}
if isSecond {
digit = digit * 2
if digit > 9 {
digit = digit - 9
}
}
sum += digit
isSecond = !isSecond
}
return sum%10 == 0
}
// Fletcher16 computes the Fletcher-16 checksum.
func (s *Service) Fletcher16(payload string) uint16 {
data := []byte(payload)
var sum1, sum2 uint16
for _, b := range data {
sum1 = (sum1 + uint16(b)) % 255
sum2 = (sum2 + sum1) % 255
}
return (sum2 << 8) | sum1
}
// Fletcher32 computes the Fletcher-32 checksum.
func (s *Service) Fletcher32(payload string) uint32 {
data := []byte(payload)
if len(data)%2 != 0 {
data = append(data, 0)
}
var sum1, sum2 uint32
for i := 0; i < len(data); i += 2 {
val := binary.LittleEndian.Uint16(data[i : i+2])
sum1 = (sum1 + uint32(val)) % 65535
sum2 = (sum2 + sum1) % 65535
}
return (sum2 << 16) | sum1
}
// Fletcher64 computes the Fletcher-64 checksum.
func (s *Service) Fletcher64(payload string) uint64 {
data := []byte(payload)
if len(data)%4 != 0 {
padding := 4 - (len(data) % 4)
data = append(data, make([]byte, padding)...)
}
var sum1, sum2 uint64
for i := 0; i < len(data); i += 4 {
val := binary.LittleEndian.Uint32(data[i : i+4])
sum1 = (sum1 + uint64(val)) % 4294967295
sum2 = (sum2 + sum1) % 4294967295
}
return (sum2 << 32) | sum1
}
// --- PGP ---
// EncryptPGP encrypts data for a recipient, optionally signing it.
func (s *Service) EncryptPGP(writer io.Writer, recipientPath, data string, signerPath, signerPassphrase *string) (string, error) {
var buf bytes.Buffer
err := openpgp.EncryptPGP(&buf, recipientPath, data, signerPath, signerPassphrase)
if err != nil {
return "", e.E("crypt.EncryptPGP", "failed to encrypt PGP message", err)
}
// Copy the encrypted data to the original writer.
if _, err := writer.Write(buf.Bytes()); err != nil {
return "", e.E("crypt.EncryptPGP", "failed to write encrypted PGP message to writer", err)
}
return buf.String(), nil
}
// DecryptPGP decrypts a PGP message, optionally verifying the signature.
func (s *Service) DecryptPGP(recipientPath, message, passphrase string, signerPath *string) (string, error) {
decrypted, err := openpgp.DecryptPGP(recipientPath, message, passphrase, signerPath)
if err != nil {
return "", e.E("crypt.DecryptPGP", "failed to decrypt PGP message", err)
}
return decrypted, nil
}

View file

@ -1,48 +0,0 @@
package lthn
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestHash(t *testing.T) {
input := "test_string"
expectedHash := "45d4027179b17265c38732fb1e7089a0b1adfe1d3ba4105fce66f7d46ba42f7d"
hashed := Hash(input)
fmt.Printf("Hash for \"%s\": %s\n", input, hashed)
assert.Equal(t, expectedHash, hashed, "The hash should match the expected value")
}
func TestCreateSalt(t *testing.T) {
// Test with default keyMap
SetKeyMap(map[rune]rune{})
assert.Equal(t, "gnirts_tset", createSalt("test_string"))
assert.Equal(t, "", createSalt(""))
assert.Equal(t, "A", createSalt("A"))
// Test with a custom keyMap
customKeyMap := map[rune]rune{
'a': 'x',
'b': 'y',
'c': 'z',
}
SetKeyMap(customKeyMap)
assert.Equal(t, "zyx", createSalt("abc"))
assert.Equal(t, "gnirts_tset", createSalt("test_string")) // 'test_string' doesn't have 'a', 'b', 'c'
// Reset keyMap to default for other tests
SetKeyMap(map[rune]rune{})
}
func TestVerify(t *testing.T) {
input := "another_test_string"
hashed := Hash(input)
assert.True(t, Verifyf(input, hashed), "Verifyf should return true for a matching hash")
assert.False(t, Verifyf(input, "wrong_hash"), "Verifyf should return false for a non-matching hash")
assert.False(t, Verifyf("different_input", hashed), "Verifyf should return false for different input")
}

View file

@ -1,61 +0,0 @@
package lthn
import (
"crypto/sha256"
"encoding/hex"
)
// keyMap is the default character-swapping map used for the quasi-salting process.
var keyMap = map[rune]rune{
'o': '0',
'l': '1',
'e': '3',
'a': '4',
's': 'z',
't': '7',
'0': 'o',
'1': 'l',
'3': 'e',
'4': 'a',
'7': 't',
}
// SetKeyMap sets the key map for the notarisation process.
func SetKeyMap(newKeyMap map[rune]rune) {
keyMap = newKeyMap
}
// GetKeyMap gets the current key map.
func GetKeyMap() map[rune]rune {
return keyMap
}
// Hash creates a reproducible hash from a string.
func Hash(input string) string {
salt := createSalt(input)
hash := sha256.Sum256([]byte(input + salt))
return hex.EncodeToString(hash[:])
}
// createSalt creates a quasi-salt from a string by reversing it and swapping characters.
func createSalt(input string) string {
if input == "" {
return ""
}
runes := []rune(input)
salt := make([]rune, len(runes))
for i := 0; i < len(runes); i++ {
char := runes[len(runes)-1-i]
if replacement, ok := keyMap[char]; ok {
salt[i] = replacement
} else {
salt[i] = char
}
}
return string(salt)
}
// Verify checks if an input string matches a given hash.
func Verifyf(input string, hash string) bool {
return Hash(input) == hash
}

View file

@ -1,233 +0,0 @@
package openpgp
import (
"bytes"
"fmt"
"io"
"os"
"strings"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/packet"
)
// readRecipientEntity reads an armored PGP public key from the given path.
func readRecipientEntity(path string) (entity *openpgp.Entity, err error) {
recipientFile, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("openpgp: failed to open recipient public key file at %s: %w", path, err)
}
defer func() {
if closeErr := recipientFile.Close(); closeErr != nil && err == nil {
err = fmt.Errorf("openpgp: failed to close recipient key file: %w", closeErr)
}
}()
block, err := armor.Decode(recipientFile)
if err != nil {
return nil, fmt.Errorf("openpgp: failed to decode armored key from %s: %w", path, err)
}
if block.Type != openpgp.PublicKeyType {
return nil, fmt.Errorf("openpgp: invalid key type in %s: expected public key, got %s", path, block.Type)
}
entity, err = openpgp.ReadEntity(packet.NewReader(block.Body))
if err != nil {
return nil, fmt.Errorf("openpgp: failed to read entity from public key: %w", err)
}
return entity, nil
}
// readSignerEntity reads and decrypts an armored PGP private key.
func readSignerEntity(path, passphrase string) (entity *openpgp.Entity, err error) {
signerFile, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("openpgp: failed to open signer private key file at %s: %w", path, err)
}
defer func() {
if closeErr := signerFile.Close(); closeErr != nil && err == nil {
err = fmt.Errorf("openpgp: failed to close signer key file: %w", closeErr)
}
}()
block, err := armor.Decode(signerFile)
if err != nil {
return nil, fmt.Errorf("openpgp: failed to decode armored key from %s: %w", path, err)
}
if block.Type != openpgp.PrivateKeyType {
return nil, fmt.Errorf("openpgp: invalid key type in %s: expected private key, got %s", path, block.Type)
}
entity, err = openpgp.ReadEntity(packet.NewReader(block.Body))
if err != nil {
return nil, fmt.Errorf("openpgp: failed to read entity from private key: %w", err)
}
// Decrypt the primary private key.
if entity.PrivateKey != nil && entity.PrivateKey.Encrypted {
if err := entity.PrivateKey.Decrypt([]byte(passphrase)); err != nil {
return nil, fmt.Errorf("openpgp: failed to decrypt private key: %w", err)
}
}
// Decrypt all subkeys.
for _, subkey := range entity.Subkeys {
if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted {
if err := subkey.PrivateKey.Decrypt([]byte(passphrase)); err != nil {
return nil, fmt.Errorf("openpgp: failed to decrypt subkey: %w", err)
}
}
}
return entity, nil
}
// readRecipientKeyRing reads an armored PGP key ring from the given path.
func readRecipientKeyRing(path string) (entityList openpgp.EntityList, err error) {
recipientFile, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("openpgp: failed to open recipient key file at %s: %w", path, err)
}
defer func() {
if closeErr := recipientFile.Close(); closeErr != nil && err == nil {
err = fmt.Errorf("openpgp: failed to close recipient key file: %w", closeErr)
}
}()
entityList, err = openpgp.ReadArmoredKeyRing(recipientFile)
if err != nil {
return nil, fmt.Errorf("openpgp: failed to read armored key ring from %s: %w", path, err)
}
if len(entityList) == 0 {
return nil, fmt.Errorf("openpgp: no keys found in recipient key file %s", path)
}
return entityList, nil
}
// EncryptPGP encrypts a string using PGP, writing the armored, encrypted
// result to the provided io.Writer.
func EncryptPGP(writer io.Writer, recipientPath, data string, signerPath, signerPassphrase *string) error {
// 1. Read the recipient's public key
recipientEntity, err := readRecipientEntity(recipientPath)
if err != nil {
return err
}
// 2. Set up the list of recipients
to := openpgp.EntityList{recipientEntity}
// 3. Handle optional signing
var signer *openpgp.Entity
if signerPath != nil {
var passphrase string
if signerPassphrase != nil {
passphrase = *signerPassphrase
}
signer, err = readSignerEntity(*signerPath, passphrase)
if err != nil {
return fmt.Errorf("openpgp: failed to prepare signer: %w", err)
}
}
// 4. Create an armored writer and encrypt the message
armoredWriter, err := armor.Encode(writer, "PGP MESSAGE", nil)
if err != nil {
return fmt.Errorf("openpgp: failed to create armored writer: %w", err)
}
plaintext, err := openpgp.Encrypt(armoredWriter, to, signer, nil, nil)
if err != nil {
_ = armoredWriter.Close() // Attempt to close, but prioritize the encryption error.
return fmt.Errorf("openpgp: failed to begin encryption: %w", err)
}
_, err = plaintext.Write([]byte(data))
if err != nil {
_ = plaintext.Close()
_ = armoredWriter.Close()
return fmt.Errorf("openpgp: failed to write data to encryption stream: %w", err)
}
// 5. Explicitly close the writers to finalize the message.
if err := plaintext.Close(); err != nil {
return fmt.Errorf("openpgp: failed to finalize plaintext writer: %w", err)
}
if err := armoredWriter.Close(); err != nil {
return fmt.Errorf("openpgp: failed to finalize armored writer: %w", err)
}
return nil
}
// DecryptPGP decrypts an armored PGP message.
func DecryptPGP(recipientPath, message, passphrase string, signerPath *string) (string, error) {
// 1. Read the recipient's private key
entityList, err := readRecipientKeyRing(recipientPath)
if err != nil {
return "", err
}
// 2. Decode the armored message
block, err := armor.Decode(strings.NewReader(message))
if err != nil {
return "", fmt.Errorf("openpgp: failed to decode armored message: %w", err)
}
if block.Type != "PGP MESSAGE" {
return "", fmt.Errorf("openpgp: invalid message type: got %s, want PGP MESSAGE", block.Type)
}
// 3. If signature verification is required, add signer's public key to keyring
var signerEntity *openpgp.Entity
keyring := entityList
if signerPath != nil {
signerEntity, err = readRecipientEntity(*signerPath)
if err != nil {
return "", fmt.Errorf("openpgp: failed to read signer public key: %w", err)
}
keyring = append(keyring, signerEntity)
}
// 4. Decrypt the message body
md, err := openpgp.ReadMessage(block.Body, keyring, func(keys []openpgp.Key, symmetric bool) ([]byte, error) {
return []byte(passphrase), nil
}, nil)
if err != nil {
return "", fmt.Errorf("openpgp: failed to read PGP message: %w", err)
}
// Buffer the unverified body. Do not return or act on it until signature checks pass.
plaintextBuffer := new(bytes.Buffer)
if _, err := io.Copy(plaintextBuffer, md.UnverifiedBody); err != nil {
return "", fmt.Errorf("openpgp: failed to buffer plaintext message body: %w", err)
}
// 5. Handle optional signature verification
if signerPath != nil {
// First, ensure a signature actually exists when one is expected.
if md.SignedByKeyId == 0 {
return "", fmt.Errorf("openpgp: signature verification failed: message is not signed")
}
if md.SignatureError != nil {
return "", fmt.Errorf("openpgp: signature verification failed: %w", md.SignatureError)
}
if signerEntity != nil && md.SignedByKeyId != signerEntity.PrimaryKey.KeyId {
match := false
for _, subkey := range signerEntity.Subkeys {
if subkey.PublicKey != nil && subkey.PublicKey.KeyId == md.SignedByKeyId {
match = true
break
}
}
if !match {
return "", fmt.Errorf("openpgp: signature from unexpected key id: got %d, want one of signer key IDs", md.SignedByKeyId)
}
}
}
return plaintextBuffer.String(), nil
}

View file

@ -1,71 +0,0 @@
package openpgp
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
// TestDecryptWithWrongPassphrase checks that DecryptPGP returns an error when the wrong passphrase is used.
func TestDecryptWithWrongPassphrase(t *testing.T) {
recipientPub, _, cleanup := generateTestKeys(t, "recipient", "") // Unencrypted key for encryption
defer cleanup()
// Use the pre-generated encrypted key for decryption test
encryptedPrivKeyPath, cleanup2 := createEncryptedKeyFile(t)
defer cleanup2()
originalMessage := "This message should fail to decrypt."
var encryptedBuf bytes.Buffer
err := EncryptPGP(&encryptedBuf, recipientPub, originalMessage, nil, nil)
assert.NoError(t, err, "Encryption failed unexpectedly")
encryptedMessage := encryptedBuf.String()
_, err = DecryptPGP(encryptedPrivKeyPath, encryptedMessage, "wrong-passphrase", nil)
assert.Error(t, err, "Decryption was expected to fail with wrong passphrase, but it succeeded.")
assert.Contains(t, err.Error(), "failed to read PGP message", "Expected error message about failing to read PGP message")
}
// TestDecryptMalformedMessage checks that DecryptPGP handles non-PGP or malformed input gracefully.
func TestDecryptMalformedMessage(t *testing.T) {
// Generate an unencrypted key for this test, as we expect failure before key usage.
_, recipientPriv, cleanup := generateTestKeys(t, "recipient", "")
defer cleanup()
malformedMessage := "This is not a PGP message."
// The passphrase here is irrelevant as the key is not encrypted, but we pass one
// to satisfy the function signature.
_, err := DecryptPGP(recipientPriv, malformedMessage, "any-pass", nil)
assert.Error(t, err, "Decryption should fail for a malformed message, but it did not.")
assert.Contains(t, err.Error(), "failed to decode armored message", "Expected error about decoding armored message")
}
// TestEncryptWithNonexistentRecipient checks that EncryptPGP fails when the recipient's public key file does not exist.
func TestEncryptWithNonexistentRecipient(t *testing.T) {
var encryptedBuf bytes.Buffer
err := EncryptPGP(&encryptedBuf, "/path/to/nonexistent/key.pub", "message", nil, nil)
assert.Error(t, err, "Encryption should fail if recipient key does not exist, but it succeeded.")
assert.Contains(t, err.Error(), "failed to open recipient public key file", "Expected file open error for recipient key")
}
// TestEncryptAndSignWithWrongPassphrase checks that signing during encryption fails with an incorrect passphrase.
func TestEncryptAndSignWithWrongPassphrase(t *testing.T) {
recipientPub, _, rCleanup := generateTestKeys(t, "recipient", "")
defer rCleanup()
// Use the pre-generated encrypted key for the signer
signerPriv, sCleanup := createEncryptedKeyFile(t)
defer sCleanup()
originalMessage := "This message should fail to sign."
wrongPassphrase := "wrong-signer-pass"
var encryptedBuf bytes.Buffer
err := EncryptPGP(&encryptedBuf, recipientPub, originalMessage, &signerPriv, &wrongPassphrase)
assert.Error(t, err, "Encryption with signing was expected to fail with a wrong passphrase, but it succeeded.")
assert.Contains(t, err.Error(), "failed to decrypt private key", "Expected error about private key decryption failure")
}

View file

@ -1,168 +0,0 @@
package openpgp
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/packet"
)
// generateTestKeys creates a new PGP entity and saves the public and private keys to temporary files.
func generateTestKeys(t *testing.T, name, passphrase string) (string, string, func()) {
t.Helper()
tempDir, err := os.MkdirTemp("", "pgp-keys-*")
if err != nil {
t.Fatalf("test setup: failed to create temp dir for keys: %v", err)
}
config := &packet.Config{
RSABits: 2048, // Use a reasonable key size for tests
}
entity, err := openpgp.NewEntity(name, "", name, config)
if err != nil {
t.Fatalf("test setup: failed to create new PGP entity: %v", err)
}
// --- Save Public Key ---
pubKeyPath := filepath.Join(tempDir, name+".pub")
pubKeyFile, err := os.Create(pubKeyPath)
if err != nil {
t.Fatalf("test setup: failed to create public key file: %v", err)
}
pubKeyWriter, err := armor.Encode(pubKeyFile, openpgp.PublicKeyType, nil)
if err != nil {
t.Fatalf("test setup: failed to create armored writer for public key: %v", err)
}
if err := entity.Serialize(pubKeyWriter); err != nil {
t.Fatalf("test setup: failed to serialize public key: %v", err)
}
if err := pubKeyWriter.Close(); err != nil {
t.Fatalf("test setup: failed to close public key writer: %v", err)
}
if err := pubKeyFile.Close(); err != nil {
t.Fatalf("test setup: failed to close public key file: %v", err)
}
// --- Save Private Key (unencrypted for test setup) ---
privKeyPath := filepath.Join(tempDir, name+".asc")
privKeyFile, err := os.Create(privKeyPath)
if err != nil {
t.Fatalf("test setup: failed to create private key file: %v", err)
}
privKeyWriter, err := armor.Encode(privKeyFile, openpgp.PrivateKeyType, nil)
if err != nil {
t.Fatalf("test setup: failed to create armored writer for private key: %v", err)
}
// Serialize the whole entity with an unencrypted private key.
if err := entity.SerializePrivate(privKeyWriter, nil); err != nil {
t.Fatalf("test setup: failed to serialize private key: %v", err)
}
if err := privKeyWriter.Close(); err != nil {
t.Fatalf("test setup: failed to close private key writer: %v", err)
}
if err := privKeyFile.Close(); err != nil {
t.Fatalf("test setup: failed to close private key file: %v", err)
}
cleanup := func() { os.RemoveAll(tempDir) }
return pubKeyPath, privKeyPath, cleanup
}
func TestEncryptDecryptPGP(t *testing.T) {
recipientPub, recipientPriv, cleanup := generateTestKeys(t, "recipient", "recipient-pass")
defer cleanup()
originalMessage := "This is a secret message."
// --- Test Encryption ---
var encryptedBuf bytes.Buffer
err := EncryptPGP(&encryptedBuf, recipientPub, originalMessage, nil, nil)
if err != nil {
t.Fatalf("EncryptPGP() failed unexpectedly: %v", err)
}
encryptedMessage := encryptedBuf.String()
if !strings.Contains(encryptedMessage, "-----BEGIN PGP MESSAGE-----") {
t.Errorf("Encrypted message does not appear to be PGP armored")
}
// --- Test Decryption ---
decryptedMessage, err := DecryptPGP(recipientPriv, encryptedMessage, "recipient-pass", nil)
if err != nil {
t.Fatalf("DecryptPGP() failed unexpectedly: %v", err)
}
if decryptedMessage != originalMessage {
t.Errorf("Decrypted message mismatch: got=%q, want=%q", decryptedMessage, originalMessage)
}
}
func TestSignAndVerifyPGP(t *testing.T) {
recipientPub, recipientPriv, rCleanup := generateTestKeys(t, "recipient", "recipient-pass")
defer rCleanup()
signerPub, signerPriv, sCleanup := generateTestKeys(t, "signer", "signer-pass")
defer sCleanup()
originalMessage := "This is a signed and verified message."
// --- Encrypt and Sign ---
var encryptedBuf bytes.Buffer
signerPass := "signer-pass"
err := EncryptPGP(&encryptedBuf, recipientPub, originalMessage, &signerPriv, &signerPass)
if err != nil {
t.Fatalf("EncryptPGP() with signing failed unexpectedly: %v", err)
}
encryptedMessage := encryptedBuf.String()
// --- Decrypt and Verify ---
decryptedMessage, err := DecryptPGP(recipientPriv, encryptedMessage, "recipient-pass", &signerPub)
if err != nil {
t.Fatalf("DecryptPGP() with verification failed unexpectedly: %v", err)
}
if decryptedMessage != originalMessage {
t.Errorf("Decrypted message mismatch after signing: got=%q, want=%q", decryptedMessage, originalMessage)
}
}
func TestVerificationFailure(t *testing.T) {
recipientPub, recipientPriv, rCleanup := generateTestKeys(t, "recipient", "recipient-pass")
defer rCleanup()
_, signerPriv, sCleanup := generateTestKeys(t, "signer", "signer-pass")
defer sCleanup()
// Generate a third, unexpected key to test verification failure
unexpectedSignerPub, _, uCleanup := generateTestKeys(t, "unexpected", "unexpected-pass")
defer uCleanup()
originalMessage := "This message should fail verification."
// --- Encrypt and Sign with the actual signer key ---
var encryptedBuf bytes.Buffer
signerPass := "signer-pass"
err := EncryptPGP(&encryptedBuf, recipientPub, originalMessage, &signerPriv, &signerPass)
if err != nil {
t.Fatalf("EncryptPGP() with signing failed unexpectedly: %v", err)
}
encryptedMessage := encryptedBuf.String()
// --- Attempt to Decrypt and Verify with the WRONG public key ---
_, err = DecryptPGP(recipientPriv, encryptedMessage, "recipient-pass", &unexpectedSignerPub)
if err == nil {
t.Fatal("DecryptPGP() did not fail, but verification with an incorrect key was expected to fail.")
}
if !strings.Contains(err.Error(), "signature from unexpected key") {
t.Errorf("Expected error to contain 'signature from unexpected key', but got: %v", err)
}
}

View file

@ -1,225 +0,0 @@
package openpgp
import (
"bytes"
"crypto"
"fmt"
"path/filepath"
"time"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/Snider/Core/pkg/crypt/lthn"
)
// CreateKeyPair generates a new OpenPGP key pair.
// The password parameter is optional. If not provided, the private key will not be encrypted.
func CreateKeyPair(username string, passwords ...string) (*KeyPair, error) {
var password string
if len(passwords) > 0 {
password = passwords[0]
}
entity, err := openpgp.NewEntity(username, "Lethean Desktop", "", &packet.Config{
RSABits: 4096,
DefaultHash: crypto.SHA256,
})
if err != nil {
return nil, fmt.Errorf("failed to create new entity: %w", err)
}
// The private key is initially unencrypted after NewEntity.
// Generate revocation certificate while the private key is unencrypted.
revocationCert, err := createRevocationCertificate(entity)
if err != nil {
revocationCert = "" // Non-critical, proceed without it if it fails
}
// Encrypt the private key only if a password is provided, after revocation cert generation.
if password != "" {
if err := entity.PrivateKey.Encrypt([]byte(password)); err != nil {
return nil, fmt.Errorf("failed to encrypt private key: %w", err)
}
}
publicKey, err := serializeEntity(entity, openpgp.PublicKeyType, "") // Public key doesn't need password
if err != nil {
return nil, err
}
// Private key serialization. The key is already in its final encrypted/unencrypted state.
privateKey, err := serializeEntity(entity, openpgp.PrivateKeyType, "") // No password needed here for serialization
if err != nil {
return nil, err
}
return &KeyPair{
PublicKey: publicKey,
PrivateKey: privateKey,
RevocationCertificate: revocationCert,
}, nil
}
// CreateServerKeyPair creates and stores a key pair for the server in a specific directory.
func CreateServerKeyPair(keysDir string) error {
serverKeyPath := filepath.Join(keysDir, "server.lthn.pub")
// Passphrase is derived from the path itself, consistent with original logic.
passphrase := lthn.Hash(serverKeyPath)
return createAndStoreKeyPair("server", passphrase, keysDir)
}
// GetPublicKey retrieves an armored public key for a given ID.
func GetPublicKey(path string) (*openpgp.Entity, error) {
return readEntity(path)
}
// GetPrivateKey retrieves and decrypts an armored private key.
func GetPrivateKey(path, passphrase string) (*openpgp.Entity, error) {
entity, err := readEntity(path)
if err != nil {
return nil, err
}
if entity.PrivateKey == nil {
return nil, fmt.Errorf("no private key found for path %s", path)
}
if entity.PrivateKey.Encrypted {
if err := entity.PrivateKey.Decrypt([]byte(passphrase)); err != nil {
return nil, fmt.Errorf("failed to decrypt private key for path %s: %w", path, err)
}
}
var primaryIdentity *openpgp.Identity
for _, identity := range entity.Identities {
if identity.SelfSignature.IsPrimaryId != nil && *identity.SelfSignature.IsPrimaryId {
primaryIdentity = identity
break
}
}
if primaryIdentity == nil {
for _, identity := range entity.Identities {
primaryIdentity = identity
break
}
}
if primaryIdentity == nil {
return nil, fmt.Errorf("key for %s has no identity", path)
}
if primaryIdentity.SelfSignature.KeyLifetimeSecs != nil {
if primaryIdentity.SelfSignature.CreationTime.Add(time.Duration(*primaryIdentity.SelfSignature.KeyLifetimeSecs) * time.Second).Before(time.Now()) {
return nil, fmt.Errorf("key for %s has expired", path)
}
}
return entity, nil
}
// --- Helper Functions ---
func createAndStoreKeyPair(id, password, dir string) error {
//var keyPair *KeyPair
var err error
//if password != "" {
// keyPair, err = CreateKeyPair(id, password)
//} else {
// keyPair, err = CreateKeyPair(id)
//}
if err != nil {
return fmt.Errorf("failed to create key pair for id %s: %w", id, err)
}
//if err := io.Local.EnsureDir(dir); err != nil {
// return fmt.Errorf("failed to ensure key directory exists: %w", err)
//}
//
//files := map[string]string{
// filepath.Join(dir, fmt.Sprintf("%s.lthn.pub", id)): keyPair.PublicKey,
// filepath.Join(dir, fmt.Sprintf("%s.lthn.key", id)): keyPair.PrivateKey,
// filepath.Join(dir, fmt.Sprintf("%s.lthn.rev", id)): keyPair.RevocationCertificate, // Re-enabled
//}
//
//for path, content := range files {
// if content == "" {
// continue
// }
// if err := io.Local.Write(path, content); err != nil {
// return fmt.Errorf("failed to write key file %s: %w", path, err)
// }
//}
return nil
}
func readEntity(path string) (*openpgp.Entity, error) {
//keyArmored, err := m.Read(path)
//if err != nil {
// return nil, fmt.Errorf("failed to read key file %s: %w", path, err)
//}
//entityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(keyArmored))
//if err != nil {
// return nil, fmt.Errorf("failed to parse key file %s: %w", path, err)
//}
//if len(entityList) == 0 {
// return nil, fmt.Errorf("no entity found in key file %s", path)
//}
//return entityList[0], nil
return nil, nil
}
func serializeEntity(entity *openpgp.Entity, keyType string, password string) (string, error) {
buf := new(bytes.Buffer)
writer, err := armor.Encode(buf, keyType, nil)
if err != nil {
return "", fmt.Errorf("failed to create armor encoder: %w", err)
}
if keyType == openpgp.PrivateKeyType {
// Serialize the private key in its current in-memory state.
// Encryption is handled by CreateKeyPair before this function is called.
err = entity.SerializePrivateWithoutSigning(writer, nil)
} else {
err = entity.Serialize(writer)
}
if err != nil {
return "", fmt.Errorf("failed to serialize entity: %w", err)
}
if err := writer.Close(); err != nil {
return "", fmt.Errorf("failed to close armor writer: %w", err)
}
return buf.String(), nil
}
func createRevocationCertificate(entity *openpgp.Entity) (string, error) {
buf := new(bytes.Buffer)
writer, err := armor.Encode(buf, openpgp.SignatureType, nil)
if err != nil {
return "", fmt.Errorf("failed to create armor encoder for revocation: %w", err)
}
sig := &packet.Signature{
SigType: packet.SigTypeKeyRevocation,
PubKeyAlgo: entity.PrimaryKey.PubKeyAlgo,
Hash: crypto.SHA256,
CreationTime: time.Now(),
IssuerKeyId: &entity.PrimaryKey.KeyId,
}
// SignKey requires an unencrypted private key.
if err := sig.SignKey(entity.PrimaryKey, entity.PrivateKey, nil); err != nil {
return "", fmt.Errorf("failed to sign revocation: %w", err)
}
if err := sig.Serialize(writer); err != nil {
return "", fmt.Errorf("failed to serialize revocation signature: %w", err)
}
if err := writer.Close(); err != nil {
return "", fmt.Errorf("failed to close revocation writer: %w", err)
}
return buf.String(), nil
}

View file

@ -1,12 +0,0 @@
package openpgp
// pgpMessageHeader is the standard armor header for PGP messages.
const pgpMessageHeader = "PGP MESSAGE"
// KeyPair holds the generated armored keys and revocation certificate.
// This is the primary data structure representing a user's PGP identity within the system.
type KeyPair struct {
PublicKey string
PrivateKey string
RevocationCertificate string
}

View file

@ -1,38 +0,0 @@
package openpgp
import (
"bytes"
"fmt"
"strings"
"github.com/ProtonMail/go-crypto/openpgp"
)
// Sign creates a detached signature for the data.
func Sign(data, privateKeyPath, passphrase string) (string, error) {
signer, err := GetPrivateKey(privateKeyPath, passphrase)
if err != nil {
return "", fmt.Errorf("failed to get private key for signing: %w", err)
}
buf := new(bytes.Buffer)
if err := openpgp.ArmoredDetachSign(buf, signer, strings.NewReader(data), nil); err != nil {
return "", fmt.Errorf("failed to create detached signature: %w", err)
}
return buf.String(), nil
}
// Verify checks a detached signature.
func Verify(data, signature, publicKeyPath string) (bool, error) {
keyring, err := GetPublicKey(publicKeyPath)
if err != nil {
return false, fmt.Errorf("failed to get public key for verification: %w", err)
}
_, err = openpgp.CheckArmoredDetachedSignature(openpgp.EntityList{keyring}, strings.NewReader(data), strings.NewReader(signature), nil)
if err != nil {
return false, fmt.Errorf("signature verification failed: %w", err)
}
return true, nil
}

View file

@ -1,96 +0,0 @@
package openpgp
import (
"os"
"path/filepath"
"testing"
)
// encryptedPrivateKey is a pre-generated, armored PGP private key, encrypted with the passphrase "test-passphrase".
// This key is used in tests where programmatic key generation and encryption is not feasible due to library limitations.
const encryptedPrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
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
hw==
=u7WH
-----END PGP PRIVATE KEY BLOCK-----`
// createEncryptedKeyFile creates a temporary file containing a pre-generated, encrypted private key.
// It returns the path to the temporary file and a cleanup function to remove the temporary directory.
func createEncryptedKeyFile(t *testing.T) (string, func()) {
t.Helper()
tempDir, err := os.MkdirTemp("", "pgp-test-key-*")
if err != nil {
t.Fatalf("test setup: failed to create temp dir for encrypted key: %v", err)
}
privKeyPath := filepath.Join(tempDir, "encrypted-key.asc")
err = os.WriteFile(privKeyPath, []byte(encryptedPrivateKey), 0600)
if err != nil {
t.Fatalf("test setup: failed to write encrypted key to file: %v", err)
}
cleanup := func() { os.RemoveAll(tempDir) }
return privKeyPath, cleanup
}

View file

@ -1,45 +0,0 @@
package io
import (
"github.com/Snider/Core/pkg/io/sftp"
"github.com/Snider/Core/pkg/io/webdav"
)
// NewSFTPMedium creates and returns a new SFTP medium.
func NewSFTPMedium(cfg sftp.ConnectionConfig) (Medium, error) {
return sftp.New(cfg)
}
// NewWebDAVMedium creates and returns a new WebDAV medium.
func NewWebDAVMedium(cfg webdav.ConnectionConfig) (Medium, error) {
return webdav.New(cfg)
}
// Read retrieves the content of a file from the given medium.
func Read(m Medium, path string) (string, error) {
return m.Read(path)
}
// Write saves content to a file on the given medium.
func Write(m Medium, path, content string) error {
return m.Write(path, content)
}
// EnsureDir ensures a directory exists on the given medium.
func EnsureDir(m Medium, path string) error {
return m.EnsureDir(path)
}
// IsFile checks if a path is a file on the given medium.
func IsFile(m Medium, path string) bool {
return m.IsFile(path)
}
// Copy copies a file from a source medium to a destination medium.
func Copy(sourceMedium Medium, sourcePath string, destMedium Medium, destPath string) error {
content, err := sourceMedium.Read(sourcePath)
if err != nil {
return err
}
return destMedium.Write(destPath, content)
}

View file

@ -1,31 +0,0 @@
package io
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRead(t *testing.T) {
m := NewMockMedium()
m.Files["test.txt"] = "hello"
content, err := Read(m, "test.txt")
assert.NoError(t, err)
assert.Equal(t, "hello", content)
}
func TestWrite(t *testing.T) {
m := NewMockMedium()
err := Write(m, "test.txt", "hello")
assert.NoError(t, err)
assert.Equal(t, "hello", m.Files["test.txt"])
}
func TestCopy(t *testing.T) {
source := NewMockMedium()
dest := NewMockMedium()
source.Files["test.txt"] = "hello"
err := Copy(source, "test.txt", dest, "test.txt")
assert.NoError(t, err)
assert.Equal(t, "hello", dest.Files["test.txt"])
}

View file

@ -1,27 +0,0 @@
package io
// Medium defines the standard interface for a storage backend.
// This allows for different implementations (e.g., local disk, S3, SFTP)
// to be used interchangeably.
type Medium interface {
// Read retrieves the content of a file as a string.
Read(path string) (string, error)
// Write saves the given content to a file, overwriting it if it exists.
Write(path, content string) error
// EnsureDir makes sure a directory exists, creating it if necessary.
EnsureDir(path string) error
// IsFile checks if a path exists and is a regular file.
IsFile(path string) bool
// FileGet is a convenience function that reads a file from the medium.
FileGet(path string) (string, error)
// FileSet is a convenience function that writes a file to the medium.
FileSet(path, content string) error
}
// Pre-initialized, sandboxed medium for the local filesystem.
var Local Medium

View file

@ -1,87 +0,0 @@
package io
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIO_Read_Good(t *testing.T) {
medium := NewMockMedium()
medium.Files["test.txt"] = "hello"
content, err := Read(medium, "test.txt")
assert.NoError(t, err)
assert.Equal(t, "hello", content)
}
func TestIO_Read_Bad(t *testing.T) {
medium := NewMockMedium()
_, err := Read(medium, "nonexistent.txt")
assert.Error(t, err)
}
func TestIO_Write_Good(t *testing.T) {
medium := NewMockMedium()
err := Write(medium, "test.txt", "hello")
assert.NoError(t, err)
writtenContent, ok := medium.Files["test.txt"]
assert.True(t, ok)
assert.Equal(t, "hello", writtenContent)
}
// TODO: The current MockMedium cannot simulate a write error.
// func TestIO_Write_Bad(t *testing.T) {
// medium := NewMockMedium()
// // How to make Write fail?
// err := Write(medium, "test.txt", "hello")
// assert.Error(t, err)
// }
func TestIO_EnsureDir_Good(t *testing.T) {
medium := NewMockMedium()
err := EnsureDir(medium, "testdir")
assert.NoError(t, err)
exists := medium.Dirs["testdir"]
assert.True(t, exists)
}
// TODO: The current MockMedium cannot simulate an EnsureDir error.
// func TestIO_EnsureDir_Bad(t *testing.T) {
// medium := NewMockMedium()
// // How to make EnsureDir fail?
// err := EnsureDir(medium, "testdir")
// assert.Error(t, err)
// }
func TestIO_IsFile_Good(t *testing.T) {
medium := NewMockMedium()
medium.Files["test.txt"] = "content"
assert.True(t, IsFile(medium, "test.txt"))
assert.False(t, IsFile(medium, "nonexistent.txt"))
}
func TestIO_Copy_Good(t *testing.T) {
source := NewMockMedium()
source.Files["source.txt"] = "hello"
dest := NewMockMedium()
err := Copy(source, "source.txt", dest, "dest.txt")
assert.NoError(t, err)
copiedContent, ok := dest.Files["dest.txt"]
assert.True(t, ok)
assert.Equal(t, "hello", copiedContent)
}
func TestIO_Copy_Bad(t *testing.T) {
source := NewMockMedium() // No source file
dest := NewMockMedium()
err := Copy(source, "source.txt", dest, "dest.txt")
assert.Error(t, err)
}

View file

@ -1,83 +0,0 @@
package local
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// New creates a new instance of the local storage medium.
// It requires a root path to sandbox all file operations.
func New(rootPath string) (*Medium, error) {
if err := os.MkdirAll(rootPath, os.ModePerm); err != nil {
return nil, fmt.Errorf("could not create root directory at %s: %w", rootPath, err)
}
return &Medium{root: rootPath}, nil
}
// path returns a full, safe path within the medium's root.
func (m *Medium) path(subpath string) (string, error) {
if strings.Contains(subpath, "..") {
return "", fmt.Errorf("path traversal attempt detected")
}
return filepath.Join(m.root, subpath), nil
}
// Read retrieves the content of a file from the local disk.
func (m *Medium) Read(path string) (string, error) {
safePath, err := m.path(path)
if err != nil {
return "", err
}
data, err := os.ReadFile(safePath)
if err != nil {
return "", err
}
return string(data), nil
}
// Write saves the given content to a file on the local disk.
func (m *Medium) Write(path, content string) error {
safePath, err := m.path(path)
if err != nil {
return err
}
dir := filepath.Dir(safePath)
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return err
}
return os.WriteFile(safePath, []byte(content), 0644)
}
// EnsureDir makes sure a directory exists on the local disk.
func (m *Medium) EnsureDir(path string) error {
safePath, err := m.path(path)
if err != nil {
return err
}
return os.MkdirAll(safePath, os.ModePerm)
}
// IsFile checks if a path exists and is a regular file on the local disk.
func (m *Medium) IsFile(path string) bool {
safePath, err := m.path(path)
if err != nil {
return false
}
info, err := os.Stat(safePath)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
// FileGet is a convenience function that reads a file from the medium.
func (m *Medium) FileGet(path string) (string, error) {
return m.Read(path)
}
// FileSet is a convenience function that writes a file to the medium.
func (m *Medium) FileSet(path, content string) error {
return m.Write(path, content)
}

View file

@ -1,154 +0,0 @@
package local
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNew(t *testing.T) {
// Create a temporary directory for testing
testRoot, err := os.MkdirTemp("", "local_test_root")
assert.NoError(t, err)
defer os.RemoveAll(testRoot) // Clean up after the test
// Test successful creation
medium, err := New(testRoot)
assert.NoError(t, err)
assert.NotNil(t, medium)
assert.Equal(t, testRoot, medium.root)
// Verify the root directory exists
info, err := os.Stat(testRoot)
assert.NoError(t, err)
assert.True(t, info.IsDir())
// Test creating a new instance with an existing directory (should not error)
medium2, err := New(testRoot)
assert.NoError(t, err)
assert.NotNil(t, medium2)
}
func TestPath(t *testing.T) {
testRoot := "/tmp/test_root"
medium := &Medium{root: testRoot}
// Valid path
validPath, err := medium.path("file.txt")
assert.NoError(t, err)
assert.Equal(t, filepath.Join(testRoot, "file.txt"), validPath)
// Subdirectory path
subDirPath, err := medium.path("dir/sub/file.txt")
assert.NoError(t, err)
assert.Equal(t, filepath.Join(testRoot, "dir", "sub", "file.txt"), subDirPath)
// Path traversal attempt
_, err = medium.path("../secret.txt")
assert.Error(t, err)
assert.Contains(t, err.Error(), "path traversal attempt detected")
_, err = medium.path("dir/../../secret.txt")
assert.Error(t, err)
assert.Contains(t, err.Error(), "path traversal attempt detected")
}
func TestReadWrite(t *testing.T) {
testRoot, err := os.MkdirTemp("", "local_read_write_test")
assert.NoError(t, err)
defer os.RemoveAll(testRoot)
medium, err := New(testRoot)
assert.NoError(t, err)
fileName := "testfile.txt"
filePath := filepath.Join("subdir", fileName)
content := "Hello, Gopher!\nThis is a test file."
// Test Write
err = medium.Write(filePath, content)
assert.NoError(t, err)
// Verify file content by reading directly from OS
readContent, err := os.ReadFile(filepath.Join(testRoot, filePath))
assert.NoError(t, err)
assert.Equal(t, content, string(readContent))
// Test Read
readByMedium, err := medium.Read(filePath)
assert.NoError(t, err)
assert.Equal(t, content, readByMedium)
// Test Read non-existent file
_, err = medium.Read("nonexistent.txt")
assert.Error(t, err)
assert.True(t, os.IsNotExist(err))
// Test Write to a path with traversal attempt
writeErr := medium.Write("../badfile.txt", "malicious content")
assert.Error(t, writeErr)
assert.Contains(t, writeErr.Error(), "path traversal attempt detected")
}
func TestEnsureDir(t *testing.T) {
testRoot, err := os.MkdirTemp("", "local_ensure_dir_test")
assert.NoError(t, err)
defer os.RemoveAll(testRoot)
medium, err := New(testRoot)
assert.NoError(t, err)
dirName := "newdir/subdir"
dirPath := filepath.Join(testRoot, dirName)
// Test creating a new directory
err = medium.EnsureDir(dirName)
assert.NoError(t, err)
info, err := os.Stat(dirPath)
assert.NoError(t, err)
assert.True(t, info.IsDir())
// Test ensuring an existing directory (should not error)
err = medium.EnsureDir(dirName)
assert.NoError(t, err)
// Test ensuring a directory with path traversal attempt
err = medium.EnsureDir("../bad_dir")
assert.Error(t, err)
assert.Contains(t, err.Error(), "path traversal attempt detected")
}
func TestIsFile(t *testing.T) {
testRoot, err := os.MkdirTemp("", "local_is_file_test")
assert.NoError(t, err)
defer os.RemoveAll(testRoot)
medium, err := New(testRoot)
assert.NoError(t, err)
// Create a test file
fileName := "existing_file.txt"
filePath := filepath.Join(testRoot, fileName)
err = os.WriteFile(filePath, []byte("content"), 0644)
assert.NoError(t, err)
// Create a test directory
dirName := "existing_dir"
dirPath := filepath.Join(testRoot, dirName)
err = os.Mkdir(dirPath, 0755)
assert.NoError(t, err)
// Test with an existing file
assert.True(t, medium.IsFile(fileName))
// Test with a non-existent file
assert.False(t, medium.IsFile("nonexistent_file.txt"))
// Test with a directory
assert.False(t, medium.IsFile(dirName))
// Test with path traversal attempt
assert.False(t, medium.IsFile("../bad_file.txt"))
}

View file

@ -1,6 +0,0 @@
package local
// Medium implements the io.Medium interface for the local disk.
type Medium struct {
root string
}

View file

@ -1,47 +0,0 @@
package io
import "github.com/stretchr/testify/assert"
// MockMedium implements the Medium interface for testing purposes.
type MockMedium struct {
Files map[string]string
Dirs map[string]bool
}
func NewMockMedium() *MockMedium {
return &MockMedium{
Files: make(map[string]string),
Dirs: make(map[string]bool),
}
}
func (m *MockMedium) Read(path string) (string, error) {
content, ok := m.Files[path]
if !ok {
return "", assert.AnError // Simulate file not found error
}
return content, nil
}
func (m *MockMedium) Write(path, content string) error {
m.Files[path] = content
return nil
}
func (m *MockMedium) EnsureDir(path string) error {
m.Dirs[path] = true
return nil
}
func (m *MockMedium) IsFile(path string) bool {
_, ok := m.Files[path]
return ok
}
func (m *MockMedium) FileGet(path string) (string, error) {
return m.Read(path)
}
func (m *MockMedium) FileSet(path, content string) error {
return m.Write(path, content)
}

View file

@ -1,139 +0,0 @@
package sftp
import (
"fmt"
"io"
"net"
"os"
"path/filepath"
"strconv"
"time"
"github.com/pkg/sftp"
"github.com/skeema/knownhosts"
"golang.org/x/crypto/ssh"
)
// New creates a new, connected instance of the SFTP storage medium.
func New(cfg ConnectionConfig) (*Medium, error) {
// Validate port
port, err := strconv.Atoi(cfg.Port)
if err != nil || port < 1 || port > 65535 {
return nil, fmt.Errorf("invalid port: %s", cfg.Port)
}
var authMethods []ssh.AuthMethod
if cfg.KeyFile != "" {
key, err := os.ReadFile(cfg.KeyFile)
if err != nil {
return nil, fmt.Errorf("unable to read private key: %w", err)
}
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
return nil, fmt.Errorf("unable to parse private key: %w", err)
}
authMethods = append(authMethods, ssh.PublicKeys(signer))
} else if cfg.Password != "" {
authMethods = append(authMethods, ssh.Password(cfg.Password))
} else {
return nil, fmt.Errorf("no authentication method provided (password or keyfile)")
}
kh, err := knownhosts.New(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"))
if err != nil {
return nil, fmt.Errorf("failed to read known_hosts: %w", err)
}
// Set a default timeout if one is not provided.
if cfg.Timeout == 0 {
cfg.Timeout = 30 * time.Second
}
sshConfig := &ssh.ClientConfig{
User: cfg.User,
Auth: authMethods,
HostKeyCallback: kh.HostKeyCallback(),
Timeout: cfg.Timeout,
}
addr := net.JoinHostPort(cfg.Host, cfg.Port)
conn, err := ssh.Dial("tcp", addr, sshConfig)
if err != nil {
return nil, fmt.Errorf("failed to dial ssh: %w", err)
}
sftpClient, err := sftp.NewClient(conn)
if err != nil {
// Ensure the underlying ssh connection is closed on failure
conn.Close()
return nil, fmt.Errorf("failed to create sftp client: %w", err)
}
return &Medium{client: sftpClient}, nil
}
// Read retrieves the content of a file from the SFTP server.
func (m *Medium) Read(path string) (string, error) {
file, err := m.client.Open(path)
if err != nil {
return "", fmt.Errorf("sftp: failed to open file %s: %w", path, err)
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return "", fmt.Errorf("sftp: failed to read file %s: %w", path, err)
}
return string(data), nil
}
// Write saves the given content to a file on the SFTP server.
func (m *Medium) Write(path, content string) error {
// Ensure the remote directory exists first.
dir := filepath.Dir(path)
if err := m.EnsureDir(dir); err != nil {
return err
}
file, err := m.client.Create(path)
if err != nil {
return fmt.Errorf("sftp: failed to create file %s: %w", path, err)
}
defer file.Close()
if _, err := file.Write([]byte(content)); err != nil {
return fmt.Errorf("sftp: failed to write to file %s: %w", path, err)
}
return nil
}
// EnsureDir makes sure a directory exists on the SFTP server.
func (m *Medium) EnsureDir(path string) error {
// MkdirAll is idempotent, so it won't error if the path already exists.
return m.client.MkdirAll(path)
}
// IsFile checks if a path exists and is a regular file on the SFTP server.
func (m *Medium) IsFile(path string) bool {
info, err := m.client.Stat(path)
if err != nil {
// If the error is "not found", it's definitely not a file.
// For any other error, we also conservatively say it's not a file.
return false
}
// Return true only if it's not a directory.
return !info.IsDir()
}
// FileGet is a convenience function that reads a file from the medium.
func (m *Medium) FileGet(path string) (string, error) {
return m.Read(path)
}
// FileSet is a convenience function that writes a file to the medium.
func (m *Medium) FileSet(path, content string) error {
return m.Write(path, content)
}

View file

@ -1,25 +0,0 @@
package sftp
import (
"time"
"github.com/pkg/sftp"
)
// Medium implements the io.Medium interface for the SFTP protocol.
type Medium struct {
client *sftp.Client
}
// ConnectionConfig holds the necessary details to connect to an SFTP server.
type ConnectionConfig struct {
Host string
Port string
User string
Password string // For password-based auth
KeyFile string // Path to a private key for key-based auth
// Timeout specifies the duration for the network connection. If set to 0,
// a default timeout of 30 seconds will be used.
Timeout time.Duration
}

View file

@ -1,165 +0,0 @@
package sftp
import (
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// setupTest creates a temporary home directory and a dummy known_hosts file
// to prevent tests from failing in CI environments where the file doesn't exist.
func setupTest(t *testing.T) {
t.Helper()
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
sshDir := filepath.Join(homeDir, ".ssh")
err := os.Mkdir(sshDir, 0700)
require.NoError(t, err)
knownHostsFile := filepath.Join(sshDir, "known_hosts")
err = os.WriteFile(knownHostsFile, []byte{}, 0600)
require.NoError(t, err)
}
func TestNew(t *testing.T) {
setupTest(t)
// Provide a dummy ConnectionConfig for testing.
// Since we are not setting up a real SFTP server, we expect an error during connection.
cfg := ConnectionConfig{
Host: "localhost",
Port: "22",
User: "testuser",
// No password or keyfile provided, so connection should fail.
}
service, err := New(cfg)
assert.Error(t, err)
assert.Nil(t, service, "New() should return a nil service instance on connection error")
assert.Contains(t, err.Error(), "no authentication method provided", "Expected authentication error")
}
func TestNew_InvalidHost(t *testing.T) {
setupTest(t)
cfg := ConnectionConfig{
Host: "non-resolvable-host.domain.invalid",
Port: "22",
User: "testuser",
Password: "password",
}
service, err := New(cfg)
assert.Error(t, err)
assert.Nil(t, service)
assert.Contains(t, err.Error(), "lookup non-resolvable-host.domain.invalid")
}
func TestNew_InvalidPort(t *testing.T) {
setupTest(t)
cfg := ConnectionConfig{
Host: "localhost",
Port: "99999", // Invalid port number
User: "testuser",
Password: "password",
}
service, err := New(cfg)
assert.Error(t, err)
assert.Nil(t, service)
assert.Contains(t, err.Error(), "invalid port")
}
func TestNew_ConnectionTimeout(t *testing.T) {
setupTest(t)
cfg := ConnectionConfig{
Host: "192.0.2.0", // Non-routable IP to simulate timeout
Port: "22",
User: "testuser",
Password: "password",
Timeout: 100 * time.Millisecond,
}
service, err := New(cfg)
assert.Error(t, err)
assert.Nil(t, service)
assert.Contains(t, err.Error(), "i/o timeout")
}
func TestNew_AuthFailure_NonexistentKeyfile(t *testing.T) {
setupTest(t)
cfg := ConnectionConfig{
Host: "localhost",
Port: "22",
User: "testuser",
KeyFile: "/path/to/nonexistent/keyfile",
}
service, err := New(cfg)
assert.Error(t, err)
assert.Nil(t, service)
assert.ErrorIs(t, err, os.ErrNotExist)
}
func TestNew_AuthFailure_InvalidKeyFormat(t *testing.T) {
setupTest(t)
// Create a temporary file with invalid key content
tmpFile, err := os.CreateTemp("", "invalid_key")
require.NoError(t, err)
defer func(name string) {
err := os.Remove(name)
if err != nil {
t.Logf("Failed to remove temporary file: %v", err)
}
}(tmpFile.Name())
_, err = tmpFile.WriteString("not a valid ssh key")
require.NoError(t, err)
err = tmpFile.Close()
require.NoError(t, err)
cfg := ConnectionConfig{
Host: "localhost",
Port: "22",
User: "testuser",
KeyFile: tmpFile.Name(),
}
service, err := New(cfg)
assert.Error(t, err)
assert.Nil(t, service)
assert.Contains(t, err.Error(), "unable to parse private key")
}
func TestNew_MultipleAuthMethods(t *testing.T) {
setupTest(t)
// Create a temporary file with invalid key content to ensure key-based auth is attempted
tmpFile, err := os.CreateTemp("", "dummy_key")
require.NoError(t, err)
defer func(name string) {
err := os.Remove(name)
if err != nil {
t.Logf("Failed to remove temporary file: %v", err)
}
}(tmpFile.Name())
_, err = tmpFile.WriteString("not a valid ssh key")
require.NoError(t, err)
err = tmpFile.Close()
require.NoError(t, err)
cfg := ConnectionConfig{
Host: "localhost",
Port: "22",
User: "testuser",
Password: "password",
KeyFile: tmpFile.Name(),
}
service, err := New(cfg)
assert.Error(t, err)
assert.Nil(t, service)
// We expect the key file to be prioritized, so we should get a parse error, not a "no auth method" error.
assert.Contains(t, err.Error(), "unable to parse private key")
}

View file

@ -1,16 +0,0 @@
package webdav
import "net/http"
// Medium implements the io.Medium interface for the WebDAV protocol.
type Medium struct {
client *http.Client
baseURL string // e.g., https://dav.example.com/remote.php/dav/files/username/
}
// ConnectionConfig holds the necessary details to connect to a WebDAV server.
type ConnectionConfig struct {
URL string // The full base URL of the WebDAV share.
User string
Password string
}

View file

@ -1,183 +0,0 @@
package webdav
import (
"bytes"
_ "context"
"fmt"
"io"
"net/http"
"path"
"strings"
)
// New creates a new, connected instance of the WebDAV storage medium.
func New(cfg ConnectionConfig) (*Medium, error) {
transport := &authTransport{
Username: cfg.User,
Password: cfg.Password,
Wrapped: http.DefaultTransport,
}
httpClient := &http.Client{Transport: transport}
// Ping the server to ensure the connection and credentials are valid.
// We do a PROPFIND on the root, which is a standard WebDAV operation.
req, err := http.NewRequest("PROPFIND", cfg.URL, nil)
if err != nil {
return nil, fmt.Errorf("webdav: failed to create ping request: %w", err)
}
req.Header.Set("Depth", "0")
resp, err := httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("webdav: connection test failed: %w", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusMultiStatus && resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("webdav: connection test failed with status %s", resp.Status)
}
return &Medium{
client: httpClient,
baseURL: cfg.URL,
}, nil
}
// Read retrieves the content of a file from the WebDAV server.
func (m *Medium) Read(p string) (string, error) {
url := m.resolveURL(p)
resp, err := m.client.Get(url)
if err != nil {
return "", fmt.Errorf("webdav: GET request for %s failed: %w", p, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("webdav: failed to read %s, status: %s", p, resp.Status)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("webdav: failed to read response body for %s: %w", p, err)
}
return string(data), nil
}
// Write saves the given content to a file on the WebDAV server.
func (m *Medium) Write(p, content string) error {
// Ensure the parent directory exists first.
dir := path.Dir(p)
if dir != "." && dir != "/" {
if err := m.EnsureDir(dir); err != nil {
return err // This will be a detailed error from EnsureDir
}
}
url := m.resolveURL(p)
req, err := http.NewRequest("PUT", url, bytes.NewReader([]byte(content)))
if err != nil {
return fmt.Errorf("webdav: failed to create PUT request: %w", err)
}
resp, err := m.client.Do(req)
if err != nil {
return fmt.Errorf("webdav: PUT request for %s failed: %w", p, err)
}
defer resp.Body.Close()
// StatusCreated (201) or StatusNoContent (204) are success codes for PUT.
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("webdav: failed to write %s, status: %s", p, resp.Status)
}
return nil
}
// EnsureDir makes sure a directory exists on the WebDAV server, creating parent dirs as needed.
func (m *Medium) EnsureDir(p string) error {
// To mimic MkdirAll, we create each part of the path sequentially.
parts := strings.Split(p, "/")
currentPath := ""
for _, part := range parts {
if part == "" {
continue
}
currentPath = path.Join(currentPath, part)
url := m.resolveURL(currentPath) + "/" // MKCOL needs a trailing slash
req, err := http.NewRequest("MKCOL", url, nil)
if err != nil {
return fmt.Errorf("webdav: failed to create MKCOL request for %s: %w", currentPath, err)
}
resp, err := m.client.Do(req)
if err != nil {
return fmt.Errorf("webdav: MKCOL request for %s failed: %w", currentPath, err)
}
resp.Body.Close()
// 405 Method Not Allowed means it already exists, which is fine for us.
// 201 Created is a success.
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusMethodNotAllowed {
return fmt.Errorf("webdav: failed to create directory %s, status: %s", currentPath, resp.Status)
}
}
return nil
}
// IsFile checks if a path exists and is a regular file on the WebDAV server.
func (m *Medium) IsFile(p string) bool {
url := m.resolveURL(p)
req, err := http.NewRequest("PROPFIND", url, nil)
if err != nil {
return false
}
req.Header.Set("Depth", "0")
resp, err := m.client.Do(req)
if err != nil {
return false
}
defer resp.Body.Close()
// If we get anything other than a Multi-Status, it's probably not a file.
if resp.StatusCode != http.StatusMultiStatus {
return false
}
// A simple check: if the response body contains the string for a collection, it's a directory.
// A more robust implementation would parse the XML response.
body, err := io.ReadAll(resp.Body)
if err != nil {
return false
}
return !strings.Contains(string(body), "<D:collection/>")
}
// resolveURL joins the base URL with a path segment, ensuring correct slashes.
func (m *Medium) resolveURL(p string) string {
return strings.TrimSuffix(m.baseURL, "/") + "/" + strings.TrimPrefix(p, "/")
}
// authTransport is a custom http.RoundTripper to inject Basic Auth.
type authTransport struct {
Username string
Password string
Wrapped http.RoundTripper
}
func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req.SetBasicAuth(t.Username, t.Password)
return t.Wrapped.RoundTrip(req)
}
// FileGet is a convenience function that reads a file from the medium.
func (m *Medium) FileGet(path string) (string, error) {
return m.Read(path)
}
// FileSet is a convenience function that writes a file to the medium.
func (m *Medium) FileSet(path, content string) error {
return m.Write(path, content)
}

View file

@ -1,155 +0,0 @@
package webdav
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
// mockWebDAVServer creates a test HTTP server that mimics a WebDAV server.
func mockWebDAVServer() *httptest.Server {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "PROPFIND":
if r.URL.Path == "/" {
w.WriteHeader(http.StatusMultiStatus)
return
}
// For IsFile test
if r.URL.Path == "/test.txt" {
w.WriteHeader(http.StatusMultiStatus)
fmt.Fprint(w, `<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<D:multistatus xmlns:D=\"DAV:\">
<D:response>
<D:href>/test.txt</D:href>
<D:propstat>
<D:prop>
<D:resourcetype/>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>`)
return
}
if r.URL.Path == "/testdir/" {
w.WriteHeader(http.StatusMultiStatus)
fmt.Fprint(w, `<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<D:multistatus xmlns:D=\"DAV:\">
<D:response>
<D:href>/testdir/</D:href>
<D:propstat>
<D:prop>
<D:resourcetype><D:collection/></D:resourcetype>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>`)
return
}
http.NotFound(w, r)
case "GET":
if r.URL.Path == "/test.txt" {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "Hello, WebDAV!")
return
}
http.NotFound(w, r)
case "PUT":
if r.URL.Path == "/test.txt" {
w.WriteHeader(http.StatusCreated)
return
}
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
case "MKCOL":
if r.URL.Path == "/testdir/" {
w.WriteHeader(http.StatusCreated)
return
}
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
default:
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
})
return httptest.NewServer(handler)
}
func TestNew_Success(t *testing.T) {
server := mockWebDAVServer()
defer server.Close()
cfg := ConnectionConfig{
URL: server.URL,
User: "user",
Password: "password",
}
medium, err := New(cfg)
assert.NoError(t, err)
assert.NotNil(t, medium)
}
func TestRead(t *testing.T) {
server := mockWebDAVServer()
defer server.Close()
cfg := ConnectionConfig{
URL: server.URL,
User: "user",
Password: "password",
}
medium, err := New(cfg)
assert.NoError(t, err)
content, err := medium.Read("test.txt")
assert.NoError(t, err)
assert.Equal(t, "Hello, WebDAV!", content)
}
func TestWrite(t *testing.T) {
server := mockWebDAVServer()
defer server.Close()
cfg := ConnectionConfig{
URL: server.URL,
User: "user",
Password: "password",
}
medium, err := New(cfg)
assert.NoError(t, err)
err = medium.Write("test.txt", "Hello, WebDAV!")
assert.NoError(t, err)
}
func TestEnsureDir(t *testing.T) {
server := mockWebDAVServer()
defer server.Close()
cfg := ConnectionConfig{
URL: server.URL,
User: "user",
Password: "password",
}
medium, err := New(cfg)
assert.NoError(t, err)
err = medium.EnsureDir("testdir")
assert.NoError(t, err)
}
func TestIsFile(t *testing.T) {
server := mockWebDAVServer()
defer server.Close()
cfg := ConnectionConfig{
URL: server.URL,
User: "user",
Password: "password",
}
medium, err := New(cfg)
assert.NoError(t, err)
assert.True(t, medium.IsFile("test.txt"))
assert.False(t, medium.IsFile("testdir"))
}

View file

@ -1,90 +0,0 @@
package runtime_test
import (
"errors"
"testing"
"github.com/Snider/Core/pkg/crypt"
"github.com/Snider/Core/pkg/runtime"
"github.com/Snider/Core/pkg/workspace"
"github.com/stretchr/testify/assert"
"github.com/wailsapp/wails/v3/pkg/application"
)
func TestNew(t *testing.T) {
testCases := []struct {
name string
app *application.App
factories map[string]runtime.ServiceFactory
expectErr bool
expectErrStr string
checkRuntime func(*testing.T, *runtime.Runtime)
}{
{
name: "Good path",
app: nil,
factories: map[string]runtime.ServiceFactory{
"crypt": func() (any, error) { return &crypt.Service{}, nil },
"workspace": func() (any, error) { return &workspace.Service{}, nil },
},
expectErr: false,
checkRuntime: func(t *testing.T, rt *runtime.Runtime) {
assert.NotNil(t, rt)
assert.NotNil(t, rt.Core)
assert.NotNil(t, rt.Crypt)
assert.NotNil(t, rt.Workspace)
},
},
{
name: "Factory returns an error",
app: nil,
factories: map[string]runtime.ServiceFactory{
"crypt": func() (any, error) { return nil, errors.New("crypt service failed") },
"workspace": func() (any, error) { return &workspace.Service{}, nil },
},
expectErr: true,
expectErrStr: "failed to create service crypt: crypt service failed",
},
{
name: "Factory returns wrong type",
app: nil,
factories: map[string]runtime.ServiceFactory{
"crypt": func() (any, error) { return "not a crypt service", nil },
"workspace": func() (any, error) { return &workspace.Service{}, nil },
},
expectErr: true,
expectErrStr: "crypt service has unexpected type",
},
{
name: "With non-nil app",
app: &application.App{},
factories: map[string]runtime.ServiceFactory{
"crypt": func() (any, error) { return &crypt.Service{}, nil },
"workspace": func() (any, error) { return &workspace.Service{}, nil },
},
expectErr: false,
checkRuntime: func(t *testing.T, rt *runtime.Runtime) {
assert.NotNil(t, rt)
assert.NotNil(t, rt.Core)
assert.NotNil(t, rt.Core.App)
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
rt, err := runtime.NewWithFactories(tc.app, tc.factories)
if tc.expectErr {
assert.Error(t, err)
assert.Contains(t, err.Error(), tc.expectErrStr)
assert.Nil(t, rt)
} else {
assert.NoError(t, err)
if tc.checkRuntime != nil {
tc.checkRuntime(t, rt)
}
}
})
}
}

View file

@ -1,41 +0,0 @@
package workspace
import "github.com/Snider/Core/pkg/io"
// localMedium implements the Medium interface for the local disk.
type localMedium struct{}
// NewLocalMedium creates a new instance of the local storage medium.
func NewLocalMedium() io.Medium {
return &localMedium{}
}
// FileGet reads a file from the local disk.
func (m *localMedium) FileGet(path string) (string, error) {
return io.Read(io.Local, path)
}
// FileSet writes a file to the local disk.
func (m *localMedium) FileSet(path, content string) error {
return io.Write(io.Local, path, content)
}
// Read reads a file from the local disk.
func (m *localMedium) Read(path string) (string, error) {
return io.Read(io.Local, path)
}
// Write writes a file to the local disk.
func (m *localMedium) Write(path, content string) error {
return io.Write(io.Local, path, content)
}
// EnsureDir creates a directory on the local disk.
func (m *localMedium) EnsureDir(path string) error {
return io.EnsureDir(io.Local, path)
}
// IsFile checks if a path exists and is a file on the local disk.
func (m *localMedium) IsFile(path string) bool {
return io.IsFile(io.Local, path)
}

View file

@ -1,227 +0,0 @@
package workspace
import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"github.com/Snider/Core/pkg/core"
"github.com/Snider/Core/pkg/crypt/lthn"
"github.com/Snider/Core/pkg/crypt/openpgp"
"github.com/Snider/Core/pkg/e"
"github.com/Snider/Core/pkg/io"
"github.com/wailsapp/wails/v3/pkg/application"
)
const (
defaultWorkspace = "default"
listFile = "list.json"
)
// Options holds configuration for the workspace service.
type Options struct{}
// Workspace represents a user's workspace.
type Workspace struct {
Name string
Path string
}
// Service manages user workspaces.
type Service struct {
*core.Runtime[Options]
activeWorkspace *Workspace
workspaceList map[string]string // Maps Workspace ID to Public Key
medium io.Medium
}
// newWorkspaceService contains the common logic for initializing a Service struct.
// It no longer takes config and medium as arguments.
func newWorkspaceService() (*Service, error) {
s := &Service{
workspaceList: make(map[string]string),
}
return s, nil
}
// New is the constructor for static dependency injection.
// It creates a Service instance without initializing the core.Runtime field.
// Dependencies are passed directly here.
func New() (*Service, error) {
s, err := newWorkspaceService()
if err != nil {
return nil, e.E("workspace.New", "failed to create new workspace service", err)
}
//s.medium = medium
// Initialize the service after creation.
// Note: ServiceStartup will now get config from s.Runtime.Config()
//if err := s.ServiceStartup(context.Background(), application.ServiceOptions{}); err != nil {
// return nil, e.E("workspace.New", "workspace service startup failed", err)
//}
return s, nil
}
// Register is the constructor for dynamic dependency injection (used with core.WithService).
// It creates a Service instance and initializes its core.Runtime field.
// Dependencies are injected during ServiceStartup.
func Register(c *core.Core) (any, error) {
s, err := newWorkspaceService()
if err != nil {
return nil, e.E("workspace.Register", "failed to create new workspace service", err)
}
s.Runtime = core.NewRuntime(c, Options{})
return s, nil
}
// HandleIPCEvents processes IPC messages, including injecting dependencies on startup.
func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error {
switch m := msg.(type) {
case map[string]any:
if action, ok := m["action"].(string); ok && action == "workspace.switch_workspace" {
return s.SwitchWorkspace(m["name"].(string))
}
case core.ActionServiceStartup:
return s.ServiceStartup(context.Background(), application.ServiceOptions{})
default:
c.App.Logger.Error("Workspace: Unknown message type", "type", fmt.Sprintf("%T", m))
}
return nil
}
// getWorkspaceDir retrieves the WorkspaceDir from the config service.
func (s *Service) getWorkspaceDir() (string, error) {
var workspaceDir string
if err := s.Config().Get("workspaceDir", &workspaceDir); err != nil {
return "", e.E("workspace.getWorkspaceDir", "failed to get WorkspaceDir from config", err)
}
return workspaceDir, nil
}
// ServiceStartup initializes the service, loading the workspace list.
func (s *Service) ServiceStartup(context.Context, application.ServiceOptions) error {
var err error
workspaceDir, err := s.getWorkspaceDir()
if err != nil {
return err
}
listPath := filepath.Join(workspaceDir, listFile)
if listPath != "" {
}
//if s.medium.IsFile(listPath) {
// content, err := s.medium.FileGet(listPath)
// if err != nil {
// return e.E("workspace.ServiceStartup", "failed to read workspace list", err)
// }
// if err := json.Unmarshal([]byte(content), &s.workspaceList); err != nil {
// fmt.Printf("Warning: could not parse workspace list: %v\n", err)
// s.workspaceList = make(map[string]string)
// }
//}
return s.SwitchWorkspace(defaultWorkspace)
}
// CreateWorkspace creates a new, obfuscated workspace on the local medium.
func (s *Service) CreateWorkspace(identifier, password string) (string, error) {
workspaceDir, err := s.getWorkspaceDir()
if err != nil {
return "", err
}
realName := lthn.Hash(identifier)
workspaceID := lthn.Hash(fmt.Sprintf("workspace/%s", realName))
workspacePath := filepath.Join(workspaceDir, workspaceID)
if _, exists := s.workspaceList[workspaceID]; exists {
return "", e.E("workspace.CreateWorkspace", "workspace for this identifier already exists", nil)
}
dirsToCreate := []string{"config", "log", "data", "files", "keys"}
for _, dir := range dirsToCreate {
if err := s.medium.EnsureDir(filepath.Join(workspacePath, dir)); err != nil {
return "", e.E("workspace.CreateWorkspace", fmt.Sprintf("failed to create workspace directory '%s'", dir), err)
}
}
keyPair, err := openpgp.CreateKeyPair(workspaceID, password)
if err != nil {
return "", e.E("workspace.CreateWorkspace", "failed to create workspace key pair", err)
}
keyFiles := map[string]string{
filepath.Join(workspacePath, "keys", "key.pub"): keyPair.PublicKey,
filepath.Join(workspacePath, "keys", "key.priv"): keyPair.PrivateKey,
}
for path, content := range keyFiles {
if err := s.medium.FileSet(path, content); err != nil {
return "", e.E("workspace.CreateWorkspace", fmt.Sprintf("failed to write key file %s", path), err)
}
}
s.workspaceList[workspaceID] = keyPair.PublicKey
listData, err := json.MarshalIndent(s.workspaceList, "", " ")
if err != nil {
return "", e.E("workspace.CreateWorkspace", "failed to marshal workspace list", err)
}
listPath := filepath.Join(workspaceDir, listFile)
if err := s.medium.FileSet(listPath, string(listData)); err != nil {
return "", e.E("workspace.CreateWorkspace", "failed to write workspace list file", err)
}
return workspaceID, nil
}
// SwitchWorkspace changes the active workspace.
func (s *Service) SwitchWorkspace(name string) error {
workspaceDir, err := s.getWorkspaceDir()
if err != nil {
return err
}
if name != defaultWorkspace {
if _, exists := s.workspaceList[name]; !exists {
return e.E("workspace.SwitchWorkspace", fmt.Sprintf("workspace '%s' does not exist", name), nil)
}
}
path := filepath.Join(workspaceDir, name)
//if err := s.medium.EnsureDir(path); err != nil {
// return e.E("workspace.SwitchWorkspace", "failed to ensure workspace directory exists", err)
//}
s.activeWorkspace = &Workspace{
Name: name,
Path: path,
}
return nil
}
// WorkspaceFileGet retrieves a file from the active workspace.
func (s *Service) WorkspaceFileGet(filename string) (string, error) {
if s.activeWorkspace == nil {
return "", e.E("workspace.WorkspaceFileGet", "no active workspace", nil)
}
path := filepath.Join(s.activeWorkspace.Path, filename)
content, err := s.medium.FileGet(path)
if err != nil {
return "", e.E("workspace.WorkspaceFileGet", "failed to get file", err)
}
return content, nil
}
// WorkspaceFileSet writes a file to the active workspace.
func (s *Service) WorkspaceFileSet(filename, content string) error {
if s.activeWorkspace == nil {
return e.E("workspace.WorkspaceFileSet", "no active workspace", nil)
}
path := filepath.Join(s.activeWorkspace.Path, filename)
err := s.medium.FileSet(path, content)
if err != nil {
return e.E("workspace.WorkspaceFileSet", "failed to set file", err)
}
return nil
}

View file

@ -1,138 +0,0 @@
package workspace
import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"testing"
"github.com/Snider/Core/pkg/core"
"github.com/stretchr/testify/assert"
"github.com/wailsapp/wails/v3/pkg/application"
)
// mockConfig is a mock implementation of the core.Config interface for testing.
type mockConfig struct {
values map[string]interface{}
}
func (m *mockConfig) Get(key string, out any) error {
val, ok := m.values[key]
if !ok {
return fmt.Errorf("key not found: %s", key)
}
// This is a simplified mock; a real one would use reflection to set `out`
switch v := out.(type) {
case *string:
*v = val.(string)
default:
return fmt.Errorf("unsupported type in mock config Get")
}
return nil
}
func (m *mockConfig) Set(key string, v any) error {
m.values[key] = v
return nil
}
// MockMedium implements the Medium interface for testing purposes.
type MockMedium struct {
Files map[string]string
Dirs map[string]bool
}
func NewMockMedium() *MockMedium {
return &MockMedium{
Files: make(map[string]string),
Dirs: make(map[string]bool),
}
}
func (m *MockMedium) FileGet(path string) (string, error) {
content, ok := m.Files[path]
if !ok {
return "", assert.AnError // Simulate file not found error
}
return content, nil
}
func (m *MockMedium) FileSet(path, content string) error {
m.Files[path] = content
return nil
}
func (m *MockMedium) EnsureDir(path string) error {
m.Dirs[path] = true
return nil
}
func (m *MockMedium) IsFile(path string) bool {
_, exists := m.Files[path]
return exists
}
func (m *MockMedium) Read(path string) (string, error) {
return m.FileGet(path)
}
func (m *MockMedium) Write(path, content string) error {
return m.FileSet(path, content)
}
// newTestService creates a workspace service instance with mocked dependencies.
func newTestService(t *testing.T, workspaceDir string) (*Service, *MockMedium) {
coreInstance, err := core.New()
assert.NoError(t, err)
mockCfg := &mockConfig{values: map[string]interface{}{"workspaceDir": workspaceDir}}
coreInstance.RegisterService("config", mockCfg)
service, err := New()
assert.NoError(t, err)
service.Runtime = core.NewRuntime(coreInstance, Options{})
mockMedium := NewMockMedium()
service.medium = mockMedium
return service, mockMedium
}
func TestServiceStartup(t *testing.T) {
workspaceDir := "/tmp/workspace"
t.Run("existing valid list.json", func(t *testing.T) {
service, mockMedium := newTestService(t, workspaceDir)
expectedWorkspaceList := map[string]string{
"workspace1": "pubkey1",
"workspace2": "pubkey2",
}
listContent, _ := json.MarshalIndent(expectedWorkspaceList, "", " ")
listPath := filepath.Join(workspaceDir, listFile)
mockMedium.Files[listPath] = string(listContent)
err := service.ServiceStartup(context.Background(), application.ServiceOptions{})
assert.NoError(t, err)
// assert.Equal(t, expectedWorkspaceList, service.workspaceList) // This check is difficult with current implementation
assert.NotNil(t, service.activeWorkspace)
assert.Equal(t, defaultWorkspace, service.activeWorkspace.Name)
})
}
func TestCreateAndSwitchWorkspace(t *testing.T) {
workspaceDir := "/tmp/workspace"
service, _ := newTestService(t, workspaceDir)
// Create
workspaceID, err := service.CreateWorkspace("test", "password")
assert.NoError(t, err)
assert.NotEmpty(t, workspaceID)
// Switch
err = service.SwitchWorkspace(workspaceID)
assert.NoError(t, err)
assert.Equal(t, workspaceID, service.activeWorkspace.Name)
}

View file

@ -4,9 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/Snider/Core/pkg/core" "github.com/Snider/Core/core"
"github.com/Snider/Core/pkg/crypt"
"github.com/Snider/Core/pkg/workspace"
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
) )
@ -15,8 +13,6 @@ import (
type Runtime struct { type Runtime struct {
app *application.App app *application.App
Core *core.Core Core *core.Core
Crypt *crypt.Service
Workspace *workspace.Service
} }
// ServiceFactory defines a function that creates a service instance. // ServiceFactory defines a function that creates a service instance.
@ -29,7 +25,7 @@ func NewWithFactories(app *application.App, factories map[string]ServiceFactory)
core.WithWails(app), core.WithWails(app),
} }
for _, name := range []string{"crypt", "workspace"} { for _, name := range []string{} {
factory, ok := factories[name] factory, ok := factories[name]
if !ok { if !ok {
return nil, fmt.Errorf("service %s factory not provided", name) return nil, fmt.Errorf("service %s factory not provided", name)
@ -49,20 +45,10 @@ func NewWithFactories(app *application.App, factories map[string]ServiceFactory)
} }
// --- Type Assertions --- // --- Type Assertions ---
cryptSvc, ok := services["crypt"].(*crypt.Service)
if !ok {
return nil, fmt.Errorf("crypt service has unexpected type")
}
workspaceSvc, ok := services["workspace"].(*workspace.Service)
if !ok {
return nil, fmt.Errorf("workspace service has unexpected type")
}
rt := &Runtime{ rt := &Runtime{
app: app, app: app,
Core: coreInstance, Core: coreInstance,
Crypt: cryptSvc,
Workspace: workspaceSvc,
} }
return rt, nil return rt, nil
@ -70,10 +56,7 @@ func NewWithFactories(app *application.App, factories map[string]ServiceFactory)
// New creates and wires together all application services. // New creates and wires together all application services.
func New(app *application.App) (*Runtime, error) { func New(app *application.App) (*Runtime, error) {
return NewWithFactories(app, map[string]ServiceFactory{ return NewWithFactories(app, map[string]ServiceFactory{})
"crypt": func() (any, error) { return crypt.New() },
"workspace": func() (any, error) { return workspace.New() },
})
} }
// ServiceName returns the name of the service. This is used by Wails to identify the service. // ServiceName returns the name of the service. This is used by Wails to identify the service.

59
runtime/runtime_test.go Normal file
View file

@ -0,0 +1,59 @@
package runtime_test
import (
"testing"
"github.com/Snider/Core/runtime"
"github.com/stretchr/testify/assert"
"github.com/wailsapp/wails/v3/pkg/application"
)
func TestNew(t *testing.T) {
testCases := []struct {
name string
app *application.App
factories map[string]runtime.ServiceFactory
expectErr bool
expectErrStr string
checkRuntime func(*testing.T, *runtime.Runtime)
}{
{
name: "Good path",
app: nil,
factories: map[string]runtime.ServiceFactory{},
expectErr: false,
checkRuntime: func(t *testing.T, rt *runtime.Runtime) {
assert.NotNil(t, rt)
assert.NotNil(t, rt.Core)
},
},
{
name: "With non-nil app",
app: &application.App{},
factories: map[string]runtime.ServiceFactory{},
expectErr: false,
checkRuntime: func(t *testing.T, rt *runtime.Runtime) {
assert.NotNil(t, rt)
assert.NotNil(t, rt.Core)
assert.NotNil(t, rt.Core.App)
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
rt, err := runtime.NewWithFactories(tc.app, tc.factories)
if tc.expectErr {
assert.Error(t, err)
assert.Contains(t, err.Error(), tc.expectErrStr)
assert.Nil(t, rt)
} else {
assert.NoError(t, err)
if tc.checkRuntime != nil {
tc.checkRuntime(t, rt)
}
}
})
}
}