fix: address CodeRabbit PR #2 findings (batch 2)
- datanode/medium.go: add compile-time Medium interface check - docs/RFC.md: remove duplicated MemoryMedium Read/Write method entries - docs/RFC-CORE-008-AGENT-EXPERIENCE.md: add text language tag to fenced code block - io.go: rename WriteMode param path→filePath to avoid shadowing path package - io.go: add directory collision check in WriteMode and MemoryWriteCloser.Close - io.go: Copy now preserves source file permissions via Stat+WriteMode - node/node.go: add goroutine-safety doc comment on Node - node/node.go: Rename handles directory prefix batch-rename - node/node.go: rename CopyFile→ExportFile, document local-only behaviour, wrap PathError with core.E() - node/node.go: filter empty path components in Walk depth calculation - node/node_test.go: update tests to use ExportFile - sigil/crypto_sigil.go: make Key/Obfuscator unexported, add Key()/Obfuscator()/SetObfuscator() accessors - sigil/crypto_sigil.go: rename receiver sigil→s to avoid shadowing package name - sigil/crypto_sigil_test.go: update to use accessor methods - store/medium.go: use KeyValueStore.ListGroups() instead of direct DB query - store/medium.go: add doc comment that WriteMode does not persist file mode - store/store.go: add ListGroups() method to KeyValueStore Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
a43a16fb0d
commit
2c18322dfe
13 changed files with 148 additions and 83 deletions
|
|
@ -14,6 +14,7 @@ import (
|
|||
"time"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
coreio "dappco.re/go/core/io"
|
||||
borgdatanode "forge.lthn.ai/Snider/Borg/pkg/datanode"
|
||||
)
|
||||
|
||||
|
|
@ -29,6 +30,8 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
var _ coreio.Medium = (*Medium)(nil)
|
||||
|
||||
// Example: medium := datanode.New()
|
||||
// Example: _ = medium.Write("jobs/run.log", "started")
|
||||
// Example: snapshot, _ := medium.Snapshot()
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ AX does not replace UX or DX. End users still need good UX. Developers still nee
|
|||
|
||||
Names are tokens that agents pattern-match across languages and contexts. Abbreviations introduce mapping overhead.
|
||||
|
||||
```
|
||||
```text
|
||||
Config not Cfg
|
||||
Service not Srv
|
||||
Embed not Emb
|
||||
|
|
|
|||
21
docs/RFC.md
21
docs/RFC.md
|
|
@ -422,21 +422,6 @@ _ = m.Write("notes.txt", "hello")
|
|||
ok := m.IsFile("notes.txt")
|
||||
```
|
||||
|
||||
**Read(path string) (string, error)**
|
||||
Example:
|
||||
```go
|
||||
m := io.NewMemoryMedium()
|
||||
_ = m.Write("notes.txt", "hello")
|
||||
value, _ := m.Read("notes.txt")
|
||||
```
|
||||
|
||||
**Write(path, content string) error**
|
||||
Example:
|
||||
```go
|
||||
m := io.NewMemoryMedium()
|
||||
_ = m.Write("notes.txt", "hello")
|
||||
```
|
||||
|
||||
**Delete(path string) error**
|
||||
Example:
|
||||
```go
|
||||
|
|
@ -869,13 +854,13 @@ _ = n.Write("file.txt", "data")
|
|||
b, _ := n.ReadFile("file.txt")
|
||||
```
|
||||
|
||||
**CopyFile(sourcePath, destinationPath string, perm fs.FileMode) error**
|
||||
Copies a file to the local filesystem.
|
||||
**ExportFile(sourcePath, destinationPath string, perm fs.FileMode) error**
|
||||
Exports a file from the in-memory tree to the local filesystem. Operates on coreio.Local directly — use CopyTo for Medium-agnostic transfers.
|
||||
Example:
|
||||
```go
|
||||
n := node.New()
|
||||
_ = n.Write("file.txt", "data")
|
||||
_ = n.CopyFile("file.txt", "/tmp/file.txt", 0644)
|
||||
_ = n.ExportFile("file.txt", "/tmp/file.txt", 0644)
|
||||
```
|
||||
|
||||
**CopyTo(target io.Medium, sourcePath, destPath string) error**
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ Test coverage is `Yes` when same-package tests directly execute or reference the
|
|||
| `New` | `func New() *Node` | `dappco.re/go/core/io/node` | New creates a new, empty Node. | Yes |
|
||||
| `Node.AddData` | `func (*Node) AddData(name string, content []byte)` | `dappco.re/go/core/io/node` | AddData stages content in the in-memory filesystem. | Yes |
|
||||
| `Node.Append` | `func (*Node) Append(p string) (goio.WriteCloser, error)` | `dappco.re/go/core/io/node` | Append opens the named file for appending, creating it if needed. | No |
|
||||
| `Node.CopyFile` | `func (*Node) CopyFile(src, dst string, perm fs.FileMode) error` | `dappco.re/go/core/io/node` | CopyFile copies a file from the in-memory tree to the local filesystem. | Yes |
|
||||
| `Node.ExportFile` | `func (*Node) ExportFile(src, dst string, perm fs.FileMode) error` | `dappco.re/go/core/io/node` | ExportFile exports a file from the in-memory tree to the local filesystem. Use CopyTo for Medium-agnostic transfers. | Yes |
|
||||
| `Node.CopyTo` | `func (*Node) CopyTo(target coreio.Medium, sourcePath, destPath string) error` | `dappco.re/go/core/io/node` | CopyTo copies a file (or directory tree) from the node to any Medium. | No |
|
||||
| `Node.Create` | `func (*Node) Create(p string) (goio.WriteCloser, error)` | `dappco.re/go/core/io/node` | Create creates or truncates the named file, returning a WriteCloser. | No |
|
||||
| `Node.Delete` | `func (*Node) Delete(p string) error` | `dappco.re/go/core/io/node` | Delete removes a single file. | No |
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ Key capabilities beyond `Medium`:
|
|||
|
||||
- **`ToTar()` / `FromTar()`** — serialise the entire tree to a tar archive and back. This enables snapshotting, transport, and archival.
|
||||
- **`Walk()` with `WalkOptions`** — extends `fs.WalkDir` with `MaxDepth`, `Filter`, and `SkipErrors` controls.
|
||||
- **`CopyFile(src, dst, perm)`** — copies a file from the in-memory tree to the real filesystem.
|
||||
- **`ExportFile(src, dst, perm)`** — exports a file from the in-memory tree to the local filesystem. Use `CopyTo` for Medium-agnostic transfers.
|
||||
- **`CopyTo(target Medium, src, dst)`** — copies a file or directory tree to any other `Medium`.
|
||||
- **`ReadFile(name)`** — returns a defensive copy of file content, preventing callers from mutating internal state.
|
||||
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ Notes:
|
|||
| `(*node.Node).Delete`, `DeleteAll`, `Rename` | `node/node.go:411`, `421`, `445` | Caller path(s) | Direct map mutation keyed by caller-supplied names | Only strips a leading `/` | Arbitrary delete/rename of any key, including `../`-style names; no directory-safe rename logic |
|
||||
| `(*node.Node).Stat`, `List`, `ReadDir`, `Exists`, `IsFile`, `IsDir` | `node/node.go:278`, `461`, `297`, `387`, `393`, `400` | Caller path/name | Directory inference from map keys and `fs` adapter methods | Only strips a leading `/` | Namespace enumeration and ambiguity around equivalent-looking path spellings |
|
||||
| `(*node.Node).WalkNode`, `Walk` | `node/node.go:128`, `145` | Caller root path, callback, filters | `fs.WalkDir` over the in-memory tree | No root normalization beyond whatever `Node` already does | Attackers who can plant names can force callback traversal over weird paths; `SkipErrors` can suppress unexpected failures |
|
||||
| `(*node.Node).CopyFile` | `node/node.go:200` | Caller source key, destination host path, permissions | Reads node content and calls `os.WriteFile(dst, ...)` directly | Only checks that `src` exists and is not a directory | Arbitrary host filesystem write to a caller-chosen `dst` path |
|
||||
| `(*node.Node).ExportFile` | `node/node.go:200` | Caller source key, destination host path, permissions | Reads node content and calls `coreio.Local.WriteMode(dst, ...)` directly | Only checks that `src` exists and is not a directory | Arbitrary host filesystem write to a caller-chosen `dst` path |
|
||||
| `(*node.Node).CopyTo` | `node/node.go:218` | Caller target medium, source path, destination path | Reads node entries and calls `target.Write(destPath or destPath/rel, content)` | Only checks that the source exists | Stored `../`-style node keys can propagate into destination paths, enabling traversal or overwrite depending on the target backend |
|
||||
| `(*node.Node).EnsureDir` | `node/node.go:380` | Caller path (ignored) | No-op | Input is ignored | Semantic mismatch: callers may assume a directory boundary was created when directories remain implicit |
|
||||
|
||||
|
|
|
|||
24
io.go
24
io.go
|
|
@ -189,7 +189,11 @@ func Copy(sourceMedium Medium, sourcePath string, destinationMedium Medium, dest
|
|||
if err != nil {
|
||||
return core.E("io.Copy", core.Concat("read failed: ", sourcePath), err)
|
||||
}
|
||||
if err := destinationMedium.Write(destinationPath, content); err != nil {
|
||||
mode := fs.FileMode(0644)
|
||||
if info, err := sourceMedium.Stat(sourcePath); err == nil {
|
||||
mode = info.Mode()
|
||||
}
|
||||
if err := destinationMedium.WriteMode(destinationPath, content, mode); err != nil {
|
||||
return core.E("io.Copy", core.Concat("write failed: ", destinationPath), err)
|
||||
}
|
||||
return nil
|
||||
|
|
@ -271,9 +275,9 @@ func (medium *MemoryMedium) Write(path, content string) error {
|
|||
}
|
||||
|
||||
// Example: _ = io.NewMemoryMedium().WriteMode("keys/private.key", "secret", 0600)
|
||||
func (medium *MemoryMedium) WriteMode(path, content string, mode fs.FileMode) error {
|
||||
func (medium *MemoryMedium) WriteMode(filePath, content string, mode fs.FileMode) error {
|
||||
// Verify no ancestor directory component is stored as a file.
|
||||
ancestor := path.Dir(path)
|
||||
ancestor := path.Dir(filePath)
|
||||
for ancestor != "." && ancestor != "" {
|
||||
if _, ok := medium.fileContents[ancestor]; ok {
|
||||
return core.E("io.MemoryMedium.WriteMode", core.Concat("ancestor path is a file: ", ancestor), fs.ErrExist)
|
||||
|
|
@ -284,10 +288,13 @@ func (medium *MemoryMedium) WriteMode(path, content string, mode fs.FileMode) er
|
|||
}
|
||||
ancestor = next
|
||||
}
|
||||
medium.ensureAncestorDirectories(path)
|
||||
medium.fileContents[path] = content
|
||||
medium.fileModes[path] = mode
|
||||
medium.modificationTimes[path] = time.Now()
|
||||
if _, ok := medium.directories[filePath]; ok {
|
||||
return core.E("io.MemoryMedium.WriteMode", core.Concat("path is a directory: ", filePath), fs.ErrExist)
|
||||
}
|
||||
medium.ensureAncestorDirectories(filePath)
|
||||
medium.fileContents[filePath] = content
|
||||
medium.fileModes[filePath] = mode
|
||||
medium.modificationTimes[filePath] = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -537,6 +544,9 @@ func (writeCloser *MemoryWriteCloser) Write(data []byte) (int, error) {
|
|||
}
|
||||
|
||||
func (writeCloser *MemoryWriteCloser) Close() error {
|
||||
if _, ok := writeCloser.medium.directories[writeCloser.path]; ok {
|
||||
return core.E("io.MemoryWriteCloser.Close", core.Concat("path is a directory: ", writeCloser.path), fs.ErrExist)
|
||||
}
|
||||
writeCloser.medium.ensureAncestorDirectories(writeCloser.path)
|
||||
writeCloser.medium.fileContents[writeCloser.path] = string(writeCloser.data)
|
||||
writeCloser.medium.fileModes[writeCloser.path] = writeCloser.mode
|
||||
|
|
|
|||
61
node/node.go
61
node/node.go
|
|
@ -22,6 +22,8 @@ import (
|
|||
// Example: nodeTree.AddData("config/app.yaml", []byte("port: 8080"))
|
||||
// Example: snapshot, _ := nodeTree.ToTar()
|
||||
// Example: restored, _ := node.FromTar(snapshot)
|
||||
// Note: Node is not goroutine-safe. All methods must be called from a single goroutine,
|
||||
// or the caller must provide external synchronisation.
|
||||
type Node struct {
|
||||
files map[string]*dataFile
|
||||
}
|
||||
|
|
@ -152,7 +154,13 @@ func (node *Node) Walk(root string, walkFunc fs.WalkDirFunc, options WalkOptions
|
|||
if walkResult == nil && options.MaxDepth > 0 && entry != nil && entry.IsDir() && entryPath != root {
|
||||
relativePath := core.TrimPrefix(entryPath, root)
|
||||
relativePath = core.TrimPrefix(relativePath, "/")
|
||||
depth := len(core.Split(relativePath, "/"))
|
||||
parts := core.Split(relativePath, "/")
|
||||
depth := 0
|
||||
for _, part := range parts {
|
||||
if part != "" {
|
||||
depth++
|
||||
}
|
||||
}
|
||||
if depth >= options.MaxDepth {
|
||||
return fs.SkipDir
|
||||
}
|
||||
|
|
@ -174,23 +182,26 @@ func (node *Node) ReadFile(name string) ([]byte, error) {
|
|||
return result, nil
|
||||
}
|
||||
|
||||
// Example: _ = nodeTree.CopyFile("config/app.yaml", "backup/app.yaml", 0644)
|
||||
func (node *Node) CopyFile(sourcePath, destinationPath string, permissions fs.FileMode) error {
|
||||
// ExportFile writes a node file to the local filesystem. It operates on coreio.Local directly
|
||||
// and is intentionally local-only — use CopyTo for Medium-agnostic transfers.
|
||||
// Example: _ = nodeTree.ExportFile("config/app.yaml", "backup/app.yaml", 0644)
|
||||
func (node *Node) ExportFile(sourcePath, destinationPath string, permissions fs.FileMode) error {
|
||||
sourcePath = core.TrimPrefix(sourcePath, "/")
|
||||
file, ok := node.files[sourcePath]
|
||||
if !ok {
|
||||
info, err := node.Stat(sourcePath)
|
||||
if err != nil {
|
||||
return core.E("node.CopyFile", core.Concat("source not found: ", sourcePath), fs.ErrNotExist)
|
||||
return core.E("node.ExportFile", core.Concat("source not found: ", sourcePath), fs.ErrNotExist)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return core.E("node.CopyFile", core.Concat("source is a directory: ", sourcePath), fs.ErrInvalid)
|
||||
return core.E("node.ExportFile", core.Concat("source is a directory: ", sourcePath), fs.ErrInvalid)
|
||||
}
|
||||
return core.E("node.CopyFile", core.Concat("source not found: ", sourcePath), fs.ErrNotExist)
|
||||
// unreachable: Stat only succeeds for directories when file is absent
|
||||
return core.E("node.ExportFile", core.Concat("source not found: ", sourcePath), fs.ErrNotExist)
|
||||
}
|
||||
parent := core.PathDir(destinationPath)
|
||||
if parent != "." && parent != "" && parent != destinationPath && !coreio.Local.IsDir(parent) {
|
||||
return &fs.PathError{Op: "copyfile", Path: destinationPath, Err: fs.ErrNotExist}
|
||||
return core.E("node.ExportFile", core.Concat("parent directory not found: ", destinationPath), fs.ErrNotExist)
|
||||
}
|
||||
return coreio.Local.WriteMode(destinationPath, string(file.content), permissions)
|
||||
}
|
||||
|
|
@ -401,18 +412,42 @@ func (node *Node) DeleteAll(filePath string) error {
|
|||
}
|
||||
|
||||
// Example: _ = nodeTree.Rename("drafts/todo.txt", "archive/todo.txt")
|
||||
// Example: _ = nodeTree.Rename("drafts", "archive")
|
||||
func (node *Node) Rename(oldPath, newPath string) error {
|
||||
oldPath = core.TrimPrefix(oldPath, "/")
|
||||
newPath = core.TrimPrefix(newPath, "/")
|
||||
|
||||
file, ok := node.files[oldPath]
|
||||
if !ok {
|
||||
return core.E("node.Rename", core.Concat("path not found: ", oldPath), fs.ErrNotExist)
|
||||
if file, ok := node.files[oldPath]; ok {
|
||||
file.name = newPath
|
||||
node.files[newPath] = file
|
||||
delete(node.files, oldPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
file.name = newPath
|
||||
node.files[newPath] = file
|
||||
delete(node.files, oldPath)
|
||||
// Directory rename: batch-rename all entries that share the prefix.
|
||||
oldPrefix := oldPath + "/"
|
||||
newPrefix := newPath + "/"
|
||||
renamed := 0
|
||||
toAdd := make(map[string]*dataFile)
|
||||
toDelete := make([]string, 0)
|
||||
for filePath, file := range node.files {
|
||||
if core.HasPrefix(filePath, oldPrefix) {
|
||||
updatedPath := core.Concat(newPrefix, core.TrimPrefix(filePath, oldPrefix))
|
||||
file.name = updatedPath
|
||||
toAdd[updatedPath] = file
|
||||
toDelete = append(toDelete, filePath)
|
||||
renamed++
|
||||
}
|
||||
}
|
||||
for _, p := range toDelete {
|
||||
delete(node.files, p)
|
||||
}
|
||||
for p, f := range toAdd {
|
||||
node.files[p] = f
|
||||
}
|
||||
if renamed == 0 {
|
||||
return core.E("node.Rename", core.Concat("path not found: ", oldPath), fs.ErrNotExist)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -313,12 +313,12 @@ func TestNode_Walk_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestNode_CopyFile_Good(t *testing.T) {
|
||||
func TestNode_ExportFile_Good(t *testing.T) {
|
||||
nodeTree := New()
|
||||
nodeTree.AddData("foo.txt", []byte("foo"))
|
||||
|
||||
destinationPath := core.Path(t.TempDir(), "test.txt")
|
||||
err := nodeTree.CopyFile("foo.txt", destinationPath, 0644)
|
||||
err := nodeTree.ExportFile("foo.txt", destinationPath, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
content, err := coreio.Local.Read(destinationPath)
|
||||
|
|
@ -326,24 +326,24 @@ func TestNode_CopyFile_Good(t *testing.T) {
|
|||
assert.Equal(t, "foo", content)
|
||||
}
|
||||
|
||||
func TestNode_CopyFile_Bad(t *testing.T) {
|
||||
func TestNode_ExportFile_Bad(t *testing.T) {
|
||||
nodeTree := New()
|
||||
destinationPath := core.Path(t.TempDir(), "test.txt")
|
||||
|
||||
err := nodeTree.CopyFile("nonexistent.txt", destinationPath, 0644)
|
||||
err := nodeTree.ExportFile("nonexistent.txt", destinationPath, 0644)
|
||||
assert.Error(t, err)
|
||||
|
||||
nodeTree.AddData("foo.txt", []byte("foo"))
|
||||
err = nodeTree.CopyFile("foo.txt", "/nonexistent_dir/test.txt", 0644)
|
||||
err = nodeTree.ExportFile("foo.txt", "/nonexistent_dir/test.txt", 0644)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNode_CopyFile_DirectorySource_Bad(t *testing.T) {
|
||||
func TestNode_ExportFile_DirectorySource_Bad(t *testing.T) {
|
||||
nodeTree := New()
|
||||
nodeTree.AddData("bar/baz.txt", []byte("baz"))
|
||||
destinationPath := core.Path(t.TempDir(), "test.txt")
|
||||
|
||||
err := nodeTree.CopyFile("bar", destinationPath, 0644)
|
||||
err := nodeTree.ExportFile("bar", destinationPath, 0644)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -182,11 +182,28 @@ func (obfuscator *ShuffleMaskObfuscator) deriveMask(entropy []byte, length int)
|
|||
// Example: &sigil.ShuffleMaskObfuscator{},
|
||||
// Example: )
|
||||
type ChaChaPolySigil struct {
|
||||
Key []byte
|
||||
Obfuscator PreObfuscator
|
||||
key []byte
|
||||
obfuscator PreObfuscator
|
||||
randomReader goio.Reader
|
||||
}
|
||||
|
||||
// Example: key := cipherSigil.Key()
|
||||
func (s *ChaChaPolySigil) Key() []byte {
|
||||
result := make([]byte, len(s.key))
|
||||
copy(result, s.key)
|
||||
return result
|
||||
}
|
||||
|
||||
// Example: ob := cipherSigil.Obfuscator()
|
||||
func (s *ChaChaPolySigil) Obfuscator() PreObfuscator {
|
||||
return s.obfuscator
|
||||
}
|
||||
|
||||
// Example: cipherSigil.SetObfuscator(nil)
|
||||
func (s *ChaChaPolySigil) SetObfuscator(obfuscator PreObfuscator) {
|
||||
s.obfuscator = obfuscator
|
||||
}
|
||||
|
||||
// Example: cipherSigil, _ := sigil.NewChaChaPolySigil([]byte("0123456789abcdef0123456789abcdef"), nil)
|
||||
// Example: ciphertext, _ := cipherSigil.In([]byte("payload"))
|
||||
// Example: plaintext, _ := cipherSigil.Out(ciphertext)
|
||||
|
|
@ -203,27 +220,27 @@ func NewChaChaPolySigil(key []byte, obfuscator PreObfuscator) (*ChaChaPolySigil,
|
|||
}
|
||||
|
||||
return &ChaChaPolySigil{
|
||||
Key: keyCopy,
|
||||
Obfuscator: obfuscator,
|
||||
key: keyCopy,
|
||||
obfuscator: obfuscator,
|
||||
randomReader: rand.Reader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (sigil *ChaChaPolySigil) In(data []byte) ([]byte, error) {
|
||||
if sigil.Key == nil {
|
||||
func (s *ChaChaPolySigil) In(data []byte) ([]byte, error) {
|
||||
if s.key == nil {
|
||||
return nil, NoKeyConfiguredError
|
||||
}
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
aead, err := chacha20poly1305.NewX(sigil.Key)
|
||||
aead, err := chacha20poly1305.NewX(s.key)
|
||||
if err != nil {
|
||||
return nil, core.E("sigil.ChaChaPolySigil.In", "create cipher", err)
|
||||
}
|
||||
|
||||
nonce := make([]byte, aead.NonceSize())
|
||||
reader := sigil.randomReader
|
||||
reader := s.randomReader
|
||||
if reader == nil {
|
||||
reader = rand.Reader
|
||||
}
|
||||
|
|
@ -232,8 +249,8 @@ func (sigil *ChaChaPolySigil) In(data []byte) ([]byte, error) {
|
|||
}
|
||||
|
||||
obfuscated := data
|
||||
if sigil.Obfuscator != nil {
|
||||
obfuscated = sigil.Obfuscator.Obfuscate(data, nonce)
|
||||
if s.obfuscator != nil {
|
||||
obfuscated = s.obfuscator.Obfuscate(data, nonce)
|
||||
}
|
||||
|
||||
ciphertext := aead.Seal(nonce, nonce, obfuscated, nil)
|
||||
|
|
@ -241,15 +258,15 @@ func (sigil *ChaChaPolySigil) In(data []byte) ([]byte, error) {
|
|||
return ciphertext, nil
|
||||
}
|
||||
|
||||
func (sigil *ChaChaPolySigil) Out(data []byte) ([]byte, error) {
|
||||
if sigil.Key == nil {
|
||||
func (s *ChaChaPolySigil) Out(data []byte) ([]byte, error) {
|
||||
if s.key == nil {
|
||||
return nil, NoKeyConfiguredError
|
||||
}
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
aead, err := chacha20poly1305.NewX(sigil.Key)
|
||||
aead, err := chacha20poly1305.NewX(s.key)
|
||||
if err != nil {
|
||||
return nil, core.E("sigil.ChaChaPolySigil.Out", "create cipher", err)
|
||||
}
|
||||
|
|
@ -270,8 +287,8 @@ func (sigil *ChaChaPolySigil) Out(data []byte) ([]byte, error) {
|
|||
}
|
||||
|
||||
plaintext := obfuscated
|
||||
if sigil.Obfuscator != nil {
|
||||
plaintext = sigil.Obfuscator.Deobfuscate(obfuscated, nonce)
|
||||
if s.obfuscator != nil {
|
||||
plaintext = s.obfuscator.Deobfuscate(obfuscated, nonce)
|
||||
}
|
||||
|
||||
if len(plaintext) == 0 {
|
||||
|
|
|
|||
|
|
@ -145,8 +145,8 @@ func TestCryptoSigil_NewChaChaPolySigil_Good(t *testing.T) {
|
|||
cipherSigil, err := NewChaChaPolySigil(key, nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, cipherSigil)
|
||||
assert.Equal(t, key, cipherSigil.Key)
|
||||
assert.NotNil(t, cipherSigil.Obfuscator)
|
||||
assert.Equal(t, key, cipherSigil.Key())
|
||||
assert.NotNil(t, cipherSigil.Obfuscator())
|
||||
}
|
||||
|
||||
func TestCryptoSigil_NewChaChaPolySigil_KeyIsCopied_Good(t *testing.T) {
|
||||
|
|
@ -159,7 +159,7 @@ func TestCryptoSigil_NewChaChaPolySigil_KeyIsCopied_Good(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
key[0] ^= 0xFF
|
||||
assert.Equal(t, original, cipherSigil.Key)
|
||||
assert.Equal(t, original, cipherSigil.Key())
|
||||
}
|
||||
|
||||
func TestCryptoSigil_NewChaChaPolySigil_ShortKey_Bad(t *testing.T) {
|
||||
|
|
@ -184,7 +184,7 @@ func TestCryptoSigil_NewChaChaPolySigil_CustomObfuscator_Good(t *testing.T) {
|
|||
ob := &ShuffleMaskObfuscator{}
|
||||
cipherSigil, err := NewChaChaPolySigil(key, ob)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, ob, cipherSigil.Obfuscator)
|
||||
assert.Equal(t, ob, cipherSigil.Obfuscator())
|
||||
}
|
||||
|
||||
func TestCryptoSigil_NewChaChaPolySigil_CustomObfuscatorNil_Good(t *testing.T) {
|
||||
|
|
@ -193,7 +193,7 @@ func TestCryptoSigil_NewChaChaPolySigil_CustomObfuscatorNil_Good(t *testing.T) {
|
|||
|
||||
cipherSigil, err := NewChaChaPolySigil(key, nil)
|
||||
require.NoError(t, err)
|
||||
assert.IsType(t, &XORObfuscator{}, cipherSigil.Obfuscator)
|
||||
assert.IsType(t, &XORObfuscator{}, cipherSigil.Obfuscator())
|
||||
}
|
||||
|
||||
func TestCryptoSigil_NewChaChaPolySigil_CustomObfuscator_InvalidKey_Bad(t *testing.T) {
|
||||
|
|
@ -351,7 +351,7 @@ func TestCryptoSigil_ChaChaPolySigil_NoObfuscator_Good(t *testing.T) {
|
|||
_, _ = rand.Read(key)
|
||||
|
||||
cipherSigil, _ := NewChaChaPolySigil(key, nil)
|
||||
cipherSigil.Obfuscator = nil
|
||||
cipherSigil.SetObfuscator(nil)
|
||||
|
||||
plaintext := []byte("raw encryption without pre-obfuscation")
|
||||
ciphertext, err := cipherSigil.In(plaintext)
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ func (medium *Medium) Write(entryPath, content string) error {
|
|||
}
|
||||
|
||||
// Example: _ = medium.WriteMode("app/theme", "midnight", 0600)
|
||||
// Note: mode is not persisted — the SQLite store has no entry_mode column.
|
||||
// Use Write when mode is irrelevant; WriteMode satisfies the Medium interface only.
|
||||
func (medium *Medium) WriteMode(entryPath, content string, mode fs.FileMode) error {
|
||||
return medium.Write(entryPath, content)
|
||||
}
|
||||
|
|
@ -144,23 +146,14 @@ func (medium *Medium) List(entryPath string) ([]fs.DirEntry, error) {
|
|||
group, key := splitGroupKeyPath(entryPath)
|
||||
|
||||
if group == "" {
|
||||
rows, err := medium.keyValueStore.database.Query("SELECT DISTINCT group_name FROM entries ORDER BY group_name")
|
||||
groups, err := medium.keyValueStore.ListGroups()
|
||||
if err != nil {
|
||||
return nil, core.E("store.List", "query groups", err)
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var entries []fs.DirEntry
|
||||
for rows.Next() {
|
||||
var groupName string
|
||||
if err := rows.Scan(&groupName); err != nil {
|
||||
return nil, core.E("store.List", "scan", err)
|
||||
}
|
||||
entries := make([]fs.DirEntry, 0, len(groups))
|
||||
for _, groupName := range groups {
|
||||
entries = append(entries, &keyValueDirEntry{name: groupName, isDir: true})
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, core.E("store.List", "rows", err)
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -112,6 +112,28 @@ func (keyValueStore *KeyValueStore) DeleteGroup(group string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Example: groups, _ := keyValueStore.ListGroups()
|
||||
func (keyValueStore *KeyValueStore) ListGroups() ([]string, error) {
|
||||
rows, err := keyValueStore.database.Query("SELECT DISTINCT group_name FROM entries ORDER BY group_name")
|
||||
if err != nil {
|
||||
return nil, core.E("store.ListGroups", "query groups", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var groups []string
|
||||
for rows.Next() {
|
||||
var groupName string
|
||||
if err := rows.Scan(&groupName); err != nil {
|
||||
return nil, core.E("store.ListGroups", "scan", err)
|
||||
}
|
||||
groups = append(groups, groupName)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, core.E("store.ListGroups", "rows", err)
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
// Example: values, _ := keyValueStore.GetAll("app")
|
||||
func (keyValueStore *KeyValueStore) GetAll(group string) (map[string]string, error) {
|
||||
rows, err := keyValueStore.database.Query("SELECT entry_key, entry_value FROM entries WHERE group_name = ?", group)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue