13 Commits

19 changed files with 346 additions and 317 deletions

View File

@@ -2,59 +2,41 @@
1. Commands are still leaking.
2. **db.odin** — Inconsistencies in how struct vs sqlite are named.
2. Add color flag and support non colored output.
3. Add color flag and support non colored output.
3. Rewrite `write_command_help` to use text/tables
4. Use text/tables for command output
4. Generate md and man pages again.
5. Generate md and man pages again.
5. Json may be an expensive encoding for remotes. Confirm with spall, and use null terminated strings if necessary.
6. **db.odin:324-327** — Map iteration (`remote_set`) is non-deterministic. Same file can produce different JSON on each backup, causing spurious DB diffs. Sort remotes before storing.
6. Make sure official path separators are used when appropriate, rather than '/'.
7. Make sure official path separators are used when appropriate, rather than '/'.
7. Consistently ignore allocator errors
8. **cmd_restore.odin:20-30 & cmd_remove.odin:19-29** — Identical path-resolution block copy-pasted. `is_abs` guard is redundant since `filepath.abs` is a no-op on absolute paths. Extract a helper.
8. Check for prealloc opportunities. i.e. `make([dynamic]string)` -> `make([dynamic]string, 5)`.
9. **cmd_restore.odin:44**`os.mkdir_all` error silently discarded. Subsequent write failure will be confusing.
9. Add a text filter to the multi_select.
10. **config.odin:178**`search_paths` silently ignores `os.user_home_dir` error. If home is empty, `~` isn't expanded. Same class of bug as issue 3.
10. Add tests for untested commands.
11. **db.odin:115**`json.unmarshal_string` error not checked. Malformed JSON silently produces empty/partial data.
11. add --format -f flag to commands that draw tables.
12. **db.odin:352-353**`hex.encode` error ignored. `string(hex_bytes)` aliases the byte slice.
12. Replace `testing.expect` calls with `testing.expect_value` calls where appropriate.
13. **cmd_sync.odin:80, cmd_list.odin:33**`make([]string, 2)` for table rows never freed. Leaks per row. Defer to memory pass.
13. procedures should be ordered by use, main at the top, then in the order they are called from main.
14. Check for prealloc opportunities. i.e. `make([dynamic]string)` -> `make([dynamic]string, 5)`.
14. Shell completion
15. Add a text filter to the multi_select.
15. Bring back windows support / cross-compilation.
16. Add tests for untested commands.
16. Test all cmds / terminal branches.
17. 2 scan tests silently skip when fd isn't installed, tests pass without actually testing anything. These should use #assert to be sure that fd is in path.
17. Fix error messages to use fmt.eprintf (stderr) instead of fmt.printf (stdout)
18. add --format -f flag to commands that draw tables.
18. Pass allocator to findr?
19. Replace `testing.expect` calls with `testing.expect_value` calls where appropriate.
20. Change struct field names from PascalCase to snake_case.
21. procedures should be ordered by use, main at the top, then in the order they are called from main.
22. Shell completion
23. Bring back windows support / cross-compilation.
24. Test all cmds / terminal branches.
25. Replace `fmt.tprintf("/tmp/envr-test-...-%d", os.get_pid())` + `os.mkdir_all` in test files with `os.mkdir_temp` (race-free, honors `$TMPDIR`, matches `findr/test_env.odin` pattern).
26. Adopt `core:log` across `db.odin`, `crypto.odin`, `config.odin`, `ssh.odin` — replace ~30 scattered `fmt.printf("Error ...")` calls with leveled logging for consistent stderr routing and source locations.
27. "Encryption failed" in tests.
28. Pass allocator to findr?
19. Update `read_wire_string` to use a slice.
## Double-check AI output

View File

@@ -5,6 +5,7 @@ import "core:fmt"
import "core:io"
import "core:os"
import "core:strings"
import "core:text/table"
Command :: struct {
name: string,
@@ -253,56 +254,47 @@ at before, restore your backup with:
%senvr%s [command]
%sAvailable Commands:%s
`,
COLOR_HEADINGS,
ANSI_RESET,
COLOR_FLAGS,
ANSI_RESET,
COLOR_HEADINGS,
ANSI_RESET,
flush = false,
)
tbl: table.Table
table.init(&tbl, context.temp_allocator, context.temp_allocator)
table.padding(&tbl, 2, 0)
table.caption(&tbl, "Available Commands:")
for c in COMMANDS {
name_start := len(c.name)
fmt.wprintf(w, " %s%s", COLOR_COMMANDS, c.name, flush = false)
name := c.name
// TODO: Can we do better?
for a in c.aliases {
fmt.wprintf(w, ", %s", a, flush = false)
name_start += len(a) + 2
name = strings.join([]string{name, a}, ", ", tbl.format_allocator)
}
fmt.wprint(w, ANSI_RESET)
padding := 20 - name_start
if padding > 0 {
for _ in 0 ..< padding {
io.write_byte(w, ' ')
}
}
fmt.wprintf(w, " %s\n", c.short, flush = false)
table.row(&tbl, table.format(&tbl, "%s%s%s", COLOR_COMMANDS, name, ANSI_RESET), c.short)
}
write_borderless_table(w, &tbl)
table_reset(&tbl)
table.caption(&tbl, "Flags:")
table.row(&tbl, COLOR_FLAGS + "-h, --help" + ANSI_RESET, `show this documentation`)
table.row(
&tbl,
COLOR_FLAGS + "-c, --config-file" + ANSI_RESET + " <path>",
`config file (default "~/.envr/config.json")`,
)
write_borderless_table(w, &tbl)
fmt.wprintf(
w,
"\n" +
COLOR_HEADINGS +
"Flags:" +
ANSI_RESET +
"\n\n " +
COLOR_FLAGS +
"-h, --help" +
ANSI_RESET +
" help for envr\n" +
COLOR_FLAGS +
` -c, --config-file` +
ANSI_RESET +
` <path> config file (default "~/.envr/config.json")
Use "` +
COLOR_FLAGS +
"envr" +
ANSI_RESET +
` [command] --help" for more information about a command.
`,
`Use "%senvr%s [command] --help" for more information about a command.`,
COLOR_FLAGS,
ANSI_RESET,
flush = false,
)
}

View File

@@ -15,7 +15,10 @@ cmd_backup :: proc(cmd: ^Command) {
return
}
// TODO: allow new_env_file to accept allocator?
// TODO: Write a test that covers this leak
file, ok := new_env_file(path)
defer delete_envfile(&file)
if !ok {
return
}

View File

@@ -7,28 +7,21 @@ import "core:path/filepath"
// TODO: What happens if you pass a non existent path to cmd_check?
// TODO: UX could be improved, so "run envr add ." if file not exists.
cmd_check :: proc(cmd: ^Command) {
check_path: string
_check_path: string
if len(cmd.args) > 0 {
check_path = cmd.args[0]
_check_path = cmd.args[0]
} else {
cwd, cwd_err := os.get_working_directory(context.temp_allocator)
if cwd_err != nil {
fmt.wprintf(cmd.err, "Error getting current directory: %v\n", cwd_err, flush = false)
return
}
check_path = cwd
_check_path = cwd
}
abs_path: string
if filepath.is_abs(check_path) {
abs_path = check_path
} else {
resolved, abs_err := filepath.abs(check_path)
if abs_err != nil {
fmt.wprintf(cmd.err, "Error getting absolute path: %v\n", abs_err, flush = false)
return
}
abs_path = resolved
check_path, abs_err := filepath.abs(_check_path, context.temp_allocator)
if abs_err != nil {
fmt.wprintf(cmd.err, "Error getting absolute path: %v\n", abs_err, flush = false)
return
}
db, db_ok := db_open(cmd.config_path)
@@ -37,20 +30,20 @@ cmd_check :: proc(cmd: ^Command) {
}
defer db_close(&db)
is_dir := os.is_directory(abs_path)
is_dir := os.is_directory(check_path)
// TODO: set a reasonable default
files_in_path := make([dynamic]string, context.temp_allocator)
if is_dir {
scanned, scan_ok := scan_path(abs_path, db.cfg)
scanned, scan_ok := scan_path(check_path, db.cfg)
if !scan_ok {
fmt.wprintln(cmd.err, "Error scanning directory for .env files", flush = false)
return
}
files_in_path = scanned
} else {
append(&files_in_path, abs_path)
append(&files_in_path, check_path)
}
db_files, list_ok := db_list(&db)

View File

@@ -7,7 +7,7 @@ import "core:testing"
@(test)
test_find_unbacked_finds_missing :: proc(t: ^testing.T) {
local := []string{"/a/.env", "/b/.env", "/c/.env"}
db := []EnvFile{{Path = "/a/.env"}, {Path = "/b/.env"}}
db := []EnvFile{{path = "/a/.env"}, {path = "/b/.env"}}
result := find_unbacked(local, db[:])
testing.expect(t, len(result) == 1, fmt.tprintf("expected 1 unbacked, got %d", len(result)))
@@ -23,7 +23,7 @@ test_find_unbacked_finds_missing :: proc(t: ^testing.T) {
@(test)
test_find_unbacked_all_backed :: proc(t: ^testing.T) {
local := []string{"/a/.env", "/b/.env"}
db := []EnvFile{{Path = "/a/.env"}, {Path = "/b/.env"}}
db := []EnvFile{{path = "/a/.env"}, {path = "/b/.env"}}
result := find_unbacked(local, db[:])
testing.expect(t, len(result) == 0, fmt.tprintf("expected 0 unbacked, got %d", len(result)))
@@ -32,7 +32,7 @@ test_find_unbacked_all_backed :: proc(t: ^testing.T) {
@(test)
test_find_unbacked_no_local :: proc(t: ^testing.T) {
local: []string
db := []EnvFile{{Path = "/a/.env"}}
db := []EnvFile{{path = "/a/.env"}}
result := find_unbacked(local, db[:])
testing.expect(t, len(result) == 0, fmt.tprintf("expected 0 unbacked, got %d", len(result)))

View File

@@ -9,8 +9,8 @@ import "core:terminal"
import "core:text/table"
ListEntry :: struct {
Directory: string `json:"directory"`,
Path: string `json:"path"`,
dir: string `json:"directory"`,
path: string `json:"path"`,
}
// TODO: Support --format flag
@@ -40,10 +40,10 @@ cmd_list :: proc(cmd: ^Command) {
for row in rows {
dir_str := strings.concatenate(
{row.Dir, os.Path_Separator_String},
{row.dir, os.Path_Separator_String},
context.temp_allocator,
)
filename := filepath.base(row.Path)
filename := filepath.base(row.path)
table.row(&t, dir_str, filename)
}
@@ -53,12 +53,12 @@ cmd_list :: proc(cmd: ^Command) {
// TODO: Should we instead print full entries here?
entries: [dynamic]ListEntry
for row in rows {
filename := filepath.base(row.Path)
filename := filepath.base(row.path)
append(
&entries,
ListEntry {
Directory = strings.concatenate({row.Dir, "/"}, context.temp_allocator),
Path = filename,
dir = strings.concatenate({row.dir, "/"}, context.temp_allocator),
path = filename,
},
)
}

View File

@@ -16,17 +16,10 @@ cmd_remove :: proc(cmd: ^Command) {
return
}
// TODO: Is this the best way to do it?
abs_path: string
if filepath.is_abs(path) {
abs_path = path
} else {
resolved, abs_err := filepath.abs(path)
if abs_err != nil {
fmt.wprintf(cmd.err, "Error getting absolute path: %v\n", abs_err, flush = false)
return
}
abs_path = resolved
abs_path, abs_err := filepath.abs(path, context.temp_allocator)
if abs_err != nil {
fmt.wprintf(cmd.err, "Error getting absolute path: %v\n", abs_err, flush = false)
return
}
db, db_ok := db_open(cmd.config_path)

View File

@@ -16,18 +16,11 @@ cmd_restore :: proc(cmd: ^Command) {
fmt.wprintln(cmd.err, "Error: No path provided", flush = false)
return
}
abs_path, abs_err := filepath.abs(path, context.temp_allocator)
if abs_err != nil {
fmt.wprintf(cmd.err, "Error getting absolute path: %v\n", abs_err, flush = false)
// TODO: Is this the right way to handle this?
abs_path: string
if filepath.is_abs(path) {
abs_path = path
} else {
resolved, abs_err := filepath.abs(path)
if abs_err != nil {
fmt.wprintf(cmd.err, "Error getting absolute path: %v\n", abs_err, flush = false)
return
}
abs_path = resolved
return
}
db, db_ok := db_open(cmd.config_path)
@@ -41,15 +34,20 @@ cmd_restore :: proc(cmd: ^Command) {
return
}
dir := filepath.dir(file.Path)
os.mkdir_all(dir)
dir := filepath.dir(file.path)
if err := os.mkdir_all(dir); err != nil {
fmt.wprintf(cmd.err, "Failed to create directory: %v\n", err, flush = false)
write_err := os.write_entire_file(file.Path, file.contents)
if write_err != nil {
fmt.wprintf(cmd.err, "Error writing file: %v\n", write_err, flush = false)
return
}
fmt.wprintf(cmd.out, "Restored %s\n", file.Path, flush = false)
write_err := os.write_entire_file(file.path, file.contents)
if write_err != nil {
fmt.wprintf(cmd.err, "Error writing file: %v\n", write_err, flush = false)
return
}
fmt.wprintf(cmd.out, "Restored %s\n", file.path, flush = false)
}

View File

@@ -72,7 +72,11 @@ cmd_scan :: proc(cmd: ^Command) {
selected, result := multi_select("Select .env files to backup:", files[:])
defer delete(selected)
if result == .Cancel {
fmt.wprintln(cmd.out, ansi.CSI + ansi.FAINT + ansi.SGR + "Cancelled." + ANSI_RESET, flush = false)
fmt.wprintln(
cmd.out,
ansi.CSI + ansi.FAINT + ansi.SGR + "Cancelled." + ANSI_RESET,
flush = false,
)
return
}
@@ -81,7 +85,9 @@ cmd_scan :: proc(cmd: ^Command) {
if !selected[i] {
continue
}
// TODO: Test cover this leak
env_file, ok := new_env_file(files[i])
defer delete_envfile(&env_file)
if !ok {
fmt.wprintf(cmd.err, "Error reading %s\n", files[i], flush = false)
continue
@@ -96,12 +102,23 @@ cmd_scan :: proc(cmd: ^Command) {
if added_count > 0 {
fmt.wprintf(
cmd.out,
ansi.CSI + ansi.BOLD + ";" + ansi.FG_GREEN + ansi.SGR + "Successfully added %d file(s) to backup." + ANSI_RESET + "\n",
ansi.CSI +
ansi.BOLD +
";" +
ansi.FG_GREEN +
ansi.SGR +
"Successfully added %d file(s) to backup." +
ANSI_RESET +
"\n",
added_count,
flush = false,
)
} else {
fmt.wprintln(cmd.out, ansi.CSI + ansi.FAINT + ansi.SGR + "No files were added." + ANSI_RESET, flush = false)
fmt.wprintln(
cmd.out,
ansi.CSI + ansi.FAINT + ansi.SGR + "No files were added." + ANSI_RESET,
flush = false,
)
}
}

View File

@@ -7,8 +7,8 @@ import "core:terminal"
import "core:text/table"
SyncEntry :: struct {
Path: string `json:"path"`,
Status: string `json:"status"`,
path: string `json:"path"`,
status: string `json:"status"`,
}
// TODO: Check for quiet failures.
@@ -44,8 +44,8 @@ cmd_sync :: proc(cmd: ^Command) {
}
results[i] = SyncEntry {
Path = file.Path,
Status = status,
path = file.path,
status = status,
}
}
@@ -62,7 +62,7 @@ cmd_sync :: proc(cmd: ^Command) {
)
for res in results {
table.row(&t, res.Path, res.Status)
table.row(&t, res.path, res.status)
}
table.write_decorated_table(cmd.out, &t, decorations, ansi_aware_width)

View File

@@ -208,17 +208,19 @@ find_git_roots :: proc(
}
search_paths :: proc(cfg: Config, allocator := context.allocator) -> [dynamic]string {
// TODO: handle error
home, _ := os.user_home_dir(context.temp_allocator)
home, err := os.user_home_dir(context.temp_allocator)
if err != nil {
panic("Failed to find home directory")
}
paths, _ := new_clone(cfg.scan_config.include, allocator)
paths := new_clone(cfg.scan_config.include, allocator)
for &include in paths {
// TODO: Do we need to manually expand ~/ in odin?
expanded, _ := strings.replace(include, "~", home, 1, allocator)
if filepath.is_abs(expanded) {
include = expanded
} else {
// TODO: show errors?
resolved, err := filepath.abs(expanded, allocator)
if err == nil {
include = resolved

View File

@@ -71,8 +71,7 @@ test_new_config_exclude_patterns :: proc(t: ^testing.T) {
@(test)
test_save_load_config_roundtrip :: proc(t: ^testing.T) {
base := fmt.tprintf("/tmp/envr-test-cfg-rt-%d", os.get_pid())
os.mkdir_all(base)
base := test_temp_dir(t, "envr-test-cfg-rt-*")
defer os.remove_all(base)
cfgPath, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
@@ -105,8 +104,7 @@ test_load_config_missing :: proc(t: ^testing.T) {
@(test)
test_save_config_no_clobber :: proc(t: ^testing.T) {
base := fmt.tprintf("/tmp/envr-test-cfg-noclobber-%d", os.get_pid())
os.mkdir_all(base)
base := test_temp_dir(t, "envr-test-cfg-noclobber-*")
defer os.remove_all(base)
cfgPath, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
@@ -123,8 +121,7 @@ test_save_config_no_clobber :: proc(t: ^testing.T) {
@(test)
test_save_config_force_overwrites :: proc(t: ^testing.T) {
base := fmt.tprintf("/tmp/envr-test-cfg-force-%d", os.get_pid())
os.mkdir_all(base)
base := test_temp_dir(t, "envr-test-cfg-force-*")
defer os.remove_all(base)
cfgPath, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)

124
db.odin
View File

@@ -1,5 +1,6 @@
package main
import "base:runtime"
import "core:crypto/hash"
import "core:encoding/hex"
import "core:encoding/ini"
@@ -38,21 +39,21 @@ Db :: struct {
}
EnvFile :: struct {
Path: string,
Dir: string,
Remotes: [dynamic]string,
Sha256: string,
path: string,
dir: string,
remotes: [dynamic]string,
sha256: string,
contents: string,
}
@(deprecated = "call db_close to clean up EnvFiles")
delete_envfile :: proc(f: ^EnvFile) {
delete(f.Path)
for &remote in f.Remotes {
delete(f.path)
for &remote in f.remotes {
delete(remote)
}
delete(f.Remotes)
delete(f.Sha256)
delete(f.remotes)
delete(f.sha256)
delete(f.contents)
}
@@ -60,6 +61,18 @@ db_open :: proc(cfg_path: string) -> (db: Db, ok: bool) {
db = db_init() or_return
db.cfg = load_config(cfg_path, db_allocator(&db)) or_return
if len(db.cfg.keys) == 0 {
fmt.eprintf("Error: no SSH keys configured in %s\n", cfg_path)
db_close(&db)
return db, false
}
_, keys_ok := ssh_to_x25519(db.cfg.keys[:], context.temp_allocator)
if !keys_ok {
db_close(&db)
return db, false
}
// TODO: Use different allocators?
data_path := data_path(db.cfg.config_path, context.temp_allocator)
if os.exists(data_path) {
@@ -138,6 +151,8 @@ db_restore_from_encrypted :: proc(db: ^Db, data_path: string) -> bool {
return true
}
// db_close will fail silently if cfg.keys is empty. If you want to save the
// Db, be sure to use db_open rather than db_init
db_close :: proc(db: ^Db) {
allocator := db_allocator(db)
@@ -149,7 +164,7 @@ db_close :: proc(db: ^Db) {
mem.dynamic_arena_destroy(&db.arena)
}
if db.changed {
if db.changed && len(db.cfg.keys) > 0 {
rc := sqlite.exec(db.conn, "VACUUM", nil, nil, nil)
if rc != sqlite.OK {
fmt.printf("Error vacuuming database: %s\n", sqlite.errmsg(db.conn))
@@ -168,7 +183,8 @@ db_close :: proc(db: ^Db) {
// TODO: PAss allocator chain
encrypted, enc_ok := encrypt(sqlite_data, db.cfg.keys[:])
if !enc_ok {
fmt.println("Error: encryption failed")
fmt.eprintln("Database encryption failed")
return
}
@@ -219,17 +235,20 @@ db_list :: proc(db: ^Db) -> ([]EnvFile, bool) {
remotes_json := string(sqlite.column_text(stmt, 1))
remotes: [dynamic]string = ---
if len(remotes_json) > 0 {
json.unmarshal_string(remotes_json, &remotes, allocator = allocator)
err := json.unmarshal_string(remotes_json, &remotes, allocator = allocator)
if err != nil {
fmt.eprintf("Warning: malformed remotes JSON: %v\n", err)
}
}
path := clone_cstring(sqlite.column_text(stmt, 0), allocator)
append(
&results,
EnvFile {
Path = path,
Dir = filepath.dir(path),
Remotes = remotes,
Sha256 = clone_cstring(sqlite.column_text(stmt, 2), allocator),
path = path,
dir = filepath.dir(path),
remotes = remotes,
sha256 = clone_cstring(sqlite.column_text(stmt, 2), allocator),
contents = clone_cstring(sqlite.column_text(stmt, 3), allocator),
},
)
@@ -240,7 +259,7 @@ db_list :: proc(db: ^Db) -> ([]EnvFile, bool) {
// TODO: Should we use context.temp_allocator for proc scoped lifetimes?
db_insert :: proc(db: ^Db, file: EnvFile) -> bool {
remotes_json, marshal_err := json.marshal(file.Remotes, allocator = context.temp_allocator)
remotes_json, marshal_err := json.marshal(file.remotes, allocator = context.temp_allocator)
if marshal_err != nil {
fmt.printf("Error marshaling remotes: %v\n", marshal_err)
return false
@@ -258,7 +277,7 @@ db_insert :: proc(db: ^Db, file: EnvFile) -> bool {
defer sqlite.finalize(stmt)
// TODO: deal with elsewhere?
cpath := to_cstring(file.Path)
cpath := to_cstring(file.path)
defer delete(cpath)
rc = sqlite.bind_text(stmt, 1, cpath, -1, nil)
if rc != sqlite.OK {
@@ -274,7 +293,7 @@ db_insert :: proc(db: ^Db, file: EnvFile) -> bool {
return false
}
csha := to_cstring(file.Sha256)
csha := to_cstring(file.sha256)
defer delete(csha)
rc = sqlite.bind_text(stmt, 3, csha, -1, nil)
if rc != sqlite.OK {
@@ -301,7 +320,11 @@ db_insert :: proc(db: ^Db, file: EnvFile) -> bool {
}
// Result will be freed when `db_close` is called.
//
// Expects an absolute path
db_fetch :: proc(db: ^Db, path: string) -> (EnvFile, bool) {
assert(os.is_absolute_path(path))
sql: cstring = "SELECT path, remotes, sha256, contents FROM envr_env_files WHERE path = ?"
stmt: sqlite.Stmt
rc := sqlite.prepare_v2(db.conn, sql, -1, &stmt, nil)
@@ -333,16 +356,19 @@ db_fetch :: proc(db: ^Db, path: string) -> (EnvFile, bool) {
remotes_json := string(sqlite.column_text(stmt, 1))
remotes: [dynamic]string = ---
if len(remotes_json) > 0 {
json.unmarshal_string(remotes_json, &remotes, allocator = allocator)
err := json.unmarshal_string(remotes_json, &remotes, allocator = allocator)
if err != nil {
fmt.eprintf("Warning: malformed remotes JSON: %v\n", err)
}
}
file_path := clone_cstring(sqlite.column_text(stmt, 0), allocator)
return EnvFile {
Path = file_path,
Dir = filepath.dir(file_path),
Remotes = remotes,
Sha256 = clone_cstring(sqlite.column_text(stmt, 2), allocator),
path = file_path,
dir = filepath.dir(file_path),
remotes = remotes,
sha256 = clone_cstring(sqlite.column_text(stmt, 2), allocator),
contents = clone_cstring(sqlite.column_text(stmt, 3), allocator),
},
true
@@ -380,6 +406,7 @@ db_delete :: proc(db: ^Db, path: string) -> bool {
return true
}
// Caller is responsible for the returned memory
new_env_file :: proc(path: string) -> (EnvFile, bool) {
abs_path, abs_err := filepath.abs(path)
if abs_err != nil {
@@ -393,21 +420,18 @@ new_env_file :: proc(path: string) -> (EnvFile, bool) {
remotes := get_git_remotes(dir, context.allocator)
data, read_err := os.read_entire_file_from_path(abs_path, context.allocator)
defer delete(data)
if read_err != nil {
fmt.printf("Error reading file %s: %v\n", abs_path, read_err)
return EnvFile{}, false
}
digest := hash.hash_bytes(hash.Algorithm.SHA256, data, context.temp_allocator)
// TODO: Handle error
hex_bytes, _ := hex.encode(digest)
hex_bytes := hex.encode(digest, context.allocator)
return EnvFile {
Path = abs_path,
Dir = dir,
Remotes = remotes,
Sha256 = string(hex_bytes),
path = abs_path,
dir = dir,
remotes = remotes,
sha256 = string(hex_bytes),
contents = string(data),
},
true
@@ -417,9 +441,9 @@ new_env_file :: proc(path: string) -> (EnvFile, bool) {
db_sync :: proc(db: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
allocator := db_allocator(db)
result: SyncFlag = {}
old_path := f.Path
old_path := f.path
if !os.exists(f.Dir) {
if !os.exists(f.dir) {
moved, err := try_move_dir(db, f, allocator)
if !moved {
return {}, err
@@ -427,10 +451,10 @@ db_sync :: proc(db: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
result += {.DirUpdated}
}
if !os.exists(f.Path) {
write_err := os.write_entire_file(f.Path, f.contents)
if !os.exists(f.path) {
write_err := os.write_entire_file(f.path, f.contents)
if write_err != nil {
fmt.eprintf("db_sync: failed to write %s: %v\n", f.Path, write_err)
fmt.eprintf("db_sync: failed to write %s: %v\n", f.path, write_err)
return result, .WriteFailed
}
@@ -440,21 +464,17 @@ db_sync :: proc(db: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
return result + {.Restored}, .None
}
data, read_err := os.read_entire_file_from_path(f.Path, allocator)
data, read_err := os.read_entire_file_from_path(f.path, allocator)
if read_err != nil {
fmt.eprintf("db_sync: failed to read %s: %v\n", f.Path, read_err)
fmt.eprintf("db_sync: failed to read %s: %v\n", f.path, read_err)
return result, .ReadFailed
}
digest := hash.hash_bytes(hash.Algorithm.SHA256, data, context.temp_allocator)
hex_bytes, hex_err := hex.encode(digest, allocator)
if hex_err != nil {
fmt.eprintf("db_sync: failed to encode hash for %s: %v\n", f.Path, hex_err)
return result, .ReadFailed
}
hex_bytes := hex.encode(digest, allocator)
current_sha := string(hex_bytes)
if current_sha == f.Sha256 {
if current_sha == f.sha256 {
if !db_persist(db, f, old_path) {
return result, .DbFailed
}
@@ -462,7 +482,7 @@ db_sync :: proc(db: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
}
f.contents = string(data)
f.Sha256 = current_sha
f.sha256 = current_sha
if !db_persist(db, f, old_path) {
return result, .DbFailed
}
@@ -470,7 +490,7 @@ db_sync :: proc(db: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
}
db_persist :: proc(db: ^Db, f: ^EnvFile, old_path: string) -> bool {
if f.Path != old_path {
if f.path != old_path {
if !db_delete(db, old_path) {
return false
}
@@ -504,11 +524,11 @@ try_move_dir :: proc(db: ^Db, f: ^EnvFile, allocator: mem.Allocator) -> (bool, S
case 0:
return false, .DirMissing
case 1:
f.Dir, _ = strings.clone(matched_dir, allocator)
base := filepath.base(f.Path)
new_path, _ := filepath.join({f.Dir, base}, allocator)
f.Path = new_path
f.Remotes = get_git_remotes(f.Dir, allocator)
f.dir, _ = strings.clone(matched_dir, allocator)
base := filepath.base(f.path)
new_path, _ := filepath.join({f.dir, base}, allocator)
f.path = new_path
f.remotes = get_git_remotes(f.dir, allocator)
return true, .None
case:
return false, .MultipleDirs
@@ -516,7 +536,7 @@ try_move_dir :: proc(db: ^Db, f: ^EnvFile, allocator: mem.Allocator) -> (bool, S
}
shares_remote :: proc(f: ^EnvFile, remotes: []string) -> bool {
for r1 in f.Remotes {
for r1 in f.remotes {
for r2 in remotes {
if r1 == r2 {
return true

View File

@@ -11,6 +11,14 @@ import "sqlite"
FIXTURES :: "fixtures"
test_temp_dir :: proc(t: ^testing.T, prefix: string) -> string {
dir, err := os.mkdir_temp("", prefix, context.temp_allocator)
if err != nil {
testing.fail_now(t, fmt.tprintf("Failed to create temp dir: %v", err))
}
return dir
}
fixture_key :: proc() -> SshKeyPair {
priv, _ := strings.concatenate(
[]string{FIXTURES, "/keys/insecure-test-key"},
@@ -111,13 +119,14 @@ test_encrypt_write_read_decrypt :: proc(t: ^testing.T) {
}
defer delete(encrypted)
tmp_enc_path := fmt.tprintf("/tmp/envr-test-ewrd-%d.envr", os.get_pid())
ewrd_dir := test_temp_dir(t, "envr-test-ewrd-*")
defer os.remove_all(ewrd_dir)
tmp_enc_path, _ := filepath.join([]string{ewrd_dir, "data.envr"}, context.temp_allocator)
write_err := os.write_entire_file(tmp_enc_path, encrypted)
testing.expectf(t, write_err == nil, "failed to write encrypted file: %v", write_err)
if write_err != nil {
return
}
defer os.remove(tmp_enc_path)
read_back, rb_err := os.read_entire_file_from_path(tmp_enc_path, context.allocator)
testing.expectf(t, rb_err == nil, "failed to read back encrypted file: %v", rb_err)
@@ -223,11 +232,15 @@ test_full_db_cycle :: proc(t: ^testing.T) {
}
defer delete(encrypted)
envr_dir_path := fmt.tprintf("/tmp/envr-test-cycle-%d/.envr", os.get_pid())
os.mkdir_all(envr_dir_path)
cycle_dir := test_temp_dir(t, "envr-test-cycle-*")
defer os.remove_all(cycle_dir)
envr_dir_path, _ := filepath.join([]string{cycle_dir, ".envr"}, context.temp_allocator)
{
err := os.mkdir_all(envr_dir_path)
testing.expect_value(t, err, nil)
}
data_path, _ := filepath.join([]string{envr_dir_path, "data.envr"})
defer delete(data_path)
data_path, _ := filepath.join([]string{envr_dir_path, "data.envr"}, context.temp_allocator)
write_err := os.write_entire_file(data_path, encrypted)
testing.expectf(t, write_err == nil, "failed to write data.envr: %v", write_err)
if write_err != nil {

View File

@@ -13,14 +13,14 @@ import "sqlite"
make_test_env_file :: proc(path, sha, contents: string, remotes: []string = {}) -> EnvFile {
f := EnvFile {
Path = path,
Dir = "",
Sha256 = sha,
path = path,
dir = "",
sha256 = sha,
contents = contents,
Remotes = make([dynamic]string, 0, len(remotes), context.temp_allocator),
remotes = make([dynamic]string, 0, len(remotes), context.temp_allocator),
}
for r in remotes {
append(&f.Remotes, r)
append(&f.remotes, r)
}
return f
}
@@ -37,7 +37,7 @@ test_db_insert_and_fetch :: proc(t: ^testing.T) {
contents := "SECRET=value"
f := make_test_env_file(path, sha, contents, []string{"git@github.com:user/repo.git"})
defer delete(f.Remotes)
defer delete(f.remotes)
testing.expect(t, db_insert(&db, f), "insert should succeed")
@@ -46,11 +46,11 @@ test_db_insert_and_fetch :: proc(t: ^testing.T) {
testing.expect(t, fetch_ok, "fetch should succeed")
if !fetch_ok do return
testing.expect_value(t, fetched.Path, path)
testing.expect_value(t, fetched.Sha256, sha)
testing.expect_value(t, fetched.path, path)
testing.expect_value(t, fetched.sha256, sha)
testing.expect_value(t, fetched.contents, contents)
testing.expect_value(t, len(fetched.Remotes), 1)
testing.expect_value(t, fetched.Remotes[0], "git@github.com:user/repo.git")
testing.expect_value(t, len(fetched.remotes), 1)
testing.expect_value(t, fetched.remotes[0], "git@github.com:user/repo.git")
}
@(test)
@@ -71,11 +71,11 @@ test_db_insert_or_replace :: proc(t: ^testing.T) {
testing.expect(t, ok, "failed to create test db")
f1 := make_test_env_file("/project/.env", "sha1", "KEY=old")
defer delete(f1.Remotes)
defer delete(f1.remotes)
testing.expect(t, db_insert(&db, f1), "first insert should succeed")
f2 := make_test_env_file("/project/.env", "sha2", "KEY=new")
defer delete(f2.Remotes)
defer delete(f2.remotes)
testing.expect(t, db_insert(&db, f2), "second insert should succeed")
results, list_ok := db_list(&db)
@@ -89,7 +89,7 @@ test_db_insert_or_replace :: proc(t: ^testing.T) {
// defer delete_envfile(&fetched)
testing.expect_value(t, fetched.contents, "KEY=new")
testing.expect_value(t, fetched.Sha256, "sha2")
testing.expect_value(t, fetched.sha256, "sha2")
}
@(test)
@@ -100,7 +100,7 @@ test_db_delete_existing :: proc(t: ^testing.T) {
defer db_close(&db)
f := make_test_env_file("/project/.env", "sha", "KEY=val")
defer delete(f.Remotes)
defer delete(f.remotes)
db_insert(&db, f)
testing.expect(t, db_delete(&db, "/project/.env"), "delete should return true")
@@ -126,9 +126,9 @@ test_db_list_multiple :: proc(t: ^testing.T) {
defer db_close(&db)
f1 := make_test_env_file("/proj1/.env", "sha1", "A=1", []string{"git@github.com:a/repo.git"})
defer delete(f1.Remotes)
defer delete(f1.remotes)
f2 := make_test_env_file("/proj2/.env", "sha2", "B=2", []string{"git@github.com:b/repo.git"})
defer delete(f2.Remotes)
defer delete(f2.remotes)
f3 := make_test_env_file("/proj3/.env", "sha3", "C=3")
db_insert(&db, f1)
@@ -162,7 +162,7 @@ test_db_insert_sets_changed :: proc(t: ^testing.T) {
testing.expect(t, !db.changed, "changed should start false")
f := make_test_env_file("/project/.env", "sha", "KEY=val")
defer delete(f.Remotes)
defer delete(f.remotes)
db_insert(&db, f)
testing.expect(t, db.changed, "changed should be true after insert")
@@ -176,7 +176,7 @@ test_db_delete_sets_changed :: proc(t: ^testing.T) {
defer db_close(&db)
f := make_test_env_file("/project/.env", "sha", "KEY=val")
defer delete(f.Remotes)
defer delete(f.remotes)
db_insert(&db, f)
db.changed = false
@@ -192,7 +192,7 @@ test_db_serialize :: proc(t: ^testing.T) {
defer db_close(&db)
f := make_test_env_file("/project/.env", "sha", "KEY=val")
defer delete(f.Remotes)
defer delete(f.remotes)
db_insert(&db, f)
sz: i64
@@ -207,10 +207,10 @@ test_db_serialize :: proc(t: ^testing.T) {
@(test)
test_shares_remote_overlap :: proc(t: ^testing.T) {
f := EnvFile {
Remotes = make([dynamic]string, 2, context.temp_allocator),
remotes = make([dynamic]string, 2, context.temp_allocator),
}
append(&f.Remotes, "git@github.com:user/repo.git")
append(&f.Remotes, "git@gitlab.com:user/repo.git")
append(&f.remotes, "git@github.com:user/repo.git")
append(&f.remotes, "git@gitlab.com:user/repo.git")
remotes := []string{"git@github.com:user/repo.git"}
testing.expect(t, shares_remote(&f, remotes), "should share remote")
@@ -219,9 +219,9 @@ test_shares_remote_overlap :: proc(t: ^testing.T) {
@(test)
test_shares_remote_no_overlap :: proc(t: ^testing.T) {
f := EnvFile {
Remotes = make([dynamic]string, 1, context.temp_allocator),
remotes = make([dynamic]string, 1, context.temp_allocator),
}
append(&f.Remotes, "git@github.com:user/repo.git")
append(&f.remotes, "git@github.com:user/repo.git")
remotes := []string{"git@github.com:other/repo.git"}
testing.expect(t, !shares_remote(&f, remotes), "should not share remote")
@@ -230,7 +230,7 @@ test_shares_remote_no_overlap :: proc(t: ^testing.T) {
@(test)
test_shares_remote_empty_file_remotes :: proc(t: ^testing.T) {
f := EnvFile {
Remotes = make([dynamic]string, 0, context.temp_allocator),
remotes = make([dynamic]string, 0, context.temp_allocator),
}
remotes := []string{"git@github.com:user/repo.git"}
@@ -240,9 +240,9 @@ test_shares_remote_empty_file_remotes :: proc(t: ^testing.T) {
@(test)
test_shares_remote_empty_check_remotes :: proc(t: ^testing.T) {
f := EnvFile {
Remotes = make([dynamic]string, 1, context.temp_allocator),
remotes = make([dynamic]string, 1, context.temp_allocator),
}
append(&f.Remotes, "git@github.com:user/repo.git")
append(&f.remotes, "git@github.com:user/repo.git")
remotes: []string
testing.expect(t, !shares_remote(&f, remotes), "empty check remotes should not share")
@@ -251,7 +251,7 @@ test_shares_remote_empty_check_remotes :: proc(t: ^testing.T) {
@(test)
test_shares_remote_both_empty :: proc(t: ^testing.T) {
f := EnvFile {
Remotes = make([dynamic]string, 0),
remotes = make([dynamic]string, 0),
}
remotes: []string
@@ -267,8 +267,7 @@ delete_remotes :: proc(remotes: [dynamic]string) {
@(test)
test_get_git_remotes_single :: proc(t: ^testing.T) {
base := fmt.tprintf("/tmp/envr-test-remotes-%d", os.get_pid())
os.mkdir_all(base)
base := test_temp_dir(t, "envr-test-remotes-*")
defer os.remove_all(base)
git_dir := fmt.tprintf("%s/.git", base)
@@ -288,8 +287,7 @@ test_get_git_remotes_single :: proc(t: ^testing.T) {
@(test)
test_get_git_remotes_multiple :: proc(t: ^testing.T) {
base := fmt.tprintf("/tmp/envr-test-remotes-multi-%d", os.get_pid())
os.mkdir_all(base)
base := test_temp_dir(t, "envr-test-remotes-multi-*")
defer os.remove_all(base)
git_dir := fmt.tprintf("%s/.git", base)
@@ -307,8 +305,7 @@ test_get_git_remotes_multiple :: proc(t: ^testing.T) {
@(test)
test_get_git_remotes_no_config :: proc(t: ^testing.T) {
base := fmt.tprintf("/tmp/envr-test-remotes-none-%d", os.get_pid())
os.mkdir_all(base)
base := test_temp_dir(t, "envr-test-remotes-none-*")
defer os.remove_all(base)
remotes := get_git_remotes(base, context.temp_allocator)
@@ -318,8 +315,7 @@ test_get_git_remotes_no_config :: proc(t: ^testing.T) {
@(test)
test_get_git_remotes_no_remotes :: proc(t: ^testing.T) {
base := fmt.tprintf("/tmp/envr-test-remotes-empty-%d", os.get_pid())
os.mkdir_all(base)
base := test_temp_dir(t, "envr-test-remotes-empty-*")
defer os.remove_all(base)
git_dir := fmt.tprintf("%s/.git", base)
@@ -337,8 +333,7 @@ test_get_git_remotes_no_remotes :: proc(t: ^testing.T) {
@(test)
test_new_env_file :: proc(t: ^testing.T) {
base := fmt.tprintf("/tmp/envr-test-envfile-%d", os.get_pid())
os.mkdir_all(base)
base := test_temp_dir(t, "envr-test-envfile-*")
defer os.remove_all(base)
env_path := fmt.tprintf("%s/.env", base)
@@ -348,14 +343,15 @@ test_new_env_file :: proc(t: ^testing.T) {
file, ok := new_env_file(env_path)
testing.expect(t, ok, "new_env_file should succeed")
if !ok do return
defer delete(file.Remotes)
defer delete(file.Sha256)
defer delete(file.Path)
defer delete(file.contents)
defer delete(file.remotes)
defer delete(file.sha256)
defer delete(file.path)
testing.expect(t, filepath.is_abs(file.Path), "path should be absolute")
testing.expect(t, strings.has_suffix(file.Path, "/.env"), "path should end with /.env")
testing.expect(t, filepath.is_abs(file.path), "path should be absolute")
testing.expect(t, strings.has_suffix(file.path, "/.env"), "path should end with /.env")
testing.expect(t, file.contents == "SECRET=value\n", "contents mismatch")
testing.expect(t, len(file.Sha256) == 64, "sha256 should be 64 hex chars")
testing.expect(t, len(file.sha256) == 64, "sha256 should be 64 hex chars")
}
@(test)
@@ -366,8 +362,7 @@ test_new_env_file_missing :: proc(t: ^testing.T) {
@(test)
test_closing_db_has_no_leaks :: proc(t: ^testing.T) {
base := fmt.tprintf("/tmp/envr-test-leak-%d", os.get_pid())
os.mkdir_all(base)
base := test_temp_dir(t, "envr-test-leak-*")
defer os.remove_all(base)
cfg_path, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
@@ -386,8 +381,7 @@ test_closing_db_has_no_leaks :: proc(t: ^testing.T) {
@(test)
test_open_existing_db_has_no_leaks :: proc(t: ^testing.T) {
base := fmt.tprintf("/tmp/envr-test-leak-existing-%d", os.get_pid())
os.mkdir_all(base)
base := test_temp_dir(t, "envr-test-leak-existing-*")
defer os.remove_all(base)
cfg_path, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
@@ -409,7 +403,7 @@ test_open_existing_db_has_no_leaks :: proc(t: ^testing.T) {
"SECRET=value",
[]string{"git@github.com:user/repo.git"},
)
defer delete(f.Remotes)
defer delete(f.remotes)
testing.expect(t, db_insert(&db, f), "insert should succeed")
db_close(&db)
@@ -422,8 +416,7 @@ test_open_existing_db_has_no_leaks :: proc(t: ^testing.T) {
@(test)
test_db_sync_noop :: proc(t: ^testing.T) {
base := fmt.tprintf("/tmp/envr-test-sync-noop-%d", os.get_pid())
os.mkdir_all(base)
base := test_temp_dir(t, "envr-test-sync-noop-*")
defer os.remove_all(base)
env_path := fmt.tprintf("%s/.env", base)
@@ -436,7 +429,7 @@ test_db_sync_noop :: proc(t: ^testing.T) {
transmute([]u8)content,
context.temp_allocator,
)
hex_bytes, _ := hex.encode(digest, context.temp_allocator)
hex_bytes := hex.encode(digest, context.temp_allocator)
sha := string(hex_bytes)
db, ok := db_init()
@@ -444,7 +437,7 @@ test_db_sync_noop :: proc(t: ^testing.T) {
defer db_close(&db)
f := make_test_env_file(env_path, sha, content)
f.Dir = base
f.dir = base
db_insert(&db, f)
result, sync_err := db_sync(&db, &f)
@@ -454,8 +447,7 @@ test_db_sync_noop :: proc(t: ^testing.T) {
@(test)
test_db_sync_backed_up :: proc(t: ^testing.T) {
base := fmt.tprintf("/tmp/envr-test-sync-backup-%d", os.get_pid())
os.mkdir_all(base)
base := test_temp_dir(t, "envr-test-sync-backup-*")
defer os.remove_all(base)
env_path := fmt.tprintf("%s/.env", base)
@@ -468,7 +460,7 @@ test_db_sync_backed_up :: proc(t: ^testing.T) {
defer db_close(&db)
f := make_test_env_file(env_path, "old_sha", "KEY=original")
f.Dir = base
f.dir = base
db_insert(&db, f)
result, sync_err := db_sync(&db, &f)
@@ -478,8 +470,7 @@ test_db_sync_backed_up :: proc(t: ^testing.T) {
@(test)
test_db_sync_restored :: proc(t: ^testing.T) {
base := fmt.tprintf("/tmp/envr-test-sync-restore-%d", os.get_pid())
os.mkdir_all(base)
base := test_temp_dir(t, "envr-test-sync-restore-*")
defer os.remove_all(base)
env_path := fmt.tprintf("%s/.env", base)
@@ -489,8 +480,8 @@ test_db_sync_restored :: proc(t: ^testing.T) {
defer db_close(&db)
f := make_test_env_file(env_path, "some_sha", "SECRET=value")
f.Dir = base
defer delete(f.Remotes)
f.dir = base
defer delete(f.remotes)
db_insert(&db, f)
result, err := db_sync(&db, &f)
@@ -520,7 +511,7 @@ test_db_sync_dir_missing :: proc(t: ^testing.T) {
@(test)
test_db_sync_moved :: proc(t: ^testing.T) {
base := fmt.tprintf("/tmp/envr-test-sync-moved-%d", os.get_pid())
base := test_temp_dir(t, "envr-test-sync-moved-*")
search_root := fmt.tprintf("%s/search", base)
repo_dir := fmt.tprintf("%s/myproject", search_root)
git_dir := fmt.tprintf("%s/.git", repo_dir)
@@ -555,8 +546,8 @@ test_db_sync_moved :: proc(t: ^testing.T) {
testing.expect(t, .Restored in result, "should have Restored flag")
expected_path := fmt.tprintf("%s/.env", repo_dir)
testing.expect_value(t, f.Path, expected_path)
testing.expect_value(t, f.Dir, repo_dir)
testing.expect_value(t, f.path, expected_path)
testing.expect_value(t, f.dir, repo_dir)
_, old_exists := db_fetch(&db, "/old/nonexistent/path/.env")
testing.expect(t, !old_exists, "old path should be deleted from db")

View File

@@ -19,7 +19,7 @@ scan_path :: proc(search_path: string, cfg: Config) -> (paths: [dynamic]string,
find_unbacked :: proc(local_files: []string, db_files: []EnvFile) -> []string {
backed_set := make(map[string]bool, len(db_files), context.temp_allocator)
for file in db_files {
backed_set[file.Path] = true
backed_set[file.path] = true
}
unbacked := make([dynamic]string, 0, len(db_files) / 2, context.temp_allocator)

View File

@@ -8,8 +8,7 @@ import "core:testing"
@(test)
test_scan_path_finds_gitignored_env_files :: proc(t: ^testing.T) {
base := fmt.tprintf("/tmp/envr-scan-test-%d", os.get_pid())
os.mkdir_all(base)
base := test_temp_dir(t, "envr-scan-test-*")
defer os.remove_all(base)
git_init := os.Process_Desc {
@@ -19,20 +18,23 @@ test_scan_path_finds_gitignored_env_files :: proc(t: ^testing.T) {
stderr = os.stderr,
}
p, err := os.process_start(git_init)
if err != nil {
return
}
_, wait_err := os.process_wait(p)
if wait_err != nil {
return
}
testing.expectf(t, err == nil, "Failed to run git: %v", err)
if err != nil do return
state, wait_err := os.process_wait(p)
testing.expectf(t, wait_err == nil, "Failed to wait: %v", wait_err)
if wait_err != nil do return
testing.expect(t, state.success, "command should succeed")
gitignore_path := fmt.tprintf("%s/.gitignore", base)
_ = os.write_entire_file(gitignore_path, ".env*\n")
err = os.write_entire_file(gitignore_path, ".env*\n")
testing.expectf(t, err == nil, "Failed: %v", err)
_ = os.write_entire_file(fmt.tprintf("%s/.env", base), "SECRET=1")
_ = os.write_entire_file(fmt.tprintf("%s/.env.testing", base), "TEST=1")
_ = os.write_entire_file(fmt.tprintf("%s/config.yaml", base), "key: value")
err = os.write_entire_file(fmt.tprintf("%s/.env", base), "SECRET=1")
testing.expectf(t, err == nil, "Failed: %v", err)
err = os.write_entire_file(fmt.tprintf("%s/.env.testing", base), "TEST=1")
testing.expectf(t, err == nil, "Failed: %v", err)
err = os.write_entire_file(fmt.tprintf("%s/config.yaml", base), "key: value")
testing.expectf(t, err == nil, "Failed: %v", err)
cfg := Config {
scan_config = ScanConfig{matcher = "\\.env"},
@@ -71,8 +73,7 @@ test_scan_path_finds_gitignored_env_files :: proc(t: ^testing.T) {
@(test)
test_scan_path_empty_dir :: proc(t: ^testing.T) {
base := fmt.tprintf("/tmp/envr-scan-empty-%d", os.get_pid())
os.mkdir_all(base)
base := test_temp_dir(t, "envr-scan-empty-*")
defer os.remove_all(base)
cfg := Config {

View File

@@ -2,7 +2,9 @@ package main
import "base:runtime"
import "core:encoding/base64"
import "core:encoding/endian"
import "core:fmt"
import "core:mem"
import "core:os"
import "core:strings"
@@ -44,9 +46,7 @@ parse_ssh_public_key :: proc(pub_path: string) -> (pub: [32]u8, ok: bool) {
return
}
for i in 0 ..< 32 {
pub[i] = pk_data[i]
}
mem.copy_non_overlapping(&pub[0], raw_data(pk_data), 32)
ok = true
return
@@ -86,15 +86,10 @@ parse_ssh_private_key :: proc(priv_path: string) -> (kp: Ed25519Keypair, ok: boo
return
}
magic := "openssh-key-v1\x00"
if len(decoded) < len(magic) {
magic :: "openssh-key-v1\x00"
if !strings.has_prefix(string(decoded), magic) {
return
}
for i in 0 ..< len(magic) {
if decoded[i] != u8(magic[i]) {
return
}
}
offset := len(magic)
@@ -116,11 +111,8 @@ parse_ssh_private_key :: proc(priv_path: string) -> (kp: Ed25519Keypair, ok: boo
if offset + 4 > len(decoded) {
return
}
num_keys :=
u32(decoded[offset]) << 24 |
u32(decoded[offset + 1]) << 16 |
u32(decoded[offset + 2]) << 8 |
u32(decoded[offset + 3])
num_keys := endian.get_u32(decoded[offset:offset + 4], .Big) or_return
offset += 4
if num_keys != 1 {
@@ -141,17 +133,16 @@ parse_ssh_private_key :: proc(priv_path: string) -> (kp: Ed25519Keypair, ok: boo
if inner_offset + 8 > len(priv_blob) {
return
}
check1 :=
u32(priv_blob[inner_offset]) << 24 |
u32(priv_blob[inner_offset + 1]) << 16 |
u32(priv_blob[inner_offset + 2]) << 8 |
u32(priv_blob[inner_offset + 3])
check1 := endian.get_u32(
transmute([]u8)(priv_blob)[inner_offset:inner_offset + 4],
.Big,
) or_return
inner_offset += 4
check2 :=
u32(priv_blob[inner_offset]) << 24 |
u32(priv_blob[inner_offset + 1]) << 16 |
u32(priv_blob[inner_offset + 2]) << 8 |
u32(priv_blob[inner_offset + 3])
check2 := endian.get_u32(
transmute([]u8)(priv_blob)[inner_offset:inner_offset + 4],
.Big,
) or_return
inner_offset += 4
if check1 != check2 {
@@ -167,17 +158,14 @@ parse_ssh_private_key :: proc(priv_path: string) -> (kp: Ed25519Keypair, ok: boo
if !pub_ok || len(pub_wire) != 32 {
return
}
for i in 0 ..< 32 {
kp.Public[i] = pub_wire[i]
}
mem.copy_non_overlapping(&kp.Public[0], raw_data(pub_wire), 32)
priv_wire, priv_ok := read_wire_string(transmute([]u8)priv_blob, &inner_offset)
if !priv_ok || len(priv_wire) != 64 {
return
}
for i in 0 ..< 32 {
kp.Private[i] = priv_wire[i]
}
mem.copy_non_overlapping(&kp.Private[0], raw_data(priv_wire), 32)
ok = true
return
@@ -198,11 +186,7 @@ read_wire_string :: proc(data: []u8, offset: ^int) -> (s: string, ok: bool) {
if offset^ + 4 > len(data) {
return
}
length :=
u32(data[offset^]) << 24 |
u32(data[offset^ + 1]) << 16 |
u32(data[offset^ + 2]) << 8 |
u32(data[offset^ + 3])
length := endian.get_u32(data[offset^:offset^ + 4], .Big) or_return
offset^ += 4
if offset^ + int(length) > len(data) {

View File

@@ -1,5 +1,7 @@
package main
import "core:fmt"
import "core:io"
import "core:text/table"
import "core:unicode/utf8"
@@ -34,3 +36,44 @@ ansi_aware_width :: proc(str: string) -> int {
return width
}
write_borderless_table :: proc(w: io.Writer, t: ^table.Table) {
table.build(t, ansi_aware_width)
write_table_separator :: proc(w: io.Writer, tbl: ^table.Table) {
io.write_byte(w, '\n')
}
if t.caption != "" {
table.write_text_align(
w,
fmt.tprintf("%s%s%s", COLOR_HEADINGS, t.caption, ANSI_RESET),
.Left,
0, //t.lpad,
0, //t.rpad,
t.tblw + t.nr_cols - 1 - ansi_aware_width(t.caption) - t.lpad - t.rpad,
)
io.write_byte(w, '\n')
}
write_table_separator(w, t)
for row in 0 ..< t.nr_rows {
for col in 0 ..< t.nr_cols {
table.write_table_cell(w, t, row, col)
}
io.write_byte(w, '\n')
if t.has_header_row && row == table.header_row(t) {
write_table_separator(w, t)
}
}
write_table_separator(w, t)
}
table_reset :: proc(t: ^table.Table) {
clear(&t.cells)
clear(&t.colw)
t.caption = ""
t.tblw = 0
t.nr_cols = 0
t.nr_rows = 0
}