Skip to content

fix(fix): preserve an existing document's file mode on rewrite - #138

Merged
pablontiv merged 1 commit into
masterfrom
gnhf/fix-a-shipped-permis-549adc
Aug 6, 2026
Merged

fix(fix): preserve an existing document's file mode on rewrite#138
pablontiv merged 1 commit into
masterfrom
gnhf/fix-a-shipped-permis-549adc

Conversation

@pablontiv

Copy link
Copy Markdown
Owner

Closes #137.

The defect

internal/fix carried its own atomic-write helper (internal/fix/writefile.go) that never stat'ed the target. It applied the caller's perm unconditionally:

tmp.Chmod(perm)   // then rename over the target

All 13 non-test call sites (8 in internal/fix/fix.go, 5 in internal/fix/repair.go) passed a hardcoded 0o644, so rootline fix and rootline repair apply widened a 0600 or 0400 governed document to 0644 on every rewrite. Rewriting frontmatter is a content operation and carries no instruction to change who may read the file, so this is a privacy regression, not a cosmetic one.

internal/fsx/writeFileAtomic was never affected: it stats the target and overrides perm with the existing mode, which makes its 0o644 arguments a create-new default only. The real defect was the divergence — two atomic writers with different mode semantics, and internal/fix was the unsafe one.

The fix

Deleted internal/fix/writefile.go and routed all 13 call sites through fsx.WriteFileAtomic, rather than patching mode logic into a second copy that can diverge again.

Failure-path cleanup is preserved and slightly strengthened: internal/fix relied on five explicit cleanup() calls, while fsx uses a defer guarded by a committed flag, so staging files are also removed on panic. TestApplyRepairLeavesNoStagingFiles pins that half of the contract at the path level.

fix.newFileMode replaces the bare 0o644 literal, with a doc comment stating that the value is a create-new default and is ignored when the target already exists.

Regression evidence

internal/fix/filemode_test.go drives both write paths (ApplyRepair and ApplyProposals) against a seeded document and asserts the mode is unchanged. Each test first asserts the document actually changed, so a mode check cannot pass vacuously.

Against the pre-fix code, all three fail:

--- FAIL: TestApplyRepairPreservesRestrictiveFileMode
    filemode_test.go:101: mode = 0644, want 0600 — the rewrite changed who can read the document
--- FAIL: TestApplyRepairPreservesReadOnlyFileMode
    filemode_test.go:115: mode = 0644, want 0400 — the rewrite changed who can read the document
--- FAIL: TestApplyProposalsPreservesRestrictiveFileMode
    filemode_test.go:134: mode = 0644, want 0600 — the rewrite changed who can read the document

With the fix they pass.

Behavior changes this consolidation introduces

Both were named in #137 before landing:

  1. fsx returns an error when os.Stat on the target fails for a reason other than NotExist — a failure mode internal/fix did not have.
  2. Callers can no longer force a mode onto an existing file. No caller wanted to; all 13 passed the same default.

Verification

just check, just test and just coverage-check all pass. internal/fix coverage moved 87.3% → 89.0%: 79 well-covered production lines were deleted along with the five helper-level tests that duplicated internal/fsx/atomic_test.go.

…e internal/fix permission-widening defect, then fixed it under strict TDD by consolidating internal/fix onto internal/fsx, with all three repo gates passing.
@pablontiv
pablontiv merged commit 7d95e35 into master Aug 6, 2026
11 checks passed
@pablontiv
pablontiv deleted the gnhf/fix-a-shipped-permis-549adc branch August 6, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rootline fix and repair apply widen a 0600 governed document to 0644 on every rewrite

1 participant