mirror of
https://github.com/sbrow/envr.git
synced 2026-06-27 18:48:33 -04:00
Compare commits
8 Commits
427a67dcb4
...
2229affe69
| Author | SHA1 | Date | |
|---|---|---|---|
| 2229affe69 | |||
| 6394c42d8b | |||
| 8e00c78f12 | |||
| 8d2c5403e8 | |||
| 7c7d3d5c23 | |||
| 6151a5efaf | |||
| fbb1f86945 | |||
| 0c5aa74256 |
7
.github/workflows/odin.yml
vendored
7
.github/workflows/odin.yml
vendored
@@ -10,12 +10,12 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libsodium-dev sqlite3 libsqlite3-dev
|
sudo apt-get install -y libsodium-dev sqlite3 libsqlite3-dev libsodium-dev
|
||||||
|
|
||||||
- name: Install Odin
|
- name: Install Odin
|
||||||
run: |
|
run: |
|
||||||
@@ -25,7 +25,8 @@ jobs:
|
|||||||
echo "/opt/odin" >> "$GITHUB_PATH"
|
echo "/opt/odin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: odin build . -o:speed -out:envr
|
run: |
|
||||||
|
odin build . -o:speed -out:envr
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: odin test .
|
run: odin test .
|
||||||
|
|||||||
6
.github/workflows/release-please.yml
vendored
6
.github/workflows/release-please.yml
vendored
@@ -2,6 +2,8 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- dev
|
||||||
|
- odin
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -14,7 +16,7 @@ jobs:
|
|||||||
release-please:
|
release-please:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: googleapis/release-please-action@v4
|
- uses: googleapis/release-please-action@v5
|
||||||
with:
|
with:
|
||||||
# this assumes that you have created a personal access token
|
# this assumes that you have created a personal access token
|
||||||
# (PAT) and configured it as a GitHub action secret named
|
# (PAT) and configured it as a GitHub action secret named
|
||||||
@@ -22,4 +24,4 @@ jobs:
|
|||||||
token: ${{ secrets.MY_RELEASE_PLEASE_TOKEN }}
|
token: ${{ secrets.MY_RELEASE_PLEASE_TOKEN }}
|
||||||
# this is a built-in strategy in release-please, see "Action Inputs"
|
# this is a built-in strategy in release-please, see "Action Inputs"
|
||||||
# for more options
|
# for more options
|
||||||
release-type: odin
|
release-type: simple
|
||||||
|
|||||||
11
Makefile
11
Makefile
@@ -19,25 +19,20 @@ all: release clean
|
|||||||
$(BUILD_DIR):
|
$(BUILD_DIR):
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
|
||||||
# Generate version.odin from flake.nix
|
|
||||||
version.odin:
|
|
||||||
@echo 'Generating version.odin (v$(VERSION))...'
|
|
||||||
@printf 'package main\n\nVERSION :: "$(VERSION)"\n' > version.odin
|
|
||||||
|
|
||||||
# Build Linux AMD64
|
# Build Linux AMD64
|
||||||
$(LINUX_AMD64_BIN): version.odin $(BUILD_DIR)
|
$(LINUX_AMD64_BIN): $(BUILD_DIR)
|
||||||
@echo "Building for Linux AMD64..."
|
@echo "Building for Linux AMD64..."
|
||||||
odin build . -target:linux_amd64 -o:speed -out:$(LINUX_AMD64_BIN)
|
odin build . -target:linux_amd64 -o:speed -out:$(LINUX_AMD64_BIN)
|
||||||
@echo "Built $(LINUX_AMD64_BIN)"
|
@echo "Built $(LINUX_AMD64_BIN)"
|
||||||
|
|
||||||
# Build Linux ARM64
|
# Build Linux ARM64
|
||||||
$(LINUX_ARM64_BIN): version.odin $(BUILD_DIR)
|
$(LINUX_ARM64_BIN): $(BUILD_DIR)
|
||||||
@echo "Building for Linux ARM64..."
|
@echo "Building for Linux ARM64..."
|
||||||
odin build . -target:linux_arm64 -o:speed -out:$(LINUX_ARM64_BIN)
|
odin build . -target:linux_arm64 -o:speed -out:$(LINUX_ARM64_BIN)
|
||||||
@echo "Built $(LINUX_ARM64_BIN)"
|
@echo "Built $(LINUX_ARM64_BIN)"
|
||||||
|
|
||||||
# Build Darwin ARM64 (Mac)
|
# Build Darwin ARM64 (Mac)
|
||||||
$(DARWIN_ARM64_BIN): version.odin $(BUILD_DIR)
|
$(DARWIN_ARM64_BIN): $(BUILD_DIR)
|
||||||
@echo "Building for Darwin ARM64..."
|
@echo "Building for Darwin ARM64..."
|
||||||
odin build . -target:darwin_arm64 -o:speed -out:$(DARWIN_ARM64_BIN)
|
odin build . -target:darwin_arm64 -o:speed -out:$(DARWIN_ARM64_BIN)
|
||||||
@echo "Built $(DARWIN_ARM64_BIN)"
|
@echo "Built $(DARWIN_ARM64_BIN)"
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -13,7 +13,7 @@ the tool [of your choosing](#backup-options).
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 🔐 **Encrypted Storage**: All `.env` files are encrypted using your ssh key and
|
- 🔐 **Encrypted Storage**: All `.env` files are encrypted using your ssh key and
|
||||||
[age](https://github.com/FiloSottile/age) encryption.
|
[libsodium](https://github.com/jedisct1/libsodium) encryption.
|
||||||
- 🔄 **Automatic Sync**: Update the database with one command, which can easily
|
- 🔄 **Automatic Sync**: Update the database with one command, which can easily
|
||||||
be run on a cron.
|
be run on a cron.
|
||||||
- 🔍 **Smart Scanning**: Automatically discover and import `.env` files in your
|
- 🔍 **Smart Scanning**: Automatically discover and import `.env` files in your
|
||||||
@@ -37,12 +37,13 @@ repositories.
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### With Go
|
### With Odin
|
||||||
|
|
||||||
If you already have `go` installed:
|
If you already have `odin` installed:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go install github.com/sbrow/envr
|
# You'll need libsodium and sqlite
|
||||||
|
odin build -o:speed
|
||||||
envr init
|
envr init
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -104,18 +105,18 @@ The configuration file is created during initialization:
|
|||||||
## Backup Options
|
## Backup Options
|
||||||
|
|
||||||
`envr` merely gathers your `.env` files in one local place. It is up to you to
|
`envr` merely gathers your `.env` files in one local place. It is up to you to
|
||||||
back up the database (found at `~/.envr/data.age`) to a *secure* and *remote*
|
back up the database (found at `~/.envr/data.envr`) to a *secure* and *remote*
|
||||||
location.
|
location.
|
||||||
|
|
||||||
### Git
|
### Git
|
||||||
|
|
||||||
`envr` preserves inodes when updating the database, so you can safely hardlink
|
`envr` preserves inodes when updating the database, so you can safely hardlink
|
||||||
`~/.envr/data.age` into your [GNU Stow](https://www.gnu.org/software/stow/),
|
`~/.envr/data.envr` into your [GNU Stow](https://www.gnu.org/software/stow/),
|
||||||
[Home Manager](https://github.com/nix-community/home-manager), or
|
[Home Manager](https://github.com/nix-community/home-manager), or
|
||||||
[NixOS](https://nixos.wiki/wiki/flakes) repository.
|
[NixOS](https://nixos.wiki/wiki/flakes) repository.
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> For **maximum security**, only save your `data.age` file to a local
|
> For **maximum security**, only save your `data.envr` file to a local
|
||||||
(i.e. non-cloud) git server that **you personally control**.
|
(i.e. non-cloud) git server that **you personally control**.
|
||||||
>
|
>
|
||||||
> I take no responsibility if you push all your secrets to a public GitHub repo.
|
> I take no responsibility if you push all your secrets to a public GitHub repo.
|
||||||
|
|||||||
268
TABLE_IMPROVEMENT_PLAN.md
Normal file
268
TABLE_IMPROVEMENT_PLAN.md
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
# Table Rendering Memory Optimization Plan
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
This plan outlines improvements to eliminate excessive memory allocations and copies in the Odin table rendering system. The current implementation makes 10+ allocations per row, while the Zig equivalent makes zero allocations for rendering. This optimization will reduce memory usage, improve performance, and align with the project's efficiency goals.
|
||||||
|
|
||||||
|
## Current State Analysis
|
||||||
|
|
||||||
|
### Zig Version (Reference Implementation)
|
||||||
|
- **Allocations**: 1 (data only)
|
||||||
|
- **Data copies**: 0
|
||||||
|
- **String allocation**: 0
|
||||||
|
- **Column widths**: Stack array
|
||||||
|
- **Output**: Direct to writer
|
||||||
|
|
||||||
|
### Odin Version (Current Implementation)
|
||||||
|
- **Allocations**: 10+ per row
|
||||||
|
- **Data copies**: Multiple per row
|
||||||
|
- **String allocation**: 2+ per row (concatenate + slice)
|
||||||
|
- **Column widths**: Heap allocated
|
||||||
|
- **Output**: Builder → stdout
|
||||||
|
|
||||||
|
### Current Issues Identified
|
||||||
|
|
||||||
|
1. **Table Infrastructure** (`table.odin`)
|
||||||
|
- Uses `strings.Builder` which allocates per-line memory
|
||||||
|
- Heap-allocated `[dynamic]int` for column widths
|
||||||
|
- Multiple `strings.concatenate()` calls creating new strings
|
||||||
|
|
||||||
|
2. **Command Implementations**
|
||||||
|
- `cmd_list`: Creates intermediate `[]string` slices per row, allocates new strings via `strings.concatenate()`
|
||||||
|
- `cmd_sync`: Creates `SyncEntry` structs with cloned strings, allocates dynamic arrays
|
||||||
|
- `cmd_deps`: Allocates dynamic rows array unnecessarily
|
||||||
|
|
||||||
|
3. **Memory Pattern**
|
||||||
|
- Each command allocates `[][]string` for table data
|
||||||
|
- Manual struct-to-row transformation creates copies
|
||||||
|
- Duplicate code across all table-using commands
|
||||||
|
|
||||||
|
## Proposed Solutions
|
||||||
|
|
||||||
|
### Phase 1: Core Table Infrastructure Overhaul
|
||||||
|
|
||||||
|
#### 1.1 Direct Writer-Based Rendering
|
||||||
|
**Current:**
|
||||||
|
```odin
|
||||||
|
b: strings.Builder
|
||||||
|
strings.builder_init(&b)
|
||||||
|
// ... build table in builder
|
||||||
|
fmt.println(strings.to_string(b))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Proposed:**
|
||||||
|
```odin
|
||||||
|
render_table :: proc(writer: io.Writer, headers: []string, rows: [][]string)
|
||||||
|
```
|
||||||
|
- Replace `strings.Builder` with `io.Writer` output
|
||||||
|
- Eliminate intermediate string allocations
|
||||||
|
- Write table components directly to output stream
|
||||||
|
|
||||||
|
#### 1.2 Stack-Based Column Widths
|
||||||
|
**Current:**
|
||||||
|
```odin
|
||||||
|
col_widths := make([dynamic]int, 0, len(headers))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Proposed:**
|
||||||
|
- Use fixed stack arrays for reasonable column counts
|
||||||
|
- Implement small buffer optimization (SBO) for variable column counts
|
||||||
|
- Only allocate for tables exceeding threshold (e.g., 16 columns)
|
||||||
|
|
||||||
|
#### 1.3 Zero-Copy String Handling
|
||||||
|
**Current:**
|
||||||
|
```odin
|
||||||
|
dir_str := strings.concatenate({row.Dir, "/"}, context.temp_allocator)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Proposed:**
|
||||||
|
- Replace `strings.concatenate()` with string slicing
|
||||||
|
- Work directly with `EnvFile.Path` and `EnvFile.Dir` fields
|
||||||
|
- Use `filepath.base()` and `filepath.dir()` without allocation where possible
|
||||||
|
|
||||||
|
### Phase 2: Generic Table Interface
|
||||||
|
|
||||||
|
#### 2.1 Field-Based Table Renderer
|
||||||
|
```odin
|
||||||
|
Table_Field :: struct {
|
||||||
|
name: string,
|
||||||
|
value: string, // String view, no allocation
|
||||||
|
alignment: Alignment,
|
||||||
|
}
|
||||||
|
|
||||||
|
Table_Config :: struct {
|
||||||
|
writer: io.Writer,
|
||||||
|
fields: []Table_Field,
|
||||||
|
col_widths: []int,
|
||||||
|
}
|
||||||
|
|
||||||
|
render_row :: proc(cfg: Table_Config, row_data: any)
|
||||||
|
```
|
||||||
|
- Accept struct fields directly without intermediate arrays
|
||||||
|
- Support field selection (show only specific fields)
|
||||||
|
- Alignment options (left/center/right)
|
||||||
|
|
||||||
|
#### 2.2 Field Extraction Procs
|
||||||
|
- Generate field extraction helpers for each struct type
|
||||||
|
- Avoid string allocation by returning string views
|
||||||
|
- Cache computed values (like formatted status strings)
|
||||||
|
|
||||||
|
#### 2.3 Streaming Table Processing
|
||||||
|
- Process rows one at a time without collecting all rows
|
||||||
|
- Reduce peak memory usage from O(N × strings) to O(table_structure)
|
||||||
|
- Enable early termination if needed
|
||||||
|
|
||||||
|
### Phase 3: Command-Specific Optimizations
|
||||||
|
|
||||||
|
#### 3.1 Eliminate Intermediate Structs
|
||||||
|
**Current (cmd_sync):**
|
||||||
|
```odin
|
||||||
|
for &file in files {
|
||||||
|
// ... processing
|
||||||
|
path_str, _ := strings.clone(file.Path)
|
||||||
|
status_str, _ := strings.clone(status)
|
||||||
|
append(&results, SyncEntry{Path = path_str, Status = status_str})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Proposed:**
|
||||||
|
```odin
|
||||||
|
for &file in files {
|
||||||
|
result, err_msg := db_sync(&db, &file)
|
||||||
|
// Direct rendering with zero-copy
|
||||||
|
render_sync_row(writer, file, result, err_msg)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- `cmd_sync`: Work directly with `EnvFile` + `SyncFlagEnum`
|
||||||
|
- `cmd_list`: Use `EnvFile` fields directly, no `ListEntry`
|
||||||
|
- Generate table content on-the-fly
|
||||||
|
|
||||||
|
#### 3.2 In-Place Status Computation
|
||||||
|
```odin
|
||||||
|
get_sync_status :: proc(result: SyncFlag, err_msg: string) -> string {
|
||||||
|
switch {
|
||||||
|
case .Error in result: return if len(err_msg) > 0 then err_msg else "error"
|
||||||
|
case .BackedUp in result: return "Backed Up"
|
||||||
|
case .Restored in result: return "Restored"
|
||||||
|
case .DirUpdated in result: return "Moved"
|
||||||
|
case: return "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Compute status strings without allocation (use static lookup)
|
||||||
|
- Cache formatted status values if needed
|
||||||
|
- Reduce allocation count from N to 0 or 1
|
||||||
|
|
||||||
|
#### 3.3 Batch Processing
|
||||||
|
- Reduce allocation count by pooling small allocations
|
||||||
|
- Use `context.temp_allocator` more effectively
|
||||||
|
- Pre-allocate buffers for expected sizes
|
||||||
|
|
||||||
|
### Phase 4: JSON Output Separation
|
||||||
|
|
||||||
|
#### 4.1 Unified JSON Rendering
|
||||||
|
```odin
|
||||||
|
render_json_rows :: proc(writer: io.Writer, rows: any, field_names: []string)
|
||||||
|
```
|
||||||
|
- Create centralized JSON rendering helper
|
||||||
|
- Work with same structs as table rendering
|
||||||
|
- Use reflection or explicit field marshaling
|
||||||
|
|
||||||
|
#### 4.2 Format-Agnostic Interface
|
||||||
|
- Commands generate data → renderers handle format
|
||||||
|
- Table renderer focuses only on ASCII/Unicode output
|
||||||
|
- Keep terminal detection in command layer
|
||||||
|
|
||||||
|
## Expected Improvements
|
||||||
|
|
||||||
|
| Metric | Current | Target | Improvement |
|
||||||
|
|--------|---------|--------|-------------|
|
||||||
|
| **Allocations** | 10+ per row | 0-1 per table | 10x+ reduction |
|
||||||
|
| **Memory copies** | 2-3 per row | 0 | 100% reduction |
|
||||||
|
| **Peak memory** | O(N × strings) | O(table_structure) | Constant factor |
|
||||||
|
| **Throughput** | Baseline | 2-3x faster | Performance boost |
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### High-Priority Changes
|
||||||
|
1. Replace `strings.Builder` with direct `io.Writer` output
|
||||||
|
2. Convert column widths to stack-based allocation
|
||||||
|
3. Eliminate intermediate struct allocations in commands
|
||||||
|
|
||||||
|
### Medium-Priority Changes
|
||||||
|
1. Create generic field-based table interface
|
||||||
|
2. Implement streaming table processing
|
||||||
|
3. Centralize JSON rendering logic
|
||||||
|
|
||||||
|
### Low-Priority Changes
|
||||||
|
1. Add alignment options beyond left-aligned
|
||||||
|
2. Implement comprehensive field introspection
|
||||||
|
3. Add advanced table formatting features
|
||||||
|
|
||||||
|
## Tradeoff Questions
|
||||||
|
|
||||||
|
Before implementation begins, we need to resolve these architectural questions:
|
||||||
|
|
||||||
|
### 1. Generality vs. Performance
|
||||||
|
**Question:** Should we create a fully generic table renderer (similar to Zig's `Table(T)`) or focus on optimizing the current 3 use cases first?
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- **Generic approach**: Higher development cost, future-proof, may have some overhead
|
||||||
|
- **Specific optimization**: Faster implementation, maximum performance for current use cases, less flexible
|
||||||
|
|
||||||
|
**Recommendation:** Start with specific optimizations for current use cases, then generalize patterns that emerge.
|
||||||
|
|
||||||
|
### 2. Alignment Support
|
||||||
|
**Question:** Does the project need left/center/right alignment support, or is left-alignment sufficient?
|
||||||
|
|
||||||
|
**Context:** Zig supports alignment options, but current Odin implementation only left-aligns. Most CLI tables work fine with left alignment.
|
||||||
|
|
||||||
|
**Recommendation:** Start with left-alignment only, add alignment if specific use cases demand it.
|
||||||
|
|
||||||
|
### 3. API Compatibility
|
||||||
|
**Question:** Should we maintain the current `render_table()` API signature, or are breaking changes acceptable?
|
||||||
|
|
||||||
|
**Current API:**
|
||||||
|
```odin
|
||||||
|
render_table :: proc(headers: []string, rows: [][]string)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- **Maintain API**: Slower to implement, backward compatible, may need adapter layers
|
||||||
|
- **Break API**: Faster implementation, cleaner code, requires updates to all callers
|
||||||
|
|
||||||
|
**Recommendation:** Breaking changes are acceptable since this is an optimization-focused effort and callers are limited to 3 commands.
|
||||||
|
|
||||||
|
### 4. Odin Capabilities
|
||||||
|
**Question:** What runtime reflection or field introspection capabilities does Odin provide?
|
||||||
|
|
||||||
|
**Context:** Zig uses `@typeInfo()` and comptime field iteration. We need to understand Odin's equivalent capabilities to design the optimal solution.
|
||||||
|
|
||||||
|
**Recommendation:** Investigate Odin's runtime type information capabilities before finalizing the generic table interface design.
|
||||||
|
|
||||||
|
### 5. Testing Strategy
|
||||||
|
**Question:** Should we add comprehensive tests for new table rendering before optimizing commands, or optimize incrementally with tests added afterwards?
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- **Test-first**: More robust, catches regressions early, slower initial development
|
||||||
|
- **Optimize-first**: Faster development, may miss edge cases, requires retroactive testing
|
||||||
|
|
||||||
|
**Recommendation:** Hybrid approach - add basic tests for core infrastructure, then optimize incrementally with additional tests for each command.
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Research Phase**: Investigate Odin's type system and reflection capabilities
|
||||||
|
2. **Prototype Phase**: Create minimal working prototype of zero-allocation table renderer
|
||||||
|
3. **Refactor Phase**: Incrementally update commands to use new infrastructure
|
||||||
|
4. **Test Phase**: Add comprehensive tests and verify memory improvements
|
||||||
|
5. **Benchmark Phase**: Measure performance improvements and memory usage
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- [ ] Zero allocations for table rendering (excluding initial data)
|
||||||
|
- [ ] Zero string copies in the happy path
|
||||||
|
- [ ] All 3 commands (`list`, `sync`, `deps`) use new infrastructure
|
||||||
|
- [ ] Performance improvement of 2x or more
|
||||||
|
- [ ] Memory usage reduction of 50% or more
|
||||||
|
- [ ] No regression in table formatting quality
|
||||||
|
- [ ] Backward compatibility with JSON output format
|
||||||
43
TODOS.md
43
TODOS.md
@@ -8,10 +8,6 @@ Note: These todos can wait until all the subcommands have been ported.
|
|||||||
|
|
||||||
3. **config.odin:52-54** — `os.user_home_dir` error silently ignored. If it fails, `home` is `""` and all paths become relative (`".envr"` instead of `"~/.envr"`).
|
3. **config.odin:52-54** — `os.user_home_dir` error silently ignored. If it fails, `home` is `""` and all paths become relative (`".envr"` instead of `"~/.envr"`).
|
||||||
|
|
||||||
30. **cmd_sync.odin:46-50, 64-68** — Double `db_insert` when `BackedUp`: first insert on line 48, then `db_update_required` is also true for `BackedUp` so second insert runs on line 65. Redundant and wasteful.
|
|
||||||
|
|
||||||
31. **db.odin:626 & env_file.go:183** — `BackedUp` discards `DirUpdated`. When `TrustFilesystem` is used and the hash differs, the result is just `BackedUp` (not `BackedUp | DirUpdated`). If a file's directory was moved AND its contents changed, the old DB entry won't be deleted because the `DirUpdated` check at `cmd_sync.odin:59` never fires. Bug exists in both Go and Odin.
|
|
||||||
|
|
||||||
## MEDIUM
|
## MEDIUM
|
||||||
|
|
||||||
4. **db.odin:29-35** — `make_temp_path` never calls `strings.builder_destroy`. Leaks builder buffer every call.
|
4. **db.odin:29-35** — `make_temp_path` never calls `strings.builder_destroy`. Leaks builder buffer every call.
|
||||||
@@ -67,3 +63,42 @@ Note: These todos can wait until all the subcommands have been ported.
|
|||||||
28. 2 scan tests silently skip Low When fd isn't installed, tests pass without actually testing anything. These should use #assert to be sure that fd is in path.
|
28. 2 scan tests silently skip Low When fd isn't installed, tests pass without actually testing anything. These should use #assert to be sure that fd is in path.
|
||||||
|
|
||||||
38. Try to do all encryption / decryption in memory - only read / write encrypted data to disk.
|
38. Try to do all encryption / decryption in memory - only read / write encrypted data to disk.
|
||||||
|
|
||||||
|
## Double-check AI output
|
||||||
|
|
||||||
|
- [ ] cli.odin
|
||||||
|
- [ ] config.odin
|
||||||
|
- [ ] crypto.odin
|
||||||
|
- [ ] db.odin
|
||||||
|
- [ ] features.odin
|
||||||
|
- [ ] main.odin
|
||||||
|
- [ ] prompt.odin
|
||||||
|
- [ ] scan.odin
|
||||||
|
- [ ] sodium.odin
|
||||||
|
- [ ] ssh.odin
|
||||||
|
- [ ] table.odin
|
||||||
|
- [ ] cmd_backup.odin
|
||||||
|
- [ ] cmd_check.odin
|
||||||
|
- [ ] cmd_deps.odin
|
||||||
|
- [ ] cmd_edit_config.odin
|
||||||
|
- [ ] cmd_init.odin
|
||||||
|
- [ ] cmd_list.odin
|
||||||
|
- [ ] cmd_nushell_completion.odin
|
||||||
|
- [ ] cmd_remove.odin
|
||||||
|
- [ ] cmd_restore.odin
|
||||||
|
- [ ] cmd_scan.odin
|
||||||
|
- [ ] cmd_sync.odin
|
||||||
|
- [ ] cmd_version.odin
|
||||||
|
- [ ] sqlite/sqlite.odin
|
||||||
|
- [ ] cli_test.odin
|
||||||
|
- [ ] cmd_check_test.odin
|
||||||
|
- [ ] cmd_list_test.odin
|
||||||
|
- [ ] cmd_nushell_completion_test.odin
|
||||||
|
- [ ] config_test.odin
|
||||||
|
- [ ] crypto_test.odin
|
||||||
|
- [ ] db_integration_test.odin
|
||||||
|
- [ ] db_test.odin
|
||||||
|
- [ ] features_test.odin
|
||||||
|
- [ ] scan_test.odin
|
||||||
|
- [ ] ssh_test.odin
|
||||||
|
- [ ] table_test.odin
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package main
|
|||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
import "core:path/filepath"
|
import "core:path/filepath"
|
||||||
import "core:strings"
|
|
||||||
|
|
||||||
cmd_check :: proc(cmd: ^Command) {
|
cmd_check :: proc(cmd: ^Command) {
|
||||||
feats := check_features()
|
feats := check_features()
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
|
import "core:io"
|
||||||
|
import "core:os"
|
||||||
|
import "core:terminal"
|
||||||
|
|
||||||
cmd_deps :: proc(cmd: ^Command) {
|
cmd_deps :: proc(cmd: ^Command) {
|
||||||
feats := check_features()
|
feats := check_features()
|
||||||
@@ -20,5 +23,12 @@ cmd_deps :: proc(cmd: ^Command) {
|
|||||||
append(&rows, []string{"fd", "\u2717 Missing"})
|
append(&rows, []string{"fd", "\u2717 Missing"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if terminal.is_terminal(os.stdout) {
|
||||||
render_table(headers, rows[:])
|
render_table(headers, rows[:])
|
||||||
|
} else {
|
||||||
|
w := io.to_writer(os.to_writer(os.stdout))
|
||||||
|
render_json_rows(w, headers, rows[:])
|
||||||
|
io.write_string(w, "\n")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package main
|
|||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
import "core:path/filepath"
|
import "core:path/filepath"
|
||||||
import "core:strings"
|
|
||||||
|
|
||||||
cmd_edit_config :: proc(cmd: ^Command) {
|
cmd_edit_config :: proc(cmd: ^Command) {
|
||||||
editor := os.get_env("EDITOR", context.allocator)
|
editor := os.get_env("EDITOR", context.allocator)
|
||||||
@@ -25,7 +24,7 @@ cmd_edit_config :: proc(cmd: ^Command) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
args := []string{editor, config_path}
|
args := []string{editor, config_path}
|
||||||
desc := os.Process_Desc{
|
desc := os.Process_Desc {
|
||||||
command = args,
|
command = args,
|
||||||
stdin = os.stdin,
|
stdin = os.stdin,
|
||||||
stdout = os.stdout,
|
stdout = os.stdout,
|
||||||
@@ -47,3 +46,4 @@ cmd_edit_config :: proc(cmd: ^Command) {
|
|||||||
os.exit(int(state.exit_code))
|
os.exit(int(state.exit_code))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ SyncEntry :: struct {
|
|||||||
Status: string `json:"status"`,
|
Status: string `json:"status"`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Check for quiet failures.
|
||||||
cmd_sync :: proc(cmd: ^Command) {
|
cmd_sync :: proc(cmd: ^Command) {
|
||||||
db, db_ok := db_open()
|
db, db_ok := db_open()
|
||||||
if !db_ok {
|
if !db_ok {
|
||||||
@@ -33,28 +34,22 @@ cmd_sync :: proc(cmd: ^Command) {
|
|||||||
result, err_msg := db_sync(&db, &file)
|
result, err_msg := db_sync(&db, &file)
|
||||||
|
|
||||||
status: string
|
status: string
|
||||||
s := i32(result)
|
is_dir_updated := .DirUpdated in result
|
||||||
is_error := (s & i32(SyncResult.Error)) != 0
|
|
||||||
is_backed := (s & i32(SyncResult.BackedUp)) != 0
|
|
||||||
is_restored := (s & i32(SyncResult.Restored)) != 0
|
|
||||||
is_dir_updated := (s & i32(SyncResult.DirUpdated)) != 0
|
|
||||||
|
|
||||||
if is_error {
|
switch {
|
||||||
|
case .Error in result:
|
||||||
if len(err_msg) > 0 {
|
if len(err_msg) > 0 {
|
||||||
status = err_msg
|
status = err_msg
|
||||||
} else {
|
} else {
|
||||||
status = "error"
|
status = "error"
|
||||||
}
|
}
|
||||||
} else if is_backed {
|
case .BackedUp in result:
|
||||||
status = "Backed Up"
|
status = "Backed Up"
|
||||||
if !db_insert(&db, file) {
|
case .Restored in result:
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if is_restored {
|
|
||||||
status = "Restored"
|
status = "Restored"
|
||||||
} else if is_dir_updated && !is_restored {
|
case .DirUpdated in result:
|
||||||
status = "Moved"
|
status = "Moved"
|
||||||
} else {
|
case:
|
||||||
status = "OK"
|
status = "OK"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package main
|
|||||||
|
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
|
|
||||||
|
VERSION :: #load("version.txt", string)
|
||||||
|
|
||||||
cmd_version :: proc(cmd: ^Command) {
|
cmd_version :: proc(cmd: ^Command) {
|
||||||
if has_flag(cmd, "long") || has_flag(cmd, "l") {
|
if has_flag(cmd, "long") || has_flag(cmd, "l") {
|
||||||
fmt.printf("envr version %s\n", VERSION)
|
fmt.printf("envr version %s\n", VERSION)
|
||||||
|
|||||||
41
crypto.odin
41
crypto.odin
@@ -6,7 +6,11 @@ import "core:mem"
|
|||||||
MAGIC :: "ENVR"
|
MAGIC :: "ENVR"
|
||||||
MAGIC_BYTES := [4]u8{u8('E'), u8('N'), u8('V'), u8('R')}
|
MAGIC_BYTES := [4]u8{u8('E'), u8('N'), u8('V'), u8('R')}
|
||||||
|
|
||||||
RECIPIENT_ENTRY_SIZE :: CRYPTO_BOX_PUBLICKEY_BYTES + CRYPTO_BOX_NONCE_BYTES + CRYPTO_SECRETBOX_KEY_BYTES + CRYPTO_BOX_MAC_BYTES
|
RECIPIENT_ENTRY_SIZE ::
|
||||||
|
CRYPTO_BOX_PUBLICKEY_BYTES +
|
||||||
|
CRYPTO_BOX_NONCE_BYTES +
|
||||||
|
CRYPTO_SECRETBOX_KEY_BYTES +
|
||||||
|
CRYPTO_BOX_MAC_BYTES
|
||||||
|
|
||||||
HEADER_SIZE :: 4 + CRYPTO_BOX_PUBLICKEY_BYTES + CRYPTO_SECRETBOX_NONCE_BYTES + 4
|
HEADER_SIZE :: 4 + CRYPTO_BOX_PUBLICKEY_BYTES + CRYPTO_SECRETBOX_NONCE_BYTES + 4
|
||||||
|
|
||||||
@@ -108,7 +112,13 @@ encrypt :: proc(plaintext: []u8, keys: []SshKeyPair) -> (ciphertext: []u8, ok: b
|
|||||||
if len(plaintext) > 0 {
|
if len(plaintext) > 0 {
|
||||||
pt_ptr = &plaintext[0]
|
pt_ptr = &plaintext[0]
|
||||||
}
|
}
|
||||||
rc := crypto_secretbox_easy(&secret_ct[0], pt_ptr, u64(len(plaintext)), &main_nonce[0], &sym_key[0])
|
rc := crypto_secretbox_easy(
|
||||||
|
&secret_ct[0],
|
||||||
|
pt_ptr,
|
||||||
|
u64(len(plaintext)),
|
||||||
|
&main_nonce[0],
|
||||||
|
&sym_key[0],
|
||||||
|
)
|
||||||
if rc != 0 {
|
if rc != 0 {
|
||||||
fmt.println("Error: symmetric encryption failed")
|
fmt.println("Error: symmetric encryption failed")
|
||||||
delete(secret_ct)
|
delete(secret_ct)
|
||||||
@@ -166,7 +176,11 @@ encrypt :: proc(plaintext: []u8, keys: []SshKeyPair) -> (ciphertext: []u8, ok: b
|
|||||||
pos += CRYPTO_BOX_PUBLICKEY_BYTES
|
pos += CRYPTO_BOX_PUBLICKEY_BYTES
|
||||||
mem.copy(&ciphertext[pos], &entries[i].Nonce[0], CRYPTO_BOX_NONCE_BYTES)
|
mem.copy(&ciphertext[pos], &entries[i].Nonce[0], CRYPTO_BOX_NONCE_BYTES)
|
||||||
pos += CRYPTO_BOX_NONCE_BYTES
|
pos += CRYPTO_BOX_NONCE_BYTES
|
||||||
mem.copy(&ciphertext[pos], &entries[i].EncryptedKey[0], CRYPTO_SECRETBOX_KEY_BYTES + CRYPTO_BOX_MAC_BYTES)
|
mem.copy(
|
||||||
|
&ciphertext[pos],
|
||||||
|
&entries[i].EncryptedKey[0],
|
||||||
|
CRYPTO_SECRETBOX_KEY_BYTES + CRYPTO_BOX_MAC_BYTES,
|
||||||
|
)
|
||||||
pos += CRYPTO_SECRETBOX_KEY_BYTES + CRYPTO_BOX_MAC_BYTES
|
pos += CRYPTO_SECRETBOX_KEY_BYTES + CRYPTO_BOX_MAC_BYTES
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,8 +223,11 @@ decrypt :: proc(ciphertext: []u8, keys: []SshKeyPair) -> (plaintext: []u8, ok: b
|
|||||||
}
|
}
|
||||||
offset += CRYPTO_SECRETBOX_NONCE_BYTES
|
offset += CRYPTO_SECRETBOX_NONCE_BYTES
|
||||||
|
|
||||||
num_recipients := u32(ciphertext[offset]) << 24 | u32(ciphertext[offset + 1]) << 16 |
|
num_recipients :=
|
||||||
u32(ciphertext[offset + 2]) << 8 | u32(ciphertext[offset + 3])
|
u32(ciphertext[offset]) << 24 |
|
||||||
|
u32(ciphertext[offset + 1]) << 16 |
|
||||||
|
u32(ciphertext[offset + 2]) << 8 |
|
||||||
|
u32(ciphertext[offset + 3])
|
||||||
offset += 4
|
offset += 4
|
||||||
|
|
||||||
recipients_end := offset + int(num_recipients) * RECIPIENT_ENTRY_SIZE
|
recipients_end := offset + int(num_recipients) * RECIPIENT_ENTRY_SIZE
|
||||||
@@ -233,7 +250,7 @@ decrypt :: proc(ciphertext: []u8, keys: []SshKeyPair) -> (plaintext: []u8, ok: b
|
|||||||
matched_pi := 0
|
matched_pi := 0
|
||||||
for pi in 0 ..< len(x25519_pairs) {
|
for pi in 0 ..< len(x25519_pairs) {
|
||||||
scan_offset := offset
|
scan_offset := offset
|
||||||
for ri in 0 ..< int(num_recipients) {
|
for _ in 0 ..< int(num_recipients) {
|
||||||
for i in 0 ..< CRYPTO_BOX_PUBLICKEY_BYTES {
|
for i in 0 ..< CRYPTO_BOX_PUBLICKEY_BYTES {
|
||||||
enc_pub[i] = ciphertext[scan_offset + i]
|
enc_pub[i] = ciphertext[scan_offset + i]
|
||||||
}
|
}
|
||||||
@@ -247,7 +264,8 @@ decrypt :: proc(ciphertext: []u8, keys: []SshKeyPair) -> (plaintext: []u8, ok: b
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !match {
|
if !match {
|
||||||
scan_offset += CRYPTO_BOX_NONCE_BYTES + CRYPTO_SECRETBOX_KEY_BYTES + CRYPTO_BOX_MAC_BYTES
|
scan_offset +=
|
||||||
|
CRYPTO_BOX_NONCE_BYTES + CRYPTO_SECRETBOX_KEY_BYTES + CRYPTO_BOX_MAC_BYTES
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +319,13 @@ decrypt :: proc(ciphertext: []u8, keys: []SshKeyPair) -> (plaintext: []u8, ok: b
|
|||||||
if len(plaintext) > 0 {
|
if len(plaintext) > 0 {
|
||||||
pt_ptr = &plaintext[0]
|
pt_ptr = &plaintext[0]
|
||||||
}
|
}
|
||||||
rc = crypto_secretbox_open_easy(pt_ptr, &ct_data[0], u64(len(ct_data)), &main_nonce[0], &sym_key[0])
|
rc = crypto_secretbox_open_easy(
|
||||||
|
pt_ptr,
|
||||||
|
&ct_data[0],
|
||||||
|
u64(len(ct_data)),
|
||||||
|
&main_nonce[0],
|
||||||
|
&sym_key[0],
|
||||||
|
)
|
||||||
if rc != 0 {
|
if rc != 0 {
|
||||||
fmt.println("Error: symmetric decryption failed")
|
fmt.println("Error: symmetric decryption failed")
|
||||||
delete(plaintext)
|
delete(plaintext)
|
||||||
@@ -311,3 +335,4 @@ decrypt :: proc(ciphertext: []u8, keys: []SshKeyPair) -> (plaintext: []u8, ok: b
|
|||||||
ok = true
|
ok = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
51
db.odin
51
db.odin
@@ -1,6 +1,5 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "core:c"
|
|
||||||
import "core:crypto/hash"
|
import "core:crypto/hash"
|
||||||
import "core:encoding/hex"
|
import "core:encoding/hex"
|
||||||
import "core:encoding/json"
|
import "core:encoding/json"
|
||||||
@@ -12,14 +11,16 @@ import "core:time"
|
|||||||
|
|
||||||
import "sqlite"
|
import "sqlite"
|
||||||
|
|
||||||
SyncResult :: enum i32 {
|
SyncFlagEnum :: enum {
|
||||||
Noop = 0,
|
Noop,
|
||||||
DirUpdated = 1,
|
DirUpdated,
|
||||||
Restored = 1 << 1,
|
Restored,
|
||||||
BackedUp = 1 << 2,
|
BackedUp,
|
||||||
Error = 1 << 3,
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SyncFlag :: bit_set[SyncFlagEnum]
|
||||||
|
|
||||||
SyncDirection :: enum {
|
SyncDirection :: enum {
|
||||||
TrustDatabase,
|
TrustDatabase,
|
||||||
TrustFilesystem,
|
TrustFilesystem,
|
||||||
@@ -449,9 +450,8 @@ string_to_cstring :: proc(s: string) -> cstring {
|
|||||||
return cs
|
return cs
|
||||||
}
|
}
|
||||||
|
|
||||||
db_update_required :: proc(status: SyncResult) -> bool {
|
db_update_required :: proc(status: SyncFlag) -> bool {
|
||||||
s := i32(status)
|
return .BackedUp in status || .DirUpdated in status
|
||||||
return (s & (i32(SyncResult.BackedUp) | i32(SyncResult.DirUpdated))) != 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
shares_remote :: proc(f: ^EnvFile, remotes: []string) -> bool {
|
shares_remote :: proc(f: ^EnvFile, remotes: []string) -> bool {
|
||||||
@@ -510,9 +510,8 @@ env_file_backup :: proc(f: ^EnvFile) -> bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
env_file_sync :: proc(f: ^EnvFile, dir: SyncDirection, d: ^Db) -> (SyncResult, string) {
|
env_file_sync :: proc(f: ^EnvFile, dir: SyncDirection, d: ^Db) -> (SyncFlag, string) {
|
||||||
result: SyncResult = .Noop
|
result: SyncFlag = {}
|
||||||
err_msg: string
|
|
||||||
|
|
||||||
_, stat_err := os.stat(f.Dir, context.allocator)
|
_, stat_err := os.stat(f.Dir, context.allocator)
|
||||||
if stat_err != nil {
|
if stat_err != nil {
|
||||||
@@ -521,18 +520,18 @@ env_file_sync :: proc(f: ^EnvFile, dir: SyncDirection, d: ^Db) -> (SyncResult, s
|
|||||||
if d != nil {
|
if d != nil {
|
||||||
dirs, dirs_ok := find_moved_dirs(d, f)
|
dirs, dirs_ok := find_moved_dirs(d, f)
|
||||||
if !dirs_ok {
|
if !dirs_ok {
|
||||||
return .Error, "failed to find moved dirs"
|
return {.Error}, "failed to find moved dirs"
|
||||||
}
|
}
|
||||||
moved_dirs = dirs
|
moved_dirs = dirs
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(moved_dirs) == 0 {
|
if len(moved_dirs) == 0 {
|
||||||
return .Error, "directory missing"
|
return {.Error}, "directory missing"
|
||||||
} else if len(moved_dirs) == 1 {
|
} else if len(moved_dirs) == 1 {
|
||||||
update_dir(f, moved_dirs[0])
|
update_dir(f, moved_dirs[0])
|
||||||
result = .DirUpdated
|
result = {.DirUpdated}
|
||||||
} else {
|
} else {
|
||||||
return .Error, "multiple directories found"
|
return {.Error}, "multiple directories found"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,11 +540,10 @@ env_file_sync :: proc(f: ^EnvFile, dir: SyncDirection, d: ^Db) -> (SyncResult, s
|
|||||||
write_err := os.write_entire_file(f.Path, f.contents)
|
write_err := os.write_entire_file(f.Path, f.contents)
|
||||||
if write_err != nil {
|
if write_err != nil {
|
||||||
msg, _ := strings.concatenate({"failed to write file: ", fmt.tprintf("%v", write_err)})
|
msg, _ := strings.concatenate({"failed to write file: ", fmt.tprintf("%v", write_err)})
|
||||||
return .Error, msg
|
return {.Error}, msg
|
||||||
}
|
}
|
||||||
|
|
||||||
s := i32(result) | i32(SyncResult.Restored)
|
return result + {.Restored}, ""
|
||||||
return SyncResult(s), ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data, read_err := os.read_entire_file_from_path(f.Path, context.allocator)
|
data, read_err := os.read_entire_file_from_path(f.Path, context.allocator)
|
||||||
@@ -553,7 +551,7 @@ env_file_sync :: proc(f: ^EnvFile, dir: SyncDirection, d: ^Db) -> (SyncResult, s
|
|||||||
msg, _ := strings.concatenate(
|
msg, _ := strings.concatenate(
|
||||||
{"failed to read file for SHA comparison: ", fmt.tprintf("%v", read_err)},
|
{"failed to read file for SHA comparison: ", fmt.tprintf("%v", read_err)},
|
||||||
)
|
)
|
||||||
return .Error, msg
|
return {.Error}, msg
|
||||||
}
|
}
|
||||||
|
|
||||||
digest := hash.hash_bytes(hash.Algorithm.SHA256, data)
|
digest := hash.hash_bytes(hash.Algorithm.SHA256, data)
|
||||||
@@ -569,21 +567,20 @@ env_file_sync :: proc(f: ^EnvFile, dir: SyncDirection, d: ^Db) -> (SyncResult, s
|
|||||||
write_err := os.write_entire_file(f.Path, f.contents)
|
write_err := os.write_entire_file(f.Path, f.contents)
|
||||||
if write_err != nil {
|
if write_err != nil {
|
||||||
msg, _ := strings.concatenate({"failed to write file: ", fmt.tprintf("%v", write_err)})
|
msg, _ := strings.concatenate({"failed to write file: ", fmt.tprintf("%v", write_err)})
|
||||||
return .Error, msg
|
return {.Error}, msg
|
||||||
}
|
}
|
||||||
s := i32(result) | i32(SyncResult.Restored)
|
return result + {.Restored}, ""
|
||||||
return SyncResult(s), ""
|
|
||||||
case .TrustFilesystem:
|
case .TrustFilesystem:
|
||||||
if !env_file_backup(f) {
|
if !env_file_backup(f) {
|
||||||
return .Error, "failed to backup file"
|
return {.Error}, "failed to backup file"
|
||||||
}
|
}
|
||||||
return .BackedUp, ""
|
return result + {.BackedUp}, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, ""
|
return result, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
db_sync :: proc(d: ^Db, f: ^EnvFile) -> (SyncResult, string) {
|
db_sync :: proc(d: ^Db, f: ^EnvFile) -> (SyncFlag, string) {
|
||||||
return env_file_sync(f, .TrustFilesystem, d)
|
return env_file_sync(f, .TrustFilesystem, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ test_encrypt_decrypt_sqlite_roundtrip :: proc(t: ^testing.T) {
|
|||||||
defer {
|
defer {
|
||||||
delete(cfg.Keys)
|
delete(cfg.Keys)
|
||||||
}
|
}
|
||||||
key := cfg.Keys[0]
|
|
||||||
|
|
||||||
db_path := fixture_db_path()
|
db_path := fixture_db_path()
|
||||||
sqlite_data, read_err := os.read_entire_file_from_path(db_path, context.allocator)
|
sqlite_data, read_err := os.read_entire_file_from_path(db_path, context.allocator)
|
||||||
@@ -319,7 +318,7 @@ test_config_load_with_fixture_key :: proc(t: ^testing.T) {
|
|||||||
testing.expectf(t, len(key.Private) > 0, "private key path should not be empty")
|
testing.expectf(t, len(key.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.Public) > 0, "public key path should not be empty")
|
||||||
|
|
||||||
priv_kp, priv_ok := parse_ssh_private_key(key.Private)
|
_, priv_ok := parse_ssh_private_key(key.Private)
|
||||||
testing.expect(t, priv_ok, "should parse private key using config paths")
|
testing.expect(t, priv_ok, "should parse private key using config paths")
|
||||||
if !priv_ok {
|
if !priv_ok {
|
||||||
fmt.printf(" private key path was: '%s'\n", key.Private)
|
fmt.printf(" private key path was: '%s'\n", key.Private)
|
||||||
|
|||||||
13
db_test.odin
13
db_test.odin
@@ -4,33 +4,32 @@ import "core:testing"
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_db_update_required_noop :: proc(t: ^testing.T) {
|
test_db_update_required_noop :: proc(t: ^testing.T) {
|
||||||
testing.expect(t, !db_update_required(.Noop), "Noop should not require update")
|
testing.expect(t, !db_update_required({}), "Noop should not require update")
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_db_update_required_backed_up :: proc(t: ^testing.T) {
|
test_db_update_required_backed_up :: proc(t: ^testing.T) {
|
||||||
testing.expect(t, db_update_required(.BackedUp), "BackedUp should require update")
|
testing.expect(t, db_update_required({.BackedUp}), "BackedUp should require update")
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_db_update_required_dir_updated :: proc(t: ^testing.T) {
|
test_db_update_required_dir_updated :: proc(t: ^testing.T) {
|
||||||
testing.expect(t, db_update_required(.DirUpdated), "DirUpdated should require update")
|
testing.expect(t, db_update_required({.DirUpdated}), "DirUpdated should require update")
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_db_update_required_restored :: proc(t: ^testing.T) {
|
test_db_update_required_restored :: proc(t: ^testing.T) {
|
||||||
testing.expect(t, !db_update_required(.Restored), "Restored alone should not require update")
|
testing.expect(t, !db_update_required({.Restored}), "Restored alone should not require update")
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_db_update_required_error :: proc(t: ^testing.T) {
|
test_db_update_required_error :: proc(t: ^testing.T) {
|
||||||
testing.expect(t, !db_update_required(.Error), "Error alone should not require update")
|
testing.expect(t, !db_update_required({.Error}), "Error alone should not require update")
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_db_update_required_combined :: proc(t: ^testing.T) {
|
test_db_update_required_combined :: proc(t: ^testing.T) {
|
||||||
s := i32(SyncResult.DirUpdated) | i32(SyncResult.Restored)
|
combined := SyncFlag{.DirUpdated, .Restored}
|
||||||
combined := SyncResult(s)
|
|
||||||
testing.expect(t, db_update_required(combined), "DirUpdated|Restored should require update")
|
testing.expect(t, db_update_required(combined), "DirUpdated|Restored should require update")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
|
|
||||||
buildPhase = ''
|
buildPhase = ''
|
||||||
runHook preBuild
|
runHook preBuild
|
||||||
make version.odin
|
echo '${version}' > version.txt
|
||||||
odin build . -o:speed -out:${pname}
|
odin build . -o:speed -out:${pname}
|
||||||
runHook postBuild
|
runHook postBuild
|
||||||
'';
|
'';
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ CRYPTO_SIGN_SECRETKEY_BYTES :: 64
|
|||||||
@(default_calling_convention = "c")
|
@(default_calling_convention = "c")
|
||||||
foreign libsodium {
|
foreign libsodium {
|
||||||
sodium_init :: proc() -> c.int ---
|
sodium_init :: proc() -> c.int ---
|
||||||
crypto_box_keypair :: proc(pk: [^]u8, sk: [^]u8) -> c.int ---
|
// crypto_box_keypair :: proc(pk: [^]u8, sk: [^]u8) -> c.int ---
|
||||||
crypto_box_easy :: proc(ciphertext: [^]u8, plaintext: [^]u8, mlen: c.ulong, nonce: [^]u8, pk: [^]u8, sk: [^]u8) -> c.int ---
|
crypto_box_easy :: proc(ciphertext: [^]u8, plaintext: [^]u8, mlen: c.ulong, nonce: [^]u8, pk: [^]u8, sk: [^]u8) -> c.int ---
|
||||||
crypto_box_open_easy :: proc(plaintext: [^]u8, ciphertext: [^]u8, clen: c.ulong, nonce: [^]u8, pk: [^]u8, sk: [^]u8) -> c.int ---
|
crypto_box_open_easy :: proc(plaintext: [^]u8, ciphertext: [^]u8, clen: c.ulong, nonce: [^]u8, pk: [^]u8, sk: [^]u8) -> c.int ---
|
||||||
crypto_secretbox_easy :: proc(ciphertext: [^]u8, plaintext: [^]u8, mlen: c.ulong, nonce: [^]u8, key: [^]u8) -> c.int ---
|
crypto_secretbox_easy :: proc(ciphertext: [^]u8, plaintext: [^]u8, mlen: c.ulong, nonce: [^]u8, key: [^]u8) -> c.int ---
|
||||||
@@ -28,3 +28,4 @@ foreign libsodium {
|
|||||||
crypto_sign_ed25519_sk_to_curve25519 :: proc(curve25519_sk: [^]u8, ed25519_sk: [^]u8) -> c.int ---
|
crypto_sign_ed25519_sk_to_curve25519 :: proc(curve25519_sk: [^]u8, ed25519_sk: [^]u8) -> c.int ---
|
||||||
randombytes_buf :: proc(buf: [^]u8, size: c.ulong) ---
|
randombytes_buf :: proc(buf: [^]u8, size: c.ulong) ---
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:os"
|
|
||||||
import "core:strings"
|
|
||||||
import "core:testing"
|
import "core:testing"
|
||||||
|
|
||||||
TEST_KEY_DIR :: "/tmp/envr-test-keys"
|
TEST_KEY_DIR :: "/tmp/envr-test-keys"
|
||||||
@@ -49,7 +47,11 @@ test_private_key_pub_matches_public_key :: proc(t: ^testing.T) {
|
|||||||
testing.expect(
|
testing.expect(
|
||||||
t,
|
t,
|
||||||
pub_from_pub == kp.Public,
|
pub_from_pub == kp.Public,
|
||||||
fmt.tprintf("public key mismatch:\n from .pub: %v\n from priv: %v", pub_from_pub, kp.Public),
|
fmt.tprintf(
|
||||||
|
"public key mismatch:\n from .pub: %v\n from priv: %v",
|
||||||
|
pub_from_pub,
|
||||||
|
kp.Public,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,3 +69,4 @@ test_read_wire_string :: proc(t: ^testing.T) {
|
|||||||
testing.expect(t, ok2, "expected second read to succeed")
|
testing.expect(t, ok2, "expected second read to succeed")
|
||||||
testing.expect(t, s2 == "", "expected empty string")
|
testing.expect(t, s2 == "", "expected empty string")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,18 +3,9 @@ package main
|
|||||||
import "core:encoding/json"
|
import "core:encoding/json"
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:io"
|
import "core:io"
|
||||||
import "core:os"
|
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
import "core:terminal"
|
|
||||||
|
|
||||||
render_table :: proc(headers: []string, rows: [][]string) {
|
render_table :: proc(headers: []string, rows: [][]string) {
|
||||||
if !terminal.is_terminal(os.stdout) {
|
|
||||||
w := io.to_writer(os.to_writer(os.stdout))
|
|
||||||
render_json_rows(w, headers, rows)
|
|
||||||
io.write_string(w, "\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
col_widths := make([dynamic]int, 0, len(headers))
|
col_widths := make([dynamic]int, 0, len(headers))
|
||||||
for i in 0 ..< len(headers) {
|
for i in 0 ..< len(headers) {
|
||||||
append(&col_widths, strings.rune_count(headers[i]))
|
append(&col_widths, strings.rune_count(headers[i]))
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import "core:encoding/json"
|
import "core:encoding/json"
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:io"
|
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
import "core:testing"
|
import "core:testing"
|
||||||
|
|
||||||
|
|||||||
1
version.txt
Normal file
1
version.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0.2.0
|
||||||
Reference in New Issue
Block a user