feat(platform): add textblock helper for idempotent config-file blocks

This commit is contained in:
MDietermann
2026-04-14 08:21:38 +02:00
parent 83bb622232
commit afed8ab600
2 changed files with 166 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
package platform
import (
"fmt"
"os"
"strings"
)
func blockMarkers(marker string) (string, string) {
return fmt.Sprintf("# >>> %s >>>", marker), fmt.Sprintf("# <<< %s <<<", marker)
}
// EnsureBlock writes a marker-delimited block into path. If the file does
// not exist it is created. If a block with the same marker already exists
// it is replaced in place. Surrounding content is preserved.
func EnsureBlock(path, marker, content string) error {
start, end := blockMarkers(marker)
existing, err := os.ReadFile(path)
if err != nil && !os.IsNotExist(err) {
return err
}
block := start + "\n" + strings.TrimRight(content, "\n") + "\n" + end + "\n"
text := string(existing)
si := strings.Index(text, start)
ei := strings.Index(text, end)
if si >= 0 && ei > si {
endPos := ei + len(end)
if endPos < len(text) && text[endPos] == '\n' {
endPos++
}
text = text[:si] + block + text[endPos:]
} else {
if text != "" && !strings.HasSuffix(text, "\n") {
text += "\n"
}
text += block
}
return os.WriteFile(path, []byte(text), 0o644)
}
// HasBlock reports whether path contains a block with the given marker.
// A missing file yields (false, nil).
func HasBlock(path, marker string) (bool, error) {
start, _ := blockMarkers(marker)
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return strings.Contains(string(data), start), nil
}
// RemoveBlock deletes a marker-delimited block if present.
func RemoveBlock(path, marker string) error {
start, end := blockMarkers(marker)
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
text := string(data)
si := strings.Index(text, start)
ei := strings.Index(text, end)
if si < 0 || ei <= si {
return nil
}
endPos := ei + len(end)
if endPos < len(text) && text[endPos] == '\n' {
endPos++
}
text = text[:si] + text[endPos:]
return os.WriteFile(path, []byte(text), 0o644)
}

View File

@@ -0,0 +1,88 @@
package platform
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestEnsureBlockInsertsIntoMissingFile(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "rc")
if err := EnsureBlock(p, "archduino", "export FOO=1"); err != nil {
t.Fatal(err)
}
got, _ := os.ReadFile(p)
want := "# >>> archduino >>>\nexport FOO=1\n# <<< archduino <<<\n"
if string(got) != want {
t.Fatalf("got %q want %q", got, want)
}
}
func TestEnsureBlockReplacesExisting(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "rc")
initial := "alias ll='ls -l'\n# >>> archduino >>>\nold content\n# <<< archduino <<<\necho tail\n"
if err := os.WriteFile(p, []byte(initial), 0o644); err != nil {
t.Fatal(err)
}
if err := EnsureBlock(p, "archduino", "new content"); err != nil {
t.Fatal(err)
}
got, _ := os.ReadFile(p)
want := "alias ll='ls -l'\n# >>> archduino >>>\nnew content\n# <<< archduino <<<\necho tail\n"
if string(got) != want {
t.Fatalf("got %q want %q", got, want)
}
}
func TestEnsureBlockIdempotent(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "rc")
_ = EnsureBlock(p, "a", "x")
before, _ := os.ReadFile(p)
_ = EnsureBlock(p, "a", "x")
after, _ := os.ReadFile(p)
if string(before) != string(after) {
t.Fatalf("not idempotent: %q vs %q", before, after)
}
}
func TestHasBlockTrueFalse(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "rc")
has, err := HasBlock(p, "a")
if err != nil {
t.Fatal(err)
}
if has {
t.Fatal("missing file should report has=false")
}
_ = os.WriteFile(p, []byte("no blocks here\n"), 0o644)
has, _ = HasBlock(p, "a")
if has {
t.Fatal("unblocked file should report has=false")
}
_ = EnsureBlock(p, "a", "x")
has, _ = HasBlock(p, "a")
if !has {
t.Fatal("blocked file should report has=true")
}
}
func TestRemoveBlock(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "rc")
_ = os.WriteFile(p, []byte("head\n# >>> a >>>\nmid\n# <<< a <<<\ntail\n"), 0o644)
if err := RemoveBlock(p, "a"); err != nil {
t.Fatal(err)
}
got, _ := os.ReadFile(p)
if strings.Contains(string(got), ">>> a >>>") {
t.Fatalf("block not removed: %q", got)
}
if !strings.Contains(string(got), "head") || !strings.Contains(string(got), "tail") {
t.Fatalf("surrounding content lost: %q", got)
}
}