Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac84ddf5a9 | ||
|
|
db18785bfc |
@@ -133,7 +133,9 @@ func PromoteNote(brainDir string, opts PromoteOptions) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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)
|
return "", fmt.Errorf("write promoted note: %w", err)
|
||||||
}
|
}
|
||||||
if err := removeBrainFile(brainDir, src); err != nil {
|
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
|
// Write-then-delete: the source survives any write failure. Both go
|
||||||
// through the brain root (hyperguild#188).
|
// 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)
|
return "", fmt.Errorf("write rejected note: %w", err)
|
||||||
}
|
}
|
||||||
if err := removeBrainFile(brainDir, src); err != nil {
|
if err := removeBrainFile(brainDir, src); err != nil {
|
||||||
|
|||||||
@@ -58,3 +58,30 @@ func TestStampProvenanceFileRefusesANoteBehindASymlinkedDirectory(t *testing.T)
|
|||||||
got, _ = os.ReadFile(real)
|
got, _ = os.ReadFile(real)
|
||||||
assert.Contains(t, string(got), "written_by: x")
|
assert.Contains(t, string(got), "written_by: x")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The same through an in-brain symlinked wing and raw/rejected, which
|
||||||
|
// os.Root alone would follow.
|
||||||
|
func TestPromoteAndRejectRefuseAnInBrainSymlinkedDestinationDirectory(t *testing.T) {
|
||||||
|
brain := t.TempDir()
|
||||||
|
raw := filepath.Join(brain, "raw")
|
||||||
|
require.NoError(t, os.MkdirAll(raw, 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(raw, "2026-10-05-n.md"), []byte("---\ntitle: n\n---\nbody\n"), 0o644))
|
||||||
|
target := filepath.Join(brain, "archive")
|
||||||
|
require.NoError(t, os.MkdirAll(target, 0o755))
|
||||||
|
require.NoError(t, os.MkdirAll(filepath.Join(brain, "wiki"), 0o755))
|
||||||
|
require.NoError(t, os.Symlink(filepath.Join("..", "archive"), filepath.Join(brain, "wiki", "lw")))
|
||||||
|
require.NoError(t, os.Symlink(filepath.Join("..", "archive"), filepath.Join(raw, "rejected")))
|
||||||
|
|
||||||
|
_, err := api.PromoteNote(brain, api.PromoteOptions{Filename: "2026-10-05-n.md", Wing: "lw", Hall: "facts"})
|
||||||
|
assert.Error(t, err)
|
||||||
|
_, err = api.RejectNote(brain, api.RejectOptions{Filename: "2026-10-05-n.md"})
|
||||||
|
assert.Error(t, err)
|
||||||
|
ents, _ := os.ReadDir(target)
|
||||||
|
assert.Empty(t, ents, "a note was written at the in-brain link's target")
|
||||||
|
_, err = os.Stat(filepath.Join(raw, "2026-10-05-n.md"))
|
||||||
|
assert.NoError(t, err, "the raw source is kept")
|
||||||
|
|
||||||
|
require.NoError(t, os.Remove(filepath.Join(raw, "rejected")))
|
||||||
|
_, err = api.RejectNote(brain, api.RejectOptions{Filename: "2026-10-05-n.md"})
|
||||||
|
require.NoError(t, err, "positive control: reject into a real raw/rejected")
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,20 +44,19 @@ func StampProvenanceFile(brainDir, relPath, origin, writtenBy string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// putBrainFile creates or replaces the file at abs (inside brainDir) through
|
// createBrainFile creates the file at abs (inside brainDir) through the
|
||||||
// the brain root, never through a symlinked directory or file (#188).
|
// brain root and fails if anything is already there.
|
||||||
func putBrainFile(brainDir, abs string, data []byte) (string, error) {
|
func createBrainFile(brainDir, abs string, data []byte) error {
|
||||||
rel, err := filepath.Rel(brainDir, abs)
|
rel, err := filepath.Rel(brainDir, abs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return err
|
||||||
}
|
}
|
||||||
rel = filepath.ToSlash(rel)
|
|
||||||
bfs, err := brainfs.Open(brainDir)
|
bfs, err := brainfs.Open(brainDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("open brain: %w", err)
|
return fmt.Errorf("open brain: %w", err)
|
||||||
}
|
}
|
||||||
defer func() { _ = bfs.Close() }()
|
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
|
// removeBrainFile removes the file at abs (inside brainDir) through the
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -31,7 +30,6 @@ func BuildWingIndex(brainDir, wing string) error {
|
|||||||
if w == "" {
|
if w == "" {
|
||||||
return fmt.Errorf("invalid wing %q", wing)
|
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
|
// 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.
|
// hall that is a symlink is not followed, and notes are regular files.
|
||||||
bfs, err := brainfs.Open(brainDir)
|
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
|
// Written through the brain root, never through a symlinked _index.md
|
||||||
// (hyperguild#158).
|
// (hyperguild#158).
|
||||||
_ = wingDir
|
|
||||||
return bfs.PutRegular("wiki/"+w+"/_index.md", []byte(b.String()))
|
return bfs.PutRegular("wiki/"+w+"/_index.md", []byte(b.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -156,6 +156,25 @@ func (b *Brain) PutRegular(rel string, data []byte) error {
|
|||||||
} else if !errors.Is(err, fs.ErrNotExist) {
|
} else if !errors.Is(err, fs.ErrNotExist) {
|
||||||
return err
|
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)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -453,3 +453,79 @@ func TestRealParentRefusesASymlinkedDirectoryOrUncleanPath(t *testing.T) {
|
|||||||
assert.ErrorIs(t, b.RealParent("wiki/../raw/x.md"), ErrNotRegular)
|
assert.ErrorIs(t, b.RealParent("wiki/../raw/x.md"), ErrNotRegular)
|
||||||
assert.NoError(t, b.RealParent("wiki/x.md"), "positive control")
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRegular is the only write path for promote and reject: it creates
|
||||||
|
// missing parents through the root, refuses an in-brain symlinked parent and
|
||||||
|
// an unclean path, and never replaces anything already there (fs.ErrExist,
|
||||||
|
// content unchanged), including a dangling link.
|
||||||
|
func TestCreateRegularCreatesOnlyNewFilesThroughRealDirectories(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
require.NoError(t, os.MkdirAll(filepath.Join(dir, "raw", "keep"), 0o755))
|
||||||
|
require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki"), 0o755))
|
||||||
|
require.NoError(t, os.Symlink(filepath.Join("..", "raw"), filepath.Join(dir, "wiki", "inwing")))
|
||||||
|
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")))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "wiki", "exists.md"), []byte("original"), 0o644))
|
||||||
|
require.NoError(t, os.Symlink("nowhere.md", filepath.Join(dir, "wiki", "dangling.md")))
|
||||||
|
b, err := Open(dir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() { _ = b.Close() }()
|
||||||
|
|
||||||
|
assert.ErrorIs(t, b.CreateRegular("wiki/inwing/facts/n.md", []byte("x")), ErrNotRegular)
|
||||||
|
_, err = os.Stat(filepath.Join(dir, "raw", "facts"))
|
||||||
|
assert.True(t, os.IsNotExist(err), "nothing created through the in-brain link")
|
||||||
|
assert.ErrorIs(t, b.CreateRegular("indir/../v.md", []byte("x")), ErrNotRegular)
|
||||||
|
got, _ := os.ReadFile(filepath.Join(dir, "raw", "v.md"))
|
||||||
|
assert.Equal(t, "untouched", string(got))
|
||||||
|
|
||||||
|
assert.ErrorIs(t, b.CreateRegular("wiki/exists.md", []byte("x")), fs.ErrExist)
|
||||||
|
got, _ = os.ReadFile(filepath.Join(dir, "wiki", "exists.md"))
|
||||||
|
assert.Equal(t, "original", string(got))
|
||||||
|
assert.ErrorIs(t, b.CreateRegular("wiki/dangling.md", []byte("x")), fs.ErrExist)
|
||||||
|
_, err = os.Stat(filepath.Join(dir, "wiki", "nowhere.md"))
|
||||||
|
assert.True(t, os.IsNotExist(err), "nothing created at the dangling link's target")
|
||||||
|
|
||||||
|
require.NoError(t, b.CreateRegular("wiki/new/facts/n.md", []byte("one")), "positive control: create with missing parents")
|
||||||
|
got, _ = os.ReadFile(filepath.Join(dir, "wiki", "new", "facts", "n.md"))
|
||||||
|
assert.Equal(t, "one", string(got))
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import (
|
|||||||
// a symlinked directory inside the brain.
|
// a symlinked directory inside the brain.
|
||||||
func TestEditRefusesANoteBehindASymlinkedDirectory(t *testing.T) {
|
func TestEditRefusesANoteBehindASymlinkedDirectory(t *testing.T) {
|
||||||
b := t.TempDir()
|
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")
|
victim := filepath.Join(b, "raw", "facts", "v.md")
|
||||||
require.NoError(t, os.MkdirAll(filepath.Dir(victim), 0o755))
|
require.NoError(t, os.MkdirAll(filepath.Dir(victim), 0o755))
|
||||||
require.NoError(t, os.WriteFile(victim, []byte("old text\n"), 0o644))
|
require.NoError(t, os.WriteFile(victim, []byte("old text\n"), 0o644))
|
||||||
@@ -22,4 +25,12 @@ func TestEditRefusesANoteBehindASymlinkedDirectory(t *testing.T) {
|
|||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
got, _ := os.ReadFile(victim)
|
got, _ := os.ReadFile(victim)
|
||||||
assert.Equal(t, "old text\n", string(got))
|
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
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// plainGroups keeps the provably plain key groups of a frontmatter block.
|
// plainGroups keeps the provably plain key groups of a frontmatter block.
|
||||||
func plainGroups(block string) string {
|
func plainGroups(block string) string {
|
||||||
lines := strings.Split(strings.TrimSuffix(block, "\n"), "\n")
|
lines := strings.Split(strings.TrimSuffix(block, "\n"), "\n")
|
||||||
|
|||||||
Reference in New Issue
Block a user