// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. //go:build windows // Windows file locking for DBF shared mode. // Uses kernel32 LockFileEx / UnlockFileEx for byte-range exclusive locks, // matching Harbour's hb_fsLockLarge on Windows. // // Lock region layout (identical to the POSIX implementation): // - FLOCK: [0, HeaderLen+1) file-wide exclusive // - RLOCK: [RecordOffset(n), RecordLen) per-record exclusive // // Non-blocking: LOCKFILE_FAIL_IMMEDIATELY returns immediately if another // process holds a conflicting lock — matching Clipper FLOCK() → .F. package dbf import ( "fmt" "syscall" "time" "unsafe" ) const flockOffset = int64(0) // appendLockOffset / Len — see locks_posix.go for the rationale. // Windows LockFileEx accepts arbitrary byte ranges past EOF. const appendLockOffset = int64(0x7FFFFFFE) const appendLockLen = int64(1) func flockLen(a *DBFArea) int64 { return int64(a.header.HeaderLen) + 1 } // lockAppendIntent — see locks_posix.go for the rationale. func (a *DBFArea) lockAppendIntent() error { if a.dataFile == nil { return fmt.Errorf("file not open") } h := a.winHandle() for i := 0; i < 100; i++ { ok, err := tryLock(h, appendLockOffset, appendLockLen) if err != nil { return err } if ok { return nil } time.Sleep(time.Millisecond) } return fmt.Errorf("APPEND: could not acquire append-intent lock") } func (a *DBFArea) unlockAppendIntent() error { if a.dataFile == nil { return nil } return unlockRange(a.winHandle(), appendLockOffset, appendLockLen) } var ( modkernel32 = syscall.NewLazyDLL("kernel32.dll") procLockFileEx = modkernel32.NewProc("LockFileEx") procUnlockFileEx = modkernel32.NewProc("UnlockFileEx") ) // Windows constants const ( _LOCKFILE_EXCLUSIVE_LOCK = 0x00000002 _LOCKFILE_FAIL_IMMEDIATELY = 0x00000001 _ERROR_LOCK_VIOLATION = 33 ) func splitOffset(off int64) (lo, hi uint32) { return uint32(off & 0xFFFFFFFF), uint32(off >> 32) } // tryLock acquires an exclusive byte-range lock via LockFileEx. // Non-blocking: returns (false, nil) on contention. func tryLock(handle syscall.Handle, start, length int64) (bool, error) { oLo, oHi := splitOffset(start) lLo, lHi := splitOffset(length) ol := &syscall.Overlapped{ Offset: oLo, OffsetHigh: oHi, } flags := uint32(_LOCKFILE_EXCLUSIVE_LOCK | _LOCKFILE_FAIL_IMMEDIATELY) r1, _, err := procLockFileEx.Call( uintptr(handle), uintptr(flags), 0, // reserved uintptr(lLo), uintptr(lHi), uintptr(unsafe.Pointer(ol)), ) if r1 == 0 { // Call failed if errno, ok := err.(syscall.Errno); ok && errno == _ERROR_LOCK_VIOLATION { return false, nil // another process holds it } return false, err } return true, nil } // unlockRange releases a previously acquired byte-range lock via UnlockFileEx. func unlockRange(handle syscall.Handle, start, length int64) error { oLo, oHi := splitOffset(start) lLo, lHi := splitOffset(length) ol := &syscall.Overlapped{ Offset: oLo, OffsetHigh: oHi, } r1, _, err := procUnlockFileEx.Call( uintptr(handle), 0, // reserved uintptr(lLo), uintptr(lHi), uintptr(unsafe.Pointer(ol)), ) if r1 == 0 { return err } return nil } // winHandle returns the Windows HANDLE for the data file. func (a *DBFArea) winHandle() syscall.Handle { return syscall.Handle(a.dataFile.Fd()) } // LockFile tries to acquire an exclusive FLOCK on the DBF file. func (a *DBFArea) LockFile() (bool, error) { if a.dataFile == nil { return false, fmt.Errorf("file not open") } if a.fileLocked { return true, nil } ok, err := tryLock(a.winHandle(), flockOffset, flockLen(a)) if err != nil { return false, err } if ok { a.fileLocked = true } return ok, nil } // UnlockFile releases the file-wide lock held by this workarea. func (a *DBFArea) UnlockFile() error { if a.dataFile == nil || !a.fileLocked { return nil } if err := unlockRange(a.winHandle(), flockOffset, flockLen(a)); err != nil { return err } a.fileLocked = false return nil } // LockRecord tries to acquire an exclusive lock on a single record. func (a *DBFArea) LockRecord(recNo uint32) (bool, error) { if a.dataFile == nil { return false, fmt.Errorf("file not open") } if recNo == 0 { recNo = a.recNo } if recNo == 0 { return false, nil } if a.lockedRecs != nil && a.lockedRecs[recNo] { return true, nil } start := a.header.RecordOffset(recNo) length := int64(a.header.RecordLen) ok, err := tryLock(a.winHandle(), start, length) if err != nil { return false, err } if ok { if a.lockedRecs == nil { a.lockedRecs = make(map[uint32]bool, 16) } a.lockedRecs[recNo] = true } return ok, nil } // UnlockRecord releases a single record lock. recNo == 0 unlocks ALL. func (a *DBFArea) UnlockRecord(recNo uint32) error { if a.dataFile == nil { return nil } if recNo == 0 { if a.lockedRecs == nil { return nil } length := int64(a.header.RecordLen) for r := range a.lockedRecs { start := a.header.RecordOffset(r) _ = unlockRange(a.winHandle(), start, length) } a.lockedRecs = nil return nil } if a.lockedRecs == nil || !a.lockedRecs[recNo] { return nil } start := a.header.RecordOffset(recNo) length := int64(a.header.RecordLen) if err := unlockRange(a.winHandle(), start, length); err != nil { return err } delete(a.lockedRecs, recNo) return nil } // releaseAllLocks is called on Close to release any held locks. func (a *DBFArea) releaseAllLocks() { _ = a.UnlockFile() _ = a.UnlockRecord(0) }