feat: Implement platform-specific hosts file path retrieval

- Created a new `platform` package to handle OS-specific hosts file paths.
- Added implementations for macOS, Linux, and Windows to retrieve the hosts file path.
- Introduced tests for the `GetHostsFilePath` function to ensure correct behavior across platforms.

feat: Add DNS resolver for IP lookups

- Implemented a `resolver` package for performing DNS lookups.
- Created a `DNSResolver` struct with a `LookupIP` method to handle A-record lookups and CNAME resolution.
- Added comprehensive tests for various DNS scenarios, including CNAME chains and error handling.

chore: Refactor project structure and update dependencies

- Restructured the project to follow Go's standard package layout.
- Updated `go.mod` to Go 1.20 and added `golang.org/x/net` dependency.
- Removed old source files and ensured a clean build with all tests passing.
This commit is contained in:
2026-03-03 18:37:02 -05:00
parent 5e28d0bd8c
commit f0ce5a4042
30 changed files with 2847 additions and 473 deletions
+29 -29
View File
@@ -26,7 +26,7 @@
- **Key types/interfaces used**: None — this phase creates the skeleton
- **Codebase conventions to follow**: Go standard project layout with sub-packages. Module name is `dns-helper` (from current `go.mod`). Build tags for platform-specific files use `//go:build` directive (Go 1.17+ format).
- [ ] T001 Create package directory structure per implementation plan
- [x] T001 Create package directory structure per implementation plan
**Context**:
- **Target state**: The project currently has a flat structure with all `.go` files in the root `main` package. Create four new package directories:
@@ -40,7 +40,7 @@
- **Key decisions**: These package names were chosen in the plan (see plan.md "Project Structure" section). Each package has a single responsibility: `resolver` = DNS lookups, `hostfile` = hosts file read/parse/write, `platform` = OS-specific paths, `lockfile` = concurrency serialization.
- **Acceptance signal**: All four directories exist at the repository root alongside the existing source files.
- [ ] T002 Update `go.mod` to Go 1.20 and add `golang.org/x/net v0.35.0`
- [x] T002 Update `go.mod` to Go 1.20 and add `golang.org/x/net v0.35.0`
**Context**:
- **Current go.mod**:
@@ -75,7 +75,7 @@
- **Key types/interfaces used**: `os.FileInfo`, `os.FileMode`, `os.File` from Go stdlib
- **Codebase conventions to follow**: All package functions return errors to caller (Constitution Principle IV). No `os.Exit()` outside `main()`. Use `fmt.Errorf("context: %w", err)` for error wrapping. Use Go 1.20 `//go:build` directive for build tags (not the legacy `// +build` comment).
- [ ] T003 [P] Create FileSystem interface and OS implementation in `hostfile/filesystem.go`
- [x] T003 [P] Create FileSystem interface and OS implementation in `hostfile/filesystem.go`
**Context**:
- **Implements**: The FileSystem abstraction from research R-007 for testability (Constitution Principle VI). All Host file I/O goes through this interface so tests can inject fakes.
@@ -111,7 +111,7 @@
- **Key decisions (R-007)**: Production code uses `OSFileSystem{}`. Tests inject a fake struct that records calls and returns configurable errors. `Chmod` is included because atomic write must copy permissions from the original hosts file to the temp file before rename (R-002 step 2).
- **Acceptance signal**: `go build ./hostfile/` compiles without errors.
- [ ] T004 [P] Create platform package with interface and implementations in `platform/`
- [x] T004 [P] Create platform package with interface and implementations in `platform/`
**Context**:
- **Implements**: The platform contract from `contracts/packages.md`:
@@ -178,7 +178,7 @@
- **Gotcha**: Use `//go:build` directive (Go 1.17+ syntax), NOT the legacy `// +build` comment. Go 1.20 supports both but the new syntax is preferred.
- **Acceptance signal**: `go build ./platform/` compiles without errors on the current OS.
- [ ] T005 [P] Create lockfile package in `lockfile/lockfile.go`
- [x] T005 [P] Create lockfile package in `lockfile/lockfile.go`
**Context**:
- **Implements**: The lockfile contract from `contracts/packages.md`:
@@ -252,7 +252,7 @@
- **Gotcha**: `Release()` should not return error if the file is already gone — check for `os.IsNotExist` and suppress that specific error.
- **Acceptance signal**: `go build ./lockfile/` compiles without errors.
- [ ] T006 [P] Create lockfile tests in `lockfile/lockfile_test.go`
- [x] T006 [P] Create lockfile tests in `lockfile/lockfile_test.go`
**Context**:
- **Target file state**: `lockfile/lockfile.go` exists from T005 with `Acquire()` and `Lock.Release()`.
@@ -266,7 +266,7 @@
- **Key decisions**: Keep max wait short in tests or provide a way to configure timeout. Since the constants are package-level, tests may need to accept the 2-second wait for the "double acquire" test case.
- **Acceptance signal**: `go test ./lockfile/` passes all test cases.
- [ ] T007 [P] Create platform tests in `platform/platform_test.go`
- [x] T007 [P] Create platform tests in `platform/platform_test.go`
**Context**:
- **Target file state**: `platform/` package exists from T004 with `GetHostsFilePath()` for each OS.
@@ -309,7 +309,7 @@
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation (TDD Red phase)**
- [ ] T008 [P] [US1] Write managed block parsing tests in `hostfile/managed_test.go`
- [x] T008 [P] [US1] Write managed block parsing tests in `hostfile/managed_test.go`
**Context**:
- **Depends on**: T003 (FileSystem interface exists so the package compiles). T009 (types must exist for tests to reference).
@@ -333,7 +333,7 @@
```
- **Acceptance signal**: Tests compile (with stubs) or fail with clear assertion errors. After T012 implementation, `go test ./hostfile/ -run TestManaged` passes.
- [ ] T009 [P] [US1] Write hostfile read/write tests in `hostfile/hostfile_test.go`
- [x] T009 [P] [US1] Write hostfile read/write tests in `hostfile/hostfile_test.go`
**Context**:
- **Depends on**: T003 (FileSystem interface for mocking).
@@ -366,7 +366,7 @@
### Implementation for User Story 1
- [ ] T010 [US1] Create HostsFile and DNSEntry types with Manager interface in `hostfile/hostfile.go`
- [x] T010 [US1] Create HostsFile and DNSEntry types with Manager interface in `hostfile/hostfile.go`
**Context**:
- **Implements**: Types from `data-model.md` and Manager interface from `contracts/packages.md`:
@@ -435,7 +435,7 @@
- **Gotcha**: Do NOT implement `Read()` or `Write()` yet — just the types, constants, and constructor. Read/Write are implemented in T013 and T014.
- **Acceptance signal**: `go build ./hostfile/` compiles. Tests from T008/T009 can now reference the types (though they'll still fail on logic).
- [ ] T011 [US1] Implement managed block parsing and corruption detection in `hostfile/managed.go`
- [x] T011 [US1] Implement managed block parsing and corruption detection in `hostfile/managed.go`
**Context**:
- **Implements**: Managed block parsing that splits raw file lines into prefix/managed/postfix sections. Also implements corruption detection (R-008, FR-011).
@@ -506,7 +506,7 @@
- **Gotcha**: The current code's `extractLinesToEdit` in `dataprep.go` (lines 14-51) has the zero-index bug. The new implementation must NOT replicate this. Use `-1` sentinel.
- **Acceptance signal**: `go test ./hostfile/ -run TestParseManagedBlock` passes (corruption scenarios and valid parsing).
- [ ] T012 [US1] Implement AddEntries and content assembly in `hostfile/managed.go`
- [x] T012 [US1] Implement AddEntries and content assembly in `hostfile/managed.go`
**Context**:
- **Implements**: The `AddEntries` function from `contracts/packages.md`:
@@ -581,7 +581,7 @@
- `AssembleContent` omits the managed block entirely when there are zero managed entries (per `data-model.md` ManagedBlock rules: "When all entries are removed, the entire block markers included is removed").
- **Acceptance signal**: `go test ./hostfile/ -run TestAddEntries` and `go test ./hostfile/ -run TestAssembleContent` pass.
- [ ] T013 [US1] Implement Read function in `hostfile/hostfile.go`
- [x] T013 [US1] Implement Read function in `hostfile/hostfile.go`
**Context**:
- **Implements**: The `Read` method on `Manager` from `contracts/packages.md`:
@@ -628,7 +628,7 @@
- **Gotcha**: The `\r` trim loop MUST be included — without it, all managed block marker matching and hostname matching will fail on Windows because lines will have a trailing `\r`. The current codebase uses `bufio.Scanner` which handles this automatically, but `strings.Split` does not.
- **Acceptance signal**: `go test ./hostfile/ -run TestRead` passes — correctly parses files with and without managed blocks.
- [ ] T014 [US1] Implement atomic Write with backup in `hostfile/hostfile.go`
- [x] T014 [US1] Implement atomic Write with backup in `hostfile/hostfile.go`
**Context**:
- **Implements**: The `Write` method on `Manager` from `contracts/packages.md`:
@@ -759,7 +759,7 @@
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation (TDD Red phase)**
- [ ] T015 [P] [US2] Write resolver tests in `resolver/resolver_test.go`
- [x] T015 [P] [US2] Write resolver tests in `resolver/resolver_test.go`
**Context**:
- **Depends on**: T016 (interface stubs must exist for tests to compile).
@@ -796,7 +796,7 @@
### Implementation for User Story 2
- [ ] T016 [US2] Create Resolver interface and DNSResolver struct in `resolver/resolver.go`
- [x] T016 [US2] Create Resolver interface and DNSResolver struct in `resolver/resolver.go`
**Context**:
- **Implements**: Resolver interface from `contracts/packages.md`:
@@ -834,7 +834,7 @@
- **Key decisions (R-001)**: Constants match the contract: 5s timeout, max 10 CNAME depth, 1232-byte UDP buffer (standard DNS over UDP limit).
- **Acceptance signal**: `go build ./resolver/` compiles.
- [ ] T017 [US2] Implement LookupIP with dnsmessage, CNAME chain, and dedup in `resolver/resolver.go`
- [x] T017 [US2] Implement LookupIP with dnsmessage, CNAME chain, and dedup in `resolver/resolver.go`
**Context**:
- **Implements**: The full `LookupIP` method on `DNSResolver`. This replaces the current `lookupIP` function in `dns.go` which has two bugs:
@@ -1041,7 +1041,7 @@
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation (TDD Red phase)**
- [ ] T018 [P] [US3] Write deletion tests in `hostfile/managed_test.go`
- [x] T018 [P] [US3] Write deletion tests in `hostfile/managed_test.go`
**Context**:
- **Target file state**: `hostfile/managed_test.go` already exists from T008 with parsing and AddEntries tests. Append new test functions for deletion.
@@ -1072,7 +1072,7 @@
### Implementation for User Story 3
- [ ] T019 [US3] Implement RemoveByHostname in `hostfile/managed.go`
- [x] T019 [US3] Implement RemoveByHostname in `hostfile/managed.go`
**Context**:
- **Implements**: `RemoveByHostname` from `contracts/packages.md`:
@@ -1106,7 +1106,7 @@
- **Key decisions (R-004)**: The critical fix is inverting the loop order — iterate LINES on the outside, check ALL hostnames on the inside. The buggy code iterated hostnames on the outside and re-scanned original content each pass, producing duplicates and failing to remove.
- **Acceptance signal**: `go test ./hostfile/ -run TestRemoveByHostname` passes all 7 test cases from T018.
- [ ] T020 [US3] Implement RemoveAll in `hostfile/managed.go`
- [x] T020 [US3] Implement RemoveAll in `hostfile/managed.go`
**Context**:
- **Implements**: `RemoveAll` from `contracts/packages.md`:
@@ -1163,7 +1163,7 @@
```
- **Codebase conventions to follow**: Only `main()` calls `os.Exit()`. Errors printed to stderr (`fmt.Fprintf(os.Stderr, ...)`). Informational output to stdout. Banner prints on every invocation. See `contracts/cli.md` for exact output format.
- [ ] T021 [US4] Rewrite `main.go` with CLI parsing, validation, and full package integration
- [x] T021 [US4] Rewrite `main.go` with CLI parsing, validation, and full package integration
**Context**:
- **Replaces**: The entire current `main.go` (100 lines). The current code has multiple problems:
@@ -1495,7 +1495,7 @@
- **Gotcha**: When `delete -host` finds no matching entries, report "No managed entries found" but exit 0 (not an error per CLI contract).
- **Acceptance signal**: `go build .` compiles. Manual testing of `dns-helper add -host ...`, `dns-helper delete -host ...`, `dns-helper delete all`, `dns-helper` (no args), and `dns-helper invalid` all produce expected output per `contracts/cli.md`.
- [ ] T022 [US4] Verify all CLI helpers compile and match `contracts/cli.md` output
- [x] T022 [US4] Verify all CLI helpers compile and match `contracts/cli.md` output
**Context**:
- **Target file state**: `main.go` now contains complete implementations from T021: `main()`, `runAdd()`, `runDelete()`, `runDeleteAll()`, and `printUsage()`.
@@ -1522,7 +1522,7 @@
- **Key types/interfaces used**: `func GetHostsFilePath() (string, error)` from T004
- **Codebase conventions to follow**: Error messages include the path that was checked. Use `fmt.Errorf` with `%w` for wrapping. No `os.Exit`, `log.Fatal`, or `term()` anywhere in the platform package.
- [ ] T023 [US5] Audit platform implementations for error consistency and fix if needed
- [x] T023 [US5] Audit platform implementations for error consistency and fix if needed
**Context**:
- **Current state**: The platform package was created from scratch in T004 with correct error handling. This task verifies the implementations are consistent and adds any missing error detail.
@@ -1548,7 +1548,7 @@
- **Expected state after this task**: All three platform files use `os.Stat` + `fmt.Errorf` and return errors. No calls to `term()`, `os.Exit()`, `log.Fatal()`, or any process-terminating function.
- **Acceptance signal**: `grep -r "os.Exit\|log.Fatal\|term(" platform/` returns no results. `go vet ./platform/` passes.
- [ ] T024 [P] [US5] Add cross-platform error message tests in `platform/platform_test.go`
- [x] T024 [P] [US5] Add cross-platform error message tests in `platform/platform_test.go`
**Context**:
- **Target file state**: `platform/platform_test.go` exists from T007 with basic happy-path tests.
@@ -1573,7 +1573,7 @@
- **Files modified**: `go.mod`, `go.sum` (via `go mod tidy`)
- **Codebase conventions to follow**: Run `go vet ./...` and `go build ./...` to verify cleanliness. No unused imports or variables.
- [ ] T025 [US6] Remove old source files from repository root
- [x] T025 [US6] Remove old source files from repository root
**Context**:
- **Files to delete** (all at repository root):
@@ -1594,7 +1594,7 @@
- **Gotcha**: After deleting these files, `go build .` should succeed because `main.go` no longer imports `miekg/dns` or references any of the old functions. If compilation fails, check that `main.go` doesn't reference any old function names.
- **Acceptance signal**: All 7 old files deleted. `go build .` succeeds on the current OS.
- [ ] T026 [US6] Run `go mod tidy` to remove `miekg/dns` and transitive dependencies
- [x] T026 [US6] Run `go mod tidy` to remove `miekg/dns` and transitive dependencies
**Context**:
- **Current go.mod dependencies** (after T002):
@@ -1619,7 +1619,7 @@
- **Expected implementation**: Run `go mod tidy`. Verify `go.mod` no longer contains `miekg/dns`, `x/mod`, or `x/tools`. Run `go list -m all` to confirm only approved dependencies remain.
- **Acceptance signal**: `go mod tidy` succeeds. `go list -m all` shows only `golang.org/x/net` and `golang.org/x/sys`. No unapproved dependencies (FR-010, SC-006).
- [ ] T027 [US6] Verify clean build, all tests pass, and no dead code
- [x] T027 [US6] Verify clean build, all tests pass, and no dead code
**Context**:
- **Verification checklist** (from spec SC-006):
@@ -1640,7 +1640,7 @@
**Purpose**: Final validation against quickstart scenarios and acceptance criteria
- [ ] T028 Run quickstart validation scenarios from `quickstart.md`
- [x] T028 Run quickstart validation scenarios from `quickstart.md`
**Context**:
- **Quickstart scenarios** (from `quickstart.md`):
@@ -1656,7 +1656,7 @@
5. `go mod tidy` produces no changes (already clean).
- **Acceptance signal**: All quickstart commands succeed. The tool is ready for manual integration testing.
- [ ] T029 Final review: verify all success criteria from spec.md
- [x] T029 Final review: verify all success criteria from spec.md
**Context**:
- **Success criteria checklist** (from spec.md):