code that works
This commit is contained in:
parent
8de3930a00
commit
746899a2c3
12 changed files with 812 additions and 0 deletions
70
pkg/editor/editor.go
Normal file
70
pkg/editor/editor.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package editor
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"git.burning.moe/celediel/agedit/pkg/tmpfile"
|
||||
)
|
||||
|
||||
// EditFile opens the specified file in the configured editor
|
||||
func EditFile(editor, filename string) error {
|
||||
if editor == "" {
|
||||
return errors.New("editor not set")
|
||||
}
|
||||
|
||||
// TODO: handle editors that require arguments
|
||||
cmd := exec.Command(editor, filename)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EditTempFile creates a temporary file with a random name, opens it in the
|
||||
// editor, and returns the byte slice of its contents.
|
||||
func EditTempFile(editor, start, prefix, suffix string, filename_length int) ([]byte, error) {
|
||||
var (
|
||||
filename string
|
||||
bytes []byte
|
||||
err error
|
||||
file *os.File
|
||||
)
|
||||
|
||||
// generator := tmpfile.NewGenerator("agedit_", ".txt", 13)
|
||||
generator := tmpfile.NewGenerator(prefix, suffix, filename_length)
|
||||
|
||||
filename = generator.GenerateFullPath()
|
||||
if file, err = os.Create(filename); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = os.WriteFile(filename, []byte(start), fs.FileMode(0600)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = EditFile(editor, filename); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bytes, err = os.ReadFile(filename); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = file.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = os.Remove(filename); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bytes, nil
|
||||
}
|
98
pkg/encrypt/encrypt.go
Normal file
98
pkg/encrypt/encrypt.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
package encrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"filippo.io/age"
|
||||
)
|
||||
|
||||
// Encrypt encrypts bytes into filename
|
||||
func Encrypt(data []byte, filename string, identity *age.X25519Identity) error {
|
||||
var (
|
||||
w io.WriteCloser
|
||||
out = &bytes.Buffer{}
|
||||
err error
|
||||
)
|
||||
|
||||
if identity == nil {
|
||||
return errors.New("nil identity??")
|
||||
}
|
||||
|
||||
if w, err = age.Encrypt(out, identity.Recipient()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
io.WriteString(w, string(data))
|
||||
if err = w.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
os.Truncate(filename, 0) // in case it exists already
|
||||
if err = os.WriteFile(filename, out.Bytes(), fs.FileMode(0600)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Decrypt decrypts bytes from filename
|
||||
func Decrypt(filename string, identity *age.X25519Identity) ([]byte, error) {
|
||||
var (
|
||||
f *os.File
|
||||
r io.Reader
|
||||
err error
|
||||
out = &bytes.Buffer{}
|
||||
)
|
||||
if f, err = os.Open(filename); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r, err = age.Decrypt(f, identity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(out, r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
|
||||
// NewIdentity generates a new Age identity
|
||||
func NewIdentity() (*age.X25519Identity, error) {
|
||||
id, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// ReadIdentityFromFile reads the identity from the supplied filename
|
||||
func ReadIdentityFromFile(filename string) (*age.X25519Identity, error) {
|
||||
bytes, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := age.ParseX25519Identity(string(bytes))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// WriteIdentityToFile writes the supplied identity to the supplied filename
|
||||
func WriteIdentityToFile(id *age.X25519Identity, filename string) error {
|
||||
err := os.WriteFile(filename, []byte(id.String()), fs.FileMode(0600))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
101
pkg/encrypt/encrypt_test.go
Normal file
101
pkg/encrypt/encrypt_test.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
package encrypt
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"git.burning.moe/celediel/agedit/pkg/tmpfile"
|
||||
)
|
||||
|
||||
var generator = tmpfile.NewGenerator("test_", ".txt", 18)
|
||||
|
||||
// TestEncryptionDecryption writes a string to a file, encrypts it, then decrypts it, and reads the string.
|
||||
func TestEncryptionDecryption(t *testing.T) {
|
||||
var (
|
||||
strings_to_write = []string{
|
||||
"hello world",
|
||||
"hola mundo",
|
||||
"مرحبا بالعالم",
|
||||
"こんにちは世界",
|
||||
"你好世界",
|
||||
"Γειά σου Κόσμε",
|
||||
"Привіт Світ",
|
||||
"Բարեւ աշխարհ",
|
||||
"გამარჯობა მსოფლიო",
|
||||
"अभिवादन पृथ्वी",
|
||||
}
|
||||
)
|
||||
|
||||
id, err := NewIdentity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, str := range strings_to_write {
|
||||
var (
|
||||
outname string = generator.GenerateFullPath()
|
||||
encrypted_outname string = outname + ".age"
|
||||
b []byte
|
||||
err error
|
||||
)
|
||||
|
||||
t.Run("testing writing "+str, func(t *testing.T) {
|
||||
if err = os.WriteFile(outname, []byte(str), fs.FileMode(0600)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b, err = os.ReadFile(outname); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err = Encrypt(b, encrypted_outname, id); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b, err = Decrypt(encrypted_outname, id); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(b) != str {
|
||||
t.Fatal(string(b) + " isn't the same as " + str)
|
||||
}
|
||||
|
||||
if err = os.Remove(outname); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err = os.Remove(encrypted_outname); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewIdentity creats a new identity, writes it to file, then re-reads it back from the file.
|
||||
func TestNewIdentity(t *testing.T) {
|
||||
for range 1000 {
|
||||
outfile := generator.GenerateFullPath()
|
||||
|
||||
identity, err := NewIdentity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = WriteIdentityToFile(identity, outfile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
other_identity, err := ReadIdentityFromFile(outfile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if identity.Recipient().String() != other_identity.Recipient().String() && identity.String() != other_identity.String() {
|
||||
t.Fatal("Identities don't match!", identity.Recipient(), "!=", identity.Recipient())
|
||||
}
|
||||
os.Remove(outfile)
|
||||
}
|
||||
}
|
107
pkg/env/env.go
vendored
Normal file
107
pkg/env/env.go
vendored
Normal file
|
@ -0,0 +1,107 @@
|
|||
package env
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetEditor gets the configured editor by checking environmental
|
||||
// variables EDITOR and VISUAL
|
||||
func GetEditor() string {
|
||||
var editor string
|
||||
if os.Getenv("EDITOR") != "" {
|
||||
editor = os.Getenv("EDITOR")
|
||||
} else if os.Getenv("VISUAL") != "" {
|
||||
editor = os.Getenv("VISUAL")
|
||||
} /* else {
|
||||
// TODO: maybe pick something based on the OS
|
||||
} */
|
||||
|
||||
return editor
|
||||
}
|
||||
|
||||
// GetConfigDir gets a config directory based from environmental variables + the app name
|
||||
//
|
||||
// On Windows, %APPDATA%\agedit is used
|
||||
//
|
||||
// On UNIX-like systems, $XDG_CONFIG_HOME/agedit is tried, if it isn't defined, $HOME/.config/agedit is used
|
||||
func GetConfigDir(appname string) string {
|
||||
var configdir string
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
configdir = os.Getenv("APPDATA")
|
||||
default:
|
||||
fallthrough
|
||||
case "darwin":
|
||||
// TODO: figure out the proper Mac OS local directories
|
||||
fallthrough
|
||||
case "linux":
|
||||
if confighome := os.Getenv("XDG_CONFIG_HOME"); confighome != "" {
|
||||
configdir = confighome
|
||||
} else {
|
||||
configdir = make_path(os.Getenv("HOME"), ".config")
|
||||
}
|
||||
}
|
||||
|
||||
return make_path(configdir, appname)
|
||||
}
|
||||
|
||||
// GetConfigDir gets a config directory based from environmental variables + the app name
|
||||
//
|
||||
// On Windows, %LOCALAPPDATA%\agedit is used
|
||||
//
|
||||
// On UNIX-like systems, $XDG_DATA_HOME/agedit is tried, if it isn't defined, $HOME/.local/share/agedit is used
|
||||
func GetDataDir(appname string) string {
|
||||
var datadir string
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
datadir = os.Getenv("LOCALAPPDATA")
|
||||
default:
|
||||
fallthrough
|
||||
case "darwin":
|
||||
// TODO: also here
|
||||
fallthrough
|
||||
case "linux":
|
||||
if datahome := os.Getenv("XDG_DATA_HOME"); datahome != "" {
|
||||
datadir = datahome
|
||||
} else {
|
||||
datadir = make_path(os.Getenv("HOME"), "local", "share")
|
||||
}
|
||||
}
|
||||
|
||||
return make_path(datadir, appname)
|
||||
}
|
||||
|
||||
// GetTempDirectory returns the systems temporary directory
|
||||
//
|
||||
// returns %TEMP% on Windows, /tmp on UNIX-like systems
|
||||
func GetTempDirectory() string {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return os.Getenv("TEMP")
|
||||
default:
|
||||
fallthrough
|
||||
case "darwin":
|
||||
fallthrough
|
||||
case "linux":
|
||||
return "/tmp"
|
||||
}
|
||||
}
|
||||
|
||||
func make_path(paths ...string) string {
|
||||
sep := string(os.PathSeparator)
|
||||
output := strings.Builder{}
|
||||
|
||||
// add / to the start if it's not already there and we're not on Windows
|
||||
if match, err := regexp.Match("^\\w", []byte(paths[0])); err == nil && match && runtime.GOOS != "windows" {
|
||||
output.WriteString(sep)
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
output.WriteString(path + sep)
|
||||
}
|
||||
|
||||
return output.String()
|
||||
}
|
30
pkg/env/env_test.go
vendored
Normal file
30
pkg/env/env_test.go
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
package env
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
editors = []string{"hx", "nano", "vi", "vim", "nvim", "micro", "emacs", "ed"}
|
||||
)
|
||||
|
||||
func clearEnvForNow() {
|
||||
for _, item := range []string{"EDITOR", "VISUAL"} {
|
||||
os.Setenv(item, "")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorFromEnv(t *testing.T) {
|
||||
for _, item := range []string{"EDITOR", "VISUAL"} {
|
||||
clearEnvForNow()
|
||||
for _, editor := range editors {
|
||||
if err := os.Setenv(item, editor); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := GetEditor(); got != editor {
|
||||
t.Fatal("got", got, "but wanted", editor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
47
pkg/tmpfile/tmpfile.go
Normal file
47
pkg/tmpfile/tmpfile.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package tmpfile
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.burning.moe/celediel/agedit/pkg/env"
|
||||
)
|
||||
|
||||
const chars string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
||||
|
||||
type Generator struct {
|
||||
Prefix, Suffix string
|
||||
Length int
|
||||
}
|
||||
|
||||
// GenerateName generates a random temporary filename like agedit_geef0XYC30RGV
|
||||
func (g *Generator) GenerateName() string {
|
||||
return g.Prefix + randomString(chars, g.Length) + g.Suffix
|
||||
}
|
||||
|
||||
// GenerateFullPath generates a random temporary filename and appends it to the OS's temporary directory
|
||||
func (g *Generator) GenerateFullPath() string {
|
||||
return env.GetTempDirectory() + string(os.PathSeparator) + g.GenerateName()
|
||||
}
|
||||
|
||||
// NewGenerator returns a new Generator
|
||||
func NewGenerator(prefix, suffix string, length int) Generator {
|
||||
return Generator{
|
||||
Prefix: prefix,
|
||||
Suffix: suffix,
|
||||
Length: length,
|
||||
}
|
||||
}
|
||||
|
||||
func randomString(set string, length int) string {
|
||||
out := strings.Builder{}
|
||||
for i := 0; i < length; i++ {
|
||||
out.WriteByte(randomChar(set))
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func randomChar(set string) byte {
|
||||
return set[rand.Intn(len(set))]
|
||||
}
|
43
pkg/tmpfile/tmpfile_test.go
Normal file
43
pkg/tmpfile/tmpfile_test.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package tmpfile
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var generator = NewGenerator("test_", ".txt", 18)
|
||||
|
||||
// TestCanCreateTmpFile tests if temporary files can be created and removed successfully
|
||||
func TestCanCreateTmpFile(t *testing.T) {
|
||||
b := []byte{104, 101, 108, 108, 111, 32, 116, 104, 101, 114, 101}
|
||||
|
||||
for range 1000 {
|
||||
outfile := generator.GenerateFullPath()
|
||||
err := os.WriteFile(outfile, b, fs.FileMode(0600))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err = os.Stat(outfile); err != nil && os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err = os.Remove(outfile); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestUniqueTmpFile generates a large number of random names to make sure they're all unique
|
||||
func TestUniqueTmpFile(t *testing.T) {
|
||||
var generated_names = map[string]string{}
|
||||
|
||||
for range 100000 {
|
||||
name := generator.GenerateName()
|
||||
if val, ok := generated_names[name]; ok {
|
||||
t.Fatal("Non unique name", val)
|
||||
}
|
||||
generated_names[name] = name
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue