test: pin the edit symlink refusal, AppendRegular's clean check and Remove (#188)
CI / Lint / Test / Vet (pull_request) Failing after 1m6s
CI / Mirror to GitHub (pull_request) Skipped

Review round 1 on PR #189 (three tests stayed green with their guard
removed):
- TestEditRefusesANoteBehindASymlinkedDirectory had no .git, so Edit
  refused "not the sync source" before any path check. It now has one,
  plus a positive control.
- AppendRegular's unclean-path case was refused by a final symlink, not
  cleanRel. New test where ".." after a symlinked dir resolves to a real
  file, for AppendRegular and PutRegular.
- brainfs.Remove had no test; new one (refuses behind a symlinked dir,
  removes a final link but never its target, positive control).

Also: promote/reject create with O_EXCL (new brainfs.CreateRegular), so a
note that appears after the collision check is never overwritten;
gofmt fixes in provenance.go and wiki/log.go; dead wingDir and
putBrainFile removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 22:57:26 +02:00
co-authored by Claude Opus 5.5
parent 138d6e2545
commit db18785bfc
7 changed files with 80 additions and 13 deletions
+4 -2
View File
@@ -133,7 +133,9 @@ func PromoteNote(brainDir string, opts PromoteOptions) (string, error) {
if err != nil {
return "", err
}
if _, err := putBrainFile(brainDir, dest, []byte(promoted)); err != nil {
// Created with O_EXCL: a note that appeared after the collision check
// above is never overwritten.
if err := createBrainFile(brainDir, dest, []byte(promoted)); err != nil {
return "", fmt.Errorf("write promoted note: %w", err)
}
if err := removeBrainFile(brainDir, src); err != nil {
@@ -196,7 +198,7 @@ func RejectNote(brainDir string, opts RejectOptions) (string, error) {
// Write-then-delete: the source survives any write failure. Both go
// through the brain root (hyperguild#188).
if _, err := putBrainFile(brainDir, dest, []byte(fm.render()+body)); err != nil {
if err := createBrainFile(brainDir, dest, []byte(fm.render()+body)); err != nil {
return "", fmt.Errorf("write rejected note: %w", err)
}
if err := removeBrainFile(brainDir, src); err != nil {
+6 -7
View File
@@ -44,20 +44,19 @@ func StampProvenanceFile(brainDir, relPath, origin, writtenBy string) error {
return nil
}
// putBrainFile creates or replaces the file at abs (inside brainDir) through
// the brain root, never through a symlinked directory or file (#188).
func putBrainFile(brainDir, abs string, data []byte) (string, error) {
// createBrainFile creates the file at abs (inside brainDir) through the
// brain root and fails if anything is already there.
func createBrainFile(brainDir, abs string, data []byte) error {
rel, err := filepath.Rel(brainDir, abs)
if err != nil {
return "", err
return err
}
rel = filepath.ToSlash(rel)
bfs, err := brainfs.Open(brainDir)
if err != nil {
return "", fmt.Errorf("open brain: %w", err)
return fmt.Errorf("open brain: %w", err)
}
defer func() { _ = bfs.Close() }()
return rel, bfs.PutRegular(rel, data)
return bfs.CreateRegular(filepath.ToSlash(rel), data)
}
// removeBrainFile removes the file at abs (inside brainDir) through the
-3
View File
@@ -7,7 +7,6 @@ import (
"fmt"
"io"
"io/fs"
"path/filepath"
"sort"
"strings"
"time"
@@ -31,7 +30,6 @@ func BuildWingIndex(brainDir, wing string) error {
if w == "" {
return fmt.Errorf("invalid wing %q", wing)
}
wingDir := filepath.Join(brainDir, "wiki", w)
// Listed and read through the brain root (hyperguild#157): a wing or
// hall that is a symlink is not followed, and notes are regular files.
bfs, err := brainfs.Open(brainDir)
@@ -64,7 +62,6 @@ func BuildWingIndex(brainDir, wing string) error {
// Written through the brain root, never through a symlinked _index.md
// (hyperguild#158).
_ = wingDir
return bfs.PutRegular("wiki/"+w+"/_index.md", []byte(b.String()))
}
+19
View File
@@ -156,6 +156,25 @@ func (b *Brain) PutRegular(rel string, data []byte) error {
} else if !errors.Is(err, fs.ErrNotExist) {
return err
}
return b.createNew(rel, data)
}
// CreateRegular creates a new regular file at a brain-relative path, with
// PutRegular's parent rules, and fails with fs.ErrExist if anything is
// already there: for writers that must never overwrite (promote, reject).
func (b *Brain) CreateRegular(rel string, data []byte) error {
if err := cleanRel(rel); err != nil {
return err
}
if err := b.mkdirReal(path.Dir(rel)); err != nil {
return err
}
return b.createNew(rel, data)
}
// createNew creates rel with O_EXCL and writes data only after confirming
// the path is the regular file it created.
func (b *Brain) createNew(rel string, data []byte) error {
f, err := b.root.OpenFile(rel, os.O_WRONLY|os.O_CREATE|os.O_EXCL|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0o644)
if err != nil {
return err
@@ -453,3 +453,43 @@ func TestRealParentRefusesASymlinkedDirectoryOrUncleanPath(t *testing.T) {
assert.ErrorIs(t, b.RealParent("wiki/../raw/x.md"), ErrNotRegular)
assert.NoError(t, b.RealParent("wiki/x.md"), "positive control")
}
// cleanRel must refuse an unclean path on its own: here the ".." resolves,
// after a symlinked directory, to a real regular file, so only the clean
// check stands between the call and that file.
func TestAppendRegularRefusesAnUncleanPathThatResolvesToARealFile(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(dir, "raw", "keep"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "raw", "v.md"), []byte("untouched"), 0o644))
require.NoError(t, os.Symlink(filepath.Join("raw", "keep"), filepath.Join(dir, "indir")))
b, err := Open(dir)
require.NoError(t, err)
defer func() { _ = b.Close() }()
assert.ErrorIs(t, b.AppendRegular("indir/../v.md", []byte("x")), ErrNotRegular)
assert.ErrorIs(t, b.PutRegular("indir/../v.md", []byte("x")), ErrNotRegular)
got, _ := os.ReadFile(filepath.Join(dir, "raw", "v.md"))
assert.Equal(t, "untouched", string(got))
}
func TestRemoveRefusesAPathBehindASymlinkedDirectoryAndRemovesOnlyTheLink(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(dir, "raw"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "raw", "v.md"), []byte("keep"), 0o644))
require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki"), 0o755))
require.NoError(t, os.Symlink(filepath.Join("..", "raw"), filepath.Join(dir, "wiki", "alias")))
require.NoError(t, os.Symlink(filepath.Join("..", "raw", "v.md"), filepath.Join(dir, "wiki", "link.md")))
require.NoError(t, os.WriteFile(filepath.Join(dir, "wiki", "n.md"), []byte("n"), 0o644))
b, err := Open(dir)
require.NoError(t, err)
defer func() { _ = b.Close() }()
assert.ErrorIs(t, b.Remove("wiki/alias/v.md"), ErrNotRegular)
_, err = os.Stat(filepath.Join(dir, "raw", "v.md"))
assert.NoError(t, err, "the file behind the symlinked directory is kept")
require.NoError(t, b.Remove("wiki/link.md"))
_, err = os.Stat(filepath.Join(dir, "raw", "v.md"))
assert.NoError(t, err, "removing a final symlink never removes its target")
require.NoError(t, b.Remove("wiki/n.md"), "positive control")
_, err = os.Stat(filepath.Join(dir, "wiki", "n.md"))
assert.True(t, os.IsNotExist(err))
}
@@ -13,6 +13,9 @@ import (
// a symlinked directory inside the brain.
func TestEditRefusesANoteBehindASymlinkedDirectory(t *testing.T) {
b := t.TempDir()
// Edit refuses a tree that is not the sync source before any path
// check, so the test needs a .git to reach the symlink refusal.
require.NoError(t, os.MkdirAll(filepath.Join(b, ".git"), 0o755))
victim := filepath.Join(b, "raw", "facts", "v.md")
require.NoError(t, os.MkdirAll(filepath.Dir(victim), 0o755))
require.NoError(t, os.WriteFile(victim, []byte("old text\n"), 0o644))
@@ -22,4 +25,12 @@ func TestEditRefusesANoteBehindASymlinkedDirectory(t *testing.T) {
assert.Error(t, err)
got, _ := os.ReadFile(victim)
assert.Equal(t, "old text\n", string(got))
real := filepath.Join(b, "wiki", "w", "facts", "r.md")
require.NoError(t, os.MkdirAll(filepath.Dir(real), 0o755))
require.NoError(t, os.WriteFile(real, []byte("old text\n"), 0o644))
_, err = Edit(b, "wiki/w/facts/r.md", "old text", "new text", ScopeNotes)
require.NoError(t, err, "positive control: a note in a real directory is edited")
got, _ = os.ReadFile(real)
assert.Equal(t, "new text\n", string(got))
}
@@ -101,7 +101,6 @@ func Apply(content string, s Stamp) (string, error) {
return out, nil
}
// plainGroups keeps the provably plain key groups of a frontmatter block.
func plainGroups(block string) string {
lines := strings.Split(strings.TrimSuffix(block, "\n"), "\n")