2025-03-21 22:51:04 -04:00
|
|
|
|
package sim
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
|
|
|
|
"io"
|
|
|
|
|
"io/fs"
|
|
|
|
|
"math/rand"
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
|
2025-04-01 10:57:37 -04:00
|
|
|
|
"git.numenor-labs.us/dsfx/internal/lib/disk"
|
2025-03-21 22:51:04 -04:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 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()
|
|
|
|
|
|
2025-03-22 13:06:51 -04:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MkdirAll creates a directory and any necessary parent directories in the simulated file system.
|
|
|
|
|
// It behaves like os.MkdirAll.
|
|
|
|
|
// If the directory already exists, it does nothing.
|
|
|
|
|
func (sd *SimDisk) MkdirAll(name string, perm fs.FileMode) error {
|
|
|
|
|
if err := sd.simulateOp(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sd.mu.Lock()
|
|
|
|
|
defer sd.mu.Unlock()
|
|
|
|
|
|
2025-03-21 22:51:04 -04:00
|
|
|
|
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
|
|
|
|
|
}
|