mirror of
https://github.com/sbrow/envr.git
synced 2026-06-27 10:38:33 -04:00
Compare commits
6 Commits
32ce44082f
...
e74fc4f35a
| Author | SHA1 | Date | |
|---|---|---|---|
| e74fc4f35a | |||
| 6fa68d10b1 | |||
| 13e9495642 | |||
| ad3de74e35 | |||
| 0b5bf4db73 | |||
| 96b3d6340a |
30
TODOS.md
30
TODOS.md
@@ -1,6 +1,6 @@
|
||||
# TODOs
|
||||
|
||||
1. Commands are still leaking.
|
||||
1. Commands are still leaking. (Write tests for everything first)
|
||||
|
||||
2. Add color flag and support non colored output.
|
||||
|
||||
@@ -8,33 +8,27 @@
|
||||
|
||||
4. 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.
|
||||
5. Check for prealloc opportunities. i.e. `make([dynamic]string)` -> `make([dynamic]string, 5)`.
|
||||
|
||||
6. Consistently ignore allocator errors
|
||||
6. Add a text filter to the multi_select.
|
||||
|
||||
7. Check for prealloc opportunities. i.e. `make([dynamic]string)` -> `make([dynamic]string, 5)`.
|
||||
7. Add tests for untested commands.
|
||||
|
||||
8. Add a text filter to the multi_select.
|
||||
8. add --format -f flag to commands that draw tables.
|
||||
|
||||
9. Add tests for untested commands.
|
||||
9. procedures should be ordered by use, main at the top, then in the order they are called from main.
|
||||
|
||||
10. add --format -f flag to commands that draw tables.
|
||||
10. Shell completion
|
||||
|
||||
11. Replace `testing.expect` calls with `testing.expect_value` calls where appropriate.
|
||||
11. Bring back windows support / cross-compilation.
|
||||
|
||||
12. procedures should be ordered by use, main at the top, then in the order they are called from main.
|
||||
12. Test all cmds / terminal branches.
|
||||
|
||||
13. Shell completion
|
||||
13. Pass allocator to findr?
|
||||
|
||||
14. Bring back windows support / cross-compilation.
|
||||
14. Update `read_wire_string` to use a slice.
|
||||
|
||||
15. Test all cmds / terminal branches.
|
||||
|
||||
16. Fix error messages to use fmt.eprintf (stderr) instead of fmt.printf (stdout)
|
||||
|
||||
17. Pass allocator to findr?
|
||||
|
||||
18. Update `read_wire_string` to use a slice.
|
||||
15. `-h` short flag seems to fail, at least with `envr list`
|
||||
|
||||
## Double-check AI output
|
||||
|
||||
|
||||
27
cli.odin
27
cli.odin
@@ -5,6 +5,7 @@ import "core:fmt"
|
||||
import "core:io"
|
||||
import "core:os"
|
||||
import "core:strings"
|
||||
import "core:terminal"
|
||||
import "core:text/table"
|
||||
|
||||
Command :: struct {
|
||||
@@ -18,6 +19,11 @@ Command :: struct {
|
||||
err: io.Writer,
|
||||
}
|
||||
|
||||
Output_Format :: enum {
|
||||
Table,
|
||||
JSON,
|
||||
}
|
||||
|
||||
CommandInfo :: struct {
|
||||
name: string,
|
||||
usage: string,
|
||||
@@ -288,6 +294,11 @@ at before, restore your backup with:
|
||||
COLOR_FLAGS + "-c, --config-file" + ANSI_RESET + " <path>",
|
||||
`config file (default "~/.envr/config.json")`,
|
||||
)
|
||||
table.row(
|
||||
&tbl,
|
||||
COLOR_FLAGS + "-f, --format" + ANSI_RESET + " 'json'|'table'",
|
||||
`the format of output data. (default 'table', unless piping)`,
|
||||
)
|
||||
write_borderless_table(w, &tbl)
|
||||
|
||||
fmt.wprintf(
|
||||
@@ -303,6 +314,22 @@ has_flag :: proc(cmd: ^Command, name: string) -> bool {
|
||||
return name in cmd.flags || name in cmd.bool_set
|
||||
}
|
||||
|
||||
get_format :: proc(cmd: ^Command) -> Output_Format {
|
||||
flags :: []string{"format", "f"}
|
||||
for name in flags {
|
||||
if val, ok := cmd.flags[name]; ok {
|
||||
switch val {
|
||||
case "json":
|
||||
return .JSON
|
||||
case "table":
|
||||
return .Table
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return terminal.is_terminal(os.stdout) ? .Table : .JSON
|
||||
}
|
||||
|
||||
delete_command :: proc(cmd: ^Command) {
|
||||
bufio.writer_flush(cmd.out_buf)
|
||||
delete(cmd.args)
|
||||
|
||||
@@ -123,7 +123,7 @@ test_command_help_unknown :: proc(t: ^testing.T) {
|
||||
|
||||
text := strings.to_string(b)
|
||||
testing.expect_value(t, len(text), 0)
|
||||
}
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_command_help_version :: proc(t: ^testing.T) {
|
||||
@@ -236,9 +236,9 @@ test_parse_args_positional :: proc(t: ^testing.T) {
|
||||
testing.expect(t, ok, "should succeed")
|
||||
|
||||
testing.expect_value(t, cmd.name, "backup")
|
||||
testing.expect(t, len(cmd.args) == 1)
|
||||
testing.expect(t, cmd.args[0] == "/project/.env")
|
||||
}
|
||||
testing.expect_value(t, len(cmd.args), 1)
|
||||
testing.expect_value(t, cmd.args[0], "/project/.env")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_parse_args_long_flag_with_value :: proc(t: ^testing.T) {
|
||||
@@ -248,7 +248,7 @@ test_parse_args_long_flag_with_value :: proc(t: ^testing.T) {
|
||||
defer delete_command(&cmd)
|
||||
|
||||
testing.expect_value(t, cmd.flags["config"], "x.json")
|
||||
}
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_parse_args_short_flag_with_value :: proc(t: ^testing.T) {
|
||||
@@ -258,7 +258,7 @@ test_parse_args_short_flag_with_value :: proc(t: ^testing.T) {
|
||||
defer delete_command(&cmd)
|
||||
|
||||
testing.expect_value(t, cmd.flags["c"], "x.json")
|
||||
}
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_parse_args_long_bool_flag :: proc(t: ^testing.T) {
|
||||
@@ -268,7 +268,7 @@ test_parse_args_long_bool_flag :: proc(t: ^testing.T) {
|
||||
defer delete_command(&cmd)
|
||||
|
||||
testing.expect_value(t, cmd.bool_set["force"], true)
|
||||
}
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_parse_args_short_bool_flag :: proc(t: ^testing.T) {
|
||||
@@ -278,7 +278,7 @@ test_parse_args_short_bool_flag :: proc(t: ^testing.T) {
|
||||
defer delete_command(&cmd)
|
||||
|
||||
testing.expect_value(t, cmd.bool_set["l"], true)
|
||||
}
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_parse_args_multiple_positionals :: proc(t: ^testing.T) {
|
||||
@@ -288,9 +288,9 @@ test_parse_args_multiple_positionals :: proc(t: ^testing.T) {
|
||||
defer delete_command(&cmd)
|
||||
|
||||
testing.expect_value(t, len(cmd.args), 2)
|
||||
testing.expect(t, cmd.args[0] == "a")
|
||||
testing.expect(t, cmd.args[1] == "b")
|
||||
}
|
||||
testing.expect_value(t, cmd.args[0], "a")
|
||||
testing.expect_value(t, cmd.args[1], "b")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_parse_args_mixed_flags_and_positionals :: proc(t: ^testing.T) {
|
||||
@@ -300,9 +300,9 @@ test_parse_args_mixed_flags_and_positionals :: proc(t: ^testing.T) {
|
||||
defer delete_command(&cmd)
|
||||
|
||||
testing.expect_value(t, cmd.bool_set["force"], true)
|
||||
testing.expect(t, len(cmd.args) == 1)
|
||||
testing.expect(t, cmd.args[0] == "/project/.env")
|
||||
}
|
||||
testing.expect_value(t, len(cmd.args), 1)
|
||||
testing.expect_value(t, cmd.args[0], "/project/.env")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_parse_args_no_args :: proc(t: ^testing.T) {
|
||||
@@ -318,10 +318,10 @@ test_parse_args_flag_then_positional_then_flag :: proc(t: ^testing.T) {
|
||||
testing.expect(t, ok, "should succeed")
|
||||
|
||||
testing.expect_value(t, cmd.bool_set["force"], true)
|
||||
testing.expect(t, cmd.bool_set["verbose"] == true)
|
||||
testing.expect(t, len(cmd.args) == 1)
|
||||
testing.expect(t, cmd.args[0] == "a.env")
|
||||
}
|
||||
testing.expect_value(t, cmd.bool_set["verbose"], true)
|
||||
testing.expect_value(t, len(cmd.args), 1)
|
||||
testing.expect_value(t, cmd.args[0], "a.env")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_parse_args_config_file_long_flag :: proc(t: ^testing.T) {
|
||||
@@ -333,11 +333,7 @@ test_parse_args_config_file_long_flag :: proc(t: ^testing.T) {
|
||||
defer delete_command(&cmd)
|
||||
|
||||
testing.expect_value(t, cmd.config_path, "/custom/config.json")
|
||||
t,
|
||||
cmd.config_path == "/custom/config.json",
|
||||
"config_path should be set from --config-file",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_parse_args_config_file_short_flag :: proc(t: ^testing.T) {
|
||||
@@ -347,11 +343,7 @@ test_parse_args_config_file_short_flag :: proc(t: ^testing.T) {
|
||||
defer delete_command(&cmd)
|
||||
|
||||
testing.expect_value(t, cmd.config_path, "/custom/config.json")
|
||||
t,
|
||||
cmd.config_path == "/custom/config.json",
|
||||
"config_path should be set from -c",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_parse_args_config_file_defaults :: proc(t: ^testing.T) {
|
||||
@@ -369,3 +361,43 @@ test_parse_args_config_file_defaults :: proc(t: ^testing.T) {
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_get_format_long_json :: proc(t: ^testing.T) {
|
||||
cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "--format", "json"})
|
||||
testing.expect(t, ok, "should succeed")
|
||||
if !ok do return
|
||||
defer delete_command(&cmd)
|
||||
|
||||
testing.expect_value(t, get_format(&cmd), Output_Format.JSON)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_get_format_short_json :: proc(t: ^testing.T) {
|
||||
cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "-f", "json"})
|
||||
testing.expect(t, ok, "should succeed")
|
||||
if !ok do return
|
||||
defer delete_command(&cmd)
|
||||
|
||||
testing.expect_value(t, get_format(&cmd), Output_Format.JSON)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_get_format_long_table :: proc(t: ^testing.T) {
|
||||
cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "--format", "table"})
|
||||
testing.expect(t, ok, "should succeed")
|
||||
if !ok do return
|
||||
defer delete_command(&cmd)
|
||||
|
||||
testing.expect_value(t, get_format(&cmd), Output_Format.Table)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_get_format_short_table :: proc(t: ^testing.T) {
|
||||
cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "-f", "table"})
|
||||
testing.expect(t, ok, "should succeed")
|
||||
if !ok do return
|
||||
defer delete_command(&cmd)
|
||||
|
||||
testing.expect_value(t, get_format(&cmd), Output_Format.Table)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#+test
|
||||
package main
|
||||
|
||||
import "core:fmt"
|
||||
import "core:testing"
|
||||
|
||||
@(test)
|
||||
@@ -10,13 +9,9 @@ test_find_unbacked_finds_missing :: proc(t: ^testing.T) {
|
||||
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)))
|
||||
testing.expect_value(t, len(result), 1)
|
||||
if len(result) > 0 {
|
||||
testing.expect(
|
||||
t,
|
||||
result[0] == "/c/.env",
|
||||
fmt.tprintf("expected /c/.env, got %s", result[0]),
|
||||
)
|
||||
testing.expect_value(t, result[0], "/c/.env")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +21,7 @@ test_find_unbacked_all_backed :: proc(t: ^testing.T) {
|
||||
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)))
|
||||
testing.expect_value(t, len(result), 0)
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -35,7 +30,7 @@ test_find_unbacked_no_local :: proc(t: ^testing.T) {
|
||||
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)))
|
||||
testing.expect_value(t, len(result), 0)
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -44,6 +39,6 @@ test_find_unbacked_none_backed :: proc(t: ^testing.T) {
|
||||
db: []EnvFile
|
||||
|
||||
result := find_unbacked(local, db[:])
|
||||
testing.expect(t, len(result) == 2, fmt.tprintf("expected 2 unbacked, got %d", len(result)))
|
||||
testing.expect_value(t, len(result), 2)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import "core:fmt"
|
||||
import "core:os"
|
||||
import "core:path/filepath"
|
||||
import "core:strings"
|
||||
import "core:terminal"
|
||||
import "core:text/table"
|
||||
|
||||
ListEntry :: struct {
|
||||
@@ -13,7 +12,6 @@ ListEntry :: struct {
|
||||
path: string `json:"path"`,
|
||||
}
|
||||
|
||||
// TODO: Support --format flag
|
||||
// TODO: Improve table rendering
|
||||
cmd_list :: proc(cmd: ^Command) {
|
||||
db, db_ok := db_open(cmd.config_path)
|
||||
@@ -27,7 +25,7 @@ cmd_list :: proc(cmd: ^Command) {
|
||||
return
|
||||
}
|
||||
|
||||
if terminal.is_terminal(os.stdout) {
|
||||
if get_format(cmd) == .Table {
|
||||
t: table.Table
|
||||
table.init(&t, context.temp_allocator, context.temp_allocator)
|
||||
table.padding(&t, 1, 1)
|
||||
@@ -51,7 +49,7 @@ cmd_list :: proc(cmd: ^Command) {
|
||||
table.write_decorated_table(cmd.out, &t, decorations, ansi_aware_width)
|
||||
} else {
|
||||
// TODO: Should we instead print full entries here?
|
||||
entries: [dynamic]ListEntry
|
||||
entries := make([dynamic]ListEntry, 0, len(rows), context.temp_allocator)
|
||||
for row in rows {
|
||||
filename := filepath.base(row.path)
|
||||
append(
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
#+feature dynamic-literals
|
||||
#+test
|
||||
package main
|
||||
|
||||
import "core:bufio"
|
||||
import "core:os"
|
||||
import "core:path/filepath"
|
||||
import "core:strings"
|
||||
import "core:testing"
|
||||
|
||||
@(test)
|
||||
@@ -11,9 +15,101 @@ test_filepath_base_equals_rel :: proc(t: ^testing.T) {
|
||||
for path in cases {
|
||||
dir := filepath.dir(path)
|
||||
rel, rel_err := filepath.rel(dir, path, context.temp_allocator)
|
||||
testing.expect(t, rel_err == nil, "filepath.rel returned an error")
|
||||
testing.expect_value(t, rel_err, nil)
|
||||
base := filepath.base(path)
|
||||
testing.expect(t, rel == base, "filepath.rel(dir, path) should equal filepath.base(path)")
|
||||
testing.expect_value(t, rel, base)
|
||||
}
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_cmd_list_format_json :: proc(t: ^testing.T) {
|
||||
base := test_temp_dir(t, "envr-test-list-json-*")
|
||||
defer os.remove_all(base)
|
||||
|
||||
cfg_path, _ := filepath.join([]string{base, "config.json"}, context.temp_allocator)
|
||||
cfg := new_config([]string{"fixtures/keys/insecure-test-key"}, cfg_path)
|
||||
testing.expect(t, save_config(cfg, force = true), "save should succeed")
|
||||
delete_config(&cfg)
|
||||
|
||||
db, db_ok := db_open(cfg_path)
|
||||
testing.expect(t, db_ok, "db should open")
|
||||
if !db_ok do return
|
||||
f := make_test_env_file("/project/.env", "abc123", "SECRET=value")
|
||||
defer delete(f.remotes)
|
||||
testing.expect(t, db_insert(&db, f), "insert should succeed")
|
||||
db_close(&db)
|
||||
|
||||
out_b: strings.Builder
|
||||
strings.builder_init(&out_b)
|
||||
defer strings.builder_destroy(&out_b)
|
||||
err_b: strings.Builder
|
||||
strings.builder_init(&err_b)
|
||||
defer strings.builder_destroy(&err_b)
|
||||
|
||||
cmd, ok := parse_args(
|
||||
[]string{"envr", "list", "--format", "json", "--config-file", cfg_path},
|
||||
strings.to_stream(&out_b),
|
||||
strings.to_stream(&err_b),
|
||||
)
|
||||
testing.expect(t, ok, "parse_args should succeed")
|
||||
if !ok do return
|
||||
defer delete_command(&cmd)
|
||||
|
||||
cmd_list(&cmd)
|
||||
bufio.writer_flush(cmd.out_buf)
|
||||
output := strings.to_string(out_b)
|
||||
|
||||
testing.expect(t, strings.contains(output, "["), "json output should contain '['")
|
||||
testing.expect(
|
||||
t,
|
||||
strings.contains(output, "\"directory\""),
|
||||
"json output should contain directory key",
|
||||
)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_cmd_list_format_table :: proc(t: ^testing.T) {
|
||||
base := test_temp_dir(t, "envr-test-list-table-*")
|
||||
defer os.remove_all(base)
|
||||
|
||||
cfg_path, _ := filepath.join([]string{base, "config.json"}, context.temp_allocator)
|
||||
cfg := new_config([]string{"fixtures/keys/insecure-test-key"}, cfg_path)
|
||||
testing.expect(t, save_config(cfg, force = true), "save should succeed")
|
||||
delete_config(&cfg)
|
||||
|
||||
db, db_ok := db_open(cfg_path)
|
||||
testing.expect(t, db_ok, "db should open")
|
||||
if !db_ok do return
|
||||
f := make_test_env_file("/project/.env", "abc123", "SECRET=value")
|
||||
defer delete(f.remotes)
|
||||
testing.expect(t, db_insert(&db, f), "insert should succeed")
|
||||
db_close(&db)
|
||||
|
||||
out_b: strings.Builder
|
||||
strings.builder_init(&out_b)
|
||||
defer strings.builder_destroy(&out_b)
|
||||
err_b: strings.Builder
|
||||
strings.builder_init(&err_b)
|
||||
defer strings.builder_destroy(&err_b)
|
||||
|
||||
cmd, ok := parse_args(
|
||||
[]string{"envr", "list", "--format", "table", "--config-file", cfg_path},
|
||||
strings.to_stream(&out_b),
|
||||
strings.to_stream(&err_b),
|
||||
)
|
||||
testing.expect(t, ok, "parse_args should succeed")
|
||||
if !ok do return
|
||||
defer delete_command(&cmd)
|
||||
|
||||
cmd_list(&cmd)
|
||||
bufio.writer_flush(cmd.out_buf)
|
||||
output := strings.to_string(out_b)
|
||||
|
||||
testing.expect(t, strings.contains(output, "│"), "table output should contain border chars")
|
||||
testing.expect(
|
||||
t,
|
||||
strings.contains(output, "Directory"),
|
||||
"table output should contain Directory header",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ package main
|
||||
|
||||
import "core:encoding/json"
|
||||
import "core:fmt"
|
||||
import "core:os"
|
||||
import "core:terminal"
|
||||
import "core:text/table"
|
||||
|
||||
SyncEntry :: struct {
|
||||
@@ -12,7 +10,6 @@ SyncEntry :: struct {
|
||||
}
|
||||
|
||||
// TODO: Check for quiet failures.
|
||||
// TODO: Support --format -f flags
|
||||
cmd_sync :: proc(cmd: ^Command) {
|
||||
db, db_ok := db_open(cmd.config_path)
|
||||
if !db_ok {
|
||||
@@ -49,7 +46,7 @@ cmd_sync :: proc(cmd: ^Command) {
|
||||
}
|
||||
}
|
||||
|
||||
if terminal.is_terminal(os.stdout) {
|
||||
if get_format(cmd) == .Table {
|
||||
t: table.Table
|
||||
table.init(&t, context.temp_allocator, context.temp_allocator)
|
||||
table.padding(&t, 1, 1)
|
||||
|
||||
20
config.odin
20
config.odin
@@ -30,14 +30,14 @@ load_config :: proc(config_path: string, allocator := context.allocator) -> (Con
|
||||
// TODO: Should we use context.allocator + defer delete()?
|
||||
data, read_err := os.read_entire_file_from_path(config_path, context.temp_allocator)
|
||||
if read_err != nil {
|
||||
fmt.println("No config file found. Please run `envr init` to generate one.")
|
||||
fmt.eprintln("No config file found. Please run `envr init` to generate one.")
|
||||
return Config{}, false
|
||||
}
|
||||
|
||||
cfg: Config
|
||||
err := json.unmarshal(data, &cfg, .JSON5, allocator)
|
||||
if err != nil {
|
||||
fmt.printf("Error parsing config: %v\n", err)
|
||||
fmt.eprintf("Error parsing config: %v\n", err)
|
||||
return Config{}, false
|
||||
}
|
||||
cfg.config_path = config_path
|
||||
@@ -79,7 +79,7 @@ save_config :: proc(cfg: Config, force: bool = false) -> bool {
|
||||
if !os.exists(config_dir) {
|
||||
mkdir_err := os.make_directory(config_dir)
|
||||
if mkdir_err != nil {
|
||||
fmt.printf("Error creating %s directory: %v\n", config_dir, mkdir_err)
|
||||
fmt.eprintf("Error creating %s directory: %v\n", config_dir, mkdir_err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@ save_config :: proc(cfg: Config, force: bool = false) -> bool {
|
||||
if stat_err == nil {
|
||||
defer os.file_info_delete(info, context.temp_allocator)
|
||||
if info.size > 0 {
|
||||
fmt.println("Config file already exists. Run again with --force to reinitialize.")
|
||||
fmt.eprintln("Config file already exists. Run again with --force to reinitialize.")
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -101,13 +101,13 @@ save_config :: proc(cfg: Config, force: bool = false) -> bool {
|
||||
context.temp_allocator,
|
||||
)
|
||||
if marshal_err != nil {
|
||||
fmt.printf("Error marshaling config: %v\n", marshal_err)
|
||||
fmt.eprintf("Error marshaling config: %v\n", marshal_err)
|
||||
return false
|
||||
}
|
||||
|
||||
write_err := os.write_entire_file(cfg.config_path, data)
|
||||
if write_err != nil {
|
||||
fmt.printf("Error writing config: %v\n", write_err)
|
||||
fmt.eprintf("Error writing config: %v\n", write_err)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ new_config :: proc(
|
||||
for priv in private_key_paths {
|
||||
// TODO: Is this bad?
|
||||
priv_key := strings.clone(priv)
|
||||
pub, _ := strings.concatenate([]string{priv_key, ".pub"})
|
||||
pub := strings.concatenate([]string{priv_key, ".pub"})
|
||||
append(&keys, SshKeyPair{private = priv_key, public = pub})
|
||||
}
|
||||
|
||||
@@ -150,19 +150,19 @@ new_config :: proc(
|
||||
find_ssh_private_keys :: proc() -> (keys: [dynamic]string, ok: bool) {
|
||||
home, home_err := os.user_home_dir(context.allocator)
|
||||
if home_err != nil {
|
||||
fmt.printf("Error getting home dir: %v\n", home_err)
|
||||
fmt.eprintf("Error getting home dir: %v\n", home_err)
|
||||
return
|
||||
}
|
||||
|
||||
ssh_dir, join_err := filepath.join([]string{home, ".ssh"})
|
||||
if join_err != nil {
|
||||
fmt.printf("Error building ssh path: %v\n", join_err)
|
||||
fmt.eprintf("Error building ssh path: %v\n", join_err)
|
||||
return
|
||||
}
|
||||
|
||||
entries, dir_err := os.read_all_directory_by_path(ssh_dir, context.allocator)
|
||||
if dir_err != nil {
|
||||
fmt.printf("Could not read ~/.ssh directory: %v\n", dir_err)
|
||||
fmt.eprintf("Could not read ~/.ssh directory: %v\n", dir_err)
|
||||
return
|
||||
}
|
||||
defer os.file_info_slice_delete(entries, context.allocator)
|
||||
|
||||
@@ -16,13 +16,9 @@ test_new_config_single_key :: proc(t: ^testing.T) {
|
||||
cfg := new_config(paths)
|
||||
defer delete_config(&cfg)
|
||||
|
||||
testing.expect(t, len(cfg.keys) == 1, "should have 1 key")
|
||||
testing.expect(t, cfg.keys[0].private == "/home/user/.ssh/id_ed25519", "Private path mismatch")
|
||||
testing.expect(
|
||||
t,
|
||||
cfg.keys[0].public == "/home/user/.ssh/id_ed25519.pub",
|
||||
"Public path mismatch",
|
||||
)
|
||||
testing.expect_value(t, len(cfg.keys), 1)
|
||||
testing.expect_value(t, cfg.keys[0].private, "/home/user/.ssh/id_ed25519")
|
||||
testing.expect_value(t, cfg.keys[0].public, "/home/user/.ssh/id_ed25519.pub")
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -31,9 +27,9 @@ test_new_config_multiple_keys :: proc(t: ^testing.T) {
|
||||
cfg := new_config(paths)
|
||||
defer delete_config(&cfg)
|
||||
|
||||
testing.expect(t, len(cfg.keys) == 2, "should have 2 keys")
|
||||
testing.expect(t, cfg.keys[0].private == "/home/user/.ssh/id_ed25519")
|
||||
testing.expect(t, cfg.keys[1].private == "/home/user/.ssh/id_rsa")
|
||||
testing.expect_value(t, len(cfg.keys), 2)
|
||||
testing.expect_value(t, cfg.keys[0].private, "/home/user/.ssh/id_ed25519")
|
||||
testing.expect_value(t, cfg.keys[1].private, "/home/user/.ssh/id_rsa")
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -42,7 +38,7 @@ test_new_config_empty_keys :: proc(t: ^testing.T) {
|
||||
cfg := new_config(paths)
|
||||
defer delete_config(&cfg)
|
||||
|
||||
testing.expect(t, len(cfg.keys) == 0, "should have 0 keys")
|
||||
testing.expect_value(t, len(cfg.keys), 0)
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -51,10 +47,10 @@ test_new_config_scan_defaults :: proc(t: ^testing.T) {
|
||||
cfg := new_config(paths)
|
||||
defer delete_config(&cfg)
|
||||
|
||||
testing.expect(t, cfg.scan_config.matcher == "\\.env", "matcher should be \\.env")
|
||||
testing.expect(t, len(cfg.scan_config.exclude) == 4, "should have 4 exclude patterns")
|
||||
testing.expect(t, len(cfg.scan_config.include) == 1, "should have 1 include path")
|
||||
testing.expect(t, cfg.scan_config.include[0] == "~", "include should be ~")
|
||||
testing.expect_value(t, cfg.scan_config.matcher, "\\.env")
|
||||
testing.expect_value(t, len(cfg.scan_config.exclude), 4)
|
||||
testing.expect_value(t, len(cfg.scan_config.include), 1)
|
||||
testing.expect_value(t, cfg.scan_config.include[0], "~")
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -65,7 +61,7 @@ test_new_config_exclude_patterns :: proc(t: ^testing.T) {
|
||||
|
||||
expected := []string{"*\\.envrc", "\\.local/", "node_modules", "vendor"}
|
||||
for i in 0 ..< len(expected) {
|
||||
testing.expect(t, cfg.scan_config.exclude[i] == expected[i])
|
||||
testing.expect_value(t, cfg.scan_config.exclude[i], expected[i])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +71,7 @@ test_save_load_config_roundtrip :: proc(t: ^testing.T) {
|
||||
defer os.remove_all(base)
|
||||
|
||||
cfgPath, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
|
||||
testing.expect(t, err == nil, "cfgPath should build successfully")
|
||||
testing.expect_value(t, err, nil)
|
||||
|
||||
cfg := new_config([]string{"/home/user/.ssh/id_ed25519"}, cfgPath)
|
||||
defer delete_config(&cfg)
|
||||
@@ -87,13 +83,13 @@ test_save_load_config_roundtrip :: proc(t: ^testing.T) {
|
||||
if !ok do return
|
||||
defer delete_config(&loaded)
|
||||
|
||||
testing.expect(t, len(loaded.keys) == 1, "should have 1 key")
|
||||
testing.expect(t, loaded.keys[0].private == "/home/user/.ssh/id_ed25519")
|
||||
testing.expect(t, loaded.keys[0].public == "/home/user/.ssh/id_ed25519.pub")
|
||||
testing.expect(t, loaded.scan_config.matcher == "\\.env")
|
||||
testing.expect(t, len(loaded.scan_config.exclude) == 4)
|
||||
testing.expect(t, len(loaded.scan_config.include) == 1)
|
||||
testing.expect(t, loaded.scan_config.include[0] == "~")
|
||||
testing.expect_value(t, len(loaded.keys), 1)
|
||||
testing.expect_value(t, loaded.keys[0].private, "/home/user/.ssh/id_ed25519")
|
||||
testing.expect_value(t, loaded.keys[0].public, "/home/user/.ssh/id_ed25519.pub")
|
||||
testing.expect_value(t, loaded.scan_config.matcher, "\\.env")
|
||||
testing.expect_value(t, len(loaded.scan_config.exclude), 4)
|
||||
testing.expect_value(t, len(loaded.scan_config.include), 1)
|
||||
testing.expect_value(t, loaded.scan_config.include[0], "~")
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -108,7 +104,7 @@ test_save_config_no_clobber :: proc(t: ^testing.T) {
|
||||
defer os.remove_all(base)
|
||||
|
||||
cfgPath, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
|
||||
testing.expect(t, err == nil, "cfgPath should build successfully")
|
||||
testing.expect_value(t, err, nil)
|
||||
|
||||
cfg := new_config([]string{"/home/user/.ssh/key1"}, cfgPath)
|
||||
defer delete_config(&cfg)
|
||||
@@ -125,7 +121,7 @@ test_save_config_force_overwrites :: proc(t: ^testing.T) {
|
||||
defer os.remove_all(base)
|
||||
|
||||
cfgPath, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
|
||||
testing.expect(t, err == nil, "cfgPath should build successfully")
|
||||
testing.expect_value(t, err, nil)
|
||||
|
||||
cfg := new_config([]string{"/home/user/.ssh/key1"}, cfgPath)
|
||||
defer delete_config(&cfg)
|
||||
@@ -140,12 +136,8 @@ test_save_config_force_overwrites :: proc(t: ^testing.T) {
|
||||
if !ok do return
|
||||
defer delete_config(&loaded)
|
||||
|
||||
testing.expect(t, len(loaded.keys) == 1, "should have 1 key")
|
||||
testing.expect(
|
||||
t,
|
||||
loaded.keys[0].private == "/home/user/.ssh/key2",
|
||||
"should be the overwritten key",
|
||||
)
|
||||
testing.expect_value(t, len(loaded.keys), 1)
|
||||
testing.expect_value(t, loaded.keys[0].private, "/home/user/.ssh/key2")
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -190,7 +182,7 @@ test_search_paths_expands_tilde :: proc(t: ^testing.T) {
|
||||
|
||||
paths := search_paths(cfg, context.temp_allocator)
|
||||
|
||||
testing.expect(t, len(paths) == 1, "should have 1 path")
|
||||
testing.expect_value(t, len(paths), 1)
|
||||
if len(paths) > 0 {
|
||||
testing.expectf(
|
||||
t,
|
||||
|
||||
26
crypto.odin
26
crypto.odin
@@ -60,7 +60,7 @@ encrypt :: proc(plaintext: []u8, keys: []SshKeyPair) -> (ciphertext: []u8, ok: b
|
||||
&sym_key[0],
|
||||
)
|
||||
if rc != 0 {
|
||||
fmt.println("Error: symmetric encryption failed")
|
||||
fmt.eprintln("Error: symmetric encryption failed")
|
||||
delete(secret_ct)
|
||||
return
|
||||
}
|
||||
@@ -84,7 +84,7 @@ encrypt :: proc(plaintext: []u8, keys: []SshKeyPair) -> (ciphertext: []u8, ok: b
|
||||
&x25519_pairs[0].Private[0],
|
||||
)
|
||||
if rc != 0 {
|
||||
fmt.printf("Error: failed to encrypt for recipient %d\n", i)
|
||||
fmt.eprintf("Error: failed to encrypt for recipient %d\n", i)
|
||||
delete(entries)
|
||||
delete(secret_ct)
|
||||
return
|
||||
@@ -132,13 +132,13 @@ encrypt :: proc(plaintext: []u8, keys: []SshKeyPair) -> (ciphertext: []u8, ok: b
|
||||
|
||||
decrypt :: proc(ciphertext: []u8, keys: []SshKeyPair) -> (plaintext: []u8, ok: bool) {
|
||||
if len(ciphertext) < HEADER_SIZE {
|
||||
fmt.println("Error: ciphertext too short (header)")
|
||||
fmt.eprintln("Error: ciphertext too short (header)")
|
||||
return
|
||||
}
|
||||
|
||||
for i in 0 ..< 4 {
|
||||
if ciphertext[i] != MAGIC_BYTES[i] {
|
||||
fmt.println("Error: invalid magic bytes")
|
||||
fmt.eprintln("Error: invalid magic bytes")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -166,7 +166,7 @@ decrypt :: proc(ciphertext: []u8, keys: []SshKeyPair) -> (plaintext: []u8, ok: b
|
||||
|
||||
recipients_end := offset + int(num_recipients) * RECIPIENT_ENTRY_SIZE
|
||||
if recipients_end > len(ciphertext) {
|
||||
fmt.println("Error: ciphertext too short (recipient data)")
|
||||
fmt.eprintln("Error: ciphertext too short (recipient data)")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ decrypt :: proc(ciphertext: []u8, keys: []SshKeyPair) -> (plaintext: []u8, ok: b
|
||||
}
|
||||
|
||||
if !found {
|
||||
fmt.println("Error: no matching recipient found")
|
||||
fmt.eprintln("Error: no matching recipient found")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -236,14 +236,14 @@ decrypt :: proc(ciphertext: []u8, keys: []SshKeyPair) -> (plaintext: []u8, ok: b
|
||||
&x25519_pairs[matched_pi].Private[0],
|
||||
)
|
||||
if rc != 0 {
|
||||
fmt.println("Error: failed to decrypt symmetric key")
|
||||
fmt.eprintln("Error: failed to decrypt symmetric key")
|
||||
return
|
||||
}
|
||||
|
||||
ct_data := ciphertext[recipients_end:]
|
||||
pt_len := len(ct_data) - CRYPTO_SECRETBOX_MAC_BYTES
|
||||
if pt_len < 0 {
|
||||
fmt.println("Error: ciphertext too short (no encrypted data)")
|
||||
fmt.eprintln("Error: ciphertext too short (no encrypted data)")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ decrypt :: proc(ciphertext: []u8, keys: []SshKeyPair) -> (plaintext: []u8, ok: b
|
||||
&sym_key[0],
|
||||
)
|
||||
if rc != 0 {
|
||||
fmt.println("Error: symmetric decryption failed")
|
||||
fmt.eprintln("Error: symmetric decryption failed")
|
||||
delete(plaintext)
|
||||
return
|
||||
}
|
||||
@@ -285,21 +285,21 @@ ssh_to_x25519 :: proc(
|
||||
for i in 0 ..< len(keys) {
|
||||
ssh_kp, parse_ok := parse_ssh_private_key(keys[i].private)
|
||||
if !parse_ok {
|
||||
fmt.printf("Error: failed to parse SSH private key: %s\n", keys[i].private)
|
||||
fmt.eprintf("Error: failed to parse SSH private key: %s\n", keys[i].private)
|
||||
delete(pairs)
|
||||
return pairs, false
|
||||
}
|
||||
|
||||
ssh_pub, pub_ok := parse_ssh_public_key(keys[i].public)
|
||||
if !pub_ok {
|
||||
fmt.printf("Error: failed to parse SSH public key: %s\n", keys[i].public)
|
||||
fmt.eprintf("Error: failed to parse SSH public key: %s\n", keys[i].public)
|
||||
delete(pairs)
|
||||
return pairs, false
|
||||
}
|
||||
|
||||
pk_rc := crypto_sign_ed25519_pk_to_curve25519(&pairs[i].Public[0], &ssh_pub[0])
|
||||
if pk_rc != 0 {
|
||||
fmt.println("Error: failed to convert ed25519 public key to curve25519")
|
||||
fmt.eprintln("Error: failed to convert ed25519 public key to curve25519")
|
||||
delete(pairs)
|
||||
return pairs, false
|
||||
}
|
||||
@@ -314,7 +314,7 @@ ssh_to_x25519 :: proc(
|
||||
|
||||
sk_rc := crypto_sign_ed25519_sk_to_curve25519(&pairs[i].Private[0], &ed25519_sk[0])
|
||||
if sk_rc != 0 {
|
||||
fmt.println("Error: failed to convert ed25519 private key to curve25519")
|
||||
fmt.eprintln("Error: failed to convert ed25519 private key to curve25519")
|
||||
delete(pairs)
|
||||
return pairs, false
|
||||
}
|
||||
|
||||
@@ -27,13 +27,10 @@ test_encrypt_decrypt_roundtrip :: proc(t: ^testing.T) {
|
||||
testing.expect(t, dec_ok, "decryption should succeed")
|
||||
defer delete(decrypted)
|
||||
|
||||
testing.expect(
|
||||
t,
|
||||
len(decrypted) == len(original),
|
||||
fmt.tprintf("expected %d bytes, got %d", len(original), len(decrypted)),
|
||||
)
|
||||
testing.expect_value(t, len(decrypted), len(original))
|
||||
// TODO: Should this be a loop?
|
||||
for i in 0 ..< len(original) {
|
||||
testing.expect(t, decrypted[i] == original[i], fmt.tprintf("byte mismatch at index %d", i))
|
||||
testing.expect_value(t, decrypted[i], original[i])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,16 +53,8 @@ test_encrypt_decrypt_multi_recipient :: proc(t: ^testing.T) {
|
||||
defer delete(decrypted2)
|
||||
|
||||
for i in 0 ..< len(original) {
|
||||
testing.expect(
|
||||
t,
|
||||
decrypted1[i] == original[i],
|
||||
fmt.tprintf("key1: byte mismatch at %d", i),
|
||||
)
|
||||
testing.expect(
|
||||
t,
|
||||
decrypted2[i] == original[i],
|
||||
fmt.tprintf("key2: byte mismatch at %d", i),
|
||||
)
|
||||
testing.expect_value(t, decrypted1[i], original[i])
|
||||
testing.expect_value(t, decrypted2[i], original[i])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +85,7 @@ test_encrypt_empty_plaintext :: proc(t: ^testing.T) {
|
||||
testing.expect(t, dec_ok, "decryption should succeed")
|
||||
defer delete(decrypted)
|
||||
|
||||
testing.expect(t, len(decrypted) == 0, "decrypted empty data should be empty")
|
||||
testing.expect_value(t, len(decrypted), 0)
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -113,8 +102,9 @@ test_recipient_can_decrypt_senders_data :: proc(t: ^testing.T) {
|
||||
testing.expect(t, dec_ok, "second recipient should decrypt without the sender key present")
|
||||
defer delete(decrypted)
|
||||
|
||||
// TODO: Should this be a loop?
|
||||
for i in 0 ..< len(original) {
|
||||
testing.expect(t, decrypted[i] == original[i], fmt.tprintf("byte mismatch at %d", i))
|
||||
testing.expect_value(t, decrypted[i], original[i])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,9 +118,9 @@ test_ciphertext_has_magic :: proc(t: ^testing.T) {
|
||||
defer delete(encrypted)
|
||||
|
||||
testing.expect(t, len(encrypted) >= 4, "ciphertext should have at least 4 bytes")
|
||||
testing.expect(t, encrypted[0] == u8('E'), "magic byte 0")
|
||||
testing.expect(t, encrypted[1] == u8('N'), "magic byte 1")
|
||||
testing.expect(t, encrypted[2] == u8('V'), "magic byte 2")
|
||||
testing.expect(t, encrypted[3] == u8('R'), "magic byte 3")
|
||||
testing.expect_value(t, encrypted[0], u8('E'))
|
||||
testing.expect_value(t, encrypted[1], u8('N'))
|
||||
testing.expect_value(t, encrypted[2], u8('V'))
|
||||
testing.expect_value(t, encrypted[3], u8('R'))
|
||||
}
|
||||
|
||||
|
||||
142
db.odin
142
db.odin
@@ -94,14 +94,14 @@ db_init :: proc() -> (db: Db, ok: bool) {
|
||||
conn: sqlite.Db
|
||||
rc := sqlite.open(":memory:", &conn)
|
||||
if rc != sqlite.OK {
|
||||
fmt.printf("Error opening in-memory database: %s\n", sqlite.errmsg(conn))
|
||||
fmt.eprintf("Error opening in-memory database: %s\n", sqlite.errmsg(conn))
|
||||
return
|
||||
}
|
||||
|
||||
create_sql: cstring = "CREATE TABLE IF NOT EXISTS envr_env_files (path TEXT PRIMARY KEY NOT NULL, remotes TEXT, sha256 TEXT NOT NULL, contents TEXT NOT NULL)"
|
||||
rc = sqlite.exec(conn, create_sql, nil, nil, nil)
|
||||
if rc != sqlite.OK {
|
||||
fmt.printf("Error creating table: %s\n", sqlite.errmsg(conn))
|
||||
fmt.eprintf("Error creating table: %s\n", sqlite.errmsg(conn))
|
||||
sqlite.close(conn)
|
||||
return
|
||||
}
|
||||
@@ -119,14 +119,14 @@ db_allocator :: proc(db: ^Db) -> mem.Allocator {
|
||||
db_restore_from_encrypted :: proc(db: ^Db, data_path: string) -> bool {
|
||||
encrypted_data, read_err := os.read_entire_file_from_path(data_path, context.temp_allocator)
|
||||
if read_err != nil {
|
||||
fmt.printf("Error reading encrypted database: %v\n", read_err)
|
||||
fmt.eprintf("Error reading encrypted database: %v\n", read_err)
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO: Use context.temp_allocator
|
||||
plaintext, dec_ok := decrypt(encrypted_data, db.cfg.keys[:])
|
||||
if !dec_ok {
|
||||
fmt.println("Error: decryption failed")
|
||||
fmt.eprintln("Error: decryption failed")
|
||||
return false
|
||||
}
|
||||
defer delete(plaintext)
|
||||
@@ -134,7 +134,7 @@ db_restore_from_encrypted :: proc(db: ^Db, data_path: string) -> bool {
|
||||
n := i64(len(plaintext))
|
||||
buf := sqlite.malloc64(n)
|
||||
if buf == nil {
|
||||
fmt.println("Error: failed to allocate buffer for deserialization")
|
||||
fmt.eprintln("Error: failed to allocate buffer for deserialization")
|
||||
return false
|
||||
}
|
||||
copy(buf[:len(plaintext)], plaintext)
|
||||
@@ -144,7 +144,7 @@ db_restore_from_encrypted :: proc(db: ^Db, data_path: string) -> bool {
|
||||
rc := sqlite.deserialize(db.conn, "main", buf, n, n, flags)
|
||||
if rc != sqlite.OK {
|
||||
sqlite.free(buf)
|
||||
fmt.printf("Error deserializing database: %s\n", sqlite.errmsg(db.conn))
|
||||
fmt.eprintf("Error deserializing database: %s\n", sqlite.errmsg(db.conn))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -167,14 +167,14 @@ db_close :: proc(db: ^Db) {
|
||||
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))
|
||||
fmt.eprintf("Error vacuuming database: %s\n", sqlite.errmsg(db.conn))
|
||||
return
|
||||
}
|
||||
|
||||
sz: i64
|
||||
data := sqlite.serialize(db.conn, "main", &sz, 0)
|
||||
if data == nil {
|
||||
fmt.println("Error: failed to serialize database")
|
||||
fmt.eprintln("Error: failed to serialize database")
|
||||
return
|
||||
}
|
||||
defer sqlite.free(data)
|
||||
@@ -195,7 +195,7 @@ db_close :: proc(db: ^Db) {
|
||||
write_err := os.write_entire_file(data_path, encrypted)
|
||||
delete(encrypted)
|
||||
if write_err != nil {
|
||||
fmt.printf("Error writing encrypted database: %v\n", write_err)
|
||||
fmt.eprintf("Error writing encrypted database: %v\n", write_err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ db_list :: proc(db: ^Db) -> ([]EnvFile, bool) {
|
||||
nil,
|
||||
)
|
||||
if rc != sqlite.OK {
|
||||
fmt.printf("Error preparing query: %s\n", sqlite.errmsg(db.conn))
|
||||
fmt.eprintf("Error preparing query: %s\n", sqlite.errmsg(db.conn))
|
||||
return []EnvFile{}, false
|
||||
}
|
||||
defer sqlite.finalize(stmt)
|
||||
@@ -222,22 +222,33 @@ db_list :: proc(db: ^Db) -> ([]EnvFile, bool) {
|
||||
allocator := db_allocator(db)
|
||||
results := make([dynamic]EnvFile, 0, 10, allocator)
|
||||
|
||||
migrate := false
|
||||
for {
|
||||
rc = sqlite.step(stmt)
|
||||
if rc == sqlite.DONE {
|
||||
break
|
||||
}
|
||||
if rc != sqlite.ROW {
|
||||
fmt.printf("Error stepping query: %s\n", sqlite.errmsg(db.conn))
|
||||
fmt.eprintf("Error stepping query: %s\n", sqlite.errmsg(db.conn))
|
||||
#no_bounds_check return results[:], false
|
||||
}
|
||||
|
||||
remotes_json := string(sqlite.column_text(stmt, 1))
|
||||
// TODO: Remove json support after next major release
|
||||
remotes: [dynamic]string = ---
|
||||
if len(remotes_json) > 0 {
|
||||
err := json.unmarshal_string(remotes_json, &remotes, allocator = allocator)
|
||||
if err != nil {
|
||||
fmt.eprintf("Warning: malformed remotes JSON: %v\n", err)
|
||||
remotes_raw := string(sqlite.column_text(stmt, 1))
|
||||
if len(remotes_raw) > 0 {
|
||||
if remotes_raw[0] == '[' {
|
||||
err := json.unmarshal_string(remotes_raw, &remotes, allocator = allocator)
|
||||
if err != nil {
|
||||
fmt.eprintf("Warning: malformed remotes JSON: %v\n", err)
|
||||
}
|
||||
migrate = true
|
||||
} else {
|
||||
split := strings.split_lines(remotes_raw, context.temp_allocator)
|
||||
remotes = make([dynamic]string, 0, len(split), allocator = allocator)
|
||||
for s in split {
|
||||
append(&remotes, strings.clone(s, allocator))
|
||||
}
|
||||
}
|
||||
}
|
||||
path := clone_cstring(sqlite.column_text(stmt, 0), allocator)
|
||||
@@ -254,16 +265,16 @@ db_list :: proc(db: ^Db) -> ([]EnvFile, bool) {
|
||||
)
|
||||
}
|
||||
|
||||
if migrate {
|
||||
migrate_remotes(db)
|
||||
}
|
||||
|
||||
#no_bounds_check return results[:], true
|
||||
}
|
||||
|
||||
// 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)
|
||||
if marshal_err != nil {
|
||||
fmt.printf("Error marshaling remotes: %v\n", marshal_err)
|
||||
return false
|
||||
}
|
||||
remotes := strings.join(file.remotes[:], "\n", allocator = context.temp_allocator)
|
||||
|
||||
sql: cstring =
|
||||
"INSERT OR REPLACE INTO " +
|
||||
@@ -271,7 +282,7 @@ db_insert :: proc(db: ^Db, file: EnvFile) -> bool {
|
||||
stmt: sqlite.Stmt
|
||||
rc := sqlite.prepare_v2(db.conn, sql, -1, &stmt, nil)
|
||||
if rc != sqlite.OK {
|
||||
fmt.printf("Error preparing insert: %s\n", sqlite.errmsg(db.conn))
|
||||
fmt.eprintf("Error preparing insert: %s\n", sqlite.errmsg(db.conn))
|
||||
return false
|
||||
}
|
||||
defer sqlite.finalize(stmt)
|
||||
@@ -281,15 +292,15 @@ db_insert :: proc(db: ^Db, file: EnvFile) -> bool {
|
||||
defer delete(cpath)
|
||||
rc = sqlite.bind_text(stmt, 1, cpath, -1, nil)
|
||||
if rc != sqlite.OK {
|
||||
fmt.printf("Error binding path: %s\n", sqlite.errmsg(db.conn))
|
||||
fmt.eprintf("Error binding path: %s\n", sqlite.errmsg(db.conn))
|
||||
return false
|
||||
}
|
||||
|
||||
cremotes := to_cstring(string(remotes_json))
|
||||
cremotes := to_cstring(remotes)
|
||||
defer delete(cremotes)
|
||||
rc = sqlite.bind_text(stmt, 2, cremotes, -1, nil)
|
||||
if rc != sqlite.OK {
|
||||
fmt.printf("Error binding remotes: %s\n", sqlite.errmsg(db.conn))
|
||||
fmt.eprintf("Error binding remotes: %s\n", sqlite.errmsg(db.conn))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -297,7 +308,7 @@ db_insert :: proc(db: ^Db, file: EnvFile) -> bool {
|
||||
defer delete(csha)
|
||||
rc = sqlite.bind_text(stmt, 3, csha, -1, nil)
|
||||
if rc != sqlite.OK {
|
||||
fmt.printf("Error binding sha256: %s\n", sqlite.errmsg(db.conn))
|
||||
fmt.eprintf("Error binding sha256: %s\n", sqlite.errmsg(db.conn))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -305,13 +316,13 @@ db_insert :: proc(db: ^Db, file: EnvFile) -> bool {
|
||||
defer delete(ccontents)
|
||||
rc = sqlite.bind_text(stmt, 4, ccontents, -1, nil)
|
||||
if rc != sqlite.OK {
|
||||
fmt.printf("Error binding contents: %s\n", sqlite.errmsg(db.conn))
|
||||
fmt.eprintf("Error binding contents: %s\n", sqlite.errmsg(db.conn))
|
||||
return false
|
||||
}
|
||||
|
||||
rc = sqlite.step(stmt)
|
||||
if rc != sqlite.DONE {
|
||||
fmt.printf("Error inserting: %s\n", sqlite.errmsg(db.conn))
|
||||
fmt.eprintf("Error inserting: %s\n", sqlite.errmsg(db.conn))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -329,7 +340,7 @@ db_fetch :: proc(db: ^Db, path: string) -> (EnvFile, bool) {
|
||||
stmt: sqlite.Stmt
|
||||
rc := sqlite.prepare_v2(db.conn, sql, -1, &stmt, nil)
|
||||
if rc != sqlite.OK {
|
||||
fmt.printf("Error preparing fetch: %s\n", sqlite.errmsg(db.conn))
|
||||
fmt.eprintf("Error preparing fetch: %s\n", sqlite.errmsg(db.conn))
|
||||
return EnvFile{}, false
|
||||
}
|
||||
defer sqlite.finalize(stmt)
|
||||
@@ -340,30 +351,46 @@ db_fetch :: proc(db: ^Db, path: string) -> (EnvFile, bool) {
|
||||
defer delete(cpath, allocator)
|
||||
rc = sqlite.bind_text(stmt, 1, cpath, -1, nil)
|
||||
if rc != sqlite.OK {
|
||||
fmt.printf("Error binding path: %s\n", sqlite.errmsg(db.conn))
|
||||
fmt.eprintf("Error binding path: %s\n", sqlite.errmsg(db.conn))
|
||||
return EnvFile{}, false
|
||||
}
|
||||
rc = sqlite.step(stmt)
|
||||
if rc == sqlite.DONE {
|
||||
fmt.printf("No file found with path: %s\n", path)
|
||||
fmt.eprintf("No file found with path: %s\n", path)
|
||||
return EnvFile{}, false
|
||||
}
|
||||
if rc != sqlite.ROW {
|
||||
fmt.printf("Error fetching: %s\n", sqlite.errmsg(db.conn))
|
||||
fmt.eprintf("Error fetching: %s\n", sqlite.errmsg(db.conn))
|
||||
return EnvFile{}, false
|
||||
}
|
||||
|
||||
remotes_json := string(sqlite.column_text(stmt, 1))
|
||||
// TODO: Remove json support after next major release
|
||||
migrate := false
|
||||
remotes: [dynamic]string = ---
|
||||
if len(remotes_json) > 0 {
|
||||
err := json.unmarshal_string(remotes_json, &remotes, allocator = allocator)
|
||||
if err != nil {
|
||||
fmt.eprintf("Warning: malformed remotes JSON: %v\n", err)
|
||||
remotes_raw := string(sqlite.column_text(stmt, 1))
|
||||
if len(remotes_raw) > 0 {
|
||||
if remotes_raw[0] == '[' {
|
||||
err := json.unmarshal_string(remotes_raw, &remotes, allocator = allocator)
|
||||
if err != nil {
|
||||
fmt.eprintf("Warning: malformed remotes JSON: %v\n", err)
|
||||
}
|
||||
|
||||
migrate = true
|
||||
} else {
|
||||
split := strings.split_lines(remotes_raw, context.temp_allocator)
|
||||
remotes = make([dynamic]string, 0, len(split), allocator = allocator)
|
||||
for s in split {
|
||||
append(&remotes, strings.clone(s, allocator))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file_path := clone_cstring(sqlite.column_text(stmt, 0), allocator)
|
||||
|
||||
if migrate {
|
||||
migrate_remotes(db)
|
||||
}
|
||||
|
||||
return EnvFile {
|
||||
path = file_path,
|
||||
dir = filepath.dir(file_path),
|
||||
@@ -379,7 +406,7 @@ db_delete :: proc(db: ^Db, path: string) -> bool {
|
||||
stmt: sqlite.Stmt
|
||||
rc := sqlite.prepare_v2(db.conn, sql, -1, &stmt, nil)
|
||||
if rc != sqlite.OK {
|
||||
fmt.printf("Error preparing delete: %s\n", sqlite.errmsg(db.conn))
|
||||
fmt.eprintf("Error preparing delete: %s\n", sqlite.errmsg(db.conn))
|
||||
return false
|
||||
}
|
||||
defer sqlite.finalize(stmt)
|
||||
@@ -388,17 +415,17 @@ db_delete :: proc(db: ^Db, path: string) -> bool {
|
||||
defer delete(cpath)
|
||||
rc = sqlite.bind_text(stmt, 1, cpath, -1, nil)
|
||||
if rc != sqlite.OK {
|
||||
fmt.printf("Error binding path: %s\n", sqlite.errmsg(db.conn))
|
||||
fmt.eprintf("Error binding path: %s\n", sqlite.errmsg(db.conn))
|
||||
return false
|
||||
}
|
||||
rc = sqlite.step(stmt)
|
||||
if rc != sqlite.DONE {
|
||||
fmt.printf("Error deleting: %s\n", sqlite.errmsg(db.conn))
|
||||
fmt.eprintf("Error deleting: %s\n", sqlite.errmsg(db.conn))
|
||||
return false
|
||||
}
|
||||
|
||||
if sqlite.changes(db.conn) == 0 {
|
||||
fmt.printf("No file found with path: %s\n", path)
|
||||
fmt.eprintf("No file found with path: %s\n", path)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -410,7 +437,7 @@ db_delete :: proc(db: ^Db, path: string) -> bool {
|
||||
new_env_file :: proc(path: string) -> (EnvFile, bool) {
|
||||
abs_path, abs_err := filepath.abs(path)
|
||||
if abs_err != nil {
|
||||
fmt.printf("Error getting absolute path: %v\n", abs_err)
|
||||
fmt.eprintf("Error getting absolute path: %v\n", abs_err)
|
||||
return EnvFile{}, false
|
||||
}
|
||||
|
||||
@@ -421,7 +448,7 @@ new_env_file :: proc(path: string) -> (EnvFile, bool) {
|
||||
|
||||
data, read_err := os.read_entire_file_from_path(abs_path, context.allocator)
|
||||
if read_err != nil {
|
||||
fmt.printf("Error reading file %s: %v\n", abs_path, read_err)
|
||||
fmt.eprintf("Error reading file %s: %v\n", abs_path, read_err)
|
||||
return EnvFile{}, false
|
||||
}
|
||||
|
||||
@@ -498,6 +525,27 @@ db_persist :: proc(db: ^Db, f: ^EnvFile, old_path: string) -> bool {
|
||||
return db_insert(db, f^)
|
||||
}
|
||||
|
||||
// TODO: Remove after the next major release
|
||||
migrate_remotes :: proc(db: ^Db) {
|
||||
sql ::
|
||||
"UPDATE envr_env_files " +
|
||||
"SET remotes = COALESCE((" +
|
||||
" SELECT group_concat(atom, char(10)) " +
|
||||
" FROM json_each(envr_env_files.remotes)" +
|
||||
"), '') " +
|
||||
"WHERE remotes LIKE '[%'"
|
||||
|
||||
rc := sqlite.exec(db.conn, sql, nil, nil, nil)
|
||||
if rc != sqlite.OK {
|
||||
fmt.eprintf("Warning: failed to migrate remotes: %s\n", sqlite.errmsg(db.conn))
|
||||
return
|
||||
}
|
||||
|
||||
if sqlite.changes(db.conn) > 0 {
|
||||
db.changed = true
|
||||
}
|
||||
}
|
||||
|
||||
try_move_dir :: proc(db: ^Db, f: ^EnvFile, allocator: mem.Allocator) -> (bool, SyncError) {
|
||||
roots, ok := find_git_roots(db.cfg)
|
||||
if !ok {
|
||||
@@ -524,7 +572,7 @@ 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)
|
||||
f.dir = strings.clone(matched_dir, allocator)
|
||||
base := filepath.base(f.path)
|
||||
new_path, _ := filepath.join({f.dir, base}, allocator)
|
||||
f.path = new_path
|
||||
@@ -565,7 +613,7 @@ get_git_remotes :: proc(dir: string, allocator: mem.Allocator) -> [dynamic]strin
|
||||
}
|
||||
if !found {
|
||||
// FIXME: Currently leaks when adding a file with envr scan
|
||||
cloned, _ := strings.clone(url, allocator)
|
||||
cloned := strings.clone(url, allocator)
|
||||
append(&remotes, cloned)
|
||||
}
|
||||
}
|
||||
@@ -583,7 +631,7 @@ to_cstring :: proc {
|
||||
string_to_cstring :: proc(s: string, allocator := context.allocator) -> cstring {
|
||||
cs, err := strings.clone_to_cstring(s, allocator)
|
||||
if err != nil {
|
||||
fmt.printf("Failed to convert string to cstring: %v\n", err)
|
||||
fmt.eprintf("Failed to convert string to cstring: %v\n", err)
|
||||
panic("Allocation Exception")
|
||||
}
|
||||
return cs
|
||||
@@ -593,7 +641,7 @@ string_to_cstring :: proc(s: string, allocator := context.allocator) -> cstring
|
||||
clone_cstring :: proc(c: cstring, allocator := context.allocator) -> string {
|
||||
str, err := strings.clone_from_cstring(c, allocator)
|
||||
if err != nil {
|
||||
fmt.printf("Failed to convert string to cstring: %v\n", err)
|
||||
fmt.eprintf("Failed to convert string to cstring: %v\n", err)
|
||||
delete(str)
|
||||
panic("Allocation Exception")
|
||||
}
|
||||
|
||||
@@ -67,10 +67,10 @@ test_encrypt_decrypt_sqlite_roundtrip :: proc(t: ^testing.T) {
|
||||
defer delete(encrypted)
|
||||
|
||||
testing.expect(t, len(encrypted) >= HEADER_SIZE, "ciphertext should have header")
|
||||
testing.expect(t, encrypted[0] == u8('E'), "magic byte 0")
|
||||
testing.expect(t, encrypted[1] == u8('N'), "magic byte 1")
|
||||
testing.expect(t, encrypted[2] == u8('V'), "magic byte 2")
|
||||
testing.expect(t, encrypted[3] == u8('R'), "magic byte 3")
|
||||
testing.expect_value(t, encrypted[0], u8('E'))
|
||||
testing.expect_value(t, encrypted[1], u8('N'))
|
||||
testing.expect_value(t, encrypted[2], u8('V'))
|
||||
testing.expect_value(t, encrypted[3], u8('R'))
|
||||
|
||||
plaintext, dec_ok := decrypt(encrypted, cfg.keys[:])
|
||||
testing.expect(t, dec_ok, "decryption should succeed")
|
||||
@@ -142,7 +142,7 @@ test_encrypt_write_read_decrypt :: proc(t: ^testing.T) {
|
||||
}
|
||||
defer delete(plaintext)
|
||||
|
||||
testing.expect(t, len(plaintext) == len(sqlite_data), "size mismatch after file round-trip")
|
||||
testing.expect_value(t, len(plaintext), len(sqlite_data))
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -189,7 +189,7 @@ test_decrypt_then_deserialize_sqlite :: proc(t: ^testing.T) {
|
||||
copy(buf[:len(plaintext)], plaintext)
|
||||
|
||||
rc = sqlite.deserialize(mem_db, "main", buf, n, n, {.FREEONCLOSE, .RESIZEABLE})
|
||||
testing.expect(t, rc == sqlite.OK, "deserialize should succeed")
|
||||
testing.expect_value(t, rc, sqlite.OK)
|
||||
if rc != sqlite.OK {
|
||||
sqlite.free(buf)
|
||||
return
|
||||
@@ -198,14 +198,14 @@ test_decrypt_then_deserialize_sqlite :: proc(t: ^testing.T) {
|
||||
sql: cstring = "SELECT path FROM envr_env_files"
|
||||
stmt: sqlite.Stmt
|
||||
rc = sqlite.prepare_v2(mem_db, sql, -1, &stmt, nil)
|
||||
testing.expect(t, rc == sqlite.OK, "prepare failed")
|
||||
testing.expect_value(t, rc, sqlite.OK)
|
||||
if rc != sqlite.OK {
|
||||
return
|
||||
}
|
||||
defer sqlite.finalize(stmt)
|
||||
|
||||
rc = sqlite.step(stmt)
|
||||
testing.expect(t, rc == sqlite.ROW, "expected at least one row")
|
||||
testing.expect_value(t, rc, sqlite.ROW)
|
||||
if rc == sqlite.ROW {
|
||||
path := string(sqlite.column_text(stmt, 0))
|
||||
testing.expect(t, len(path) > 0, "path should not be empty")
|
||||
@@ -275,15 +275,7 @@ test_full_db_cycle :: proc(t: ^testing.T) {
|
||||
}
|
||||
defer delete(plaintext2)
|
||||
|
||||
testing.expect(
|
||||
t,
|
||||
len(plaintext2) == len(original_data),
|
||||
fmt.tprintf(
|
||||
"double round-trip size mismatch: expected %d, got %d",
|
||||
len(original_data),
|
||||
len(plaintext2),
|
||||
),
|
||||
)
|
||||
testing.expect_value(t, len(plaintext2), len(original_data))
|
||||
|
||||
os.remove(data_path)
|
||||
os.remove(envr_dir_path)
|
||||
@@ -317,7 +309,7 @@ test_ssh_key_parse_from_fixtures :: proc(t: ^testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
testing.expect(t, len(x25519_pairs) == 1, "should have 1 x25519 keypair")
|
||||
testing.expect_value(t, len(x25519_pairs), 1)
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -327,7 +319,7 @@ test_config_load_with_fixture_key :: proc(t: ^testing.T) {
|
||||
delete(cfg.keys)
|
||||
}
|
||||
|
||||
testing.expect(t, len(cfg.keys) == 1, "should have 1 key")
|
||||
testing.expect_value(t, len(cfg.keys), 1)
|
||||
|
||||
key := cfg.keys[0]
|
||||
|
||||
|
||||
46
db_test.odin
46
db_test.odin
@@ -81,7 +81,7 @@ test_db_insert_or_replace :: proc(t: ^testing.T) {
|
||||
results, list_ok := db_list(&db)
|
||||
testing.expect(t, list_ok, "list should succeed")
|
||||
|
||||
testing.expect(t, len(results) == 1, "should have 1 row, not 2")
|
||||
testing.expect_value(t, len(results), 1)
|
||||
|
||||
fetched, fetch_ok := db_fetch(&db, "/project/.env")
|
||||
testing.expect(t, fetch_ok, "fetch should succeed")
|
||||
@@ -149,7 +149,7 @@ test_db_list_empty :: proc(t: ^testing.T) {
|
||||
|
||||
results, list_ok := db_list(&db)
|
||||
testing.expect(t, list_ok, "list should succeed on empty db")
|
||||
testing.expect(t, len(results) == 0, "should have 0 rows")
|
||||
testing.expect_value(t, len(results), 0)
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -276,11 +276,11 @@ test_get_git_remotes_single :: proc(t: ^testing.T) {
|
||||
config_content := "[core]\n\trepositoryformatversion = 0\n[remote \"origin\"]\n\turl = git@github.com:user/repo.git\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n"
|
||||
config_path := fmt.tprintf("%s/config", git_dir)
|
||||
err := os.write_entire_file(config_path, transmute([]u8)config_content)
|
||||
testing.expect(t, err == nil, "should write .git/config")
|
||||
testing.expect_value(t, err, nil)
|
||||
|
||||
remotes := get_git_remotes(base, context.temp_allocator)
|
||||
|
||||
testing.expect(t, len(remotes) == 1, "should find 1 remote")
|
||||
testing.expect_value(t, len(remotes), 1)
|
||||
if len(remotes) != 1 do return
|
||||
testing.expect_value(t, remotes[0], "git@github.com:user/repo.git")
|
||||
}
|
||||
@@ -296,11 +296,11 @@ test_get_git_remotes_multiple :: proc(t: ^testing.T) {
|
||||
config_content := "[remote \"origin\"]\n\turl = git@github.com:user/repo.git\n[remote \"upstream\"]\n\turl = https://gitlab.com/upstream/repo.git\n"
|
||||
config_path := fmt.tprintf("%s/config", git_dir)
|
||||
err := os.write_entire_file(config_path, transmute([]u8)config_content)
|
||||
testing.expect(t, err == nil, "should write .git/config")
|
||||
testing.expect_value(t, err, nil)
|
||||
|
||||
remotes := get_git_remotes(base, context.temp_allocator)
|
||||
|
||||
testing.expect(t, len(remotes) == 2, "should find 2 remotes")
|
||||
testing.expect_value(t, len(remotes), 2)
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -310,7 +310,7 @@ test_get_git_remotes_no_config :: proc(t: ^testing.T) {
|
||||
|
||||
remotes := get_git_remotes(base, context.temp_allocator)
|
||||
|
||||
testing.expect(t, len(remotes) == 0, "should return empty when no .git/config")
|
||||
testing.expect_value(t, len(remotes), 0)
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -324,11 +324,11 @@ test_get_git_remotes_no_remotes :: proc(t: ^testing.T) {
|
||||
config_content := "[core]\n\trepositoryformatversion = 0\n\tbare = false\n"
|
||||
config_path := fmt.tprintf("%s/config", git_dir)
|
||||
err := os.write_entire_file(config_path, transmute([]u8)config_content)
|
||||
testing.expect(t, err == nil, "should write .git/config")
|
||||
testing.expect_value(t, err, nil)
|
||||
|
||||
remotes := get_git_remotes(base, context.temp_allocator)
|
||||
|
||||
testing.expect(t, len(remotes) == 0, "should return empty when no remote sections")
|
||||
testing.expect_value(t, len(remotes), 0)
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -338,7 +338,7 @@ test_new_env_file :: proc(t: ^testing.T) {
|
||||
|
||||
env_path := fmt.tprintf("%s/.env", base)
|
||||
err := os.write_entire_file(env_path, "SECRET=value\n")
|
||||
testing.expect(t, err == nil, ".env file should exists")
|
||||
testing.expect_value(t, err, nil)
|
||||
|
||||
file, ok := new_env_file(env_path)
|
||||
testing.expect(t, ok, "new_env_file should succeed")
|
||||
@@ -350,8 +350,8 @@ test_new_env_file :: proc(t: ^testing.T) {
|
||||
|
||||
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_value(t, file.contents, "SECRET=value\n")
|
||||
testing.expect_value(t, len(file.sha256), 64)
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -366,7 +366,7 @@ test_closing_db_has_no_leaks :: proc(t: ^testing.T) {
|
||||
defer os.remove_all(base)
|
||||
|
||||
cfg_path, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
|
||||
testing.expect(t, err == nil, "cfgPath should build successfully")
|
||||
testing.expect_value(t, err, nil)
|
||||
|
||||
{
|
||||
cfg := new_config([]string{"fixtures/keys/insecure-test-key"}, cfg_path)
|
||||
@@ -385,7 +385,7 @@ test_open_existing_db_has_no_leaks :: proc(t: ^testing.T) {
|
||||
defer os.remove_all(base)
|
||||
|
||||
cfg_path, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
|
||||
testing.expect(t, err == nil, "cfgPath should build successfully")
|
||||
testing.expect_value(t, err, nil)
|
||||
|
||||
{
|
||||
cfg := new_config([]string{"fixtures/keys/insecure-test-key"}, cfg_path)
|
||||
@@ -422,7 +422,7 @@ test_db_sync_noop :: proc(t: ^testing.T) {
|
||||
env_path := fmt.tprintf("%s/.env", base)
|
||||
content := "KEY=value\n"
|
||||
write_err := os.write_entire_file(env_path, transmute([]u8)content)
|
||||
testing.expect(t, write_err == nil, "should write .env file")
|
||||
testing.expect_value(t, write_err, nil)
|
||||
|
||||
digest := hash.hash_bytes(
|
||||
hash.Algorithm.SHA256,
|
||||
@@ -441,8 +441,8 @@ test_db_sync_noop :: proc(t: ^testing.T) {
|
||||
db_insert(&db, f)
|
||||
|
||||
result, sync_err := db_sync(&db, &f)
|
||||
testing.expect(t, sync_err == .None, "sync should not error")
|
||||
testing.expect(t, result == {}, "should be noop")
|
||||
testing.expect_value(t, sync_err, SyncError.None)
|
||||
testing.expect_value(t, result, nil)
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -453,7 +453,7 @@ test_db_sync_backed_up :: proc(t: ^testing.T) {
|
||||
env_path := fmt.tprintf("%s/.env", base)
|
||||
changed_content := "KEY=changed\n"
|
||||
write_err := os.write_entire_file(env_path, transmute([]u8)changed_content)
|
||||
testing.expect(t, write_err == nil, "should write .env file")
|
||||
testing.expect_value(t, write_err, nil)
|
||||
|
||||
db, ok := db_init()
|
||||
testing.expect(t, ok, "failed to create test db")
|
||||
@@ -464,7 +464,7 @@ test_db_sync_backed_up :: proc(t: ^testing.T) {
|
||||
db_insert(&db, f)
|
||||
|
||||
result, sync_err := db_sync(&db, &f)
|
||||
testing.expect(t, sync_err == .None, "sync should not error")
|
||||
testing.expect_value(t, sync_err, SyncError.None)
|
||||
testing.expect(t, .BackedUp in result, "should be backed up")
|
||||
}
|
||||
|
||||
@@ -485,11 +485,11 @@ test_db_sync_restored :: proc(t: ^testing.T) {
|
||||
db_insert(&db, f)
|
||||
|
||||
result, err := db_sync(&db, &f)
|
||||
testing.expect(t, err == .None, "sync should not error")
|
||||
testing.expect_value(t, err, SyncError.None)
|
||||
testing.expect(t, .Restored in result, "should be restored")
|
||||
|
||||
data, read_err := os.read_entire_file_from_path(env_path, context.temp_allocator)
|
||||
testing.expect(t, read_err == nil, "file should exist after restore")
|
||||
testing.expect_value(t, read_err, nil)
|
||||
if read_err == nil {
|
||||
testing.expect_value(t, string(data), "SECRET=value")
|
||||
}
|
||||
@@ -522,7 +522,7 @@ test_db_sync_moved :: proc(t: ^testing.T) {
|
||||
config_content := "[remote \"origin\"]\n\turl = git@github.com:user/repo.git\n"
|
||||
config_path := fmt.tprintf("%s/config", git_dir)
|
||||
write_err := os.write_entire_file(config_path, transmute([]u8)config_content)
|
||||
testing.expect(t, write_err == nil, "should write .git/config")
|
||||
testing.expect_value(t, write_err, nil)
|
||||
|
||||
db, ok := db_init()
|
||||
testing.expect(t, ok, "failed to create test db")
|
||||
@@ -540,7 +540,7 @@ test_db_sync_moved :: proc(t: ^testing.T) {
|
||||
testing.expect(t, db_insert(&db, f), "insert should succeed")
|
||||
|
||||
result, err := db_sync(&db, &f)
|
||||
testing.expect(t, err == .None, "sync should not error")
|
||||
testing.expect_value(t, err, SyncError.None)
|
||||
if err != .None do return
|
||||
testing.expect(t, .DirUpdated in result, "should have DirUpdated flag")
|
||||
testing.expect(t, .Restored in result, "should have Restored flag")
|
||||
|
||||
@@ -26,7 +26,7 @@ find_repos :: proc(roots: []string, results: ^[dynamic]string, thread_count: int
|
||||
pool.threads = make([]^thread.Thread, thread_count)
|
||||
|
||||
for root in roots {
|
||||
root_clone, _ := strings.clone(root)
|
||||
root_clone := strings.clone(root)
|
||||
append(&pool.queue, root_clone)
|
||||
sync.atomic_sema_post(&pool.queue_sema)
|
||||
}
|
||||
@@ -97,7 +97,7 @@ process_repo_dir :: proc(pool: ^RepoPool, dir_path: string) {
|
||||
defer linux.close(fd)
|
||||
|
||||
if has_git_dir(fd) {
|
||||
cloned, _ := strings.clone(dir_path)
|
||||
cloned := strings.clone(dir_path)
|
||||
sync.mutex_lock(&pool.results_lock)
|
||||
append(pool.results, cloned)
|
||||
sync.mutex_unlock(&pool.results_lock)
|
||||
|
||||
@@ -137,7 +137,7 @@ collect_results :: proc(env: TestEnv, args: []string, opts: WalkOptions) -> [dyn
|
||||
if len(stripped) > 0 && stripped[0] == os.Path_Separator {
|
||||
stripped = stripped[1:]
|
||||
}
|
||||
new_r, _ := strings.clone(stripped)
|
||||
new_r := strings.clone(stripped)
|
||||
delete(r)
|
||||
results[i] = new_r
|
||||
}
|
||||
|
||||
@@ -83,6 +83,6 @@ test_scan_path_empty_dir :: proc(t: ^testing.T) {
|
||||
results, ok := scan_path(base, cfg)
|
||||
defer delete(results)
|
||||
testing.expect(t, ok, "scan_path should succeed")
|
||||
testing.expect(t, len(results) == 0, fmt.tprintf("expected 0 results, got %d", len(results)))
|
||||
testing.expect_value(t, len(results), 0)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,12 @@ DESERIALIZE_FLAGS :: bit_set[DESERIALIZE_FLAG]
|
||||
DESERIALIZE_FLAG :: enum u32 {
|
||||
FREEONCLOSE = 1,
|
||||
RESIZEABLE = 2,
|
||||
READONLY = 4,
|
||||
}
|
||||
|
||||
SERIALIZE_FLAGS :: bit_set[SERIALIZE_FLAG]
|
||||
SERIALIZE_FLAG :: enum u32 {
|
||||
NOCOPY = 1,
|
||||
}
|
||||
|
||||
foreign lib {
|
||||
@@ -43,7 +49,7 @@ foreign lib {
|
||||
@(link_name = "sqlite3_changes")
|
||||
changes :: proc(db: Db) -> c.int ---
|
||||
@(link_name = "sqlite3_serialize")
|
||||
serialize :: proc(db: Db, zSchema: cstring, piSize: ^i64, mFlags: u32) -> [^]u8 ---
|
||||
serialize :: proc(db: Db, zSchema: cstring, piSize: ^i64, mFlags: SERIALIZE_FLAGS) -> [^]u8 ---
|
||||
@(link_name = "sqlite3_deserialize")
|
||||
deserialize :: proc(db: Db, zSchema: cstring, pData: [^]u8, szDb: i64, szBuf: i64, mFlags: DESERIALIZE_FLAGS) -> c.int ---
|
||||
@(link_name = "sqlite3_malloc64")
|
||||
|
||||
@@ -46,15 +46,7 @@ test_private_key_pub_matches_public_key :: proc(t: ^testing.T) {
|
||||
kp, priv_ok := parse_ssh_private_key(TEST_KEY_DIR + "/test_ed25519")
|
||||
testing.expect(t, priv_ok, "expected private key to parse")
|
||||
|
||||
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,
|
||||
),
|
||||
)
|
||||
testing.expect_value(t, pub_from_pub, kp.Public)
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -64,12 +56,11 @@ test_read_wire_string :: proc(t: ^testing.T) {
|
||||
|
||||
s, ok := read_wire_string(data, &offset)
|
||||
testing.expect(t, ok, "expected read_wire_string to succeed")
|
||||
testing.expect(t, s == "hello", fmt.tprintf("expected 'hello', got %q", s))
|
||||
testing.expect(t, offset == 9, fmt.tprintf("expected offset 9, got %d", offset))
|
||||
testing.expect_value(t, s, "hello")
|
||||
testing.expect_value(t, offset, 9)
|
||||
|
||||
s2, ok2 := read_wire_string(data, &offset)
|
||||
testing.expect(t, ok2, "expected second read to succeed")
|
||||
testing.expect(t, s2 == "", "expected empty string")
|
||||
testing.expect_value(t, s2, "")
|
||||
}
|
||||
|
||||
|
||||
|
||||
14
table.odin
14
table.odin
@@ -3,7 +3,6 @@ package main
|
||||
import "core:fmt"
|
||||
import "core:io"
|
||||
import "core:text/table"
|
||||
import "core:unicode/utf8"
|
||||
|
||||
decorations := table.Decorations {
|
||||
"┌",
|
||||
@@ -19,20 +18,17 @@ decorations := table.Decorations {
|
||||
"─",
|
||||
}
|
||||
|
||||
// TODO: Optimize ansi_aware_width
|
||||
ansi_aware_width :: proc(str: string) -> int {
|
||||
buf: [4096]byte
|
||||
pos := 0
|
||||
i := 0
|
||||
for i < len(str) {
|
||||
ansi_aware_width :: proc(str: string) -> int #no_bounds_check {
|
||||
width := 0
|
||||
for i := 0; i < len(str); {
|
||||
if i + 1 < len(str) && str[i] == 0x1b && str[i + 1] == '[' {
|
||||
i += 2
|
||||
for i < len(str) {c := str[i]; i += 1; if c >= 0x40 && c <= 0x7E {break}}
|
||||
} else {
|
||||
buf[pos] = str[i]; pos += 1; i += 1
|
||||
width += 1
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
_, _, width := utf8.grapheme_count(string(buf[:pos]))
|
||||
return width
|
||||
}
|
||||
|
||||
|
||||
@@ -21,9 +21,9 @@ test_ansi_aware_width_with_color_codes :: proc(t: ^testing.T) {
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_ansi_aware_width_unicode :: proc(t: ^testing.T) {
|
||||
testing.expect_value(t, ansi_aware_width("\u2713 Available"), 11)
|
||||
testing.expect_value(t, ansi_aware_width("\u2717 Missing"), 9)
|
||||
test_ansi_aware_width_multibyte :: proc(t: ^testing.T) {
|
||||
testing.expect_value(t, ansi_aware_width("\u2713 Available"), 13)
|
||||
testing.expect_value(t, ansi_aware_width("\u2717 Missing"), 11)
|
||||
}
|
||||
|
||||
@(test)
|
||||
|
||||
Reference in New Issue
Block a user