refactor(ax): trim prose comments to examples
Some checks failed
CI / auto-fix (push) Failing after 0s
CI / test (push) Failing after 2s
CI / auto-merge (push) Failing after 0s

This commit is contained in:
Virgil 2026-03-30 23:02:53 +00:00
parent f8988c51cb
commit 347c4b1b57
14 changed files with 29 additions and 190 deletions

View file

@ -1,9 +1,7 @@
// Package datanode provides an io.Medium implementation backed by Borg's DataNode.
//
// medium := datanode.New()
// _ = medium.Write("jobs/run.log", "started")
// snapshot, _ := medium.Snapshot()
// restored, _ := datanode.FromTar(snapshot)
// medium := datanode.New()
// _ = medium.Write("jobs/run.log", "started")
// snapshot, _ := medium.Snapshot()
// restored, _ := datanode.FromTar(snapshot)
package datanode
import (
@ -36,7 +34,7 @@ var (
// snapshot, _ := medium.Snapshot()
type Medium struct {
dataNode *borgdatanode.DataNode
directorySet map[string]bool // explicit directories that exist without file contents
directorySet map[string]bool
lock sync.RWMutex
}
@ -101,8 +99,6 @@ func normaliseEntryPath(filePath string) string {
return filePath
}
// --- io.Medium interface ---
func (medium *Medium) Read(filePath string) (string, error) {
medium.lock.RLock()
defer medium.lock.RUnlock()
@ -139,7 +135,6 @@ func (medium *Medium) Write(filePath, content string) error {
}
medium.dataNode.AddData(filePath, []byte(content))
// ensure parent directories are tracked
medium.ensureDirsLocked(path.Dir(filePath))
return nil
}
@ -160,8 +155,6 @@ func (medium *Medium) EnsureDir(filePath string) error {
return nil
}
// ensureDirsLocked marks a directory and all ancestors as existing.
// Caller must hold medium.lock.
func (medium *Medium) ensureDirsLocked(directoryPath string) {
for directoryPath != "" && directoryPath != "." {
medium.directorySet[directoryPath] = true
@ -226,7 +219,6 @@ func (medium *Medium) Delete(filePath string) error {
return nil
}
// Remove the file by creating a new DataNode without it
if err := medium.removeFileLocked(filePath); err != nil {
return core.E("datanode.Delete", core.Concat("failed to delete file: ", filePath), err)
}
@ -253,7 +245,6 @@ func (medium *Medium) DeleteAll(filePath string) error {
found = true
}
// Remove all files under prefix
entries, err := medium.collectAllLocked()
if err != nil {
return core.E("datanode.DeleteAll", core.Concat("failed to inspect tree: ", filePath), err)
@ -267,7 +258,6 @@ func (medium *Medium) DeleteAll(filePath string) error {
}
}
// Remove explicit directories under prefix
for directoryPath := range medium.directorySet {
if directoryPath == filePath || core.HasPrefix(directoryPath, prefix) {
delete(medium.directorySet, directoryPath)
@ -466,7 +456,7 @@ func (medium *Medium) Exists(filePath string) bool {
filePath = normaliseEntryPath(filePath)
if filePath == "" {
return true // root always exists
return true
}
_, err := medium.dataNode.Stat(filePath)
if err == nil {
@ -490,9 +480,6 @@ func (medium *Medium) IsDir(filePath string) bool {
return medium.directorySet[filePath]
}
// --- internal helpers ---
// hasPrefixLocked checks if any file path starts with prefix. Caller holds lock.
func (medium *Medium) hasPrefixLocked(prefix string) (bool, error) {
entries, err := medium.collectAllLocked()
if err != nil {
@ -511,7 +498,6 @@ func (medium *Medium) hasPrefixLocked(prefix string) (bool, error) {
return false, nil
}
// collectAllLocked returns all file paths in the DataNode. Caller holds lock.
func (medium *Medium) collectAllLocked() ([]string, error) {
var names []string
err := dataNodeWalkDir(medium.dataNode, ".", func(filePath string, entry fs.DirEntry, err error) error {
@ -542,9 +528,6 @@ func (medium *Medium) readFileLocked(filePath string) ([]byte, error) {
return data, nil
}
// removeFileLocked removes a single file by rebuilding the DataNode.
// This is necessary because Borg's DataNode doesn't expose a Remove method.
// Caller must hold medium.lock write lock.
func (medium *Medium) removeFileLocked(target string) error {
entries, err := medium.collectAllLocked()
if err != nil {
@ -565,8 +548,6 @@ func (medium *Medium) removeFileLocked(target string) error {
return nil
}
// --- writeCloser buffers writes and flushes to DataNode on Close ---
type writeCloser struct {
medium *Medium
path string
@ -587,8 +568,6 @@ func (writer *writeCloser) Close() error {
return nil
}
// --- fs types for explicit directories ---
type dirEntry struct {
name string
}

10
doc.go
View file

@ -1,7 +1,5 @@
// Package io exposes CoreGO's storage surface.
//
// medium, _ := io.NewSandboxed("/srv/app")
// _ = medium.Write("config/app.yaml", "port: 8080")
// backup, _ := io.NewSandboxed("/srv/backup")
// _ = io.Copy(medium, "data/report.json", backup, "daily/report.json")
// medium, _ := io.NewSandboxed("/srv/app")
// _ = medium.Write("config/app.yaml", "port: 8080")
// backup, _ := io.NewSandboxed("/srv/backup")
// _ = io.Copy(medium, "data/report.json", backup, "daily/report.json")
package io

7
io.go
View file

@ -188,7 +188,6 @@ type MemoryMedium struct {
modTimes map[string]time.Time
}
// MockMedium is a compatibility alias for MemoryMedium.
type MockMedium = MemoryMedium
var _ Medium = (*MemoryMedium)(nil)
@ -203,8 +202,6 @@ func NewMemoryMedium() *MemoryMedium {
}
}
// NewMockMedium is a compatibility alias for NewMemoryMedium.
//
// Example: medium := io.NewMockMedium()
// _ = medium.Write("config/app.yaml", "port: 8080")
func NewMockMedium() *MemoryMedium {
@ -396,14 +393,12 @@ func (medium *MemoryMedium) WriteStream(path string) (goio.WriteCloser, error) {
return medium.Create(path)
}
// MemoryFile implements fs.File for MemoryMedium.
type MemoryFile struct {
name string
content []byte
offset int64
}
// MockFile is a compatibility alias for MemoryFile.
type MockFile = MemoryFile
func (file *MemoryFile) Stat() (fs.FileInfo, error) {
@ -423,14 +418,12 @@ func (file *MemoryFile) Close() error {
return nil
}
// MemoryWriteCloser implements WriteCloser for MemoryMedium.
type MemoryWriteCloser struct {
medium *MemoryMedium
path string
data []byte
}
// MockWriteCloser is a compatibility alias for MemoryWriteCloser.
type MockWriteCloser = MemoryWriteCloser
func (writeCloser *MemoryWriteCloser) Write(data []byte) (int, error) {

View file

@ -23,7 +23,6 @@ var unrestrictedFileSystem = (&core.Fs{}).NewUnrestricted()
// _ = medium.Write("config/app.yaml", "port: 8080")
func New(root string) (*Medium, error) {
absoluteRoot := absolutePath(root)
// Example: local.New("/srv/app") resolves macOS "/var" to "/private/var" before sandbox checks.
if resolvedRoot, err := resolveSymlinksPath(absoluteRoot); err == nil {
absoluteRoot = resolvedRoot
}
@ -181,16 +180,12 @@ func (medium *Medium) sandboxedPath(path string) string {
return core.Path(currentWorkingDir(), normalisePath(path))
}
// Use a cleaned absolute path to resolve all .. and . internally
// before joining with the root. This is a standard way to sandbox paths.
clean := cleanSandboxPath(path)
// If root is "/", allow absolute paths through
if medium.filesystemRoot == dirSeparator() {
return clean
}
// Join cleaned relative path with root
return core.Path(medium.filesystemRoot, core.TrimPrefix(clean, dirSeparator()))
}
@ -199,7 +194,6 @@ func (medium *Medium) validatePath(path string) (string, error) {
return medium.sandboxedPath(path), nil
}
// Split the cleaned path into components
parts := splitPathParts(cleanSandboxPath(path))
current := medium.filesystemRoot
@ -208,16 +202,12 @@ func (medium *Medium) validatePath(path string) (string, error) {
realNext, err := resolveSymlinksPath(next)
if err != nil {
if core.Is(err, syscall.ENOENT) {
// Part doesn't exist, we can't follow symlinks anymore.
// Since the path is already Cleaned and current is safe,
// appending a component to current will not escape.
current = next
continue
}
return "", err
}
// Verify the resolved part is still within the root
if !isWithinRoot(medium.filesystemRoot, realNext) {
logSandboxEscape(medium.filesystemRoot, path, realNext)
return "", fs.ErrPermission

View file

@ -129,20 +129,14 @@ func (node *Node) WalkNode(root string, fn fs.WalkDirFunc) error {
// Example: options := node.WalkOptions{MaxDepth: 1, SkipErrors: true}
type WalkOptions struct {
// MaxDepth limits how many directory levels to descend. 0 means unlimited.
MaxDepth int
// Filter, if set, is called for each entry. Return true to include the
// entry (and descend into it if it is a directory).
Filter func(entryPath string, entry fs.DirEntry) bool
// SkipErrors suppresses errors (e.g. nonexistent root) instead of
// propagating them through the callback.
MaxDepth int
Filter func(entryPath string, entry fs.DirEntry) bool
SkipErrors bool
}
// Example: _ = nodeTree.WalkWithOptions(".", callback, node.WalkOptions{MaxDepth: 1, SkipErrors: true})
func (node *Node) WalkWithOptions(root string, fn fs.WalkDirFunc, options WalkOptions) error {
if options.SkipErrors {
// If root doesn't exist, silently return nil.
if _, err := node.Stat(root); err != nil {
return nil
}
@ -160,7 +154,6 @@ func (node *Node) WalkWithOptions(root string, fn fs.WalkDirFunc, options WalkOp
result := fn(entryPath, entry, err)
// After visiting a directory at MaxDepth, prevent descending further.
if result == nil && options.MaxDepth > 0 && entry != nil && entry.IsDir() && entryPath != root {
rel := core.TrimPrefix(entryPath, root)
rel = core.TrimPrefix(rel, "/")
@ -181,7 +174,6 @@ func (node *Node) ReadFile(name string) ([]byte, error) {
if !ok {
return nil, core.E("node.ReadFile", core.Concat("path not found: ", name), fs.ErrNotExist)
}
// Return a copy to prevent callers from mutating internal state.
result := make([]byte, len(file.content))
copy(result, file.content)
return result, nil
@ -217,7 +209,6 @@ func (node *Node) CopyTo(target coreio.Medium, sourcePath, destPath string) erro
}
if !info.IsDir() {
// Single file copy
file, ok := node.files[sourcePath]
if !ok {
return core.E("node.CopyTo", core.Concat("path not found: ", sourcePath), fs.ErrNotExist)
@ -225,7 +216,6 @@ func (node *Node) CopyTo(target coreio.Medium, sourcePath, destPath string) erro
return target.Write(destPath, string(file.content))
}
// Directory: walk and copy all files underneath
prefix := sourcePath
if prefix != "" && !core.HasSuffix(prefix, "/") {
prefix += "/"
@ -247,8 +237,6 @@ func (node *Node) CopyTo(target coreio.Medium, sourcePath, destPath string) erro
return nil
}
// ---------- Medium interface: fs.FS methods ----------
func (node *Node) Open(name string) (fs.File, error) {
name = core.TrimPrefix(name, "/")
if dataFile, ok := node.files[name]; ok {
@ -289,7 +277,6 @@ func (node *Node) ReadDir(name string) ([]fs.DirEntry, error) {
name = ""
}
// Disallow reading a file as a directory.
if info, err := node.Stat(name); err == nil && !info.IsDir() {
return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrInvalid}
}
@ -332,8 +319,6 @@ func (node *Node) ReadDir(name string) ([]fs.DirEntry, error) {
return entries, nil
}
// ---------- Medium interface: read/write ----------
func (node *Node) Read(filePath string) (string, error) {
filePath = core.TrimPrefix(filePath, "/")
file, ok := node.files[filePath]
@ -365,8 +350,6 @@ func (node *Node) EnsureDir(_ string) error {
return nil
}
// ---------- Medium interface: existence checks ----------
func (node *Node) Exists(filePath string) bool {
_, err := node.Stat(filePath)
return err == nil
@ -386,8 +369,6 @@ func (node *Node) IsDir(filePath string) bool {
return info.IsDir()
}
// ---------- Medium interface: mutations ----------
func (node *Node) Delete(filePath string) error {
filePath = core.TrimPrefix(filePath, "/")
if _, ok := node.files[filePath]; ok {
@ -443,8 +424,6 @@ func (node *Node) List(filePath string) ([]fs.DirEntry, error) {
return node.ReadDir(filePath)
}
// ---------- Medium interface: streams ----------
func (node *Node) Create(filePath string) (goio.WriteCloser, error) {
filePath = core.TrimPrefix(filePath, "/")
return &nodeWriter{node: node, path: filePath}, nil
@ -472,9 +451,6 @@ func (node *Node) WriteStream(filePath string) (goio.WriteCloser, error) {
return node.Create(filePath)
}
// ---------- Internal types ----------
// nodeWriter buffers writes and commits them to the Node on Close.
type nodeWriter struct {
node *Node
path string

View file

@ -1,5 +1,3 @@
// Package s3 stores io.Medium data in S3 objects.
//
// Example: client := awss3.NewFromConfig(aws.Config{Region: "us-east-1"})
// Example: medium, _ := s3.New(s3.Options{Bucket: "backups", Client: client, Prefix: "daily/"})
// Example: _ = medium.Write("reports/daily.txt", "done")
@ -45,11 +43,8 @@ var _ coreio.Medium = (*Medium)(nil)
// Example: medium, _ := s3.New(s3.Options{Bucket: "backups", Client: client, Prefix: "daily/"})
type Options struct {
// Bucket is the target S3 bucket name.
Bucket string
// Client is the AWS S3 client or test double used for requests.
Client Client
// Prefix is prepended to every object key.
Prefix string
}
@ -109,8 +104,6 @@ func New(options Options) (*Medium, error) {
}
func (medium *Medium) objectKey(filePath string) string {
// Clean the path using a leading "/" to sandbox traversal attempts,
// then strip the "/" prefix. This ensures ".." can't escape.
clean := path.Clean("/" + filePath)
if clean == "/" {
clean = ""
@ -181,7 +174,6 @@ func (medium *Medium) IsFile(filePath string) bool {
if key == "" {
return false
}
// A "file" in S3 is an object whose key does not end with "/"
if core.HasSuffix(key, "/") {
return false
}
@ -223,7 +215,6 @@ func (medium *Medium) DeleteAll(filePath string) error {
return core.E("s3.DeleteAll", "path is required", fs.ErrInvalid)
}
// First, try deleting the exact key
_, err := medium.client.DeleteObject(context.Background(), &awss3.DeleteObjectInput{
Bucket: aws.String(medium.bucket),
Key: aws.String(key),
@ -232,7 +223,6 @@ func (medium *Medium) DeleteAll(filePath string) error {
return core.E("s3.DeleteAll", core.Concat("failed to delete object: ", key), err)
}
// Then delete all objects under the prefix
prefix := key
if !core.HasSuffix(prefix, "/") {
prefix += "/"
@ -561,8 +551,6 @@ func (medium *Medium) IsDir(filePath string) bool {
return len(listOutput.Contents) > 0 || len(listOutput.CommonPrefixes) > 0
}
// --- Internal types ---
type fileInfo struct {
name string
size int64

View file

@ -14,35 +14,23 @@ import (
)
var (
// InvalidKeyError is returned when the encryption key is not 32 bytes.
InvalidKeyError = core.E("sigil.InvalidKeyError", "invalid key size, must be 32 bytes", nil)
// CiphertextTooShortError is returned when the ciphertext is too short to decrypt.
CiphertextTooShortError = core.E("sigil.CiphertextTooShortError", "ciphertext too short", nil)
// DecryptionFailedError is returned when decryption or authentication fails.
DecryptionFailedError = core.E("sigil.DecryptionFailedError", "decryption failed", nil)
// NoKeyConfiguredError is returned when no encryption key has been set.
NoKeyConfiguredError = core.E("sigil.NoKeyConfiguredError", "no encryption key configured", nil)
)
// PreObfuscator customises the bytes mixed in before and after encryption.
type PreObfuscator interface {
// Obfuscate transforms plaintext before encryption using the provided entropy.
// The entropy is typically the encryption nonce, ensuring the transformation
// is unique per-encryption without additional random generation.
Obfuscate(data []byte, entropy []byte) []byte
// Deobfuscate reverses the transformation after decryption.
// Must be called with the same entropy used during Obfuscate.
Deobfuscate(data []byte, entropy []byte) []byte
}
// Example: cipherSigil, _ := sigil.NewChaChaPolySigil(key)
type XORObfuscator struct{}
// Obfuscate XORs the data with a key stream derived from the entropy.
func (obfuscator *XORObfuscator) Obfuscate(data []byte, entropy []byte) []byte {
if len(data) == 0 {
return data
@ -50,7 +38,6 @@ func (obfuscator *XORObfuscator) Obfuscate(data []byte, entropy []byte) []byte {
return obfuscator.transform(data, entropy)
}
// Deobfuscate reverses the XOR transformation (XOR is symmetric).
func (obfuscator *XORObfuscator) Deobfuscate(data []byte, entropy []byte) []byte {
if len(data) == 0 {
return data
@ -58,7 +45,6 @@ func (obfuscator *XORObfuscator) Deobfuscate(data []byte, entropy []byte) []byte
return obfuscator.transform(data, entropy)
}
// transform applies XOR with an entropy-derived key stream.
func (obfuscator *XORObfuscator) transform(data []byte, entropy []byte) []byte {
result := make([]byte, len(data))
keyStream := obfuscator.deriveKeyStream(entropy, len(data))
@ -68,12 +54,10 @@ func (obfuscator *XORObfuscator) transform(data []byte, entropy []byte) []byte {
return result
}
// deriveKeyStream creates a deterministic key stream from entropy.
func (obfuscator *XORObfuscator) deriveKeyStream(entropy []byte, length int) []byte {
stream := make([]byte, length)
h := sha256.New()
// Generate key stream in 32-byte blocks
blockNum := uint64(0)
offset := 0
for offset < length {
@ -92,10 +76,8 @@ func (obfuscator *XORObfuscator) deriveKeyStream(entropy []byte, length int) []b
return stream
}
// ShuffleMaskObfuscator adds byte shuffling on top of XOR masking.
type ShuffleMaskObfuscator struct{}
// Obfuscate shuffles bytes and applies a mask derived from entropy.
func (obfuscator *ShuffleMaskObfuscator) Obfuscate(data []byte, entropy []byte) []byte {
if len(data) == 0 {
return data
@ -104,16 +86,13 @@ func (obfuscator *ShuffleMaskObfuscator) Obfuscate(data []byte, entropy []byte)
result := make([]byte, len(data))
copy(result, data)
// Generate permutation and mask from entropy
perm := obfuscator.generatePermutation(entropy, len(data))
mask := obfuscator.deriveMask(entropy, len(data))
// Apply mask first, then shuffle
for i := range result {
result[i] ^= mask[i]
}
// Shuffle using Fisher-Yates with deterministic seed
shuffled := make([]byte, len(data))
for i, p := range perm {
shuffled[i] = result[p]
@ -122,7 +101,6 @@ func (obfuscator *ShuffleMaskObfuscator) Obfuscate(data []byte, entropy []byte)
return shuffled
}
// Deobfuscate reverses the shuffle and mask operations.
func (obfuscator *ShuffleMaskObfuscator) Deobfuscate(data []byte, entropy []byte) []byte {
if len(data) == 0 {
return data
@ -130,16 +108,13 @@ func (obfuscator *ShuffleMaskObfuscator) Deobfuscate(data []byte, entropy []byte
result := make([]byte, len(data))
// Generate permutation and mask from entropy
perm := obfuscator.generatePermutation(entropy, len(data))
mask := obfuscator.deriveMask(entropy, len(data))
// Unshuffle first
for i, p := range perm {
result[p] = data[i]
}
// Remove mask
for i := range result {
result[i] ^= mask[i]
}
@ -147,20 +122,17 @@ func (obfuscator *ShuffleMaskObfuscator) Deobfuscate(data []byte, entropy []byte
return result
}
// generatePermutation creates a deterministic permutation from entropy.
func (obfuscator *ShuffleMaskObfuscator) generatePermutation(entropy []byte, length int) []int {
perm := make([]int, length)
for i := range perm {
perm[i] = i
}
// Use entropy to seed a deterministic shuffle
h := sha256.New()
h.Write(entropy)
h.Write([]byte("permutation"))
seed := h.Sum(nil)
// Fisher-Yates shuffle with deterministic randomness
for i := length - 1; i > 0; i-- {
h.Reset()
h.Write(seed)
@ -175,7 +147,6 @@ func (obfuscator *ShuffleMaskObfuscator) generatePermutation(entropy []byte, len
return perm
}
// deriveMask creates a mask byte array from entropy.
func (obfuscator *ShuffleMaskObfuscator) deriveMask(entropy []byte, length int) []byte {
mask := make([]byte, length)
h := sha256.New()
@ -199,12 +170,11 @@ func (obfuscator *ShuffleMaskObfuscator) deriveMask(entropy []byte, length int)
return mask
}
// Example: cipherSigil, _ := sigil.NewChaChaPolySigil(key)
// Example: cipherSigil, _ := sigil.NewChaChaPolySigilWithObfuscator(key, &sigil.ShuffleMaskObfuscator{})
type ChaChaPolySigil struct {
Key []byte
Obfuscator PreObfuscator
randomReader goio.Reader // for testing injection
randomReader goio.Reader
}
// Example: cipherSigil, _ := sigil.NewChaChaPolySigil([]byte("0123456789abcdef0123456789abcdef"))
@ -244,7 +214,6 @@ func NewChaChaPolySigilWithObfuscator(key []byte, obfuscator PreObfuscator) (*Ch
return cipherSigil, nil
}
// In encrypts plaintext with the configured pre-obfuscator.
func (sigil *ChaChaPolySigil) In(data []byte) ([]byte, error) {
if sigil.Key == nil {
return nil, NoKeyConfiguredError
@ -258,7 +227,6 @@ func (sigil *ChaChaPolySigil) In(data []byte) ([]byte, error) {
return nil, core.E("sigil.ChaChaPolySigil.In", "create cipher", err)
}
// Generate nonce
nonce := make([]byte, aead.NonceSize())
reader := sigil.randomReader
if reader == nil {
@ -268,21 +236,16 @@ func (sigil *ChaChaPolySigil) In(data []byte) ([]byte, error) {
return nil, core.E("sigil.ChaChaPolySigil.In", "read nonce", err)
}
// Pre-obfuscate the plaintext using nonce as entropy
// This ensures CPU encryption routines never see raw plaintext
obfuscated := data
if sigil.Obfuscator != nil {
obfuscated = sigil.Obfuscator.Obfuscate(data, nonce)
}
// Encrypt the obfuscated data
// Output: [nonce | ciphertext | auth tag]
ciphertext := aead.Seal(nonce, nonce, obfuscated, nil)
return ciphertext, nil
}
// Out decrypts ciphertext and reverses the pre-obfuscation step.
func (sigil *ChaChaPolySigil) Out(data []byte) ([]byte, error) {
if sigil.Key == nil {
return nil, NoKeyConfiguredError
@ -301,17 +264,14 @@ func (sigil *ChaChaPolySigil) Out(data []byte) ([]byte, error) {
return nil, CiphertextTooShortError
}
// Extract nonce from ciphertext
nonce := data[:aead.NonceSize()]
ciphertext := data[aead.NonceSize():]
// Decrypt
obfuscated, err := aead.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, core.E("sigil.ChaChaPolySigil.Out", "decrypt ciphertext", DecryptionFailedError)
}
// Deobfuscate using the same nonce as entropy
plaintext := obfuscated
if sigil.Obfuscator != nil {
plaintext = sigil.Obfuscator.Deobfuscate(obfuscated, nonce)

View file

@ -1,14 +1,11 @@
// Package sigil chains reversible byte transformations.
//
// hexSigil, _ := sigil.NewSigil("hex")
// gzipSigil, _ := sigil.NewSigil("gzip")
// encoded, _ := sigil.Transmute([]byte("payload"), []sigil.Sigil{hexSigil, gzipSigil})
// decoded, _ := sigil.Untransmute(encoded, []sigil.Sigil{hexSigil, gzipSigil})
// hexSigil, _ := sigil.NewSigil("hex")
// gzipSigil, _ := sigil.NewSigil("gzip")
// encoded, _ := sigil.Transmute([]byte("payload"), []sigil.Sigil{hexSigil, gzipSigil})
// decoded, _ := sigil.Untransmute(encoded, []sigil.Sigil{hexSigil, gzipSigil})
package sigil
import core "dappco.re/go/core"
// Sigil transforms byte slices.
type Sigil interface {
// Example: encoded, _ := hexSigil.In([]byte("payload"))
In(data []byte) ([]byte, error)

View file

@ -20,11 +20,8 @@ import (
"golang.org/x/crypto/sha3"
)
// ReverseSigil is a Sigil that reverses the bytes of the payload.
// It is a symmetrical Sigil, meaning that the In and Out methods perform the same operation.
type ReverseSigil struct{}
// In reverses the bytes of the data.
func (sigil *ReverseSigil) In(data []byte) ([]byte, error) {
if data == nil {
return nil, nil
@ -36,16 +33,12 @@ func (sigil *ReverseSigil) In(data []byte) ([]byte, error) {
return reversed, nil
}
// Out reverses the bytes of the data.
func (sigil *ReverseSigil) Out(data []byte) ([]byte, error) {
return sigil.In(data)
}
// HexSigil is a Sigil that encodes/decodes data to/from hexadecimal.
// The In method encodes the data, and the Out method decodes it.
type HexSigil struct{}
// In encodes the data to hexadecimal.
func (sigil *HexSigil) In(data []byte) ([]byte, error) {
if data == nil {
return nil, nil
@ -55,7 +48,6 @@ func (sigil *HexSigil) In(data []byte) ([]byte, error) {
return dst, nil
}
// Out decodes the data from hexadecimal.
func (sigil *HexSigil) Out(data []byte) ([]byte, error) {
if data == nil {
return nil, nil
@ -65,11 +57,8 @@ func (sigil *HexSigil) Out(data []byte) ([]byte, error) {
return dst, err
}
// Base64Sigil is a Sigil that encodes/decodes data to/from base64.
// The In method encodes the data, and the Out method decodes it.
type Base64Sigil struct{}
// In encodes the data to base64.
func (sigil *Base64Sigil) In(data []byte) ([]byte, error) {
if data == nil {
return nil, nil
@ -79,7 +68,6 @@ func (sigil *Base64Sigil) In(data []byte) ([]byte, error) {
return dst, nil
}
// Out decodes the data from base64.
func (sigil *Base64Sigil) Out(data []byte) ([]byte, error) {
if data == nil {
return nil, nil
@ -89,13 +77,10 @@ func (sigil *Base64Sigil) Out(data []byte) ([]byte, error) {
return dst[:n], err
}
// GzipSigil is a Sigil that compresses/decompresses data using gzip.
// The In method compresses the data, and the Out method decompresses it.
type GzipSigil struct {
outputWriter goio.Writer
}
// In compresses the data using gzip.
func (sigil *GzipSigil) In(data []byte) ([]byte, error) {
if data == nil {
return nil, nil
@ -115,7 +100,6 @@ func (sigil *GzipSigil) In(data []byte) ([]byte, error) {
return b.Bytes(), nil
}
// Out decompresses the data using gzip.
func (sigil *GzipSigil) Out(data []byte) ([]byte, error) {
if data == nil {
return nil, nil
@ -132,11 +116,8 @@ func (sigil *GzipSigil) Out(data []byte) ([]byte, error) {
return out, nil
}
// JSONSigil is a Sigil that compacts or indents JSON data.
// The Out method is a no-op.
type JSONSigil struct{ Indent bool }
// In compacts or indents the JSON data.
func (sigil *JSONSigil) In(data []byte) ([]byte, error) {
if data == nil {
return nil, nil
@ -158,27 +139,20 @@ func (sigil *JSONSigil) In(data []byte) ([]byte, error) {
return []byte(compact), nil
}
// Out is a no-op for JSONSigil.
func (sigil *JSONSigil) Out(data []byte) ([]byte, error) {
// For simplicity, Out is a no-op. The primary use is formatting.
return data, nil
}
// HashSigil is a Sigil that hashes the data using a specified algorithm.
// The In method hashes the data, and the Out method is a no-op.
type HashSigil struct {
Hash crypto.Hash
}
// Use NewHashSigil to hash payloads with a specific crypto.Hash.
//
// hashSigil := sigil.NewHashSigil(crypto.SHA256)
// digest, _ := hashSigil.In([]byte("payload"))
// hashSigil := sigil.NewHashSigil(crypto.SHA256)
// digest, _ := hashSigil.In([]byte("payload"))
func NewHashSigil(h crypto.Hash) *HashSigil {
return &HashSigil{Hash: h}
}
// In hashes the data.
func (sigil *HashSigil) In(data []byte) ([]byte, error) {
var hasher goio.Writer
switch sigil.Hash {
@ -219,7 +193,6 @@ func (sigil *HashSigil) In(data []byte) ([]byte, error) {
case crypto.BLAKE2b_512:
hasher, _ = blake2b.New512(nil)
default:
// MD5SHA1 is not supported as a direct hash
return nil, core.E("sigil.HashSigil.In", "hash algorithm not available", nil)
}
@ -227,16 +200,13 @@ func (sigil *HashSigil) In(data []byte) ([]byte, error) {
return hasher.(interface{ Sum([]byte) []byte }).Sum(nil), nil
}
// Out is a no-op for HashSigil.
func (sigil *HashSigil) Out(data []byte) ([]byte, error) {
return data, nil
}
// Use NewSigil("hex") or NewSigil("gzip") to construct a sigil by name.
//
// hexSigil, _ := sigil.NewSigil("hex")
// gzipSigil, _ := sigil.NewSigil("gzip")
// transformed, _ := sigil.Transmute([]byte("payload"), []sigil.Sigil{hexSigil, gzipSigil})
// hexSigil, _ := sigil.NewSigil("hex")
// gzipSigil, _ := sigil.NewSigil("gzip")
// transformed, _ := sigil.Transmute([]byte("payload"), []sigil.Sigil{hexSigil, gzipSigil})
func NewSigil(name string) (Sigil, error) {
switch name {
case "reverse":

View file

@ -13,7 +13,7 @@ import (
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
_ "modernc.org/sqlite" // Pure Go SQLite driver
_ "modernc.org/sqlite"
)
// Example: medium, _ := sqlite.New(sqlite.Options{Path: ":memory:"})
@ -26,9 +26,7 @@ type Medium struct {
var _ coreio.Medium = (*Medium)(nil)
type Options struct {
// Path is the SQLite database path. Use ":memory:" for tests.
Path string
// Table is the table name used for file storage. Empty defaults to "files".
Path string
Table string
}
@ -564,8 +562,6 @@ func (medium *Medium) IsDir(filePath string) bool {
return isDir
}
// --- Internal types ---
type fileInfo struct {
name string
size int64

View file

@ -1,7 +1,5 @@
// Package store maps grouped keys onto SQLite rows.
//
// keyValueStore, _ := store.New(store.Options{Path: ":memory:"})
// _ = keyValueStore.Set("app", "theme", "midnight")
// medium := keyValueStore.AsMedium()
// _ = medium.Write("app/theme", "midnight")
// keyValueStore, _ := store.New(store.Options{Path: ":memory:"})
// _ = keyValueStore.Set("app", "theme", "midnight")
// medium := keyValueStore.AsMedium()
// _ = medium.Write("app/theme", "midnight")
package store

View file

@ -172,7 +172,7 @@ func (medium *Medium) List(entryPath string) ([]fs.DirEntry, error) {
}
if key != "" {
return nil, nil // leaf node, nothing beneath
return nil, nil
}
all, err := medium.store.GetAll(group)
@ -276,8 +276,6 @@ func (medium *Medium) IsDir(entryPath string) bool {
return err == nil && entryCount > 0
}
// --- fs helper types ---
type keyValueFileInfo struct {
name string
size int64

View file

@ -11,7 +11,6 @@ import (
)
// Example: _, err := keyValueStore.Get("app", "theme")
// err matches store.NotFoundError when the key is missing.
var NotFoundError = errors.New("key not found")
// Example: keyValueStore, _ := store.New(store.Options{Path: ":memory:"})
@ -20,7 +19,6 @@ type Store struct {
}
type Options struct {
// Path is the SQLite database path. Use ":memory:" for tests.
Path string
}

View file

@ -1,5 +1,3 @@
// Package workspace creates encrypted workspaces on top of io.Medium.
//
// Example: service, _ := workspace.New(workspace.Options{CryptProvider: cryptProvider})
// workspaceID, _ := service.CreateWorkspace("alice", "pass123")
// _ = service.SwitchWorkspace(workspaceID)