mirror of
https://git.numenor-labs.us/dsfx.git
synced 2025-04-29 16:20:34 +00:00
288 lines
6.9 KiB
Go
288 lines
6.9 KiB
Go
package sim
|
||
|
||
import (
|
||
"errors"
|
||
"io"
|
||
"io/fs"
|
||
"math/rand"
|
||
"sync"
|
||
"time"
|
||
|
||
"koti.casa/numenor-labs/dsfx/internal/lib/disk"
|
||
)
|
||
|
||
// Tolerance defines simulation tolerance parameters.
|
||
type Tolerance struct {
|
||
// Latency to wait before executing an operation.
|
||
Latency time.Duration
|
||
// FailureChance is the probability (0.0–1.0) that an operation fails.
|
||
FailureChance float64
|
||
// CorruptionChance is the probability (0.0–1.0) that a write is corrupted.
|
||
CorruptionChance float64
|
||
}
|
||
|
||
// simEntry represents an entry in our in–memory file system.
|
||
type simEntry struct {
|
||
name string
|
||
isDir bool
|
||
perm fs.FileMode
|
||
modTime time.Time
|
||
|
||
// For files, data holds the file contents.
|
||
data []byte
|
||
mu sync.Mutex
|
||
}
|
||
|
||
// simFile is a simulated file object. It implements fs.File and io.Writer.
|
||
// The same underlying simEntry is shared among all handles to a given file.
|
||
type simFile struct {
|
||
entry *simEntry
|
||
// offset to simulate reading sequentially.
|
||
offset int
|
||
// readOnly indicates whether writes are allowed.
|
||
readOnly bool
|
||
// closed is set once Close() has been called.
|
||
closed bool
|
||
// You could add a mutex here if you want to protect concurrent access per handle.
|
||
mu sync.Mutex
|
||
|
||
// tol copied from the SimDisk for per–file simulation.
|
||
tol Tolerance
|
||
}
|
||
|
||
// Ensure simFile implements the interfaces.
|
||
var _ disk.File = (*simFile)(nil)
|
||
|
||
// Read reads from the simulated file starting at the current offset.
|
||
func (sf *simFile) Read(p []byte) (int, error) {
|
||
sf.mu.Lock()
|
||
defer sf.mu.Unlock()
|
||
|
||
if sf.closed {
|
||
return 0, errors.New("read from closed file")
|
||
}
|
||
|
||
// Lock the underlying data.
|
||
sf.entry.mu.Lock()
|
||
defer sf.entry.mu.Unlock()
|
||
|
||
if sf.offset >= len(sf.entry.data) {
|
||
return 0, io.EOF
|
||
}
|
||
n := copy(p, sf.entry.data[sf.offset:])
|
||
sf.offset += n
|
||
return n, nil
|
||
}
|
||
|
||
// Write writes the given bytes at the end of the file (for a writable file).
|
||
// It may simulate corruption.
|
||
func (sf *simFile) Write(p []byte) (int, error) {
|
||
if sf.readOnly {
|
||
return 0, errors.New("cannot write to read-only file")
|
||
}
|
||
sf.mu.Lock()
|
||
defer sf.mu.Unlock()
|
||
|
||
if sf.closed {
|
||
return 0, errors.New("write to closed file")
|
||
}
|
||
|
||
// simulate latency
|
||
time.Sleep(sf.tol.Latency)
|
||
// simulate failure
|
||
if rand.Float64() < sf.tol.FailureChance {
|
||
return 0, errors.New("simulated write failure")
|
||
}
|
||
|
||
// prepare data to write
|
||
dataToWrite := make([]byte, len(p))
|
||
copy(dataToWrite, p)
|
||
|
||
// simulate corruption by flipping bits in the data (if triggered)
|
||
if rand.Float64() < sf.tol.CorruptionChance {
|
||
for i := range dataToWrite {
|
||
dataToWrite[i] = ^dataToWrite[i] // simple corruption: bitwise complement
|
||
}
|
||
}
|
||
|
||
// lock the underlying entry and append data
|
||
sf.entry.mu.Lock()
|
||
defer sf.entry.mu.Unlock()
|
||
sf.entry.data = append(sf.entry.data, dataToWrite...)
|
||
// update modification time
|
||
sf.entry.modTime = time.Now()
|
||
return len(p), nil
|
||
}
|
||
|
||
// Close marks the file as closed.
|
||
func (sf *simFile) Close() error {
|
||
sf.mu.Lock()
|
||
defer sf.mu.Unlock()
|
||
if sf.closed {
|
||
return errors.New("file already closed")
|
||
}
|
||
sf.closed = true
|
||
return nil
|
||
}
|
||
|
||
// Stat returns file information for the simulated file.
|
||
func (sf *simFile) Stat() (fs.FileInfo, error) {
|
||
sf.mu.Lock()
|
||
defer sf.mu.Unlock()
|
||
if sf.closed {
|
||
return nil, errors.New("stat on closed file")
|
||
}
|
||
return &simFileInfo{entry: sf.entry}, nil
|
||
}
|
||
|
||
// simFileInfo implements fs.FileInfo for a simEntry.
|
||
type simFileInfo struct {
|
||
entry *simEntry
|
||
}
|
||
|
||
var _ fs.FileInfo = (*simFileInfo)(nil)
|
||
|
||
func (fi *simFileInfo) Name() string { return fi.entry.name }
|
||
func (fi *simFileInfo) Size() int64 {
|
||
fi.entry.mu.Lock()
|
||
defer fi.entry.mu.Unlock()
|
||
return int64(len(fi.entry.data))
|
||
}
|
||
func (fi *simFileInfo) Mode() fs.FileMode { return fi.entry.perm }
|
||
func (fi *simFileInfo) ModTime() time.Time { return fi.entry.modTime }
|
||
func (fi *simFileInfo) IsDir() bool { return fi.entry.isDir }
|
||
func (fi *simFileInfo) Sys() interface{} { return nil }
|
||
|
||
// SimDisk is our in–memory simulation that implements the disk.Disk interface.
|
||
type SimDisk struct {
|
||
// tol holds the tolerance parameters to simulate latency, failures, corruption.
|
||
tol Tolerance
|
||
|
||
// entries is a map from file or directory name to its corresponding simEntry.
|
||
entries map[string]*simEntry
|
||
|
||
mu sync.Mutex
|
||
}
|
||
|
||
// NewSimDisk returns a new simulated disk with the given tolerance parameters.
|
||
func NewSimDisk(tol Tolerance) disk.Disk {
|
||
return &SimDisk{
|
||
tol: tol,
|
||
entries: make(map[string]*simEntry),
|
||
}
|
||
}
|
||
|
||
// simulateOp sleeps for the configured latency and then returns an error if a failure is simulated.
|
||
func (sd *SimDisk) simulateOp() error {
|
||
time.Sleep(sd.tol.Latency)
|
||
if rand.Float64() < sd.tol.FailureChance {
|
||
return errors.New("simulated disk operation failure")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Mkdir creates a directory in the simulated file system.
|
||
func (sd *SimDisk) Mkdir(name string, perm fs.FileMode) error {
|
||
if err := sd.simulateOp(); err != nil {
|
||
return err
|
||
}
|
||
|
||
sd.mu.Lock()
|
||
defer sd.mu.Unlock()
|
||
|
||
if _, exists := sd.entries[name]; exists {
|
||
return errors.New("directory already exists")
|
||
}
|
||
sd.entries[name] = &simEntry{
|
||
name: name,
|
||
isDir: true,
|
||
perm: perm,
|
||
modTime: time.Now(),
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Create creates (or truncates) a file in the simulated file system and returns a writable file.
|
||
func (sd *SimDisk) Create(name string) (disk.File, error) {
|
||
if err := sd.simulateOp(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
sd.mu.Lock()
|
||
// For simplicity, Create always truncates (or creates new)
|
||
se := &simEntry{
|
||
name: name,
|
||
isDir: false,
|
||
perm: 0644,
|
||
modTime: time.Now(),
|
||
data: []byte{},
|
||
}
|
||
sd.entries[name] = se
|
||
sd.mu.Unlock()
|
||
|
||
// Return a writable file handle
|
||
return &simFile{
|
||
entry: se,
|
||
offset: 0,
|
||
readOnly: false,
|
||
tol: sd.tol,
|
||
}, nil
|
||
}
|
||
|
||
// Open opens an existing file in the simulated file system.
|
||
// The returned file is "read-only" (write attempts will error),
|
||
func (sd *SimDisk) Open(name string) (disk.File, error) {
|
||
if err := sd.simulateOp(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
sd.mu.Lock()
|
||
se, exists := sd.entries[name]
|
||
sd.mu.Unlock()
|
||
if !exists {
|
||
return nil, errors.New("file does not exist")
|
||
}
|
||
if se.isDir {
|
||
return nil, errors.New("cannot open directory")
|
||
}
|
||
|
||
// Return a new file handle starting at offset 0; marked as readOnly.
|
||
return &simFile{
|
||
entry: se,
|
||
offset: 0,
|
||
readOnly: true,
|
||
tol: sd.tol,
|
||
}, nil
|
||
}
|
||
|
||
// Stat returns information about the file or directory.
|
||
func (sd *SimDisk) Stat(name string) (fs.FileInfo, error) {
|
||
if err := sd.simulateOp(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
sd.mu.Lock()
|
||
se, exists := sd.entries[name]
|
||
sd.mu.Unlock()
|
||
if !exists {
|
||
return nil, errors.New("no such file or directory")
|
||
}
|
||
|
||
return &simFileInfo{entry: se}, nil
|
||
}
|
||
|
||
// Remove deletes a file or directory from the simulated file system.
|
||
func (sd *SimDisk) Remove(name string) error {
|
||
if err := sd.simulateOp(); err != nil {
|
||
return err
|
||
}
|
||
|
||
sd.mu.Lock()
|
||
defer sd.mu.Unlock()
|
||
if _, exists := sd.entries[name]; !exists {
|
||
return errors.New("file or directory does not exist")
|
||
}
|
||
delete(sd.entries, name)
|
||
return nil
|
||
}
|