mirror of
https://github.com/sbrow/envr.git
synced 2026-06-27 18:48:33 -04:00
Compare commits
6 Commits
2229affe69
...
427a67dcb4
| Author | SHA1 | Date | |
|---|---|---|---|
| 427a67dcb4 | |||
| 656894dbea | |||
| b7fdb88f34 | |||
| d620e2646e | |||
| dd89b2dd9a | |||
| cc935bda7d |
7
.github/workflows/odin.yml
vendored
7
.github/workflows/odin.yml
vendored
@@ -10,12 +10,12 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libsodium-dev sqlite3 libsqlite3-dev
|
||||
sudo apt-get install -y libsodium-dev sqlite3 libsqlite3-dev libsodium-dev
|
||||
|
||||
- name: Install Odin
|
||||
run: |
|
||||
@@ -25,7 +25,8 @@ jobs:
|
||||
echo "/opt/odin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Build
|
||||
run: odin build . -o:speed -out:envr
|
||||
run: |
|
||||
odin build . -o:speed -out:envr
|
||||
|
||||
- name: Test
|
||||
run: odin test .
|
||||
|
||||
6
.github/workflows/release-please.yml
vendored
6
.github/workflows/release-please.yml
vendored
@@ -2,6 +2,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- odin
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -14,7 +16,7 @@ jobs:
|
||||
release-please:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: googleapis/release-please-action@v4
|
||||
- uses: googleapis/release-please-action@v5
|
||||
with:
|
||||
# this assumes that you have created a personal access token
|
||||
# (PAT) and configured it as a GitHub action secret named
|
||||
@@ -22,4 +24,4 @@ jobs:
|
||||
token: ${{ secrets.MY_RELEASE_PLEASE_TOKEN }}
|
||||
# this is a built-in strategy in release-please, see "Action Inputs"
|
||||
# for more options
|
||||
release-type: odin
|
||||
release-type: simple
|
||||
|
||||
11
Makefile
11
Makefile
@@ -19,25 +19,20 @@ all: release clean
|
||||
$(BUILD_DIR):
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
|
||||
# Generate version.odin from flake.nix
|
||||
version.odin:
|
||||
@echo 'Generating version.odin (v$(VERSION))...'
|
||||
@printf 'package main\n\nVERSION :: "$(VERSION)"\n' > version.odin
|
||||
|
||||
# Build Linux AMD64
|
||||
$(LINUX_AMD64_BIN): version.odin $(BUILD_DIR)
|
||||
$(LINUX_AMD64_BIN): $(BUILD_DIR)
|
||||
@echo "Building for Linux AMD64..."
|
||||
odin build . -target:linux_amd64 -o:speed -out:$(LINUX_AMD64_BIN)
|
||||
@echo "Built $(LINUX_AMD64_BIN)"
|
||||
|
||||
# Build Linux ARM64
|
||||
$(LINUX_ARM64_BIN): version.odin $(BUILD_DIR)
|
||||
$(LINUX_ARM64_BIN): $(BUILD_DIR)
|
||||
@echo "Building for Linux ARM64..."
|
||||
odin build . -target:linux_arm64 -o:speed -out:$(LINUX_ARM64_BIN)
|
||||
@echo "Built $(LINUX_ARM64_BIN)"
|
||||
|
||||
# Build Darwin ARM64 (Mac)
|
||||
$(DARWIN_ARM64_BIN): version.odin $(BUILD_DIR)
|
||||
$(DARWIN_ARM64_BIN): $(BUILD_DIR)
|
||||
@echo "Building for Darwin ARM64..."
|
||||
odin build . -target:darwin_arm64 -o:speed -out:$(DARWIN_ARM64_BIN)
|
||||
@echo "Built $(DARWIN_ARM64_BIN)"
|
||||
|
||||
15
README.md
15
README.md
@@ -13,7 +13,7 @@ the tool [of your choosing](#backup-options).
|
||||
## Features
|
||||
|
||||
- 🔐 **Encrypted Storage**: All `.env` files are encrypted using your ssh key and
|
||||
[age](https://github.com/FiloSottile/age) encryption.
|
||||
[libsodium](https://github.com/jedisct1/libsodium) encryption.
|
||||
- 🔄 **Automatic Sync**: Update the database with one command, which can easily
|
||||
be run on a cron.
|
||||
- 🔍 **Smart Scanning**: Automatically discover and import `.env` files in your
|
||||
@@ -37,12 +37,13 @@ repositories.
|
||||
|
||||
## Installation
|
||||
|
||||
### With Go
|
||||
### With Odin
|
||||
|
||||
If you already have `go` installed:
|
||||
If you already have `odin` installed:
|
||||
|
||||
```bash
|
||||
go install github.com/sbrow/envr
|
||||
# You'll need libsodium and sqlite
|
||||
odin build -o:speed
|
||||
envr init
|
||||
```
|
||||
|
||||
@@ -104,18 +105,18 @@ The configuration file is created during initialization:
|
||||
## Backup Options
|
||||
|
||||
`envr` merely gathers your `.env` files in one local place. It is up to you to
|
||||
back up the database (found at `~/.envr/data.age`) to a *secure* and *remote*
|
||||
back up the database (found at `~/.envr/data.envr`) to a *secure* and *remote*
|
||||
location.
|
||||
|
||||
### Git
|
||||
|
||||
`envr` preserves inodes when updating the database, so you can safely hardlink
|
||||
`~/.envr/data.age` into your [GNU Stow](https://www.gnu.org/software/stow/),
|
||||
`~/.envr/data.envr` into your [GNU Stow](https://www.gnu.org/software/stow/),
|
||||
[Home Manager](https://github.com/nix-community/home-manager), or
|
||||
[NixOS](https://nixos.wiki/wiki/flakes) repository.
|
||||
|
||||
> [!CAUTION]
|
||||
> For **maximum security**, only save your `data.age` file to a local
|
||||
> For **maximum security**, only save your `data.envr` file to a local
|
||||
(i.e. non-cloud) git server that **you personally control**.
|
||||
>
|
||||
> I take no responsibility if you push all your secrets to a public GitHub repo.
|
||||
|
||||
4
TODOS.md
4
TODOS.md
@@ -8,10 +8,6 @@ Note: These todos can wait until all the subcommands have been ported.
|
||||
|
||||
3. **config.odin:52-54** — `os.user_home_dir` error silently ignored. If it fails, `home` is `""` and all paths become relative (`".envr"` instead of `"~/.envr"`).
|
||||
|
||||
30. **cmd_sync.odin:46-50, 64-68** — Double `db_insert` when `BackedUp`: first insert on line 48, then `db_update_required` is also true for `BackedUp` so second insert runs on line 65. Redundant and wasteful.
|
||||
|
||||
31. **db.odin:626 & env_file.go:183** — `BackedUp` discards `DirUpdated`. When `TrustFilesystem` is used and the hash differs, the result is just `BackedUp` (not `BackedUp | DirUpdated`). If a file's directory was moved AND its contents changed, the old DB entry won't be deleted because the `DirUpdated` check at `cmd_sync.odin:59` never fires. Bug exists in both Go and Odin.
|
||||
|
||||
## MEDIUM
|
||||
|
||||
4. **db.odin:29-35** — `make_temp_path` never calls `strings.builder_destroy`. Leaks builder buffer every call.
|
||||
|
||||
@@ -3,7 +3,6 @@ package main
|
||||
import "core:fmt"
|
||||
import "core:os"
|
||||
import "core:path/filepath"
|
||||
import "core:strings"
|
||||
|
||||
cmd_check :: proc(cmd: ^Command) {
|
||||
feats := check_features()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package main
|
||||
|
||||
import "core:fmt"
|
||||
|
||||
cmd_deps :: proc(cmd: ^Command) {
|
||||
feats := check_features()
|
||||
|
||||
@@ -22,3 +20,4 @@ cmd_deps :: proc(cmd: ^Command) {
|
||||
|
||||
render_table(headers, rows[:])
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package main
|
||||
import "core:fmt"
|
||||
import "core:os"
|
||||
import "core:path/filepath"
|
||||
import "core:strings"
|
||||
|
||||
cmd_edit_config :: proc(cmd: ^Command) {
|
||||
editor := os.get_env("EDITOR", context.allocator)
|
||||
@@ -25,7 +24,7 @@ cmd_edit_config :: proc(cmd: ^Command) {
|
||||
}
|
||||
|
||||
args := []string{editor, config_path}
|
||||
desc := os.Process_Desc{
|
||||
desc := os.Process_Desc {
|
||||
command = args,
|
||||
stdin = os.stdin,
|
||||
stdout = os.stdout,
|
||||
@@ -47,3 +46,4 @@ cmd_edit_config :: proc(cmd: ^Command) {
|
||||
os.exit(int(state.exit_code))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ SyncEntry :: struct {
|
||||
Status: string `json:"status"`,
|
||||
}
|
||||
|
||||
// TODO: Check for quiet failures.
|
||||
cmd_sync :: proc(cmd: ^Command) {
|
||||
db, db_ok := db_open()
|
||||
if !db_ok {
|
||||
@@ -33,28 +34,22 @@ cmd_sync :: proc(cmd: ^Command) {
|
||||
result, err_msg := db_sync(&db, &file)
|
||||
|
||||
status: string
|
||||
s := i32(result)
|
||||
is_error := (s & i32(SyncResult.Error)) != 0
|
||||
is_backed := (s & i32(SyncResult.BackedUp)) != 0
|
||||
is_restored := (s & i32(SyncResult.Restored)) != 0
|
||||
is_dir_updated := (s & i32(SyncResult.DirUpdated)) != 0
|
||||
is_dir_updated := .DirUpdated in result
|
||||
|
||||
if is_error {
|
||||
switch {
|
||||
case .Error in result:
|
||||
if len(err_msg) > 0 {
|
||||
status = err_msg
|
||||
} else {
|
||||
status = "error"
|
||||
}
|
||||
} else if is_backed {
|
||||
case .BackedUp in result:
|
||||
status = "Backed Up"
|
||||
if !db_insert(&db, file) {
|
||||
return
|
||||
}
|
||||
} else if is_restored {
|
||||
case .Restored in result:
|
||||
status = "Restored"
|
||||
} else if is_dir_updated && !is_restored {
|
||||
case .DirUpdated in result:
|
||||
status = "Moved"
|
||||
} else {
|
||||
case:
|
||||
status = "OK"
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ package main
|
||||
|
||||
import "core:fmt"
|
||||
|
||||
VERSION :: #load("version.txt", string)
|
||||
|
||||
cmd_version :: proc(cmd: ^Command) {
|
||||
if has_flag(cmd, "long") || has_flag(cmd, "l") {
|
||||
fmt.printf("envr version %s\n", VERSION)
|
||||
|
||||
41
crypto.odin
41
crypto.odin
@@ -6,7 +6,11 @@ import "core:mem"
|
||||
MAGIC :: "ENVR"
|
||||
MAGIC_BYTES := [4]u8{u8('E'), u8('N'), u8('V'), u8('R')}
|
||||
|
||||
RECIPIENT_ENTRY_SIZE :: CRYPTO_BOX_PUBLICKEY_BYTES + CRYPTO_BOX_NONCE_BYTES + CRYPTO_SECRETBOX_KEY_BYTES + CRYPTO_BOX_MAC_BYTES
|
||||
RECIPIENT_ENTRY_SIZE ::
|
||||
CRYPTO_BOX_PUBLICKEY_BYTES +
|
||||
CRYPTO_BOX_NONCE_BYTES +
|
||||
CRYPTO_SECRETBOX_KEY_BYTES +
|
||||
CRYPTO_BOX_MAC_BYTES
|
||||
|
||||
HEADER_SIZE :: 4 + CRYPTO_BOX_PUBLICKEY_BYTES + CRYPTO_SECRETBOX_NONCE_BYTES + 4
|
||||
|
||||
@@ -108,7 +112,13 @@ encrypt :: proc(plaintext: []u8, keys: []SshKeyPair) -> (ciphertext: []u8, ok: b
|
||||
if len(plaintext) > 0 {
|
||||
pt_ptr = &plaintext[0]
|
||||
}
|
||||
rc := crypto_secretbox_easy(&secret_ct[0], pt_ptr, u64(len(plaintext)), &main_nonce[0], &sym_key[0])
|
||||
rc := crypto_secretbox_easy(
|
||||
&secret_ct[0],
|
||||
pt_ptr,
|
||||
u64(len(plaintext)),
|
||||
&main_nonce[0],
|
||||
&sym_key[0],
|
||||
)
|
||||
if rc != 0 {
|
||||
fmt.println("Error: symmetric encryption failed")
|
||||
delete(secret_ct)
|
||||
@@ -166,7 +176,11 @@ encrypt :: proc(plaintext: []u8, keys: []SshKeyPair) -> (ciphertext: []u8, ok: b
|
||||
pos += CRYPTO_BOX_PUBLICKEY_BYTES
|
||||
mem.copy(&ciphertext[pos], &entries[i].Nonce[0], CRYPTO_BOX_NONCE_BYTES)
|
||||
pos += CRYPTO_BOX_NONCE_BYTES
|
||||
mem.copy(&ciphertext[pos], &entries[i].EncryptedKey[0], CRYPTO_SECRETBOX_KEY_BYTES + CRYPTO_BOX_MAC_BYTES)
|
||||
mem.copy(
|
||||
&ciphertext[pos],
|
||||
&entries[i].EncryptedKey[0],
|
||||
CRYPTO_SECRETBOX_KEY_BYTES + CRYPTO_BOX_MAC_BYTES,
|
||||
)
|
||||
pos += CRYPTO_SECRETBOX_KEY_BYTES + CRYPTO_BOX_MAC_BYTES
|
||||
}
|
||||
|
||||
@@ -209,8 +223,11 @@ decrypt :: proc(ciphertext: []u8, keys: []SshKeyPair) -> (plaintext: []u8, ok: b
|
||||
}
|
||||
offset += CRYPTO_SECRETBOX_NONCE_BYTES
|
||||
|
||||
num_recipients := u32(ciphertext[offset]) << 24 | u32(ciphertext[offset + 1]) << 16 |
|
||||
u32(ciphertext[offset + 2]) << 8 | u32(ciphertext[offset + 3])
|
||||
num_recipients :=
|
||||
u32(ciphertext[offset]) << 24 |
|
||||
u32(ciphertext[offset + 1]) << 16 |
|
||||
u32(ciphertext[offset + 2]) << 8 |
|
||||
u32(ciphertext[offset + 3])
|
||||
offset += 4
|
||||
|
||||
recipients_end := offset + int(num_recipients) * RECIPIENT_ENTRY_SIZE
|
||||
@@ -233,7 +250,7 @@ decrypt :: proc(ciphertext: []u8, keys: []SshKeyPair) -> (plaintext: []u8, ok: b
|
||||
matched_pi := 0
|
||||
for pi in 0 ..< len(x25519_pairs) {
|
||||
scan_offset := offset
|
||||
for ri in 0 ..< int(num_recipients) {
|
||||
for _ in 0 ..< int(num_recipients) {
|
||||
for i in 0 ..< CRYPTO_BOX_PUBLICKEY_BYTES {
|
||||
enc_pub[i] = ciphertext[scan_offset + i]
|
||||
}
|
||||
@@ -247,7 +264,8 @@ decrypt :: proc(ciphertext: []u8, keys: []SshKeyPair) -> (plaintext: []u8, ok: b
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
scan_offset += CRYPTO_BOX_NONCE_BYTES + CRYPTO_SECRETBOX_KEY_BYTES + CRYPTO_BOX_MAC_BYTES
|
||||
scan_offset +=
|
||||
CRYPTO_BOX_NONCE_BYTES + CRYPTO_SECRETBOX_KEY_BYTES + CRYPTO_BOX_MAC_BYTES
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -301,7 +319,13 @@ decrypt :: proc(ciphertext: []u8, keys: []SshKeyPair) -> (plaintext: []u8, ok: b
|
||||
if len(plaintext) > 0 {
|
||||
pt_ptr = &plaintext[0]
|
||||
}
|
||||
rc = crypto_secretbox_open_easy(pt_ptr, &ct_data[0], u64(len(ct_data)), &main_nonce[0], &sym_key[0])
|
||||
rc = crypto_secretbox_open_easy(
|
||||
pt_ptr,
|
||||
&ct_data[0],
|
||||
u64(len(ct_data)),
|
||||
&main_nonce[0],
|
||||
&sym_key[0],
|
||||
)
|
||||
if rc != 0 {
|
||||
fmt.println("Error: symmetric decryption failed")
|
||||
delete(plaintext)
|
||||
@@ -311,3 +335,4 @@ decrypt :: proc(ciphertext: []u8, keys: []SshKeyPair) -> (plaintext: []u8, ok: b
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
51
db.odin
51
db.odin
@@ -1,6 +1,5 @@
|
||||
package main
|
||||
|
||||
import "core:c"
|
||||
import "core:crypto/hash"
|
||||
import "core:encoding/hex"
|
||||
import "core:encoding/json"
|
||||
@@ -12,14 +11,16 @@ import "core:time"
|
||||
|
||||
import "sqlite"
|
||||
|
||||
SyncResult :: enum i32 {
|
||||
Noop = 0,
|
||||
DirUpdated = 1,
|
||||
Restored = 1 << 1,
|
||||
BackedUp = 1 << 2,
|
||||
Error = 1 << 3,
|
||||
SyncFlagEnum :: enum {
|
||||
Noop,
|
||||
DirUpdated,
|
||||
Restored,
|
||||
BackedUp,
|
||||
Error,
|
||||
}
|
||||
|
||||
SyncFlag :: bit_set[SyncFlagEnum]
|
||||
|
||||
SyncDirection :: enum {
|
||||
TrustDatabase,
|
||||
TrustFilesystem,
|
||||
@@ -449,9 +450,8 @@ string_to_cstring :: proc(s: string) -> cstring {
|
||||
return cs
|
||||
}
|
||||
|
||||
db_update_required :: proc(status: SyncResult) -> bool {
|
||||
s := i32(status)
|
||||
return (s & (i32(SyncResult.BackedUp) | i32(SyncResult.DirUpdated))) != 0
|
||||
db_update_required :: proc(status: SyncFlag) -> bool {
|
||||
return .BackedUp in status || .DirUpdated in status
|
||||
}
|
||||
|
||||
shares_remote :: proc(f: ^EnvFile, remotes: []string) -> bool {
|
||||
@@ -510,9 +510,8 @@ env_file_backup :: proc(f: ^EnvFile) -> bool {
|
||||
return true
|
||||
}
|
||||
|
||||
env_file_sync :: proc(f: ^EnvFile, dir: SyncDirection, d: ^Db) -> (SyncResult, string) {
|
||||
result: SyncResult = .Noop
|
||||
err_msg: string
|
||||
env_file_sync :: proc(f: ^EnvFile, dir: SyncDirection, d: ^Db) -> (SyncFlag, string) {
|
||||
result: SyncFlag = {}
|
||||
|
||||
_, stat_err := os.stat(f.Dir, context.allocator)
|
||||
if stat_err != nil {
|
||||
@@ -521,18 +520,18 @@ env_file_sync :: proc(f: ^EnvFile, dir: SyncDirection, d: ^Db) -> (SyncResult, s
|
||||
if d != nil {
|
||||
dirs, dirs_ok := find_moved_dirs(d, f)
|
||||
if !dirs_ok {
|
||||
return .Error, "failed to find moved dirs"
|
||||
return {.Error}, "failed to find moved dirs"
|
||||
}
|
||||
moved_dirs = dirs
|
||||
}
|
||||
|
||||
if len(moved_dirs) == 0 {
|
||||
return .Error, "directory missing"
|
||||
return {.Error}, "directory missing"
|
||||
} else if len(moved_dirs) == 1 {
|
||||
update_dir(f, moved_dirs[0])
|
||||
result = .DirUpdated
|
||||
result = {.DirUpdated}
|
||||
} else {
|
||||
return .Error, "multiple directories found"
|
||||
return {.Error}, "multiple directories found"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,11 +540,10 @@ env_file_sync :: proc(f: ^EnvFile, dir: SyncDirection, d: ^Db) -> (SyncResult, s
|
||||
write_err := os.write_entire_file(f.Path, f.contents)
|
||||
if write_err != nil {
|
||||
msg, _ := strings.concatenate({"failed to write file: ", fmt.tprintf("%v", write_err)})
|
||||
return .Error, msg
|
||||
return {.Error}, msg
|
||||
}
|
||||
|
||||
s := i32(result) | i32(SyncResult.Restored)
|
||||
return SyncResult(s), ""
|
||||
return result + {.Restored}, ""
|
||||
}
|
||||
|
||||
data, read_err := os.read_entire_file_from_path(f.Path, context.allocator)
|
||||
@@ -553,7 +551,7 @@ env_file_sync :: proc(f: ^EnvFile, dir: SyncDirection, d: ^Db) -> (SyncResult, s
|
||||
msg, _ := strings.concatenate(
|
||||
{"failed to read file for SHA comparison: ", fmt.tprintf("%v", read_err)},
|
||||
)
|
||||
return .Error, msg
|
||||
return {.Error}, msg
|
||||
}
|
||||
|
||||
digest := hash.hash_bytes(hash.Algorithm.SHA256, data)
|
||||
@@ -569,21 +567,20 @@ env_file_sync :: proc(f: ^EnvFile, dir: SyncDirection, d: ^Db) -> (SyncResult, s
|
||||
write_err := os.write_entire_file(f.Path, f.contents)
|
||||
if write_err != nil {
|
||||
msg, _ := strings.concatenate({"failed to write file: ", fmt.tprintf("%v", write_err)})
|
||||
return .Error, msg
|
||||
return {.Error}, msg
|
||||
}
|
||||
s := i32(result) | i32(SyncResult.Restored)
|
||||
return SyncResult(s), ""
|
||||
return result + {.Restored}, ""
|
||||
case .TrustFilesystem:
|
||||
if !env_file_backup(f) {
|
||||
return .Error, "failed to backup file"
|
||||
return {.Error}, "failed to backup file"
|
||||
}
|
||||
return .BackedUp, ""
|
||||
return result + {.BackedUp}, ""
|
||||
}
|
||||
|
||||
return result, ""
|
||||
}
|
||||
|
||||
db_sync :: proc(d: ^Db, f: ^EnvFile) -> (SyncResult, string) {
|
||||
db_sync :: proc(d: ^Db, f: ^EnvFile) -> (SyncFlag, string) {
|
||||
return env_file_sync(f, .TrustFilesystem, d)
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ test_encrypt_decrypt_sqlite_roundtrip :: proc(t: ^testing.T) {
|
||||
defer {
|
||||
delete(cfg.Keys)
|
||||
}
|
||||
key := cfg.Keys[0]
|
||||
|
||||
db_path := fixture_db_path()
|
||||
sqlite_data, read_err := os.read_entire_file_from_path(db_path, context.allocator)
|
||||
@@ -319,7 +318,7 @@ test_config_load_with_fixture_key :: proc(t: ^testing.T) {
|
||||
testing.expectf(t, len(key.Private) > 0, "private key path should not be empty")
|
||||
testing.expectf(t, len(key.Public) > 0, "public key path should not be empty")
|
||||
|
||||
priv_kp, priv_ok := parse_ssh_private_key(key.Private)
|
||||
_, priv_ok := parse_ssh_private_key(key.Private)
|
||||
testing.expect(t, priv_ok, "should parse private key using config paths")
|
||||
if !priv_ok {
|
||||
fmt.printf(" private key path was: '%s'\n", key.Private)
|
||||
|
||||
13
db_test.odin
13
db_test.odin
@@ -4,33 +4,32 @@ import "core:testing"
|
||||
|
||||
@(test)
|
||||
test_db_update_required_noop :: proc(t: ^testing.T) {
|
||||
testing.expect(t, !db_update_required(.Noop), "Noop should not require update")
|
||||
testing.expect(t, !db_update_required({}), "Noop should not require update")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_db_update_required_backed_up :: proc(t: ^testing.T) {
|
||||
testing.expect(t, db_update_required(.BackedUp), "BackedUp should require update")
|
||||
testing.expect(t, db_update_required({.BackedUp}), "BackedUp should require update")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_db_update_required_dir_updated :: proc(t: ^testing.T) {
|
||||
testing.expect(t, db_update_required(.DirUpdated), "DirUpdated should require update")
|
||||
testing.expect(t, db_update_required({.DirUpdated}), "DirUpdated should require update")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_db_update_required_restored :: proc(t: ^testing.T) {
|
||||
testing.expect(t, !db_update_required(.Restored), "Restored alone should not require update")
|
||||
testing.expect(t, !db_update_required({.Restored}), "Restored alone should not require update")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_db_update_required_error :: proc(t: ^testing.T) {
|
||||
testing.expect(t, !db_update_required(.Error), "Error alone should not require update")
|
||||
testing.expect(t, !db_update_required({.Error}), "Error alone should not require update")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_db_update_required_combined :: proc(t: ^testing.T) {
|
||||
s := i32(SyncResult.DirUpdated) | i32(SyncResult.Restored)
|
||||
combined := SyncResult(s)
|
||||
combined := SyncFlag{.DirUpdated, .Restored}
|
||||
testing.expect(t, db_update_required(combined), "DirUpdated|Restored should require update")
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
make version.odin
|
||||
echo '${version}' > version.txt
|
||||
odin build . -o:speed -out:${pname}
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
109
quash
Normal file
109
quash
Normal file
@@ -0,0 +1,109 @@
|
||||
[1m[38;5;2m@[0m [1m[38;5;13muk[38;5;8mspssxz[39m [38;5;3mspencer.brower@proton.me[39m [38;5;14m2026-06-12 16:45:22[39m [38;5;10mdefault@[39m [38;5;12m54[38;5;8m8fe7ec[39m[0m
|
||||
│ [1m[38;5;3m(no description set)[39m[0m
|
||||
○ [1m[38;5;5msu[0m[38;5;8mwmwvkl[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-12 16:40:25[39m [38;5;5modin[39m [1m[38;5;4ma1e93[0m[38;5;8m345[39m
|
||||
│ ci: Updated github action.
|
||||
○ [1m[38;5;5mtq[0m[38;5;8mpkpmus[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-12 16:35:39[39m [1m[38;5;4mee[0m[38;5;8md36089[39m
|
||||
│ feat: Removed go code.
|
||||
○ [1m[38;5;5myzz[0m[38;5;8mzmznw[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-12 16:35:34[39m [1m[38;5;4m75[0m[38;5;8mb77845[39m
|
||||
│ build: Converted Makefile and flake package.
|
||||
○ [1m[38;5;5mkv[0m[38;5;8mtmxpyn[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-12 15:54:44[39m [1m[38;5;4m4ec[0m[38;5;8m2b22b[39m
|
||||
│ refactor: removed `is_tty`.
|
||||
○ [1m[38;5;5mpo[0m[38;5;8muwppuo[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-12 15:48:12[39m [1m[38;5;4m027[0m[38;5;8m6db76[39m
|
||||
│ refactor: Switched from age to libsodium.
|
||||
○ [1m[38;5;5mtx[0m[38;5;8moxnuzl[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-12 15:36:10[39m [1m[38;5;4ma0[0m[38;5;8me2c995[39m
|
||||
│ docs: Updated TODOs.
|
||||
○ [1m[38;5;5mzv[0m[38;5;8mrkmqpk[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-12 15:01:50[39m [1m[38;5;4md0[0m[38;5;8mdc93ab[39m
|
||||
│ feat(odin): Migrated nushell-completion command to go.
|
||||
○ [1m[38;5;5mzp[0m[38;5;8mmvtmzx[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-12 14:50:42[39m [1m[38;5;4m91[0m[38;5;8mada61c[39m
|
||||
│ feat: Added tests.
|
||||
○ [1m[38;5;5mvs[0m[38;5;8mqmlvlq[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-12 14:17:56[39m [1m[38;5;4m9b[0m[38;5;8m395677[39m
|
||||
│ fix: Fixed the rest of the (tested) leaks.
|
||||
○ [1m[38;5;5mrw[0m[38;5;8mzttsll[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-12 13:37:09[39m [1m[38;5;4m43d[0m[38;5;8md8aca[39m
|
||||
│ perf: Improved writer performance.
|
||||
○ [1m[38;5;5mro[0m[38;5;8mvqumvz[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-12 13:25:50[39m [1m[38;5;4mdb[0m[38;5;8m1b863e[39m
|
||||
│ fix: fixing leaks.
|
||||
○ [1m[38;5;5mqu[0m[38;5;8mqsmwmx[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-12 10:45:43[39m [1m[38;5;4me9[0m[38;5;8m660501[39m
|
||||
│ fix: Added proper help text to all commands.
|
||||
○ [1m[38;5;5muu[0m[38;5;8mpootzn[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-12 10:28:41[39m [1m[38;5;4m76[0m[38;5;8m29dd2c[39m
|
||||
│ fix: Got rid of go fallback code.
|
||||
○ [1m[38;5;5msv[0m[38;5;8mkzoqxq[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-12 10:22:21[39m [1m[38;5;4m7c[0m[38;5;8m7ddf46[39m
|
||||
│ fix: Fixed memory leaks in `find_binary`.
|
||||
○ [1m[38;5;5myzv[0m[38;5;8mwlzvq[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-12 10:22:21[39m [1m[38;5;4ma1e94[0m[38;5;8m5a6[39m
|
||||
│ feat(odin): Ported init command.
|
||||
○ [1m[38;5;5myk[0m[38;5;8mlwuqrm[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-12 09:12:55[39m [1m[38;5;4m0a[0m[38;5;8m332adf[39m
|
||||
│ feat(odin): Ported scan command.
|
||||
○ [1m[38;5;5munkt[0m[38;5;8mymmr[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-12 08:27:14[39m [1m[38;5;4m4e1[0m[38;5;8me3590[39m
|
||||
│ feat(odin): port check command to odin.
|
||||
○ [1m[38;5;5moy[0m[38;5;8mllntvp[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-12 08:02:08[39m [1m[38;5;4m82[0m[38;5;8mbec68b[39m
|
||||
│ fix: Fixing AI oopsies.
|
||||
○ [1m[38;5;5ml[0m[38;5;8mowokuok[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-11 21:26:59[39m [1m[38;5;4m2c[0m[38;5;8mb6067a[39m
|
||||
│ feat(odin): ported edit-config command to odin.
|
||||
○ [1m[38;5;5mvl[0m[38;5;8mssoopk[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-11 21:25:11[39m [1m[38;5;4m36[0m[38;5;8m68df57[39m
|
||||
│ feat(odin): ported restore command to odin.
|
||||
○ [1m[38;5;5mtu[0m[38;5;8mnwtypr[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-11 21:21:59[39m [1m[38;5;4md2[0m[38;5;8m127e47[39m
|
||||
│ feat(odin): Ported remove command.
|
||||
○ [1m[38;5;5mnr[0m[38;5;8mnpskps[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-11 21:17:52[39m [1m[38;5;4mcb[0m[38;5;8m7db967[39m
|
||||
│ feat(odin): Added long text and --help flags.
|
||||
○ [1m[38;5;5msw[0m[38;5;8mwzkunx[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-11 21:14:11[39m [1m[38;5;4mc9[0m[38;5;8m2155a1[39m
|
||||
│ feat(odin): ported backup command.
|
||||
○ [1m[38;5;5mts[0m[38;5;8mnurnzr[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-11 21:05:39[39m [1m[38;5;4mb1[0m[38;5;8md24161[39m
|
||||
│ feat(odin): ported list command.
|
||||
○ [1m[38;5;5mvw[0m[38;5;8molkxsl[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-11 21:05:33[39m [1m[38;5;4m40[0m[38;5;8mf0b3c3[39m
|
||||
│ feat(odin): ported deps command, added utilities (features, tty, table).
|
||||
○ [1m[38;5;5mrqr[0m[38;5;8mrlqlk[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-11 20:34:53[39m [1m[38;5;4md8[0m[38;5;8m4e43d0[39m
|
||||
│ odin: scaffold project with CLI parser, version command, Go fallback
|
||||
○ [1m[38;5;5mznn[0m[38;5;8mskorn[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-11 20:08:27[39m [1m[38;5;4m28[0m[38;5;8mf96df4[39m
|
||||
│ feat: Started odin setup.
|
||||
│ ○ [1m[38;5;5mry[0m[38;5;8mkmnnwl[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-11 20:00:08[39m [38;5;5mzig[39m [1m[38;5;4m42[0m[38;5;8mc01a08[39m
|
||||
│ │ feat: init command.
|
||||
│ ○ [1m[38;5;5mzt[0m[38;5;8mntvnnw[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-09 11:01:15[39m [1m[38;5;4md3[0m[38;5;8meb4e84[39m
|
||||
│ │ fix: Fixed issue with buffer size.
|
||||
│ ○ [1m[38;5;5mpq[0m[38;5;8mzlpytk[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-09 09:50:38[39m [1m[38;5;4m6ac[0m[38;5;8md1f9d[39m
|
||||
│ │ refactor: Moved deps into `root.zig`.
|
||||
│ ○ [1m[38;5;5msl[0m[38;5;8mkwsoqy[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-06-09 09:41:13[39m [1m[38;5;4m68[0m[38;5;8m1931fb[39m
|
||||
│ │ feat: Added table viewer.
|
||||
│ ○ [1m[38;5;5mqk[0m[38;5;8mmlntsm[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-05-27 19:30:19[39m [1m[38;5;4macb[0m[38;5;8mda090[39m
|
||||
│ │ feat: list cmd.
|
||||
│ ○ [1m[38;5;5mvx[0m[38;5;8mnsyxqp[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-05-27 18:27:21[39m [1m[38;5;4mfc[0m[38;5;8m8474d7[39m
|
||||
│ │ feat: Restore db from file.
|
||||
│ ○ [1m[38;5;5muo[0m[38;5;8mowvkxx[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-05-03 12:45:43[39m [1m[38;5;4m8f[0m[38;5;8m2c2419[39m
|
||||
│ │ feat(config): Added data path.
|
||||
│ ○ [1m[38;5;5mqr[0m[38;5;8mkuztko[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-05-01 10:30:12[39m [1m[38;5;4m3e[0m[38;5;8m6c1752[39m
|
||||
│ │ feat: accept config in Db
|
||||
│ ○ [1m[38;5;5mvr[0m[38;5;8mxoyzlo[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-04-30 22:37:31[39m [1m[38;5;4mfd[0m[38;5;8m0f8bba[39m
|
||||
│ │ feat(age): accept multiple recipients.
|
||||
│ ○ [1m[38;5;5mrqu[0m[38;5;8mvonut[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-04-30 21:03:38[39m [1m[38;5;4m65[0m[38;5;8m571393[39m
|
||||
│ │ feat: Implemented basic db operation.
|
||||
│ ○ [1m[38;5;5mnw[0m[38;5;8mzoqvoq[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-04-29 16:35:38[39m [1m[38;5;4me5[0m[38;5;8m286527[39m
|
||||
│ │ feat: Created own age wrapper.
|
||||
│ ○ [1m[38;5;5mrl[0m[38;5;8mtyxtqr[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-04-28 17:49:04[39m [1m[38;5;4m02c[0m[38;5;8me5e46[39m
|
||||
│ │ feat: Added age-ffi.
|
||||
│ ○ [1m[38;5;5mkr[0m[38;5;8mzuylpu[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-04-26 17:29:37[39m [1m[38;5;4ma13[0m[38;5;8m264c8[39m
|
||||
│ │ feat: zig-sqlite.
|
||||
│ ○ [1m[38;5;5mnq[0m[38;5;8mlotzkk[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-04-24 11:19:31[39m [1m[38;5;4m79[0m[38;5;8m9d95a4[39m
|
||||
│ │ feat: added Config parsing.
|
||||
│ ○ [1m[38;5;5mnp[0m[38;5;8mvzptmw[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-04-23 16:53:47[39m [1m[38;5;4m21[0m[38;5;8m7bb413[39m
|
||||
│ │ feat(comma): Added help method.
|
||||
│ ○ [1m[38;5;5mrr[0m[38;5;8mlywnkm[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-04-21 19:42:02[39m [1m[38;5;4ma5[0m[38;5;8m47409e[39m
|
||||
│ │ docs: Added AI Disclaimer to README.md.
|
||||
│ ○ [1m[38;5;5mpl[0m[38;5;8mqqwlws[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-04-21 19:34:09[39m [1m[38;5;4m53[0m[38;5;8mcf22bc[39m
|
||||
│ │ feat: Added help output for commands.
|
||||
│ ○ [1m[38;5;5mznp[0m[38;5;8mvknpm[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-04-21 18:13:35[39m [1m[38;5;4mae[0m[38;5;8m445459[39m
|
||||
│ │ feat(comma): Added enum value for unknown commands.
|
||||
│ ○ [1m[38;5;5mzq[0m[38;5;8mpvlvms[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-04-21 18:02:58[39m [1m[38;5;4mbd[0m[38;5;8m2a5455[39m
|
||||
│ │ feat: Migrated `deps` command.
|
||||
│ ○ [1m[38;5;5mw[0m[38;5;8mqslwyqo[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-04-20 17:08:26[39m [1m[38;5;4m8a[0m[38;5;8m503ced[39m
|
||||
│ │ refactor: Broke comma into a separate package.
|
||||
│ ○ [1m[38;5;5mtr[0m[38;5;8mqurnkq[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-04-20 16:14:43[39m [1m[38;5;4m33[0m[38;5;8mb0063c[39m
|
||||
│ │ feat: Added command structure.
|
||||
│ │ ○ [1m[38;5;5msp[0m[38;5;8mllvvwm[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-04-20 10:15:48[39m [38;5;2menvr-zig@[39m [1m[38;5;4mac9[0m[38;5;8m4b33e[39m
|
||||
│ ├─╯ [38;5;2m(empty)[39m [38;5;2m(no description set)[39m
|
||||
│ ○ [1m[38;5;5mol[0m[38;5;8mwurpsw[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-04-18 16:28:30[39m [1m[38;5;4m43b[0m[38;5;8m03e0a[39m
|
||||
│ │ wip: feat: Migrated version command to zig.
|
||||
│ ○ [1m[38;5;5mm[0m[38;5;8mnqunpro[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-04-17 16:41:45[39m [1m[38;5;4mce[0m[38;5;8m135e9c[39m
|
||||
│ │ feat: Created zig wrapper.
|
||||
│ ○ [1m[38;5;5munkr[0m[38;5;8mrvon[39m [38;5;3mspencer.brower@proton.me[39m [38;5;6m2026-04-17 15:49:00[39m [1m[38;5;4m6a6[0m[38;5;8m11150[39m
|
||||
├─╯ feat: Added zig config.
|
||||
[1m[38;5;14m◆[0m [1m[38;5;5mps[0m[38;5;8mmotwus[39m [38;5;3m6729162+sbrow@users.noreply.github.com[39m [38;5;6m2026-01-12 15:42:05[39m [38;5;5mgo main[39m [38;5;5mv0.2.1[39m [1m[38;5;4mc6[0m[38;5;8md03088[39m
|
||||
│ chore(main): release 0.2.1
|
||||
~
|
||||
@@ -19,7 +19,7 @@ CRYPTO_SIGN_SECRETKEY_BYTES :: 64
|
||||
@(default_calling_convention = "c")
|
||||
foreign libsodium {
|
||||
sodium_init :: proc() -> c.int ---
|
||||
crypto_box_keypair :: proc(pk: [^]u8, sk: [^]u8) -> c.int ---
|
||||
// crypto_box_keypair :: proc(pk: [^]u8, sk: [^]u8) -> c.int ---
|
||||
crypto_box_easy :: proc(ciphertext: [^]u8, plaintext: [^]u8, mlen: c.ulong, nonce: [^]u8, pk: [^]u8, sk: [^]u8) -> c.int ---
|
||||
crypto_box_open_easy :: proc(plaintext: [^]u8, ciphertext: [^]u8, clen: c.ulong, nonce: [^]u8, pk: [^]u8, sk: [^]u8) -> c.int ---
|
||||
crypto_secretbox_easy :: proc(ciphertext: [^]u8, plaintext: [^]u8, mlen: c.ulong, nonce: [^]u8, key: [^]u8) -> c.int ---
|
||||
@@ -28,3 +28,4 @@ foreign libsodium {
|
||||
crypto_sign_ed25519_sk_to_curve25519 :: proc(curve25519_sk: [^]u8, ed25519_sk: [^]u8) -> c.int ---
|
||||
randombytes_buf :: proc(buf: [^]u8, size: c.ulong) ---
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package main
|
||||
|
||||
import "core:fmt"
|
||||
import "core:os"
|
||||
import "core:strings"
|
||||
import "core:testing"
|
||||
|
||||
TEST_KEY_DIR :: "/tmp/envr-test-keys"
|
||||
@@ -49,7 +47,11 @@ test_private_key_pub_matches_public_key :: proc(t: ^testing.T) {
|
||||
testing.expect(
|
||||
t,
|
||||
pub_from_pub == kp.Public,
|
||||
fmt.tprintf("public key mismatch:\n from .pub: %v\n from priv: %v", pub_from_pub, kp.Public),
|
||||
fmt.tprintf(
|
||||
"public key mismatch:\n from .pub: %v\n from priv: %v",
|
||||
pub_from_pub,
|
||||
kp.Public,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -67,3 +69,4 @@ test_read_wire_string :: proc(t: ^testing.T) {
|
||||
testing.expect(t, ok2, "expected second read to succeed")
|
||||
testing.expect(t, s2 == "", "expected empty string")
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import "core:encoding/json"
|
||||
import "core:fmt"
|
||||
import "core:io"
|
||||
import "core:strings"
|
||||
import "core:testing"
|
||||
|
||||
|
||||
1
version.txt
Normal file
1
version.txt
Normal file
@@ -0,0 +1 @@
|
||||
0.2.0
|
||||
Reference in New Issue
Block a user