2026-03-20 12:15:57 +00:00
|
|
|
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
|
|
|
|
|
|
// Utility functions for the Core framework.
|
feat: string.go — core string primitives, same pattern as array.go
HasPrefix, HasSuffix, TrimPrefix, TrimSuffix, Contains, Split, SplitN,
StringJoin, Replace, Lower, Upper, Trim, RuneCount.
utils.go and command.go now use string.go helpers — zero direct
strings import in either file.
234 tests, 79.8% coverage.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-20 12:29:15 +00:00
|
|
|
// Built on core string.go primitives.
|
2026-03-20 12:15:57 +00:00
|
|
|
|
|
|
|
|
package core
|
|
|
|
|
|
|
|
|
|
import (
|
feat: implement RFC plans 1-5 — Registry[T], Action/Task, Process, primitives
Plans 1-5 complete for core/go scope. 456 tests, 84.4% coverage, 100% AX-7 naming.
Critical bugs (Plan 1):
- P4-3+P7-3: ACTION broadcast calls all handlers with panic recovery
- P7-2+P7-4: RunE() with defer ServiceShutdown, Run() delegates
- P3-1: Startable/Stoppable return Result (breaking, clean)
- P9-1: Zero os/exec — App.Find() rewritten with os.Stat+PATH
- I3: Embed() removed, I15: New() comment fixed
- I9: CommandLifecycle removed → Command.Managed field
Registry[T] (Plan 2):
- Universal thread-safe named collection with 3 lock modes
- All 5 registries migrated: services, commands, drive, data, lock
- Insertion order preserved (fixes P4-1)
- c.RegistryOf("name") cross-cutting accessor
Action/Task system (Plan 3):
- Action type with Run()/Exists(), ActionHandler signature
- c.Action("name") dual-purpose accessor (register/invoke)
- TaskDef with Steps — sequential chain, async dispatch, previous-input piping
- Panic recovery on all Action execution
- broadcast() internal, ACTION() sugar
Process primitive (Plan 4):
- c.Process() returns Action sugar — Run/RunIn/RunWithEnv/Start/Kill/Exists
- No deps added — delegates to c.Action("process.*")
- Permission-by-registration: no handler = no capability
Missing primitives (Plan 5):
- core.ID() — atomic counter + crypto/rand suffix
- ValidateName() / SanitisePath() — reusable validation
- Fs.WriteAtomic() — write-to-temp-then-rename
- Fs.NewUnrestricted() / Fs.Root() — legitimate sandbox bypass
- AX-7: 456/456 tests renamed to TestFile_Function_{Good,Bad,Ugly}
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-25 15:18:25 +00:00
|
|
|
crand "crypto/rand"
|
|
|
|
|
"encoding/hex"
|
2026-03-20 12:19:11 +00:00
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"os"
|
feat: implement RFC plans 1-5 — Registry[T], Action/Task, Process, primitives
Plans 1-5 complete for core/go scope. 456 tests, 84.4% coverage, 100% AX-7 naming.
Critical bugs (Plan 1):
- P4-3+P7-3: ACTION broadcast calls all handlers with panic recovery
- P7-2+P7-4: RunE() with defer ServiceShutdown, Run() delegates
- P3-1: Startable/Stoppable return Result (breaking, clean)
- P9-1: Zero os/exec — App.Find() rewritten with os.Stat+PATH
- I3: Embed() removed, I15: New() comment fixed
- I9: CommandLifecycle removed → Command.Managed field
Registry[T] (Plan 2):
- Universal thread-safe named collection with 3 lock modes
- All 5 registries migrated: services, commands, drive, data, lock
- Insertion order preserved (fixes P4-1)
- c.RegistryOf("name") cross-cutting accessor
Action/Task system (Plan 3):
- Action type with Run()/Exists(), ActionHandler signature
- c.Action("name") dual-purpose accessor (register/invoke)
- TaskDef with Steps — sequential chain, async dispatch, previous-input piping
- Panic recovery on all Action execution
- broadcast() internal, ACTION() sugar
Process primitive (Plan 4):
- c.Process() returns Action sugar — Run/RunIn/RunWithEnv/Start/Kill/Exists
- No deps added — delegates to c.Action("process.*")
- Permission-by-registration: no handler = no capability
Missing primitives (Plan 5):
- core.ID() — atomic counter + crypto/rand suffix
- ValidateName() / SanitisePath() — reusable validation
- Fs.WriteAtomic() — write-to-temp-then-rename
- Fs.NewUnrestricted() / Fs.Root() — legitimate sandbox bypass
- AX-7: 456/456 tests renamed to TestFile_Function_{Good,Bad,Ugly}
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-25 15:18:25 +00:00
|
|
|
"strconv"
|
|
|
|
|
"sync/atomic"
|
2026-03-20 12:15:57 +00:00
|
|
|
)
|
|
|
|
|
|
feat: implement RFC plans 1-5 — Registry[T], Action/Task, Process, primitives
Plans 1-5 complete for core/go scope. 456 tests, 84.4% coverage, 100% AX-7 naming.
Critical bugs (Plan 1):
- P4-3+P7-3: ACTION broadcast calls all handlers with panic recovery
- P7-2+P7-4: RunE() with defer ServiceShutdown, Run() delegates
- P3-1: Startable/Stoppable return Result (breaking, clean)
- P9-1: Zero os/exec — App.Find() rewritten with os.Stat+PATH
- I3: Embed() removed, I15: New() comment fixed
- I9: CommandLifecycle removed → Command.Managed field
Registry[T] (Plan 2):
- Universal thread-safe named collection with 3 lock modes
- All 5 registries migrated: services, commands, drive, data, lock
- Insertion order preserved (fixes P4-1)
- c.RegistryOf("name") cross-cutting accessor
Action/Task system (Plan 3):
- Action type with Run()/Exists(), ActionHandler signature
- c.Action("name") dual-purpose accessor (register/invoke)
- TaskDef with Steps — sequential chain, async dispatch, previous-input piping
- Panic recovery on all Action execution
- broadcast() internal, ACTION() sugar
Process primitive (Plan 4):
- c.Process() returns Action sugar — Run/RunIn/RunWithEnv/Start/Kill/Exists
- No deps added — delegates to c.Action("process.*")
- Permission-by-registration: no handler = no capability
Missing primitives (Plan 5):
- core.ID() — atomic counter + crypto/rand suffix
- ValidateName() / SanitisePath() — reusable validation
- Fs.WriteAtomic() — write-to-temp-then-rename
- Fs.NewUnrestricted() / Fs.Root() — legitimate sandbox bypass
- AX-7: 456/456 tests renamed to TestFile_Function_{Good,Bad,Ugly}
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-25 15:18:25 +00:00
|
|
|
// --- ID Generation ---
|
|
|
|
|
|
|
|
|
|
var idCounter atomic.Uint64
|
|
|
|
|
|
|
|
|
|
// ID returns a unique identifier. Format: "id-{counter}-{random}".
|
|
|
|
|
// Counter is process-wide atomic. Random suffix prevents collision across restarts.
|
|
|
|
|
//
|
|
|
|
|
// id := core.ID() // "id-1-a3f2b1"
|
|
|
|
|
// id2 := core.ID() // "id-2-c7e4d9"
|
|
|
|
|
func ID() string {
|
|
|
|
|
return Concat("id-", strconv.FormatUint(idCounter.Add(1), 10), "-", shortRand())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func shortRand() string {
|
|
|
|
|
b := make([]byte, 3)
|
|
|
|
|
crand.Read(b)
|
|
|
|
|
return hex.EncodeToString(b)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Validation ---
|
|
|
|
|
|
|
|
|
|
// ValidateName checks that a string is a valid service/action/command name.
|
|
|
|
|
// Rejects empty, ".", "..", and names containing path separators.
|
|
|
|
|
//
|
|
|
|
|
// r := core.ValidateName("brain") // Result{"brain", true}
|
|
|
|
|
// r := core.ValidateName("") // Result{error, false}
|
|
|
|
|
// r := core.ValidateName("../escape") // Result{error, false}
|
|
|
|
|
func ValidateName(name string) Result {
|
|
|
|
|
if name == "" || name == "." || name == ".." {
|
|
|
|
|
return Result{E("validate", Concat("invalid name: ", name), nil), false}
|
|
|
|
|
}
|
|
|
|
|
if Contains(name, "/") || Contains(name, "\\") {
|
|
|
|
|
return Result{E("validate", Concat("name contains path separator: ", name), nil), false}
|
|
|
|
|
}
|
|
|
|
|
return Result{name, true}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SanitisePath extracts the base filename and rejects traversal attempts.
|
|
|
|
|
// Returns "invalid" for dangerous inputs.
|
|
|
|
|
//
|
|
|
|
|
// core.SanitisePath("../../etc/passwd") // "passwd"
|
|
|
|
|
// core.SanitisePath("") // "invalid"
|
|
|
|
|
// core.SanitisePath("..") // "invalid"
|
|
|
|
|
func SanitisePath(path string) string {
|
|
|
|
|
safe := PathBase(path)
|
|
|
|
|
if safe == "." || safe == ".." || safe == "" {
|
|
|
|
|
return "invalid"
|
|
|
|
|
}
|
|
|
|
|
return safe
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- I/O ---
|
|
|
|
|
|
feat: eliminate fmt, string concat — add core.Println, use Concat/Path everywhere
New primitive: core.Println() wraps fmt.Println.
Replaced across all test + example files:
- fmt.Println → Println (17 example files)
- fmt.Sprintf → Concat + Sprint
- dir + "/file" → Path(dir, "file") (path security)
- "str" + var → Concat("str", var) (AX consistency)
"fmt" import is now zero across all test files.
String concat with + is zero across all test files.
Remaining 9 stdlib imports (all Go infrastructure):
testing, context, time, sync, embed, io/fs, bytes, gzip, base64
558 tests, 84.5% coverage.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-25 19:42:39 +00:00
|
|
|
// Println prints values to stdout with a newline. Replaces fmt.Println.
|
|
|
|
|
//
|
|
|
|
|
// core.Println("hello", 42, true)
|
|
|
|
|
func Println(args ...any) {
|
|
|
|
|
fmt.Println(args...)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 12:20:36 +00:00
|
|
|
// Print writes a formatted line to a writer, defaulting to os.Stdout.
|
2026-03-20 12:19:11 +00:00
|
|
|
//
|
2026-03-20 12:20:36 +00:00
|
|
|
// core.Print(nil, "hello %s", "world") // → stdout
|
|
|
|
|
// core.Print(w, "port: %d", 8080) // → w
|
|
|
|
|
func Print(w io.Writer, format string, args ...any) {
|
2026-03-20 12:19:11 +00:00
|
|
|
if w == nil {
|
|
|
|
|
w = os.Stdout
|
|
|
|
|
}
|
|
|
|
|
fmt.Fprintf(w, format+"\n", args...)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 12:23:05 +00:00
|
|
|
// JoinPath joins string segments into a path with "/" separator.
|
|
|
|
|
//
|
|
|
|
|
// core.JoinPath("deploy", "to", "homelab") // → "deploy/to/homelab"
|
|
|
|
|
func JoinPath(segments ...string) string {
|
2026-03-20 12:42:10 +00:00
|
|
|
return Join("/", segments...)
|
2026-03-20 12:23:05 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-20 12:24:39 +00:00
|
|
|
// IsFlag returns true if the argument starts with a dash.
|
|
|
|
|
//
|
|
|
|
|
// core.IsFlag("--verbose") // true
|
|
|
|
|
// core.IsFlag("-v") // true
|
|
|
|
|
// core.IsFlag("deploy") // false
|
|
|
|
|
func IsFlag(arg string) bool {
|
feat: string.go — core string primitives, same pattern as array.go
HasPrefix, HasSuffix, TrimPrefix, TrimSuffix, Contains, Split, SplitN,
StringJoin, Replace, Lower, Upper, Trim, RuneCount.
utils.go and command.go now use string.go helpers — zero direct
strings import in either file.
234 tests, 79.8% coverage.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-20 12:29:15 +00:00
|
|
|
return HasPrefix(arg, "-")
|
2026-03-20 12:24:39 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-20 12:50:59 +00:00
|
|
|
// Arg extracts a value from variadic args at the given index.
|
|
|
|
|
// Type-checks and delegates to the appropriate typed extractor.
|
2026-03-20 13:30:22 +00:00
|
|
|
// Returns Result — OK is false if index is out of bounds.
|
2026-03-20 12:46:52 +00:00
|
|
|
//
|
2026-03-20 13:30:22 +00:00
|
|
|
// r := core.Arg(0, args...)
|
|
|
|
|
// if r.OK { path = r.Value.(string) }
|
|
|
|
|
func Arg(index int, args ...any) Result {
|
2026-03-20 12:46:52 +00:00
|
|
|
if index >= len(args) {
|
2026-03-20 13:30:22 +00:00
|
|
|
return Result{}
|
2026-03-20 12:46:52 +00:00
|
|
|
}
|
2026-03-20 12:50:59 +00:00
|
|
|
v := args[index]
|
|
|
|
|
switch v.(type) {
|
|
|
|
|
case string:
|
2026-03-20 15:36:33 +00:00
|
|
|
return Result{ArgString(index, args...), true}
|
2026-03-20 12:50:59 +00:00
|
|
|
case int:
|
2026-03-20 15:36:33 +00:00
|
|
|
return Result{ArgInt(index, args...), true}
|
2026-03-20 12:50:59 +00:00
|
|
|
case bool:
|
2026-03-20 15:36:33 +00:00
|
|
|
return Result{ArgBool(index, args...), true}
|
2026-03-20 12:50:59 +00:00
|
|
|
default:
|
2026-03-20 15:36:33 +00:00
|
|
|
return Result{v, true}
|
2026-03-20 12:50:59 +00:00
|
|
|
}
|
2026-03-20 12:46:52 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-20 12:50:59 +00:00
|
|
|
// ArgString extracts a string at the given index.
|
2026-03-20 12:44:57 +00:00
|
|
|
//
|
2026-03-20 12:50:59 +00:00
|
|
|
// name := core.ArgString(0, args...)
|
|
|
|
|
func ArgString(index int, args ...any) string {
|
|
|
|
|
if index >= len(args) {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
s, ok := args[index].(string)
|
|
|
|
|
if !ok {
|
2026-03-20 12:44:57 +00:00
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 12:50:59 +00:00
|
|
|
// ArgInt extracts an int at the given index.
|
2026-03-20 12:46:52 +00:00
|
|
|
//
|
2026-03-20 12:50:59 +00:00
|
|
|
// port := core.ArgInt(1, args...)
|
|
|
|
|
func ArgInt(index int, args ...any) int {
|
|
|
|
|
if index >= len(args) {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
i, ok := args[index].(int)
|
|
|
|
|
if !ok {
|
2026-03-20 12:46:52 +00:00
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
return i
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 12:50:59 +00:00
|
|
|
// ArgBool extracts a bool at the given index.
|
2026-03-20 12:46:52 +00:00
|
|
|
//
|
2026-03-20 12:50:59 +00:00
|
|
|
// debug := core.ArgBool(2, args...)
|
|
|
|
|
func ArgBool(index int, args ...any) bool {
|
|
|
|
|
if index >= len(args) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
b, ok := args[index].(bool)
|
|
|
|
|
if !ok {
|
2026-03-20 12:46:52 +00:00
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 12:15:57 +00:00
|
|
|
// FilterArgs removes empty strings and Go test runner flags from an argument list.
|
|
|
|
|
//
|
|
|
|
|
// clean := core.FilterArgs(os.Args[1:])
|
|
|
|
|
func FilterArgs(args []string) []string {
|
|
|
|
|
var clean []string
|
|
|
|
|
for _, a := range args {
|
feat: string.go — core string primitives, same pattern as array.go
HasPrefix, HasSuffix, TrimPrefix, TrimSuffix, Contains, Split, SplitN,
StringJoin, Replace, Lower, Upper, Trim, RuneCount.
utils.go and command.go now use string.go helpers — zero direct
strings import in either file.
234 tests, 79.8% coverage.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-20 12:29:15 +00:00
|
|
|
if a == "" || HasPrefix(a, "-test.") {
|
2026-03-20 12:15:57 +00:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
clean = append(clean, a)
|
|
|
|
|
}
|
|
|
|
|
return clean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ParseFlag parses a single flag argument into key, value, and validity.
|
|
|
|
|
// Single dash (-) requires exactly 1 character (letter, emoji, unicode).
|
|
|
|
|
// Double dash (--) requires 2+ characters.
|
|
|
|
|
//
|
|
|
|
|
// "-v" → "v", "", true
|
|
|
|
|
// "-🔥" → "🔥", "", true
|
|
|
|
|
// "--verbose" → "verbose", "", true
|
|
|
|
|
// "--port=8080" → "port", "8080", true
|
|
|
|
|
// "-verbose" → "", "", false (single dash, 2+ chars)
|
|
|
|
|
// "--v" → "", "", false (double dash, 1 char)
|
|
|
|
|
// "hello" → "", "", false (not a flag)
|
|
|
|
|
func ParseFlag(arg string) (key, value string, valid bool) {
|
feat: string.go — core string primitives, same pattern as array.go
HasPrefix, HasSuffix, TrimPrefix, TrimSuffix, Contains, Split, SplitN,
StringJoin, Replace, Lower, Upper, Trim, RuneCount.
utils.go and command.go now use string.go helpers — zero direct
strings import in either file.
234 tests, 79.8% coverage.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-20 12:29:15 +00:00
|
|
|
if HasPrefix(arg, "--") {
|
|
|
|
|
rest := TrimPrefix(arg, "--")
|
|
|
|
|
parts := SplitN(rest, "=", 2)
|
2026-03-20 12:15:57 +00:00
|
|
|
name := parts[0]
|
feat: string.go — core string primitives, same pattern as array.go
HasPrefix, HasSuffix, TrimPrefix, TrimSuffix, Contains, Split, SplitN,
StringJoin, Replace, Lower, Upper, Trim, RuneCount.
utils.go and command.go now use string.go helpers — zero direct
strings import in either file.
234 tests, 79.8% coverage.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-20 12:29:15 +00:00
|
|
|
if RuneCount(name) < 2 {
|
2026-03-20 12:15:57 +00:00
|
|
|
return "", "", false
|
|
|
|
|
}
|
|
|
|
|
if len(parts) == 2 {
|
|
|
|
|
return name, parts[1], true
|
|
|
|
|
}
|
|
|
|
|
return name, "", true
|
|
|
|
|
}
|
|
|
|
|
|
feat: string.go — core string primitives, same pattern as array.go
HasPrefix, HasSuffix, TrimPrefix, TrimSuffix, Contains, Split, SplitN,
StringJoin, Replace, Lower, Upper, Trim, RuneCount.
utils.go and command.go now use string.go helpers — zero direct
strings import in either file.
234 tests, 79.8% coverage.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-20 12:29:15 +00:00
|
|
|
if HasPrefix(arg, "-") {
|
|
|
|
|
rest := TrimPrefix(arg, "-")
|
|
|
|
|
parts := SplitN(rest, "=", 2)
|
2026-03-20 12:15:57 +00:00
|
|
|
name := parts[0]
|
feat: string.go — core string primitives, same pattern as array.go
HasPrefix, HasSuffix, TrimPrefix, TrimSuffix, Contains, Split, SplitN,
StringJoin, Replace, Lower, Upper, Trim, RuneCount.
utils.go and command.go now use string.go helpers — zero direct
strings import in either file.
234 tests, 79.8% coverage.
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-20 12:29:15 +00:00
|
|
|
if RuneCount(name) != 1 {
|
2026-03-20 12:15:57 +00:00
|
|
|
return "", "", false
|
|
|
|
|
}
|
|
|
|
|
if len(parts) == 2 {
|
|
|
|
|
return name, parts[1], true
|
|
|
|
|
}
|
|
|
|
|
return name, "", true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "", "", false
|
|
|
|
|
}
|