package sim import ( "errors" "io" "io/fs" "math/rand" "sync" "time" "numenor-labs.us/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 } // 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() 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 }