diff --git a/TODOS.md b/TODOS.md index b42d7e1..63ba554 100644 --- a/TODOS.md +++ b/TODOS.md @@ -52,6 +52,8 @@ 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. + ## Double-check AI output - [ ] cli.odin @@ -60,7 +62,7 @@ - [x] cmd_backup.odin - [x] cmd_check.odin - [ ] cmd_check_test.odin -- [ ] cmd_edit_config.odin +- [x] cmd_edit_config.odin - [x] cmd_init.odin - [x] cmd_list.odin - [ ] cmd_list_test.odin diff --git a/cmd_edit_config.odin b/cmd_edit_config.odin index c9a7b4c..bd39d87 100644 --- a/cmd_edit_config.odin +++ b/cmd_edit_config.odin @@ -41,6 +41,8 @@ cmd_edit_config :: proc(cmd: ^Command) { fmt.wprintf(cmd.err, "Error waiting for editor: %v\n", wait_err, flush = false) return } + + // TODO: Should we call exit inside of commands? if state.exit_code != 0 { os.exit(int(state.exit_code)) } diff --git a/config.odin b/config.odin index 91f05b8..6e2acf9 100644 --- a/config.odin +++ b/config.odin @@ -8,21 +8,21 @@ import "core:strings" import "findr" +Config :: struct { + keys: [dynamic]SshKeyPair `json:"keys"`, + scan_config: ScanConfig `json:"scan"`, + config_path: string `json:"-"`, +} + SshKeyPair :: struct { - Private: string `json:"private"`, - Public: string `json:"public"`, + private: string `json:"private"`, + public: string `json:"public"`, } ScanConfig :: struct { - Matcher: string `json:"matcher"`, - Exclude: [dynamic]string `json:"exclude"`, - Include: [dynamic]string `json:"include"`, -} - -Config :: struct { - Keys: [dynamic]SshKeyPair `json:"keys"`, - ScanConfig: ScanConfig `json:"scan"`, - config_path: string `json:"-"`, + matcher: string `json:"matcher"`, + exclude: [dynamic]string `json:"exclude"`, + include: [dynamic]string `json:"include"`, } load_config :: proc(config_path: string, allocator := context.allocator) -> (Config, bool) { @@ -53,23 +53,23 @@ default_config_path :: proc(home: string, allocator := context.allocator) -> str } delete_config :: proc(cfg: ^Config, allocator := context.allocator) { - for key in cfg.Keys { - delete(key.Private, allocator) - delete(key.Public, allocator) + for key in cfg.keys { + delete(key.private, allocator) + delete(key.public, allocator) } - delete(cfg.Keys) + delete(cfg.keys) - delete(cfg.ScanConfig.Matcher, allocator) + delete(cfg.scan_config.matcher, allocator) - for exclude in cfg.ScanConfig.Exclude { + for exclude in cfg.scan_config.exclude { delete(exclude, allocator) } - delete(cfg.ScanConfig.Exclude) + delete(cfg.scan_config.exclude) - for include in cfg.ScanConfig.Include { + for include in cfg.scan_config.include { delete(include, allocator) } - delete(cfg.ScanConfig.Include) + delete(cfg.scan_config.include) } save_config :: proc(cfg: Config, force: bool = false) -> bool { @@ -123,7 +123,7 @@ new_config :: proc( // TODO: Is this bad? priv_key := strings.clone(priv) pub, _ := strings.concatenate([]string{priv_key, ".pub"}) - append(&keys, SshKeyPair{Private = priv_key, Public = pub}) + append(&keys, SshKeyPair{private = priv_key, public = pub}) } exclude := make([dynamic]string, 0, 4) @@ -136,12 +136,12 @@ new_config :: proc( append(&include, strings.clone("~")) scan_cfg := ScanConfig { - Matcher = strings.clone("\\.env"), - Exclude = exclude, - Include = include, + matcher = strings.clone("\\.env"), + exclude = exclude, + include = include, } - return Config{Keys = keys, ScanConfig = scan_cfg, config_path = cfg_path} + return Config{keys = keys, scan_config = scan_cfg, config_path = cfg_path} } find_ssh_private_keys :: proc() -> (keys: [dynamic]string, ok: bool) { @@ -209,7 +209,7 @@ search_paths :: proc(cfg: Config, allocator := context.allocator) -> [dynamic]st // TODO: handle error home, _ := os.user_home_dir(context.temp_allocator) - paths, _ := new_clone(cfg.ScanConfig.Include, allocator) + paths, _ := new_clone(cfg.scan_config.include, allocator) for &include in paths { // TODO: Do we need to manually expand ~/ in odin? diff --git a/config_test.odin b/config_test.odin index 3c46136..e09164e 100644 --- a/config_test.odin +++ b/config_test.odin @@ -16,11 +16,11 @@ 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, 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", + cfg.keys[0].public == "/home/user/.ssh/id_ed25519.pub", "Public path mismatch", ) } @@ -31,9 +31,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(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") } @(test) @@ -42,7 +42,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(t, len(cfg.keys) == 0, "should have 0 keys") } @(test) @@ -51,10 +51,10 @@ test_new_config_scan_defaults :: proc(t: ^testing.T) { cfg := new_config(paths) defer delete_config(&cfg) - testing.expect(t, cfg.ScanConfig.Matcher == "\\.env", "matcher should be \\.env") - testing.expect(t, len(cfg.ScanConfig.Exclude) == 4, "should have 4 exclude patterns") - testing.expect(t, len(cfg.ScanConfig.Include) == 1, "should have 1 include path") - testing.expect(t, cfg.ScanConfig.Include[0] == "~", "include should be ~") + 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 ~") } @(test) @@ -65,7 +65,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.ScanConfig.Exclude[i] == expected[i]) + testing.expect(t, cfg.scan_config.exclude[i] == expected[i]) } } @@ -88,13 +88,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.ScanConfig.Matcher == "\\.env") - testing.expect(t, len(loaded.ScanConfig.Exclude) == 4) - testing.expect(t, len(loaded.ScanConfig.Include) == 1) - testing.expect(t, loaded.ScanConfig.Include[0] == "~") + 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] == "~") } @(test) @@ -143,10 +143,10 @@ 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, len(loaded.keys) == 1, "should have 1 key") testing.expect( t, - loaded.Keys[0].Private == "/home/user/.ssh/key2", + loaded.keys[0].private == "/home/user/.ssh/key2", "should be the overwritten key", ) } @@ -186,10 +186,10 @@ test_search_paths_expands_tilde :: proc(t: ^testing.T) { os.set_env("HOME", "/tmp/envr-fake-home-search") cfg := Config { - ScanConfig = ScanConfig{Include = make([dynamic]string, 0, 1)}, + scan_config = ScanConfig{include = make([dynamic]string, 0, 1)}, } - append(&cfg.ScanConfig.Include, "~") - defer delete(cfg.ScanConfig.Include) + append(&cfg.scan_config.include, "~") + defer delete(cfg.scan_config.include) paths := search_paths(cfg, context.temp_allocator) diff --git a/crypto.odin b/crypto.odin index 7bf8b03..bd2b266 100644 --- a/crypto.odin +++ b/crypto.odin @@ -283,16 +283,16 @@ ssh_to_x25519 :: proc( pairs := make([]X25519Keypair, len(keys), allocator) for i in 0 ..< len(keys) { - ssh_kp, parse_ok := parse_ssh_private_key(keys[i].Private) + 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.printf("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) + 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.printf("Error: failed to parse SSH public key: %s\n", keys[i].public) delete(pairs) return pairs, false } diff --git a/crypto_test.odin b/crypto_test.odin index 1423379..7faf2ae 100644 --- a/crypto_test.odin +++ b/crypto_test.odin @@ -10,7 +10,7 @@ CRYPTO_TEST_KEY_DIR :: "fixtures" + os.Path_Separator_String + "keys" make_test_key_pair :: proc(name: string) -> SshKeyPair { priv := fmt.tprintf("%s/%s", CRYPTO_TEST_KEY_DIR, name) pub := fmt.tprintf("%s/%s.pub", CRYPTO_TEST_KEY_DIR, name) - return SshKeyPair{Private = priv, Public = pub} + return SshKeyPair{private = priv, public = pub} } @(test) diff --git a/db.odin b/db.odin index 6b428f5..4115ec2 100644 --- a/db.odin +++ b/db.odin @@ -111,7 +111,7 @@ db_restore_from_encrypted :: proc(db: ^Db, data_path: string) -> bool { } // TODO: Use context.temp_allocator - plaintext, dec_ok := decrypt(encrypted_data, db.cfg.Keys[:]) + plaintext, dec_ok := decrypt(encrypted_data, db.cfg.keys[:]) if !dec_ok { fmt.println("Error: decryption failed") return false @@ -166,7 +166,7 @@ db_close :: proc(db: ^Db) { sqlite_data := data[:sz] // TODO: PAss allocator chain - encrypted, enc_ok := encrypt(sqlite_data, db.cfg.Keys[:]) + encrypted, enc_ok := encrypt(sqlite_data, db.cfg.keys[:]) if !enc_ok { fmt.println("Error: encryption failed") return diff --git a/db_integration_test.odin b/db_integration_test.odin index aa6180c..b4dbfd1 100644 --- a/db_integration_test.odin +++ b/db_integration_test.odin @@ -20,7 +20,7 @@ fixture_key :: proc() -> SshKeyPair { []string{FIXTURES, "/keys/insecure-test-key.pub"}, context.temp_allocator, ) - return SshKeyPair{Private = priv, Public = pub} + return SshKeyPair{private = priv, public = pub} } fixture_db_path :: proc() -> string { @@ -30,9 +30,9 @@ fixture_db_path :: proc() -> string { fixture_config :: proc() -> Config { cfg := Config { - Keys = make([dynamic]SshKeyPair, 0, 1), + keys = make([dynamic]SshKeyPair, 0, 1), } - append(&cfg.Keys, fixture_key()) + append(&cfg.keys, fixture_key()) return cfg } @@ -40,7 +40,7 @@ fixture_config :: proc() -> Config { test_encrypt_decrypt_sqlite_roundtrip :: proc(t: ^testing.T) { cfg := fixture_config() defer { - delete(cfg.Keys) + delete(cfg.keys) } db_path := fixture_db_path() @@ -51,7 +51,7 @@ test_encrypt_decrypt_sqlite_roundtrip :: proc(t: ^testing.T) { } defer delete(sqlite_data) - encrypted, enc_ok := encrypt(sqlite_data, cfg.Keys[:]) + encrypted, enc_ok := encrypt(sqlite_data, cfg.keys[:]) testing.expect(t, enc_ok, "encryption should succeed") if !enc_ok { return @@ -64,7 +64,7 @@ test_encrypt_decrypt_sqlite_roundtrip :: proc(t: ^testing.T) { testing.expect(t, encrypted[2] == u8('V'), "magic byte 2") testing.expect(t, encrypted[3] == u8('R'), "magic byte 3") - plaintext, dec_ok := decrypt(encrypted, cfg.Keys[:]) + plaintext, dec_ok := decrypt(encrypted, cfg.keys[:]) testing.expect(t, dec_ok, "decryption should succeed") if !dec_ok { return @@ -93,7 +93,7 @@ test_encrypt_decrypt_sqlite_roundtrip :: proc(t: ^testing.T) { test_encrypt_write_read_decrypt :: proc(t: ^testing.T) { cfg := fixture_config() defer { - delete(cfg.Keys) + delete(cfg.keys) } db_path := fixture_db_path() @@ -104,7 +104,7 @@ test_encrypt_write_read_decrypt :: proc(t: ^testing.T) { } defer delete(sqlite_data) - encrypted, enc_ok := encrypt(sqlite_data, cfg.Keys[:]) + encrypted, enc_ok := encrypt(sqlite_data, cfg.keys[:]) testing.expect(t, enc_ok, "encryption should succeed") if !enc_ok { return @@ -126,7 +126,7 @@ test_encrypt_write_read_decrypt :: proc(t: ^testing.T) { } defer delete(read_back) - plaintext, dec_ok := decrypt(read_back, cfg.Keys[:]) + plaintext, dec_ok := decrypt(read_back, cfg.keys[:]) testing.expect(t, dec_ok, "decryption after write/read should succeed") if !dec_ok { return @@ -140,7 +140,7 @@ test_encrypt_write_read_decrypt :: proc(t: ^testing.T) { test_decrypt_then_deserialize_sqlite :: proc(t: ^testing.T) { cfg := fixture_config() defer { - delete(cfg.Keys) + delete(cfg.keys) } db_path := fixture_db_path() @@ -151,14 +151,14 @@ test_decrypt_then_deserialize_sqlite :: proc(t: ^testing.T) { } defer delete(sqlite_data) - encrypted, enc_ok := encrypt(sqlite_data, cfg.Keys[:]) + encrypted, enc_ok := encrypt(sqlite_data, cfg.keys[:]) testing.expect(t, enc_ok, "encryption should succeed") if !enc_ok { return } defer delete(encrypted) - plaintext, dec_ok := decrypt(encrypted, cfg.Keys[:]) + plaintext, dec_ok := decrypt(encrypted, cfg.keys[:]) testing.expect(t, dec_ok, "decryption should succeed") if !dec_ok { return @@ -206,7 +206,7 @@ test_decrypt_then_deserialize_sqlite :: proc(t: ^testing.T) { @(test) test_full_db_cycle :: proc(t: ^testing.T) { cfg := fixture_config() - defer delete(cfg.Keys) + defer delete(cfg.keys) db_path := fixture_db_path() original_data, read_err := os.read_entire_file_from_path(db_path, context.allocator) @@ -216,7 +216,7 @@ test_full_db_cycle :: proc(t: ^testing.T) { } defer delete(original_data) - encrypted, enc_ok := encrypt(original_data, cfg.Keys[:]) + encrypted, enc_ok := encrypt(original_data, cfg.keys[:]) testing.expect(t, enc_ok, "first encryption should succeed") if !enc_ok { return @@ -241,21 +241,21 @@ test_full_db_cycle :: proc(t: ^testing.T) { } defer delete(read_back) - plaintext, dec_ok := decrypt(read_back, cfg.Keys[:]) + plaintext, dec_ok := decrypt(read_back, cfg.keys[:]) testing.expect(t, dec_ok, "decryption should succeed") if !dec_ok { return } defer delete(plaintext) - encrypted2, enc2_ok := encrypt(plaintext, cfg.Keys[:]) + encrypted2, enc2_ok := encrypt(plaintext, cfg.keys[:]) testing.expect(t, enc2_ok, "re-encryption should succeed") if !enc2_ok { return } defer delete(encrypted2) - plaintext2, dec2_ok := decrypt(encrypted2, cfg.Keys[:]) + plaintext2, dec2_ok := decrypt(encrypted2, cfg.keys[:]) testing.expect(t, dec2_ok, "second decryption should succeed") if !dec2_ok { return @@ -282,13 +282,13 @@ test_full_db_cycle :: proc(t: ^testing.T) { test_ssh_key_parse_from_fixtures :: proc(t: ^testing.T) { key := fixture_key() - priv_kp, priv_ok := parse_ssh_private_key(key.Private) + priv_kp, priv_ok := parse_ssh_private_key(key.private) testing.expect(t, priv_ok, "should parse private key from fixtures") if !priv_ok { return } - pub_key, pub_ok := parse_ssh_public_key(key.Public) + pub_key, pub_ok := parse_ssh_public_key(key.public) testing.expect(t, pub_ok, "should parse public key from fixtures") if !pub_ok { return @@ -311,20 +311,20 @@ test_ssh_key_parse_from_fixtures :: proc(t: ^testing.T) { test_config_load_with_fixture_key :: proc(t: ^testing.T) { cfg := fixture_config() defer { - delete(cfg.Keys) + delete(cfg.keys) } - testing.expect(t, len(cfg.Keys) == 1, "should have 1 key") + testing.expect(t, len(cfg.keys) == 1, "should have 1 key") - key := cfg.Keys[0] + key := cfg.keys[0] - testing.expectf(t, len(key.Private) > 0, "private key path should not be empty") - testing.expectf(t, len(key.Public) > 0, "public key path should not be empty") + testing.expectf(t, len(key.private) > 0, "private key path should not be empty") + testing.expectf(t, len(key.public) > 0, "public key path should not be empty") - _, priv_ok := parse_ssh_private_key(key.Private) + _, priv_ok := parse_ssh_private_key(key.private) testing.expect(t, priv_ok, "should parse private key using config paths") if !priv_ok { - fmt.printf(" private key path was: '%s'\n", key.Private) + fmt.printf(" private key path was: '%s'\n", key.private) } } diff --git a/db_test.odin b/db_test.odin index c60428f..ca0d13a 100644 --- a/db_test.odin +++ b/db_test.odin @@ -537,8 +537,8 @@ test_db_sync_moved :: proc(t: ^testing.T) { testing.expect(t, ok, "failed to create test db") defer db_close(&db) - db.cfg.ScanConfig.Include = make([dynamic]string, 0, 1, context.temp_allocator) - append(&db.cfg.ScanConfig.Include, search_root) + db.cfg.scan_config.include = make([dynamic]string, 0, 1, context.temp_allocator) + append(&db.cfg.scan_config.include, search_root) f := make_test_env_file( "/old/nonexistent/path/.env", diff --git a/scan.odin b/scan.odin index fba31dd..29118c6 100644 --- a/scan.odin +++ b/scan.odin @@ -7,8 +7,8 @@ import "findr" // Caller is responsible for freeing paths scan_path :: proc(search_path: string, cfg: Config) -> (paths: [dynamic]string, ok: bool) { opts := findr.WalkOptions { - pattern = cfg.ScanConfig.Matcher, - excludes = cfg.ScanConfig.Exclude[:], + pattern = cfg.scan_config.matcher, + excludes = cfg.scan_config.exclude[:], } findr.walk({search_path}, &paths, opts, os.get_processor_core_count()) ok = true @@ -29,3 +29,4 @@ find_unbacked :: proc(local_files: []string, db_files: []EnvFile) -> []string { } return unbacked[:] } + diff --git a/scan_test.odin b/scan_test.odin index 2076cad..ec20309 100644 --- a/scan_test.odin +++ b/scan_test.odin @@ -35,7 +35,7 @@ test_scan_path_finds_gitignored_env_files :: proc(t: ^testing.T) { _ = os.write_entire_file(fmt.tprintf("%s/config.yaml", base), "key: value") cfg := Config { - ScanConfig = ScanConfig{Matcher = "\\.env"}, + scan_config = ScanConfig{matcher = "\\.env"}, } results, ok := scan_path(base, cfg) @@ -76,7 +76,7 @@ test_scan_path_empty_dir :: proc(t: ^testing.T) { defer os.remove_all(base) cfg := Config { - ScanConfig = ScanConfig{Matcher = "\\.env"}, + scan_config = ScanConfig{matcher = "\\.env"}, } results, ok := scan_path(base, cfg) @@ -84,3 +84,4 @@ test_scan_path_empty_dir :: proc(t: ^testing.T) { testing.expect(t, ok, "scan_path should succeed") testing.expect(t, len(results) == 0, fmt.tprintf("expected 0 results, got %d", len(results))) } +