fix: fixing leaks.

This commit is contained in:
2026-06-12 11:05:34 -04:00
parent e966050137
commit a28b8d10bc
5 changed files with 207 additions and 203 deletions

View File

@@ -6,74 +6,79 @@ import "core:path/filepath"
import "core:strings" import "core:strings"
cmd_check :: proc(cmd: ^Command) { cmd_check :: proc(cmd: ^Command) {
check_path: string feats := check_features()
if len(cmd.args) > 0 {
check_path = cmd.args[0]
} else {
cwd, cwd_err := os.get_working_directory(context.allocator)
if cwd_err != nil {
fmt.printf("Error getting current directory: %v\n", cwd_err)
return
}
check_path = cwd
}
abs_path: string check_path: string
if filepath.is_abs(check_path) { if len(cmd.args) > 0 {
abs_path = check_path check_path = cmd.args[0]
} else { } else {
resolved, abs_err := filepath.abs(check_path) cwd, cwd_err := os.get_working_directory(context.allocator)
if abs_err != nil { if cwd_err != nil {
fmt.printf("Error getting absolute path: %v\n", abs_err) fmt.printf("Error getting current directory: %v\n", cwd_err)
return return
} }
abs_path = resolved check_path = cwd
} }
db, db_ok := db_open() abs_path: string
if !db_ok { if filepath.is_abs(check_path) {
return abs_path = check_path
} } else {
defer db_close(&db) resolved, abs_err := filepath.abs(check_path)
if abs_err != nil {
fmt.printf("Error getting absolute path: %v\n", abs_err)
return
}
abs_path = resolved
}
is_dir := os.is_directory(abs_path) db, db_ok := db_open()
if !db_ok {
return
}
defer db_close(&db)
files_in_path: [dynamic]string is_dir := os.is_directory(abs_path)
if is_dir { files_in_path: [dynamic]string
if !can_scan() {
fmt.println("Error: please install fd to use the check command (https://github.com/sharkdp/fd)")
return
}
scanned, scan_ok := scan_path(abs_path, db.cfg) if is_dir {
if !scan_ok { if cant_scan(feats) {
fmt.println("Error scanning directory for .env files") fmt.println(
return "Error: please install fd to use the check command (https://github.com/sharkdp/fd)",
} )
files_in_path = scanned return
} else { }
append(&files_in_path, abs_path)
}
db_files, list_ok := db_list(&db) scanned, scan_ok := scan_path(abs_path, db.cfg)
if !list_ok { if !scan_ok {
return fmt.println("Error scanning directory for .env files")
} return
}
files_in_path = scanned
} else {
append(&files_in_path, abs_path)
}
not_backed := find_unbacked(files_in_path[:], db_files[:]) db_files, list_ok := db_list(&db)
if !list_ok {
return
}
if len(not_backed) == 0 { not_backed := find_unbacked(files_in_path[:], db_files[:])
if len(files_in_path) == 0 {
fmt.println("No .env files found in the specified directory.") if len(not_backed) == 0 {
} else { if len(files_in_path) == 0 {
fmt.println("✓ All .env files in the directory are backed up.") fmt.println("No .env files found in the specified directory.")
} } else {
} else { fmt.println("✓ All .env files in the directory are backed up.")
fmt.printf("Found %d .env file(s) that are not backed up:\n", len(not_backed)) }
for file in not_backed { } else {
fmt.printf(" %s\n", file) fmt.printf("Found %d .env file(s) that are not backed up:\n", len(not_backed))
} for file in not_backed {
fmt.println("\nRun 'envr sync' to back up these files.") fmt.printf(" %s\n", file)
} }
fmt.println("\nRun 'envr sync' to back up these files.")
}
} }

View File

@@ -5,39 +5,44 @@ import "core:testing"
@(test) @(test)
test_find_unbacked_finds_missing :: proc(t: ^testing.T) { test_find_unbacked_finds_missing :: proc(t: ^testing.T) {
local := []string{"/a/.env", "/b/.env", "/c/.env"} 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[:]) result := find_unbacked(local, db[:])
testing.expect(t, len(result) == 1, fmt.aprintf("expected 1 unbacked, got %d", len(result))) testing.expect(t, len(result) == 1, fmt.aprintf("expected 1 unbacked, got %d", len(result)))
if len(result) > 0 { if len(result) > 0 {
testing.expect(t, result[0] == "/c/.env", fmt.aprintf("expected /c/.env, got %s", result[0])) testing.expect(
} t,
result[0] == "/c/.env",
fmt.aprintf("expected /c/.env, got %s", result[0]),
)
}
} }
@(test) @(test)
test_find_unbacked_all_backed :: proc(t: ^testing.T) { test_find_unbacked_all_backed :: proc(t: ^testing.T) {
local := []string{"/a/.env", "/b/.env"} 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[:]) result := find_unbacked(local, db[:])
testing.expect(t, len(result) == 0, fmt.aprintf("expected 0 unbacked, got %d", len(result))) testing.expect(t, len(result) == 0, fmt.aprintf("expected 0 unbacked, got %d", len(result)))
} }
@(test) @(test)
test_find_unbacked_no_local :: proc(t: ^testing.T) { test_find_unbacked_no_local :: proc(t: ^testing.T) {
local: []string local: []string
db := []EnvFile{{Path = "/a/.env"}} db := []EnvFile{{Path = "/a/.env"}}
result := find_unbacked(local, db[:]) result := find_unbacked(local, db[:])
testing.expect(t, len(result) == 0, fmt.aprintf("expected 0 unbacked, got %d", len(result))) testing.expect(t, len(result) == 0, fmt.aprintf("expected 0 unbacked, got %d", len(result)))
} }
@(test) @(test)
test_find_unbacked_none_backed :: proc(t: ^testing.T) { test_find_unbacked_none_backed :: proc(t: ^testing.T) {
local := []string{"/a/.env", "/b/.env"} local := []string{"/a/.env", "/b/.env"}
db: []EnvFile db: []EnvFile
result := find_unbacked(local, db[:]) result := find_unbacked(local, db[:])
testing.expect(t, len(result) == 2, fmt.aprintf("expected 2 unbacked, got %d", len(result))) testing.expect(t, len(result) == 2, fmt.aprintf("expected 2 unbacked, got %d", len(result)))
} }

View File

@@ -4,7 +4,8 @@ import "core:encoding/json"
import "core:fmt" import "core:fmt"
cmd_scan :: proc(cmd: ^Command) { cmd_scan :: proc(cmd: ^Command) {
if !can_scan() { feats := check_features()
if cant_scan(feats) {
fmt.println( fmt.println(
"Error: please install fd to use the scan command (https://github.com/sharkdp/fd)", "Error: please install fd to use the scan command (https://github.com/sharkdp/fd)",
) )

108
scan.odin
View File

@@ -2,23 +2,49 @@ package main
import "core:fmt" import "core:fmt"
import "core:os" import "core:os"
import "core:path/filepath"
import "core:strings" import "core:strings"
import "core:sync" import "core:sync"
fd_counter: sync.Atomic_Mutex fd_counter: sync.Atomic_Mutex
fd_seq: int fd_seq: int
next_fd_tmp_path :: proc() -> string { // Caller is responsible for freeing paths
sync.atomic_mutex_lock(&fd_counter) scan_path :: proc(search_path: string, cfg: Config) -> (paths: [dynamic]string, ok: bool) {
n := fd_seq if is_tty() {
fd_seq += 1 fmt.printf("Searching for all files in \"%s\"...\n", search_path)
sync.atomic_mutex_unlock(&fd_counter) }
return fmt.aprintf("/tmp/envr-fd-%d-%d", os.get_pid(), n) all_files, all_ok := run_fd(build_fd_args(search_path, cfg, true))
if !all_ok {
return
}
if is_tty() {
fmt.printf("Search for unignored fies in \"%s\"...\n", search_path)
}
unignored_files, unignored_ok := run_fd(build_fd_args(search_path, cfg, false))
if !unignored_ok {
return
}
unignored_set := make(map[string]bool, len(unignored_files), context.temp_allocator)
for file in unignored_files {
unignored_set[file] = true
}
for file in all_files {
if !(file in unignored_set) {
append(&paths, file)
}
}
ok = true
return
} }
@(private = "file")
build_fd_args :: proc(search_path: string, cfg: Config, include_ignored: bool) -> []string { build_fd_args :: proc(search_path: string, cfg: Config, include_ignored: bool) -> []string {
args := make([dynamic]string, 0, 3 + 2 * len(cfg.ScanConfig.Exclude) + 2) args_len := 3 + 2 * len(cfg.ScanConfig.Exclude) + 2
args := make([dynamic]string, 0, args_len, context.temp_allocator)
append(&args, "fd") append(&args, "fd")
append(&args, "-a") append(&args, "-a")
append(&args, cfg.ScanConfig.Matcher) append(&args, cfg.ScanConfig.Matcher)
@@ -38,7 +64,7 @@ build_fd_args :: proc(search_path: string, cfg: Config, include_ignored: bool) -
return args[:] return args[:]
} }
run_fd :: proc(args: []string) -> (lines: [dynamic]string, ok: bool) { run_fd :: proc(args: []string) -> (lines: []string, ok: bool) {
tmp_path := next_fd_tmp_path() tmp_path := next_fd_tmp_path()
tmp_file, tmp_err := os.open(tmp_path, os.O_CREATE | os.O_WRONLY | os.O_TRUNC) tmp_file, tmp_err := os.open(tmp_path, os.O_CREATE | os.O_WRONLY | os.O_TRUNC)
if tmp_err != nil { if tmp_err != nil {
@@ -64,7 +90,7 @@ run_fd :: proc(args: []string) -> (lines: [dynamic]string, ok: bool) {
return return
} }
data, read_err := os.read_entire_file_from_path(tmp_path, context.allocator) data, read_err := os.read_entire_file_from_path(tmp_path, context.temp_allocator)
os.remove(tmp_path) os.remove(tmp_path)
if read_err != nil { if read_err != nil {
return return
@@ -77,69 +103,43 @@ run_fd :: proc(args: []string) -> (lines: [dynamic]string, ok: bool) {
return return
} }
raw_lines := strings.split(output, "\n") raw_lines := strings.split(output, "\n", context.temp_allocator)
result := make([dynamic]string, 0, len(raw_lines), context.temp_allocator)
for line in raw_lines { for line in raw_lines {
trimmed, _ := strings.clone(strings.trim_space(line)) trimmed := strings.trim_space(line)
if len(trimmed) > 0 { if len(trimmed) > 0 {
append(&lines, trimmed) append(&result, trimmed)
} }
} }
ok = true return result[:], true
return
} }
scan_path :: proc(search_path: string, cfg: Config) -> (paths: [dynamic]string, ok: bool) { @(private = "file")
if is_tty() { next_fd_tmp_path :: proc() -> string {
fmt.printf("Searching for all files in \"%s\"...\n", search_path) sync.atomic_mutex_lock(&fd_counter)
} n := fd_seq
all_args := build_fd_args(search_path, cfg, true) fd_seq += 1
all_files, all_ok := run_fd(all_args) sync.atomic_mutex_unlock(&fd_counter)
if !all_ok { return fmt.aprintf("/tmp/envr-fd-%d-%d", os.get_pid(), n, allocator = context.temp_allocator)
return
}
if is_tty() {
fmt.printf("Search for unignored fies in \"%s\"...\n", search_path)
}
unignored_args := build_fd_args(search_path, cfg, false)
unignored_files, unignored_ok := run_fd(unignored_args)
if !unignored_ok {
return
}
unignored_set: map[string]bool
for file in unignored_files {
unignored_set[file] = true
}
for file in all_files {
if !(file in unignored_set) {
append(&paths, file)
}
}
ok = true
return
} }
can_scan :: proc() -> bool { cant_scan :: proc(feats: AvailableFeatures) -> bool {
feats := check_features() return Feature.Fd not_in feats
return Feature.Fd in feats
} }
find_unbacked :: proc(local_files: []string, db_files: []EnvFile) -> [dynamic]string { find_unbacked :: proc(local_files: []string, db_files: []EnvFile) -> []string {
backed_set: map[string]bool backed_set := make(map[string]bool, len(db_files), context.temp_allocator)
for file in db_files { for file in db_files {
backed_set[file.Path] = true backed_set[file.Path] = true
} }
unbacked: [dynamic]string unbacked := make([dynamic]string, 0, len(db_files) / 2, context.temp_allocator)
for file in local_files { for file in local_files {
if !(file in backed_set) { if !(file in backed_set) {
append(&unbacked, file) append(&unbacked, file)
} }
} }
return unbacked return unbacked[:]
} }

View File

@@ -7,88 +7,81 @@ import "core:testing"
@(test) @(test)
test_scan_path_finds_gitignored_env_files :: proc(t: ^testing.T) { test_scan_path_finds_gitignored_env_files :: proc(t: ^testing.T) {
if !can_scan() { feats := check_features()
return testing.expect(t, cant_scan(feats) == false)
}
base := fmt.aprintf("/tmp/envr-scan-test-%d", os.get_pid()) base := fmt.aprintf("/tmp/envr-scan-test-%d", os.get_pid())
os.mkdir_all(base) os.mkdir_all(base)
defer os.remove_all(base) defer os.remove_all(base)
git_init := os.Process_Desc{ git_init := os.Process_Desc {
command = []string{"git", "-c", "advice.defaultBranchName=false", "init"}, command = []string{"git", "-c", "advice.defaultBranchName=false", "init"},
working_dir = base, working_dir = base,
stdout = os.stderr, stdout = os.stderr,
stderr = os.stderr, stderr = os.stderr,
} }
p, err := os.process_start(git_init) p, err := os.process_start(git_init)
if err != nil { if err != nil {
return return
} }
_, wait_err := os.process_wait(p) _, wait_err := os.process_wait(p)
if wait_err != nil { if wait_err != nil {
return return
} }
gitignore_path := fmt.aprintf("%s/.gitignore", base) gitignore_path := fmt.aprintf("%s/.gitignore", base)
_ = os.write_entire_file(gitignore_path, ".env*\n") _ = os.write_entire_file(gitignore_path, ".env*\n")
_ = os.write_entire_file(fmt.aprintf("%s/.env", base), "SECRET=1") _ = os.write_entire_file(fmt.aprintf("%s/.env", base), "SECRET=1")
_ = os.write_entire_file(fmt.aprintf("%s/.env.testing", base), "TEST=1") _ = os.write_entire_file(fmt.aprintf("%s/.env.testing", base), "TEST=1")
_ = os.write_entire_file(fmt.aprintf("%s/config.yaml", base), "key: value") _ = os.write_entire_file(fmt.aprintf("%s/config.yaml", base), "key: value")
cfg := Config{ cfg := Config {
ScanConfig = ScanConfig{ ScanConfig = ScanConfig{Matcher = "\\.env", Exclude = []string{}, Include = []string{}},
Matcher = "\\.env", }
Exclude = []string{},
Include = []string{},
},
}
results, ok := scan_path(base, cfg) results, ok := scan_path(base, cfg)
testing.expect(t, ok, "scan_path should succeed") defer delete(results)
testing.expect(t, ok, "scan_path should succeed")
found_env := false found_env := false
found_testing := false found_testing := false
found_config := false found_config := false
for path in results { for path in results {
_, filename := filepath.split(path) _, filename := filepath.split(path)
if filename == ".env" { if filename == ".env" {
found_env = true found_env = true
} }
if filename == ".env.testing" { if filename == ".env.testing" {
found_testing = true found_testing = true
} }
if filename == "config.yaml" { if filename == "config.yaml" {
found_config = true found_config = true
} }
} }
testing.expect(t, found_env, "should find .env (gitignored)") testing.expect(t, found_env, "should find .env (gitignored)")
testing.expect(t, found_testing, "should find .env.testing (gitignored)") testing.expect(t, found_testing, "should find .env.testing (gitignored)")
testing.expect(t, !found_config, "should NOT find config.yaml (not gitignored)") testing.expect(t, !found_config, "should NOT find config.yaml (not gitignored)")
} }
@(test) @(test)
test_scan_path_empty_dir :: proc(t: ^testing.T) { test_scan_path_empty_dir :: proc(t: ^testing.T) {
if !can_scan() { feats := check_features()
return testing.expect(t, cant_scan(feats) == false)
}
base := fmt.aprintf("/tmp/envr-scan-empty-%d", os.get_pid()) base := fmt.aprintf("/tmp/envr-scan-empty-%d", os.get_pid())
os.mkdir_all(base) os.mkdir_all(base)
defer os.remove_all(base) defer os.remove_all(base)
cfg := Config{ cfg := Config {
ScanConfig = ScanConfig{ ScanConfig = ScanConfig{Matcher = "\\.env", Exclude = []string{}, Include = []string{}},
Matcher = "\\.env", }
Exclude = []string{},
Include = []string{},
},
}
results, ok := scan_path(base, cfg) results, ok := scan_path(base, cfg)
testing.expect(t, ok, "scan_path should succeed") defer delete(results)
testing.expect(t, len(results) == 0, fmt.aprintf("expected 0 results, got %d", len(results))) testing.expect(t, ok, "scan_path should succeed")
testing.expect(t, len(results) == 0, fmt.aprintf("expected 0 results, got %d", len(results)))
} }