<|start_filename|>internal/model/model_test.go<|end_filename|>
package model_test

import (
	"encoding/json"
	"testing"
	"time"

	"filippo.io/age"
	"github.com/3c7/aen/internal/model"
	"github.com/google/uuid"
)

const pub string = "<KEY>"
const key string = "<KEY>"

const pub2 string = "<KEY>"
const key2 string = "<KEY>"

func TestNoteCreation(t *testing.T) {
	title := "Read this"
	text := "Hello World!"
	note := model.NewNote(title, text)
	if title != note.Title {
		t.Error("Note title is not as expected.")
	}
	if text != note.Text {
		t.Error("Note text is not as expected.")
	}
}

func TestNoteEncryptionAndDecryption(t *testing.T) {
	const message string = "Testing note encryption!"

	recipient, err := age.ParseX25519Recipient(pub)
	if err != nil {
		t.Errorf("Could not parse public key: %v", err)
	}

	note := model.NewNote("Test", "Testing note encryption!")
	noteJson, err := note.Json()
	if err != nil {
		t.Logf(">>> DEBUG: Could not get json string of note: %v", err)
	} else {
		t.Logf(">>> DEBUG: %s\n", noteJson)
	}
	encryptedNote, err := note.ToEncryptedNote(*recipient)
	if err != nil {
		t.Errorf("Could not encrypt note: %v", err)
	}
	noteJson, err = encryptedNote.Json()
	if err != nil {
		t.Logf(">>> DEBUG: Could not get json string of note: %v", err)
	} else {
		t.Logf(">>> DEBUG: %s\n", noteJson)
	}

	identity, err := age.ParseX25519Identity(key)
	if err != nil {
		t.Errorf("Could not parse private key: %v", err)
	}

	exp, err := encryptedNote.Decrypt(identity)
	if err != nil {
		t.Errorf("Could not decrypt note: %v", err)
	}

	if message != exp {
		t.Errorf("Decrypted message should be \"%s\" but was \"%s\"", message, exp)
	}
}

func TestSlugCreation(t *testing.T) {
	title := "!\"§$%&/()=?Hello World!"
	expected := "hello-world"
	note := model.EncryptedNote{
		Title: title,
	}
	if note.Slug() != expected {
		t.Errorf("Slug mismatch: %s <> %s (current <> exp)", note.Slug(), expected)
	}
}

func TestJsonMarshallNotes(t *testing.T) {
	id, err := uuid.Parse("12345678-1234-5678-9012-123456789012")
	if err != nil {
		t.Errorf("Error parsing UUID: %v", err)
	}
	note := model.EncryptedNote{
		Uuid:       id,
		Time:       time.Now(),
		Title:      "This is my Title!",
		Ciphertext: "Imagine some base64 encoded ciphertext here.",
	}
	jsonNote, err := json.Marshal(note)
	if err != nil {
		t.Errorf("Error during marshalling encrypted note to JSON: %v", err)
	}
	t.Logf(">>> DEBUG - TestJsonMarshallNotes: %s\n", string(jsonNote))
}

func TestRecipientFromKey(t *testing.T) {
	r1 := model.NewRecipient("r1", pub)
	identity, err := age.ParseX25519Identity(key)
	if err != nil {
		t.Errorf("Could not parse identity: %v", err)
	}
	r2 := model.NewRecipientFromIdentity("r2", *identity)

	if r1.Publickey != r2.Publickey {
		t.Errorf("Public keys differ, but should be the same.")
	}
}

func TestEncryptionWithTwoRecipients(t *testing.T) {
	content := "Content"
	i1, err := age.ParseX25519Identity(key)
	if err != nil {
		t.Fatalf("could not parse private key: %v", err)
	}
	i2, err := age.ParseX25519Identity(key2)
	if err != nil {
		t.Fatalf("could not parse private key (2): %v", err)
	}
	note := model.NewNote("Title", content)
	encryptedNote, err := note.ToEncryptedNote(*i1.Recipient(), *i2.Recipient())
	if err != nil {
		t.Fatalf("could not encrypt note: %v", err)
	}

	noteText1, err := encryptedNote.Decrypt(i1)
	if err != nil {
		t.Fatalf("could not decrypt note: %v", err)
	}
	if noteText1 != content {
		t.Fatalf("decrypted content should be %s but was %s", content, noteText1)
	}
	noteText2, err := encryptedNote.Decrypt(i2)
	if err != nil {
		t.Fatalf("could not decrypt note (2): %v", err)
	}
	if noteText2 != content {
		t.Fatalf("decrypted content (2) should be %s but was %s", content, noteText2)
	}
}


<|start_filename|>internal/model/model.go<|end_filename|>
package model

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"io"
	"log"
	"os"
	"regexp"
	"sort"
	"strings"
	"time"

	"filippo.io/age"
	uuid "github.com/google/uuid"
)

type Recipient struct {
	Alias     string
	Publickey string
}

func NewRecipient(alias string, pubkey string) (recipient *Recipient) {
	return &Recipient{
		Alias:     alias,
		Publickey: pubkey,
	}
}

func NewRecipientFromIdentity(alias string, identity age.X25519Identity) (recipient *Recipient) {
	return &Recipient{
		Alias:     alias,
		Publickey: identity.Recipient().String(),
	}
}

func (r *Recipient) Json() (j []byte, err error) {
	return json.Marshal(r)
}

type Note struct {
	Uuid  uuid.UUID
	Time  time.Time
	Title string
	Text  string
}

func NewNote(title string, text string) (note *Note) {
	return &Note{
		uuid.New(),
		time.Now(),
		title,
		text,
	}
}

func (note *Note) Slug() (slug string) {
	regex, err := regexp.Compile("[^a-zA-Z0-9 ]+")
	if err != nil {
		log.Fatalf("Error compiling regular expression: %v", err)
	}

	slug = regex.ReplaceAllString(note.Title, "")
	slug = strings.ReplaceAll(slug, " ", "-")
	slug = strings.ToLower(slug)
	return slug
}

// Reads a file and parses it's content. The resulting Note does not have a UUID or a Time set.
func NotefileToNote(path string) (note *Note, err error) {
	if _, err = os.Stat(path); err != nil {
		return nil, err
	}

	content, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}

	lines := strings.Split(string(content), "\n")
	return &Note{
		Title: lines[0],
		Text:  strings.Join(lines[1:], "\n"),
	}, nil
}

func (note *Note) Encrypt(x25519recipients ...age.X25519Recipient) (ciphertext string, err error) {
	var recipients []age.Recipient
	for r := range x25519recipients {
		recipients = append(recipients, &x25519recipients[r])
	}
	out := &bytes.Buffer{}
	w, err := age.Encrypt(out, recipients...)
	if err != nil {
		log.Fatalf("Failed to create encrypted note with uuid %s.", note.Uuid.String())
	}
	if _, err := io.WriteString(w, note.Text); err != nil {
		log.Fatalf("Failed to write encrypted note with uuid %s.", note.Uuid.String())
	}
	if err := w.Close(); err != nil {
		log.Fatalf("Error on closing encrypted note with uuid %s.", note.Uuid.String())
	}
	return base64.StdEncoding.EncodeToString(out.Bytes()), nil
}

func (note *Note) ToEncryptedNote(x25519recipients ...age.X25519Recipient) (encryptedNote EncryptedNote, err error) {
	ciphertext, err := note.Encrypt(x25519recipients...)
	return EncryptedNote{
		note.Uuid,
		note.Time,
		note.Title,
		ciphertext,
	}, err
}

func (note *Note) Json() (encodedNote []byte, err error) {
	return json.Marshal(note)
}

// Writes note to a file in the format of Title\nText.
func (note *Note) ToFile(path string) (err error) {
	content := strings.Join([]string{note.Title, note.Text}, "\n")
	return os.WriteFile(path, []byte(content), 0600)
}

type EncryptedNote struct {
	Uuid       uuid.UUID
	Time       time.Time
	Title      string
	Ciphertext string
}

func (encryptedNote *EncryptedNote) Slug() (slug string) {
	regex, err := regexp.Compile("[^a-zA-Z0-9 ]+")
	if err != nil {
		log.Fatalf("Error compiling regular expression: %v", err)
	}

	slug = regex.ReplaceAllString(encryptedNote.Title, "")
	slug = strings.ReplaceAll(slug, " ", "-")
	slug = strings.ToLower(slug)
	return slug
}

func (encryptedNote EncryptedNote) Decrypt(identity age.Identity) (text string, err error) {
	var decoded []byte
	if decoded, err = base64.StdEncoding.DecodeString(encryptedNote.Ciphertext); err != nil {
		log.Fatalf("Error decoding encrypted note's ciphertext: %v.", err)
	}
	r, err := age.Decrypt(bytes.NewReader(decoded), identity)
	if err != nil {
		return "", err
	}
	buffer := &bytes.Buffer{}
	if _, err := io.Copy(buffer, r); err != nil {
		log.Fatalf("Could not decrypt note with uuid %s.", encryptedNote.Uuid.String())
	}
	return buffer.String(), nil
}

func (encryptedNote EncryptedNote) ToDecryptedNote(identity age.Identity) (note Note, err error) {
	text, err := encryptedNote.Decrypt(identity)
	return Note{
		encryptedNote.Uuid,
		encryptedNote.Time,
		encryptedNote.Title,
		text,
	}, err
}

func (encryptedNote EncryptedNote) Json() (encodedNote []byte, err error) {
	return json.Marshal(encryptedNote)
}

func SortNoteSlice(notes []EncryptedNote) []EncryptedNote {
	sort.Slice(notes, func(i, j int) bool {
		return notes[i].Time.After(notes[j].Time)
	})
	return notes
}


<|start_filename|>internal/database/database_test.go<|end_filename|>
package database_test

import (
	"io/ioutil"
	"os"
	"testing"
	"time"

	"filippo.io/age"
	"github.com/3c7/aen/internal/database"
	"github.com/3c7/aen/internal/model"
	"github.com/google/uuid"
)

// const pub string = "<KEY>"
// const key string = "<KEY>"

func TestDBCreation(t *testing.T) {
	file, err := ioutil.TempFile("", "notes.*.db")
	if err != nil {
		t.Errorf("Could not create temp file: %v", err)
	}
	defer os.Remove(file.Name())

	DB := database.NewDatabaseInstance(file.Name())
	if err := DB.Open(); err != nil {
		t.Errorf("Could not open database: %v", err)
	}
	defer DB.Close()
}

func TestOpenDBMultiple(t *testing.T) {
	file, err := ioutil.TempFile("", "notes.*.db")
	if err != nil {
		t.Errorf("Could not create temp file: %v", err)
	}
	defer os.Remove(file.Name())

	DB := database.NewDatabaseInstance(file.Name())
	if err := DB.Open(); err != nil {
		t.Errorf("Could not open database: %v", err)
	}
	defer DB.Close()

	if err := DB.Open(); err == nil {
		t.Error("Call to DB.Open has not returned an error, but should, as the database is already opened.")
	}
	defer DB.Close()
}

func TestWriteEncryptedNote(t *testing.T) {
	file, err := ioutil.TempFile("", "notes.*.db")
	if err != nil {
		t.Errorf("Could not create temp file: %v", err)
	}
	defer os.Remove(file.Name())

	DB := database.NewDatabaseInstance(file.Name())
	if err := DB.Open(); err != nil {
		t.Errorf("Could not open database: %v", err)
	}
	defer DB.Close()

	id, err := uuid.Parse("12345678-1234-5678-9012-123456789012")
	if err != nil {
		t.Errorf("Error parsing UUID: %v", err)
	}
	note := model.EncryptedNote{
		Uuid:       id,
		Time:       time.Now(),
		Title:      "This is my Title!",
		Ciphertext: "Imagine some base64 encoded ciphertext here.",
	}

	err = DB.SaveEncryptedNote(&note)
	if err != nil {
		t.Errorf("Could not save note: %v", err)
	}
}

// Tests for multiple recipients in database
func TestMultipleRecipients(t *testing.T) {
	file, err := ioutil.TempFile("", "notes.*.db")
	if err != nil {
		t.Errorf("Could not create temp file: %v", err)
	}
	defer os.Remove(file.Name())

	DB := database.NewDatabaseInstance(file.Name())
	if err := DB.Open(); err != nil {
		t.Errorf("Could not open database: %v", err)
	}
	defer DB.Close()

	i1, _ := age.GenerateX25519Identity()
	i2, err := age.GenerateX25519Identity()
	if err != nil {
		t.Errorf("Error during identity generation: %v", err)
	}
	t.Logf("Got recipient %s", i1.Recipient().String())
	t.Logf("Got recipient %s", i2.Recipient().String())

	r1 := model.Recipient{
		Alias:     "Test1",
		Publickey: i1.Recipient().String(),
	}
	r2 := model.Recipient{
		Alias:     "Test2",
		Publickey: i2.Recipient().String(),
	}

	_ = DB.AddRecipient(r1)
	err = DB.AddRecipient(r2)
	if err != nil {
		t.Errorf("Error during recipient addition: %v", err)
	}

	r, err := DB.GetRecipients()
	if err != nil {
		t.Errorf("Error loading recipients: %v", err)
	}
	if len(r) != 2 {
		t.Errorf("Length should be 2 but was %d.", len(r))
	}
	for _, r3 := range r {
		t.Logf("Loaded recipient %s", r3.Publickey)
		if r3.Publickey != i1.Recipient().String() && r3.Publickey != i2.Recipient().String() {
			t.Errorf(
				"r3 should be either %s or %s but was %s.",
				i1.Recipient().String(),
				i2.Recipient().String(),
				r3.Publickey,
			)
		}
	}
}

func TestRemovingRecipients(t *testing.T) {
	file, err := ioutil.TempFile("", "notes.*.db")
	if err != nil {
		t.Errorf("Could not create temp file: %v", err)
	}
	defer os.Remove(file.Name())

	DB := database.NewDatabaseInstance(file.Name())
	if err := DB.Open(); err != nil {
		t.Errorf("Could not open database: %v", err)
	}
	defer DB.Close()

	i1, _ := age.GenerateX25519Identity()
	i2, err := age.GenerateX25519Identity()
	if err != nil {
		t.Errorf("Error during identity generation: %v", err)
	}
	t.Logf("Got recipient %s", i1.Recipient().String())
	t.Logf("Got recipient %s", i2.Recipient().String())

	r1 := model.Recipient{
		Alias:     "Test1",
		Publickey: i1.Recipient().String(),
	}
	r2 := model.Recipient{
		Alias:     "Test2",
		Publickey: i2.Recipient().String(),
	}

	_ = DB.AddRecipient(r1)
	err = DB.AddRecipient(r2)
	if err != nil {
		t.Errorf("Error during recipient addition: %v", err)
	}

	recipients, err := DB.GetRecipients()
	if err != nil {
		t.Errorf("Error during loading recipients: %v", err)
	}

	for _, r := range recipients {
		if r.Alias != r1.Alias && r.Alias != r2.Alias {
			t.Errorf("Unknown recipient: %s", r.Alias)
		}
	}

	err = DB.RemoveRecipientByAlias("Test1")
	if err != nil {
		t.Errorf("Error during removing recipient: %v", err)
	}

	recipients, err = DB.GetRecipients()
	if err != nil {
		t.Errorf("Error during loading recipients: %v", err)
	}

	for _, r := range recipients {
		if r.Alias == r1.Alias {
			t.Errorf("Recipient uses alias which should already be removed: %s", r.Alias)
		}
	}
}


<|start_filename|>internal/utils/utils.go<|end_filename|>
package utils

import (
	"crypto/rand"
	"errors"
	"os"
	"regexp"

	"filippo.io/age"
)

// Loads private key file, strips content (e.g. additional linebreaks) and passes it to age.ParseX25519Identity.
func IdentityFromKeyfile(path string) (identity *age.X25519Identity, err error) {
	if _, err = os.Stat(path); err != nil {
		return nil, err
	}
	content, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	regex, err := regexp.Compile("[^a-zA-Z0-9-]")
	if err != nil {
		return nil, err
	}
	keyString := regex.ReplaceAllString(string(content), "")
	return age.ParseX25519Identity(keyString)
}

// Overwrites file content with generated pseudo random numbers
func OverwriteFileContent(path string) (err error) {
	info, err := os.Stat(path)
	if err != nil {
		return err
	}
	b := make([]byte, info.Size())
	_, err = rand.Read(b)
	if err != nil {
		return err
	}
	return os.WriteFile(path, b, 0600)
}

// Checks for paths given as parameters and envs. Returns the correct path prioritizing parameters over envs.
func GetPaths(pathFlag, pathEnv, keyFlag, keyEnv string, keyNeeded bool) (path, key string, err error) {
	if len(pathFlag) > 0 {
		path = pathFlag
	} else if len(pathEnv) > 0 {
		path = pathEnv
	} else {
		return "", "", errors.New("path to database must be given")
	}

	if len(keyFlag) > 0 {
		key = keyFlag
	} else if len(keyEnv) > 0 {
		key = keyEnv
	} else {
		if keyNeeded {
			return "", "", errors.New("path to keyfile must be given")
		}
	}
	return path, key, nil
}

// Checks if programm is called in a pipe
func IsPipe() bool {
	fi, _ := os.Stdin.Stat()
	return fi.Mode()&os.ModeCharDevice == 0
}


<|start_filename|>cmd/aen.go<|end_filename|>
package main

import (
	"bufio"
	"errors"
	"flag"
	"fmt"
	"hash/crc32"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"regexp"
	"runtime/debug"
	"strings"
	"time"

	"github.com/3c7/aen"
	"github.com/3c7/aen/internal/model"
	"github.com/3c7/aen/internal/utils"
)

var Version string

const usage string = `Age Encrypted Notebook $(VERSION)

Write age encrypted text snippets ("notes") into a Bolt database.

Subcommands:
  help        (?)   (-b|--brief)
  init        (in)  (-o|--output) <DB path> (-k|--key) <key path>
  list        (ls)  (-d|--db) <DB path>
  create      (cr)  (-d|--db) <DB path> (-S|--shred)
  edit        (ed)  (-d|--db) <DB path> (-k|--key) <key path>
                    (-s|--slug) <slug> (-i|--id) <id> (-S|--shred)
  write       (wr)  (-d|--db) <DB path> (-t|--title) <title> (-m|--message) <message>
  get         (g)   (-d|--db) <DB path> (-k|--key) <key path>
                    (-s|--slug) <slug> (-i|--id) <id> (-r|--raw)
  remove      (rm)  (-d|--db) <DB path> (-s|--slug) <slug> (-i|--id) <id>
  recipients  (re)  (-d|--db) <DB path> (-r|--remove) <alias>

More details via "aen help" or with parameter "--help".
`

const help string = `Age Encrypted Notebook $(VERSION)

* DB and keyfile paths can also be given via environment variables AENDB and AENKEY.
** The default editor can be changed through setting the environment variable AENEDITOR.

Usage:

aen init (in)          Initializes the private key and the database if not already given
                       and adds the own public key to the database
  -o, --output         - Path to DB *
  -k, --key            - Path to age keyfile *

aen list (ls)          Lists the slugs of available notes sorted by their timestamp
  -d, --db             - Path to DB *

aen create (cr)        Creates a new note with an editor using the first line of the created
                       note as title
                       By default the command calls 'codium -w' **
  -d, --db             - Path to DB *
  -S, --shred          - Overwrites temporary file with random data

aen edit (ed)          Edits a note given by slug or id
                       By default the command calls 'codium -w' **
  -d, --db             - Path to DB *
  -k, --key            - Path to age keyfile *
  -s, --slug           - Slug of note to get
  -i, --id             - ID of note to get
  -S, --shred          - Overwrites temporary file with random data

aen write (wr)         Writes a new note
  -d, --db             - Path to DB *
  -t, --title          - Title of the note
  -m, --message        - Message of the note

aen get (g)            Get and decrypt a note by its slug or id
  -d, --db             - Path to DB *
  -k, --key            - Path to age keyfile *
  -s, --slug           - Slug of note to get
  -i, --id             - ID of note to get
  -r, --raw            - Only print note content without any metadata

aen remove (rm)        Removes note by its slug or id from the database
                       NOTE: While the note is not retrievable through aen anymore,
                       the data reside in the database file until its overwritten by a new note.
  -d, --db             - Path to DB *
  -s, --slug           - Slug of note to get
  -i, --id             - ID of note to get

aen recipients (re)   Lists all recipients and their aliases
  -d, --db            - Path to DB *
  -r, --remove        - Remove recipient identified by its alias
`

func main() {
	if buildinfo, ok := debug.ReadBuildInfo(); ok && Version == "" {
		Version = buildinfo.Main.Version
	} else if Version == "" {
		Version = "(unknown)"
	}

	log.SetFlags(0)
	flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", strings.Replace(usage, "$(VERSION)", Version, 1)) }
	DetailedUsage := func() { fmt.Fprintf(os.Stderr, "%s\n", strings.Replace(help, "$(VERSION)", Version, 1)) }

	if len(os.Args) == 1 {
		flag.Usage()
		os.Exit(1)
	}

	argString := strings.Join(os.Args, "")
	if strings.Contains(argString, "--help") {
		DetailedUsage()
		os.Exit(1)
	}

	var (
		pathFlag, keyFlag, titleFlag, messageFlag, slugFlag, aliasFlag string
		pathEnv, keyEnv, editorEnv                                     string
		editorCmd                                                      []string
		idFlag                                                         uint
		briefFlag, shredFlag, rawFlag                                  bool
	)

	HelpCmd := flag.NewFlagSet("help", flag.ExitOnError)
	HelpCmd.BoolVar(&briefFlag, "brief", false, "Shows only brief usage information.")
	HelpCmd.BoolVar(&briefFlag, "b", false, "Shows only brief usage information.")

	InitCmd := flag.NewFlagSet("init", flag.ExitOnError)
	InitCmd.StringVar(&pathFlag, "output", "", "Filepath to database file which will be created, if not already available.")
	InitCmd.StringVar(&pathFlag, "o", "", "Filepath to database file which will be created, if not already available.")
	InitCmd.StringVar(&keyFlag, "key", "", "Filepath to key file, will be created if not available.")
	InitCmd.StringVar(&keyFlag, "k", "", "Filepath to key file, will be created if not available.")
	InitCmd.StringVar(&aliasFlag, "alias", "", "Alias to be used for the public key.")
	InitCmd.StringVar(&aliasFlag, "a", "", "Alias to be used for the public key.")

	ListCmd := flag.NewFlagSet("list", flag.ExitOnError)
	ListCmd.StringVar(&pathFlag, "db", "", "Path to database")
	ListCmd.StringVar(&pathFlag, "d", "", "Path to database")

	WriteCmd := flag.NewFlagSet("write", flag.ExitOnError)
	WriteCmd.StringVar(&pathFlag, "db", "", "Path to database")
	WriteCmd.StringVar(&pathFlag, "d", "", "Path to database")
	WriteCmd.StringVar(&titleFlag, "title", "", "Title for test writing a note.")
	WriteCmd.StringVar(&titleFlag, "t", "", "Title for test writing a note.")
	WriteCmd.StringVar(&messageFlag, "message", "", "Content for writing a note.")
	WriteCmd.StringVar(&messageFlag, "m", "", "Content for writing a note.")

	GetCmd := flag.NewFlagSet("get", flag.ExitOnError)
	GetCmd.StringVar(&pathFlag, "db", "", "Path to database")
	GetCmd.StringVar(&pathFlag, "d", "", "Path to database")
	GetCmd.StringVar(&keyFlag, "key", "", "Path to keyfile")
	GetCmd.StringVar(&keyFlag, "k", "", "Path to keyfile")
	GetCmd.StringVar(&slugFlag, "slug", "", "Slug for note")
	GetCmd.StringVar(&slugFlag, "s", "", "Slug for note")
	GetCmd.UintVar(&idFlag, "id", 0, "ID for note")
	GetCmd.UintVar(&idFlag, "i", 0, "ID for note")
	GetCmd.BoolVar(&rawFlag, "raw", false, "Only print note content")
	GetCmd.BoolVar(&rawFlag, "r", false, "Only print note content")

	CreateCmd := flag.NewFlagSet("create", flag.ExitOnError)
	CreateCmd.StringVar(&pathFlag, "db", "", "Path to database")
	CreateCmd.StringVar(&pathFlag, "d", "", "Path to database")
	CreateCmd.BoolVar(&shredFlag, "shred", false, "Shred file contents afterwards")
	CreateCmd.BoolVar(&shredFlag, "S", false, "Shred file contents afterwards")

	RmCmd := flag.NewFlagSet("remove", flag.ExitOnError)
	RmCmd.StringVar(&pathFlag, "db", "", "Path to database")
	RmCmd.StringVar(&pathFlag, "d", "", "Path to database")
	RmCmd.StringVar(&keyFlag, "key", "", "Path to keyfile")
	RmCmd.StringVar(&keyFlag, "k", "", "Path to keyfile")
	RmCmd.StringVar(&slugFlag, "slug", "", "Slug for note")
	RmCmd.StringVar(&slugFlag, "s", "", "Slug for note")
	RmCmd.UintVar(&idFlag, "id", 0, "ID for note")
	RmCmd.UintVar(&idFlag, "i", 0, "ID for note")

	EditCmd := flag.NewFlagSet("edit", flag.ExitOnError)
	EditCmd.StringVar(&pathFlag, "db", "", "Path to database")
	EditCmd.StringVar(&pathFlag, "d", "", "Path to database")
	EditCmd.StringVar(&keyFlag, "key", "", "Path to keyfile")
	EditCmd.StringVar(&keyFlag, "k", "", "Path to keyfile")
	EditCmd.StringVar(&slugFlag, "slug", "", "Slug for note")
	EditCmd.StringVar(&slugFlag, "s", "", "Slug for note")
	EditCmd.UintVar(&idFlag, "id", 0, "ID for note")
	EditCmd.UintVar(&idFlag, "i", 0, "ID for note")
	EditCmd.BoolVar(&shredFlag, "shred", false, "Shred file contents afterwards")
	EditCmd.BoolVar(&shredFlag, "S", false, "Shred file contents afterwards")

	RecipientsCmd := flag.NewFlagSet("recipients", flag.ExitOnError)
	RecipientsCmd.StringVar(&pathFlag, "db", "", "Path to database")
	RecipientsCmd.StringVar(&pathFlag, "d", "", "Path to database")
	RecipientsCmd.StringVar(&aliasFlag, "remove", "", "Remove recipient with this alias")
	RecipientsCmd.StringVar(&aliasFlag, "r", "", "Remove recipient with this alias")

	pathEnv = os.Getenv("AENDB")
	keyEnv = os.Getenv("AENKEY")
	editorEnv = os.Getenv("AENEDITOR")

	if len(editorEnv) > 0 {
		editorCmd = strings.Split(editorEnv, " ")
	} else {
		editorCmd = strings.Split("codium -w", " ")
	}

	switch os.Args[1] {
	case "help", "he", "?":
		HelpCmd.Parse(os.Args[2:])
		if briefFlag {
			flag.Usage()
		} else {
			DetailedUsage()
		}

	case "init", "in":
		InitCmd.Parse(os.Args[2:])
		path, key, err := utils.GetPaths(pathFlag, pathEnv, keyFlag, keyEnv, true)
		if err != nil {
			log.Fatalf("Error initializing database: %v", err)
		}
		err = initAen(path, key, aliasFlag)
		if err != nil {
			log.Fatalf("Error initializing aen: %v", err)
		}

	case "list", "ls":
		ListCmd.Parse(os.Args[2:])
		path, _, err := utils.GetPaths(pathFlag, pathEnv, "", "", false)
		if err != nil {
			log.Fatalf("Error listing notes: %v", err)
		}
		listNotes(path)

	case "write", "wr":
		WriteCmd.Parse(os.Args[2:])
		path, _, err := utils.GetPaths(pathFlag, pathEnv, "", "", false)
		if err != nil {
			log.Fatalf("Error writing note: %v", err)
		}

		// Check if this program is used in a unix pipe and read from stdin, if this is the case
		if utils.IsPipe() {
			messageFlag = ""
			log.Println("Using text from stdin as message.")
			reader := bufio.NewReader(os.Stdin)
			var err error = nil
			var s string
			for err == nil {
				s, err = reader.ReadString('\n')
				messageFlag = messageFlag + s
			}
		}

		if len(titleFlag) == 0 {
			log.Fatal("Error writing note: title must be given.")
		}

		if len(messageFlag) == 0 {
			log.Fatal("Error writing note: message must be given.")
		}
		writeNote(path, titleFlag, messageFlag)

	case "get", "g":
		GetCmd.Parse(os.Args[2:])
		path, key, err := utils.GetPaths(pathFlag, pathEnv, keyFlag, keyEnv, true)
		if err != nil {
			log.Fatalf("Error getting note: %v", err)
		}
		if len(slugFlag) == 0 && idFlag == 0 {
			log.Fatal("Error getting note: ID or Slug must be given.")
		}
		getNote(path, key, slugFlag, idFlag, rawFlag)

	case "create", "cr":
		CreateCmd.Parse(os.Args[2:])
		path, _, err := utils.GetPaths(pathFlag, pathEnv, "", "", false)
		if err != nil {
			log.Fatalf("Error creating note: %v", err)
		}
		createNote(path, editorCmd, shredFlag)

	case "edit", "ed":
		EditCmd.Parse(os.Args[2:])
		path, key, err := utils.GetPaths(pathFlag, pathEnv, keyFlag, keyEnv, true)
		if err != nil {
			log.Fatalf("Error editing note: %v", err)
		}
		editNote(path, key, slugFlag, int(idFlag), editorCmd, shredFlag)

	case "remove", "del", "rm":
		RmCmd.Parse(os.Args[2:])
		path, _, err := utils.GetPaths(pathFlag, pathEnv, "", "", false)
		if err != nil {
			log.Fatalf("Error deleting note: %v", err)
		}
		deleteNote(path, slugFlag, idFlag)

	case "version", "ver", "v":
		log.Printf("Age Encrypted Notebook version: %s", Version)

	case "recipients", "re":
		RecipientsCmd.Parse(os.Args[2:])
		path, _, err := utils.GetPaths(pathFlag, pathEnv, "", "", false)
		if err != nil {
			log.Fatalf("Error listing recipients: %v", err)
		}
		listRecipients(path, aliasFlag)

	default:
		flag.Usage()
		log.Fatalf("Subcommand unknown: %s", os.Args[1])
	}
}

// Initializes AEN with a database and a key.
// If database is already available, a key will be generated.
// If both are available, the public key will be added as recipient.
func initAen(path string, keyPath string, aliasFlag string) (err error) {
	key, err := aen.EnsureKey(keyPath)
	if err != nil {
		return err
	}
	fmt.Printf("Public key: %s\n", key.Recipient().String())

	db, err := aen.OpenDatabase(path, true)
	if err != nil {
		return err
	}
	defer db.Close()

	if len(aliasFlag) == 0 {
		aliasFlag = fmt.Sprintf("%x", crc32.ChecksumIEEE([]byte(key.Recipient().String())))
	}

	recipient := model.Recipient{
		Alias:     aliasFlag,
		Publickey: key.Recipient().String(),
	}

	err = db.AddRecipient(recipient)
	return
}

// List all notes available in the database and print them ordered by the creation time.
func listNotes(pathFlag string) {
	db, err := aen.OpenDatabase(pathFlag, false)
	if err != nil {
		log.Fatalf("Error opening database file: %v", err)
	}
	defer db.Close()

	notes, err := db.GetEncryptedNotes()
	if err != nil {
		log.Fatalf("Error reading notes: %v", err)
	}
	if len(notes) == 0 {
		log.Println("No notes available.")
		return
	}
	model.SortNoteSlice(notes)
	fmt.Printf("| %-5s | %-25s | %-25s | %-25s |\n", "ID", "Title", "Creation time", "Slug")
	var title string
	for idx, note := range notes {
		if len(note.Title) > 25 {
			title = note.Title[:22] + "..."
		} else {
			title = note.Title
		}
		fmt.Printf("| %-5s | %-25s | %-25s | %-25s |\n", fmt.Sprintf("%d", idx+1), title, note.Time.Format("2006-01-02 15:04:05"), note.Slug())
	}
}

// Creates a new note through
// - creating a temporary file
// - opening the file with the configured editor
// - wait until the process exits
// - read the file
// - use the first line as title and the remaining content as note text
func createNote(pathFlag string, cmdString []string, shredFlag bool) {
	db, err := aen.OpenDatabase(pathFlag, false)
	if err != nil {
		log.Fatalf("Error opening database file: %v", err)
	}
	db.Close()

	tmpfile, err := ioutil.TempFile("", "note")
	if err != nil {
		log.Fatalf("Error creating temporary file: %v", err)
	}
	defer os.Remove(tmpfile.Name())

	cmdString = append(cmdString, tmpfile.Name())

	Cmd := exec.Command(cmdString[0], cmdString[1:]...)
	err = Cmd.Run()
	if err != nil {
		log.Fatalf("Error running command: %v", err)
	}
	data, err := os.ReadFile(tmpfile.Name())
	if err != nil {
		log.Fatalf("Error reading temporary file %s: %v", tmpfile.Chdir(), err)
	}
	slicedData := strings.Split(string(data), "\n")
	regex, err := regexp.Compile("[^a-zA-Z0-9 !\"§$%&/()=]+")
	if err != nil {
		log.Fatalf("Error compiling regular expression: %v", err)
	}
	title := regex.ReplaceAllString(slicedData[0], "")
	message := strings.Join(slicedData[1:], "\n")
	if len(title) == 0 || len(message) == 0 {
		log.Fatalf("Error creating note: both, title and message, must be given.")
	}
	if shredFlag {
		utils.OverwriteFileContent(tmpfile.Name())
	}
	writeNote(pathFlag, title, message)
}

// Sililar to createNote this function decrypts and writes a note to a temporary file
// which then can be edited through the configured editor.
func editNote(pathFlag, keyFlag, slugFlag string, idFlag int, editorCmd []string, shredFlag bool) {
	var note *model.EncryptedNote
	db, err := aen.OpenDatabase(pathFlag, false)
	if err != nil {
		log.Fatalf("Error opening database: %v", err)
	}
	defer db.Close()

	if len(slugFlag) > 0 {
		note, err = db.GetEncryptedNoteBySlug(slugFlag)
		if err != nil {
			log.Fatalf("Error receiving note %s from DB: %v", slugFlag, err)
		}
	} else if idFlag > 0 {
		note, err = db.GetEncryptedNoteByIndex(idFlag)
		if err != nil {
			log.Fatalf("Error receiving note %d from DB: %v", idFlag, err)
		}
	} else {
		log.Fatal("Error receiving note from DB: either slug or id must be given.")
	}

	identity, err := utils.IdentityFromKeyfile(keyFlag)
	if err != nil {
		log.Fatalf("Could not load private key: %v", err)
	}

	decryptedNote, err := note.ToDecryptedNote(identity)
	if err != nil {
		log.Fatalf("Could not decrypt note %s: %v", note.Slug(), err)
	}

	file, err := os.CreateTemp("", "note")
	if err != nil {
		log.Fatalf("Error creating temporary file: %v", err)
	}
	defer os.Remove(file.Name())

	err = decryptedNote.ToFile(file.Name())
	if err != nil {
		log.Fatalf("Error writing note to file %s: %v", file.Name(), err)
	}

	editorCmd = append(editorCmd, file.Name())
	Cmd := exec.Command(editorCmd[0], editorCmd[1:]...)
	err = Cmd.Run()
	if err != nil {
		log.Fatalf("Error running command: %v", err)
	}

	newNote, err := model.NotefileToNote(file.Name())
	if err != nil {
		log.Fatalf("Error reading note from file %s: %v", file.Name(), err)
	}

	newNote.Uuid = note.Uuid
	newNote.Time = time.Now()
	if newNote.Slug() != note.Slug() {
		err = db.DeleteNoteBySlug(note.Slug())
		if err != nil {
			log.Fatalf("Could not delete old note by slug %s: %v", note.Slug(), err)
		}
	}
	recipients, err := db.GetAgeRecipients()
	if err != nil {
		log.Fatalf("Could not get recipients: %v", err)
	}
	newEncryptedNote, err := newNote.ToEncryptedNote(recipients...)
	if err != nil {
		log.Fatalf("Could not encrypt note: %v", err)
	}
	err = db.SaveEncryptedNote(&newEncryptedNote)
	if err != nil {
		log.Fatalf("Could not save encrypted note: %v", err)
	}
	log.Printf("Written note %s.", newEncryptedNote.Slug())
	if shredFlag {
		utils.OverwriteFileContent(file.Name())
	}
}

// Writes a new note based on the parameters given.
func writeNote(pathFlag, titleFlag, messageFlag string) {
	db, err := aen.OpenDatabase(pathFlag, false)
	if err != nil {
		log.Fatalf("Error opening database: %v", err)
	}
	defer db.Close()

	note := model.NewNote(titleFlag, messageFlag)
	x25519Recipients, err := db.GetAgeRecipients()
	if err != nil {
		log.Fatalf("Error loading recipients: %v", err)
	}
	encryptedNote, err := note.ToEncryptedNote(x25519Recipients...)
	if err != nil {
		log.Fatalf("Error during note encryption: %v", err)
	}

	err = db.SaveEncryptedNote(&encryptedNote)
	if err != nil {
		log.Fatalf("Error storing note: %v", err)
	}
	log.Printf("Successfully written note %s.", encryptedNote.Slug())
}

func getNote(pathFlag, keyFlag, slugFlag string, idFlag uint, rawFlag bool) {
	var encryptedNote *model.EncryptedNote
	db, err := aen.OpenDatabase(pathFlag, false)
	if err != nil {
		log.Fatalf("Error opening database: %v", err)
	}
	defer db.Close()

	identity, err := utils.IdentityFromKeyfile(keyFlag)
	if err != nil {
		log.Fatalf("Could not load private key: %v", err)
	}

	if slugFlag != "" {
		encryptedNote, err = db.GetEncryptedNoteBySlug(slugFlag)
		if err != nil {
			log.Fatalf("Could not load note by slug: %v", err)
		}
	} else if idFlag != 0 {
		encryptedNote, err = db.GetEncryptedNoteByIndex(int(idFlag))
		if err != nil {
			log.Fatalf("Could not load note by id: %v", err)
		}
	}

	note, err := encryptedNote.ToDecryptedNote(identity)
	if err != nil {
		log.Fatalf("Could not decrypt note: %v", err)
	}

	if rawFlag {
		fmt.Printf("%s\n", note.Text)
	} else {
		fmt.Printf("Title: %s (%s)\n", note.Title, note.Uuid.String())
		fmt.Printf("Created: %s\n", note.Time.Format("2006-01-02 15:04:05"))
		fmt.Printf("Content:\n%s\n", note.Text)
	}
}

func deleteNote(pathFlag, slugFlag string, idFlag uint) {
	var err error
	var note *model.EncryptedNote
	if len(slugFlag) == 0 && idFlag == 0 {
		log.Fatal("Error deleting note: either slug or id must be given.")
	}

	db, err := aen.OpenDatabase(pathFlag, false)
	if err != nil {
		log.Fatalf("Error opening database: %v", err)
	}
	defer db.Close()

	if len(slugFlag) > 0 {
		err = db.DeleteNoteBySlug(slugFlag)
	} else if idFlag > 0 {
		note, err = db.GetEncryptedNoteByIndex(int(idFlag))
		if err != nil {
			log.Fatalf("Couldn't get note by index: %v", err)
		}
		slugFlag = note.Slug()
		err = db.DeleteNoteBySlug(note.Slug())
	} else {
		err = errors.New("either of slug or id must be given")
	}
	if err != nil {
		log.Fatalf("Could not delete note: %v", err)
	}
	log.Printf("Deleted note %s.", slugFlag)
}

func listRecipients(pathFlag, aliasFlag string) {
	db, err := aen.OpenDatabase(pathFlag, false)
	if err != nil {
		log.Fatalf("Error opening database: %v", err)
	}
	defer db.Close()

	if aliasFlag != "" {
		err = db.RemoveRecipientByAlias(aliasFlag)
		if err != nil {
			log.Fatalf("Error removing recipient %s: %v", aliasFlag, err)
		}
		return
	}

	recipients, err := db.GetRecipients()
	if err != nil {
		log.Fatalf("Error loading recipients: %v", err)
	}
	if len(recipients) == 0 {
		// Should not really be the case, but anyway...
		log.Println("Recipient list is empty.")
	} else {
		log.Printf("| %-20s | %-62s |", "Alias", "Public Key")
		for _, r := range recipients {
			log.Printf("| %-20s | %-62s |", r.Alias, r.Publickey)
		}
	}
}


<|start_filename|>aen.go<|end_filename|>
package aen

import (
	"errors"
	"fmt"
	"log"
	"os"
	"strings"

	"filippo.io/age"
	"github.com/3c7/aen/internal/database"
)

// OpenDatabase returns an instanciated Database struct.
// If the database file is not available, a custom error will be returned.
// However, if the parameter ensure is given, the database will be created.
// Calling this function should be followed with a "defer db.Close()"
func OpenDatabase(path string, ensure bool) (db *database.Database, err error) {
	_, err = os.Stat(path)
	if err == nil {
		db = database.NewDatabaseInstance(path)
	} else if errors.Is(err, os.ErrNotExist) && ensure {
		db = database.NewDatabaseInstance(path)
	} else {
		if errors.Is(err, os.ErrNotExist) {
			return nil, fmt.Errorf("database file %s not available", path)
		}
		return nil, err
	}
	err = db.Open()
	return db, err
}

// EnsureKey returns a pointer to an age.X25519Identity struct.
// The struct is created through the according parsing function of age.
// If the keyfile is not available, a new keyfile will be generated.
func EnsureKey(path string) (identity *age.X25519Identity, err error) {
	if _, err := os.Stat(path); err == nil {
		buf, err := os.ReadFile(path)
		if err != nil {
			return nil, err
		}
		content := string(buf)
		if strings.Contains(content, "\n") {
			content = strings.ReplaceAll(content, "\n", "")
		}
		identity, err = age.ParseX25519Identity(content)
		return identity, err
	} else if errors.Is(err, os.ErrNotExist) {
		identity, err = age.GenerateX25519Identity()
		if err != nil {
			return nil, err
		}
		err = os.WriteFile(path, []byte(fmt.Sprintf("%s\n", identity.String())), 0600|os.ModeExclusive)
		if err != nil {
			return nil, err
		}
		log.Printf("Written key file to %s.", path)
		return identity, err
	} else {
		return nil, err
	}
}


<|start_filename|>internal/database/database.go<|end_filename|>
package database

import (
	"encoding/json"
	"errors"
	"fmt"

	"filippo.io/age"
	"github.com/3c7/aen/internal/model"
	bolt "go.etcd.io/bbolt"
)

type Database struct {
	Path   string
	Handle *bolt.DB
	isOpen bool
}

func NewDatabaseInstance(path string) *Database {
	return &Database{
		Path:   path,
		isOpen: false,
	}
}

func (db *Database) Open() (err error) {
	if db.isOpen {
		return errors.New("Database is already open")
	}
	db.Handle, err = bolt.Open(db.Path, 0600, nil)
	if err == nil {
		db.isOpen = true
	}
	return err
}

func (db *Database) Close() (err error) {
	err = db.Handle.Close()
	if err == nil {
		db.isOpen = false
	}
	return err
}

func (db *Database) ensureBucket(tx *bolt.Tx, bucket []byte) (b *bolt.Bucket, err error) {
	if tx.DB().IsReadOnly() {
		return nil, errors.New("database is read-only")
	}
	b = tx.Bucket(bucket)
	if b == nil {
		return tx.CreateBucket(bucket)
	}
	return b, nil
}

func (db *Database) writeToBucket(tx *bolt.Tx, bucket []byte, key []byte, value []byte) (err error) {
	if !db.isOpen {
		return errors.New("database is not open")
	}
	b, err := db.ensureBucket(tx, bucket)
	if err != nil {
		return err
	}
	return b.Put(key, value)
}

func (db *Database) readFromBucket(tx *bolt.Tx, bucket []byte, key []byte) (value []byte, err error) {
	if !db.isOpen {
		return nil, errors.New("database is not open")
	}
	b, err := db.ensureBucket(tx, bucket)
	if err != nil {
		return nil, err
	}
	return b.Get(key), nil
}

func (db *Database) deleteFromBucket(tx *bolt.Tx, bucket []byte, key []byte) (err error) {
	if !db.isOpen {
		return errors.New("database is not open")
	}
	b, err := db.ensureBucket(tx, bucket)
	if err != nil {
		return err
	}
	return b.Delete(key)
}

func (db *Database) SaveEncryptedNote(encryptedNote *model.EncryptedNote) (err error) {
	return db.Handle.Update(func(tx *bolt.Tx) error {
		buf, err := json.Marshal(encryptedNote)
		if err != nil {
			return err
		}
		slug := encryptedNote.Slug()
		return db.writeToBucket(tx, []byte("notes"), []byte(slug), buf)
	})
}

func (db *Database) GetEncryptedNotes() (notes []model.EncryptedNote, err error) {
	err = db.Handle.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("notes"))
		if b == nil {
			return nil
		}
		b.ForEach(func(k, v []byte) error {
			var note model.EncryptedNote
			err := json.Unmarshal(v, &note)
			if err != nil {
				return err
			}
			notes = append(notes, note)
			return nil
		})
		return nil
	})
	return notes, err
}

func (db *Database) GetEncryptedNoteBySlug(slug string) (encryptedNote *model.EncryptedNote, err error) {
	var note model.EncryptedNote
	err = db.Handle.View(func(tx *bolt.Tx) error {
		buf, err := db.readFromBucket(tx, []byte("notes"), []byte(slug))
		if err != nil {
			return err
		}
		err = json.Unmarshal(buf, &note)
		return err
	})
	if err != nil {
		return nil, fmt.Errorf("could not get encrypted note from database: %v", err)
	}
	return &note, nil
}

func (db *Database) DeleteNoteBySlug(slug string) (err error) {
	_, err = db.GetEncryptedNoteBySlug(slug)
	if err != nil {
		return errors.New("note with slug not available")
	}
	err = db.Handle.Update(func(tx *bolt.Tx) error {
		err := db.deleteFromBucket(tx, []byte("notes"), []byte(slug))
		return err
	})
	return err
}

func (db *Database) GetEncryptedNoteByIndex(idx int) (encryptedNote *model.EncryptedNote, err error) {
	notes, err := db.GetEncryptedNotes()
	if err != nil {
		return nil, err
	}

	if len(notes) < idx {
		return nil, errors.New("index is out of range")
	}

	model.SortNoteSlice(notes)
	return &notes[idx-1], nil
}

// GetRecipients receives recipients as model.Recipient from database
func (db *Database) GetRecipients() (recipients []model.Recipient, err error) {
	if !db.isOpen {
		return nil, errors.New("database is not open")
	}
	// Using read/write access because the Bucket might not exist yet
	err = db.Handle.Update(func(tx *bolt.Tx) error {
		buf, err := db.readFromBucket(tx, []byte("config"), []byte("recipients"))
		if err != nil {
			return err
		}
		if len(string(buf)) > 0 {
			err = json.Unmarshal(buf, &recipients)
		}
		return err
	})
	return recipients, err
}

// GetAgeRecipients calls GetRecipients and converts the results to []age.X25519Recipient
func (db *Database) GetAgeRecipients() (ageRecipients []age.X25519Recipient, err error) {
	recipients, err := db.GetRecipients()
	if err != nil {
		return nil, err
	}
	for _, recipient := range recipients {
		r, err := age.ParseX25519Recipient(recipient.Publickey)
		if err != nil {
			return nil, err
		}
		ageRecipients = append(ageRecipients, *r)
	}
	return ageRecipients, nil
}

// AddRecipient adds a recipient via model.Recipient struct. If the alias matches an already given recipient,
// the public key will be overwritten.
func (db *Database) AddRecipient(r model.Recipient) (err error) {
	recipients, err := db.GetRecipients()
	if err != nil {
		return err
	}

	changed := false
	for idx, recipient := range recipients {
		if r.Publickey == recipient.Publickey {
			return nil
		} else if r.Alias == recipient.Alias {
			recipients[idx].Publickey = r.Publickey
			changed = true
		}
	}

	if !changed {
		recipients = append(recipients, r)
	}

	buf, err := json.Marshal(recipients)
	if err != nil {
		return err
	}
	err = db.Handle.Update(func(tx *bolt.Tx) error {
		return db.writeToBucket(tx, []byte("config"), []byte("recipients"), buf)
	})
	return err
}

// RemoveRecipientByAlias removes a recipient identified by its model.Recipient.Alias from the database.
func (db *Database) RemoveRecipientByAlias(alias string) (err error) {
	recipients, err := db.GetRecipients()
	for idx, r := range recipients {
		if r.Alias == alias {
			recipients = append(recipients[:idx], recipients[idx+1:]...)
			buf, err := json.Marshal(recipients)
			if err != nil {
				return err
			}
			return db.Handle.Update(func(tx *bolt.Tx) error {
				return db.writeToBucket(tx, []byte("config"), []byte("recipients"), buf)
			})
		}
	}
	return errors.New("alias not found")
}


<|start_filename|>aen_test.go<|end_filename|>
package aen_test

import (
	"os"
	"testing"

	"github.com/3c7/aen"
	"go.etcd.io/bbolt"
)

// Provides a temporary file path
func ProvideTemporaryFilepath(deleted bool) (path string, err error) {
	tmpfile, err := os.CreateTemp("", "database")
	if err != nil {
		return "", err
	}
	tmpfile.Close()
	if deleted {
		os.Remove(tmpfile.Name())
	}
	return tmpfile.Name(), nil
}

func TestOpenDatabaseFailure(t *testing.T) {
	tmpfile, err := ProvideTemporaryFilepath(true)
	if err != nil {
		t.Errorf("Error creating temporary file path: %v", err)
	}

	_, err = aen.OpenDatabase(tmpfile, false)
	if err == nil {
		t.Errorf("OpenDatabase should return an error as the file %s was deleted before.", tmpfile)
	}
	t.Logf("OpenDatabase returned expected error: %v", err)
}

func TestOpenDatabaseEnsured(t *testing.T) {
	tmpfile, err := ProvideTemporaryFilepath(true)
	if err != nil {
		t.Errorf("Error creating temporary file path: %v", err)
	}

	db, err := aen.OpenDatabase(tmpfile, true)
	if err != nil {
		t.Errorf("Error opening database: %v", err)
	}
	db.Close()
	os.Remove(tmpfile)
}

func TestOpenDatabase(t *testing.T) {
	tmpfile, err := ProvideTemporaryFilepath(true)
	if err != nil {
		t.Errorf("Could not create temporary file: %v", err)
	}
	db, err := bbolt.Open(tmpfile, 0600, nil)
	if err != nil {
		t.Errorf("Could not create database instance: %v", err)
	}
	db.Close()

	db2, err := aen.OpenDatabase(tmpfile, false)
	if err != nil {
		t.Fatalf("Could not open database file created by bolt.")
	}
	db2.Close()
	os.Remove(tmpfile)
}


<|start_filename|>internal/utils/utils_test.go<|end_filename|>
package utils_test

import (
	"os"
	"strings"
	"testing"

	"github.com/3c7/aen/internal/utils"
)

func TestOverwriteFileContents(t *testing.T) {
	tmp, err := os.CreateTemp("", "overwrite")
	if err != nil {
		t.Errorf("Could not create temporary file: %v", err)
	}

	err = os.WriteFile(tmp.Name(), []byte("Test"), 0600)
	if err != nil {
		t.Errorf("Could not write to temporary file: %v", err)
	}

	err = tmp.Close()
	if err != nil {
		t.Errorf("Could not close temporary file: %v", err)
	}

	err = utils.OverwriteFileContent(tmp.Name())
	if err != nil {
		t.Errorf("Could not write random data to file: %v", err)
	}

	content, err := os.ReadFile(tmp.Name())
	if err != nil {
		t.Errorf("Could not read from temporary file: %v", err)
	}

	if strings.Contains(string(content), "Test") {
		t.Errorf("Content of file was not overwritten.")
	}
	t.Logf("File content seems to be overwritten: %s", string(content))
	os.Remove(tmp.Name())
}
