This document describes how to load rulesets from files and serialize them efficiently in the SPOCP engine.
The pkg/persist package provides:
- File Loading: Read rules from text files (canonical or advanced form)
- Binary Serialization: Efficient binary format for large rulesets
- Engine Integration: Direct loading/saving methods on
EngineandAdaptiveEngine
engine := spocp.NewEngine()
// Load rules from a file (auto-detects format)
if err := engine.LoadRulesFromFile("policies.txt"); err != nil {
log.Fatal(err)
}// Save in canonical format
engine.SaveRulesToFile("policies.txt", persist.FormatCanonical)
// Save in binary format
engine.SaveRulesToFile("policies.spocp", persist.FormatBinary)Text file with one rule per line in canonical S-expression format:
(4:http3:GET)
(4:http4:POST)
(4:file11:/etc/passwd)
Advantages:
- Human-readable (with practice)
- Version control friendly
- Compact for simple rules
- Standard S-expression format
Disadvantages:
- Harder to edit manually than advanced form
- Parsing overhead on load
Human-readable format (not yet fully implemented in parser, but supported for saving):
(http GET)
(http POST)
(file /etc/passwd)
Advantages:
- Easy to read and write
- Good for documentation
Disadvantages:
- Requires conversion to canonical form
- Not as compact
Efficient binary encoding for large rulesets:
File structure:
- Magic: "SPOCP" (5 bytes)
- Version: 1 (1 byte)
- Rule count: N (4 bytes)
- For each rule:
- Length: L (4 bytes)
- Data: canonical form (L bytes)
Advantages:
- Faster loading (no parsing overhead)
- Good for large rulesets
- Versioned format
Disadvantages:
- Not human-readable
- Not version control friendly
- May be larger than text for simple rules
func LoadFile(filename string, opts LoadOptions) ([]sexp.Element, error)Loads rules from a file with options:
opts := persist.LoadOptions{
Format: persist.FormatCanonical, // or FormatBinary
SkipInvalid: false, // Continue on parse errors?
MaxRules: 0, // Limit (0 = unlimited)
Comments: []string{"#", "//", ";"}, // Comment prefixes
}
rules, err := persist.LoadFile("rules.txt", opts)func SaveFile(filename string, rules []sexp.Element, format FileFormat) errorSaves rules to a file in the specified format:
// Canonical format
persist.SaveFile("rules.txt", rules, persist.FormatCanonical)
// Binary format
persist.SaveFile("rules.spocp", rules, persist.FormatBinary)func LoadFileToSlice(filename string) ([]sexp.Element, error)Simplified loading with default options:
rules, err := persist.LoadFileToSlice("rules.txt")func (e *Engine) LoadRulesFromFile(filename string) errorLoad rules directly into the engine:
engine := spocp.NewEngine()
err := engine.LoadRulesFromFile("policies.txt")func (e *Engine) LoadRulesFromFileWithOptions(filename string, opts persist.LoadOptions) errorLoad with custom options:
opts := persist.LoadOptions{
SkipInvalid: true, // Skip malformed rules
MaxRules: 1000, // Load at most 1000 rules
}
err := engine.LoadRulesFromFileWithOptions("policies.txt", opts)func (e *Engine) SaveRulesToFile(filename string, format persist.FileFormat) errorSave all engine rules to a file:
// Text format
engine.SaveRulesToFile("backup.txt", persist.FormatCanonical)
// Binary format
engine.SaveRulesToFile("backup.spocp", persist.FormatBinary)func (e *Engine) ExportRules() []sexp.Element
func (e *Engine) ImportRules(rules []sexp.Element)For programmatic transfer:
// Export from one engine
rules := engine1.ExportRules()
// Import to another
engine2 := spocp.NewEngine()
engine2.ImportRules(rules)Create a file policies.txt:
# HTTP access control
# Updated: 2025-12-10
(4:http3:GET) # Allow GET requests
(4:http4:POST) # Allow POST requests
// File access rules
(4:file11:/etc/passwd)
(4:file8:/var/log)
Load it:
engine := spocp.NewEngine()
engine.LoadRulesFromFile("policies.txt") // Comments automatically filteredopts := persist.LoadOptions{
SkipInvalid: true, // Don't fail on invalid rules
}
engine := spocp.NewEngine()
err := engine.LoadRulesFromFileWithOptions("untrusted.txt", opts)
// Invalid rules are skipped, valid ones loadedFor large rulesets (>10,000 rules), use binary format:
// Initial save (from canonical)
engine := spocp.NewEngine()
engine.LoadRulesFromFile("large_policy.txt")
engine.SaveRulesToFile("large_policy.spocp", persist.FormatBinary)
// Fast subsequent loads
engine2 := spocp.NewEngine()
opts := persist.LoadOptions{Format: persist.FormatBinary}
engine2.LoadRulesFromFileWithOptions("large_policy.spocp", opts)engine := spocp.NewAdaptiveEngine()
// Load large ruleset - indexing automatically adapts
engine.LoadRulesFromFile("policies.txt")
// Check adaptive decision
stats := engine.Stats()
fmt.Printf("Indexing enabled: %v\n", stats.IndexingEnabled)
fmt.Printf("Unique tags: %d\n", stats.UniqueTags)
fmt.Printf("Avg fanout: %.2f\n", stats.AvgTagFanout)| Format | Load Speed | File Size | Use Case |
|---|---|---|---|
| Canonical | Medium | Small | Default, version control |
| Advanced | Slow | Medium | Human editing |
| Binary | Fast | Varies | Large rulesets, production deploy |
The binary format has overhead (10 bytes per file + 4 bytes per rule), so:
- Small rulesets (<100 rules): Text format is comparable or better
- Medium rulesets (100-1,000 rules): Binary may be 10-20% larger
- Large rulesets (>10,000 rules): Binary saves ~10-30% space and loads faster
// Fastest loading (skip validation)
opts := persist.LoadOptions{
SkipInvalid: true, // Don't validate each rule deeply
}
// Limited loading (for testing)
opts := persist.LoadOptions{
MaxRules: 100, // Load first 100 rules only
}// In development
engine.SaveRulesToFile("policies.txt", persist.FormatCanonical)
// Commit policies.txt to git// Build step: convert to binary
engine.LoadRulesFromFile("policies.txt")
engine.SaveRulesToFile("policies.spocp", persist.FormatBinary)
// Production: load binary
prodEngine := spocp.NewEngine()
prodEngine.LoadRulesFromFile("policies.spocp")if err := engine.LoadRulesFromFile(filename); err != nil {
log.Printf("Failed to load rules from %s: %v", filename, err)
// Fall back to default policy
engine.AddRule("(5:admin)") // Default: only admin access
}engine.LoadRulesFromFile("policies.txt")
// Verify rule count
if engine.RuleCount() == 0 {
log.Fatal("No rules loaded!")
}
// Test a known query
allowed, _ := engine.Query("(4:http3:GET)")
if !allowed {
log.Fatal("Expected policy doesn't work!")
}# Section: HTTP Access Control
# Purpose: Allow read-only HTTP operations
# Owner: security-team@example.com
# Last updated: 2025-12-10
(4:http3:GET)
(4:http4:HEAD)
Simple approach for small deployments:
policies.txt
For larger systems, organize by domain:
policies/
http.txt # HTTP rules
file.txt # File access rules
admin.txt # Admin rules
Load all:
files := []string{
"policies/http.txt",
"policies/file.txt",
"policies/admin.txt",
}
engine := spocp.NewEngine()
for _, file := range files {
if err := engine.LoadRulesFromFile(file); err != nil {
log.Printf("Warning: failed to load %s: %v", file, err)
}
}policies/
src/ # Source files (version controlled)
http.txt
file.txt
cache/ # Binary cache (not version controlled)
http.spocp
file.spocp
Build script:
func buildCache() {
srcFiles, _ := filepath.Glob("policies/src/*.txt")
for _, src := range srcFiles {
base := filepath.Base(src)
cache := strings.TrimSuffix(base, ".txt") + ".spocp"
engine := spocp.NewEngine()
engine.LoadRulesFromFile(src)
engine.SaveRulesToFile("policies/cache/"+cache, persist.FormatBinary)
}
}if err := engine.LoadRulesFromFile(filename); err != nil {
if os.IsNotExist(err) {
// File doesn't exist
log.Fatal("Policy file not found")
} else if strings.Contains(err.Error(), "failed to parse") {
// Malformed rule
log.Fatal("Invalid rule syntax")
} else {
// Other error
log.Fatal(err)
}
}- API.md - Complete API reference
- README.md - Getting started guide
- examples/fileio/ - Working examples