From a61b7c7a5764860ab287e3bb9bb2bbe7b59f092c Mon Sep 17 00:00:00 2001 From: Hansjoerg Petriffer Date: Fri, 11 Sep 2026 16:05:12 +0200 Subject: [PATCH 1/3] [Runtime] Fix XsWin32FileStream position handling on .Net 6 and later XsWin32FileStream does all its IO with the Win32 API on the OS file pointer but never overrode Position, so Position came from the base class. Until .Net 5 that worked, because FileStream resynced its cached position from the OS whenever the SafeFileHandle had been exposed. The .Net 6 FileStream rewrite removed that resync, so Position stayed at 0 forever while the real file pointer moved. FTell(), FEof(), FReadLine() and the SafeReadAt() / SafeWriteAt() / SafeSetPos() calls that the RDDs use for every record all read and wrote at the wrong offset, silently and without an exception, for every file opened with shared access. - Track the position in a field and pass it explicitly in an OVERLAPPED on every ReadFile() / WriteFile(), the way the .Net FileStream does it. The OS file pointer is no longer relevant for correctness and Seek() is arithmetic. - Translate ERROR_HANDLE_EOF back into "0 bytes read": with an explicit offset Windows reports reading at or past the end of the file as a failure, where a read from the file pointer simply returned 0 bytes. - Override ReadByte() and the Span overloads. FileStream overrides those on .Net Core and they bypassed the Read() / Write() overrides. - Override SafeFileHandle: the base class moves the OS file pointer when the handle is exposed. SetLength() is the only operation that still moves it. - Read() with offset != 0 copied count bytes instead of the number of bytes really read, overwriting the caller buffer past the end of the file. - Read() returned -1 on failure, which violates the Stream.Read contract. - Construct the base stream unbuffered. Its buffer was filled from the base class position and was never used by the overrides anyway. Co-Authored-By: Claude Opus 5 --- .../XSharp.Core/Types/SharedFileStream.prg | 176 +++++++++++++++--- 1 file changed, 154 insertions(+), 22 deletions(-) diff --git a/src/Runtime/XSharp.Core/Types/SharedFileStream.prg b/src/Runtime/XSharp.Core/Types/SharedFileStream.prg index ec1e66c261..9eb0992970 100644 --- a/src/Runtime/XSharp.Core/Types/SharedFileStream.prg +++ b/src/Runtime/XSharp.Core/Types/SharedFileStream.prg @@ -8,36 +8,108 @@ USING System.IO USING System.Runtime USING System.Runtime.InteropServices USING System.Collections.Generic +USING System.Threading +USING Microsoft.Win32.SafeHandles BEGIN NAMESPACE XSharp.IO /// CLASS XsWin32FileStream INHERIT XsFileStream + PRIVATE CONST ERROR_HANDLE_EOF := 38 AS DWORD + PRIVATE hFile AS IntPtr PRIVATE smallBuff AS BYTE[] - INTERNAL CONSTRUCTOR(path AS STRING, mode AS FileMode, faccess AS FileAccess, share AS FileShare, bufferSize AS LONG, options AS FileOptions) - SUPER(path, mode, faccess, share, bufferSize, options) - hFile := SELF:SafeFileHandle:DangerousGetHandle() + // The current position of this stream. We do not ask the OS for it, and we do not let the OS file + // pointer decide where we read and write: every ReadFile() / WriteFile() below passes this offset + // explicitly in an OVERLAPPED, the way the .Net FileStream does it itself. That makes this field the + // single source of truth, so nothing that touches the file handle behind our back can desync us, and + // it saves a syscall per access: no SetFilePointerEx() before the read, and none for FTell(), + // FEof() or the SafeReadAt() / SafeSetPos() pattern that the RDDs use on every record. + PRIVATE nPos AS INT64 + INTERNAL CONSTRUCTOR(path AS STRING, mode AS FileMode, faccess AS FileAccess, share AS FileShare, bufferSize AS LONG, options AS FileOptions) + // All IO in this class is done with the Win32 API on the OS file pointer. The base class must + // therefore not buffer: its buffer is filled from the position that the base class maintains + // itself, which is not the position that we read from and write to. + SUPER(path, mode, faccess, share, 1, options) + // SUPER:SafeFileHandle and not SELF:SafeFileHandle, because our override below needs hFile. + hFile := SUPER:SafeFileHandle:DangerousGetHandle() smallBuff := BYTE[]{1} + // Read the start position from the OS once. It is not always 0: FileMode.Append opens the file + // positioned at the end. + IF ! SetFilePointerEx(hFile, 0, OUT nPos, SeekOrigin.Current) + nPos := 0 + ENDIF RETURN + + /// + PUBLIC OVERRIDE PROPERTY SafeFileHandle AS SafeFileHandle + GET + // When the handle is exposed, the base class moves the OS file pointer to the position that + // IT maintains, and that is not our position. Move it back, otherwise our position and the + // OS file pointer drift apart and every following read uses the wrong offset. + VAR oHandle := SUPER:SafeFileHandle + IF SELF:hFile != IntPtr.Zero + LOCAL nNew AS INT64 + IF SetFilePointerEx(SELF:hFile, SELF:nPos, OUT nNew, SeekOrigin.Begin) + SELF:nPos := nNew + ENDIF + ENDIF + RETURN oHandle + END GET + END PROPERTY /// PUBLIC OVERRIDE METHOD Seek(offset AS INT64, origin AS SeekOrigin) AS INT64 + // Pure arithmetic: reads and writes carry their own offset, so seeking never needs a syscall. + // Only SeekOrigin.End has to ask for the file size. Seeking past the end of the file is legal, + // exactly like SetFilePointerEx() allowed. LOCAL result AS INT64 - LOCAL lOk AS LOGIC - lOk := SetFilePointerEx(hFile, offset, OUT result, origin) - IF lOk + SWITCH origin + CASE SeekOrigin.Begin + result := offset + CASE SeekOrigin.Current + result := SELF:nPos + offset + OTHERWISE + result := SELF:Length + offset + END SWITCH + IF result >= 0 + SELF:nPos := result RETURN result ENDIF - VAR nErr := (DWORD) Marshal.GetLastWin32Error() - if (nErr == 0) - nErr := 30 // Dos error Read Fault - ENDIF - FError(nErr) + FError(131) // ERROR_NEGATIVE_SEEK, what SetFilePointerEx() used to report here THROW IOException{i"Error moving file pointer from {origin} to {offset}"} - + + /// + PUBLIC OVERRIDE PROPERTY Position AS INT64 + // Not the position that the base class caches: that one is only updated by the base class' own + // IO and therefore stays at 0 forever. Until .Net 5 FileStream resynced it from the OS whenever + // the SafeFileHandle had been exposed. The .Net 6 FileStream rewrite removed that, which made + // every FTell(), FEof() and SafeReadAt() / SafeWriteAt() on a shared file use the wrong offset. + GET + RETURN SELF:nPos + END GET + SET + // The RDDs constantly save the position, read somewhere else and restore it afterwards. + // When the position does not really change there is nothing to do. + IF value != SELF:nPos + SELF:Seek(value, SeekOrigin.Begin) + ENDIF + END SET + END PROPERTY + /// PUBLIC OVERRIDE METHOD SetLength(length AS INT64 ) AS VOID // warning: does not restore original file pos - SELF:Seek(length, SeekOrigin.Begin) + // SetEndOfFile() truncates at the OS file pointer, so this is the one operation that still has + // to move it. Our reads and writes do not care where it ends up. + LOCAL nNew AS INT64 + IF ! SetFilePointerEx(SELF:hFile, length, OUT nNew, SeekOrigin.Begin) + VAR nErr := (DWORD) Marshal.GetLastWin32Error() + if (nErr == 0) + nErr := 30 // Dos error Read Fault + ENDIF + FError(nErr) + THROW IOException{i"Error moving file pointer from {SeekOrigin.Begin} to {length}"} + ENDIF + SELF:nPos := nNew SetEndOfFile(hFile) RETURN /// @@ -58,38 +130,77 @@ BEGIN NAMESPACE XSharp.IO PUBLIC OVERRIDE METHOD Read(bytes AS BYTE[] , offset AS INT, count AS INT) AS INT LOCAL ret := FALSE AS LOGIC LOCAL bytesRead := 0 AS INT + LOCAL ov := SELF:__Overlapped() AS NativeOverlapped IF offset == 0 - ret := ReadFile(SELF:hFile, bytes, count, OUT bytesRead, IntPtr.Zero) + ret := ReadFile(SELF:hFile, bytes, count, OUT bytesRead, REF ov) ELSE LOCAL data AS BYTE[] data := BYTE[]{count} - ret := ReadFile(SELF:hFile, data, count, OUT bytesRead, IntPtr.Zero) - System.Array.Copy(data, 0, bytes, offset, count) + ret := ReadFile(SELF:hFile, data, count, OUT bytesRead, REF ov) + IF ret .and. bytesRead > 0 + // Copy the bytes that were really read. Copying count bytes would overwrite bytes in + // the target buffer beyond the end of the file with the zeroes from our temp buffer, + // and would throw when bytes is only large enough for offset + bytesRead. + System.Array.Copy(data, 0, bytes, offset, bytesRead) + ENDIF ENDIF IF !ret - RETURN -1 + VAR nErr := (DWORD) Marshal.GetLastWin32Error() + IF nErr == ERROR_HANDLE_EOF + // Reading at or past the end of the file. With an explicit offset Windows reports that + // as a failure, where a read from the file pointer simply returned 0 bytes. It is not + // an error: the caller decides that 0 bytes means EOF, and FError() stays untouched. + RETURN 0 + ENDIF + if (nErr == 0) + nErr := 30 // Dos error Read Fault + ENDIF + FError(nErr) + // Stream:Read() must return a value between 0 and count. Returning -1 breaks every + // generic Stream consumer. The failure is reported through FError() instead. + RETURN 0 ENDIF + SELF:nPos += bytesRead RETURN bytesRead + + // The offset that the next read or write has to use, in the form the Win32 API wants it. + PRIVATE METHOD __Overlapped() AS NativeOverlapped + LOCAL ov AS NativeOverlapped + ov := NativeOverlapped{} + ov:OffsetLow := (INT) SELF:nPos + ov:OffsetHigh := (INT) (SELF:nPos >> 32) + RETURN ov + + /// + PUBLIC OVERRIDE METHOD ReadByte() AS INT + // FileStream overrides ReadByte() on .Net Core and reads at its own position, bypassing our + // Read() override, so it has to be overridden here as well. + IF SELF:Read(SELF:smallBuff, 0, 1) != 1 + RETURN -1 + ENDIF + RETURN SELF:smallBuff[0] /// PUBLIC OVERRIDE METHOD Write(bytes AS BYTE[] , offset AS INT , count AS INT) AS VOID LOCAL ret := FALSE AS LOGIC LOCAL bytesWritten := 0 AS INT + LOCAL ov := SELF:__Overlapped() AS NativeOverlapped IF offset == 0 - ret := WriteFile(SELF:hFile, bytes, count, OUT bytesWritten, 0) + ret := WriteFile(SELF:hFile, bytes, count, OUT bytesWritten, REF ov) ELSE LOCAL aCopy AS BYTE[] aCopy := BYTE[]{count} System.Array.Copy(bytes,offset, aCopy,0, count) - ret := WriteFile(SELF:hFile, aCopy, count, OUT bytesWritten, 0) + ret := WriteFile(SELF:hFile, aCopy, count, OUT bytesWritten, REF ov) ENDIF IF !ret VAR nErr := (DWORD) Marshal.GetLastWin32Error() if (nErr == 0) nErr := 29 // Dos error Write Fault ENDIF - FError(nErr) + FError(nErr) THROW IOException{i"Write: File write failed offset {offset} count {count}"} ENDIF + SELF:nPos += bytesWritten IF bytesWritten != count VAR nErr := (DWORD) Marshal.GetLastWin32Error() if (nErr == 0) @@ -99,6 +210,25 @@ BEGIN NAMESPACE XSharp.IO THROW IOException{i"Write: Not all bytes written to file offset {offset} count {count} written {bytesWritten}"} ENDIF RETURN +#ifdef NET5_0_OR_GREATER + /// + PUBLIC OVERRIDE METHOD Read(buffer AS System.Span) AS INT + // FileStream overrides the Span overloads on .Net Core. Without these overrides they would + // read and write at the position that the base class maintains instead of the OS file pointer. + LOCAL bytes := BYTE[]{buffer:Length} AS BYTE[] + LOCAL bytesRead := SELF:Read(bytes, 0, buffer:Length) AS INT + IF bytesRead > 0 + System.MemoryExtensions.AsSpan(bytes, 0, bytesRead):CopyTo(buffer) + ENDIF + RETURN bytesRead + + /// + PUBLIC OVERRIDE METHOD Write(buffer AS System.ReadOnlySpan) AS VOID + LOCAL bytes := buffer:ToArray() AS BYTE[] + SELF:Write(bytes, 0, bytes:Length) + RETURN +#endif + /// PUBLIC OVERRIDE METHOD WriteByte(b AS BYTE ) AS VOID SELF:smallBuff[0] := b @@ -155,11 +285,13 @@ BEGIN NAMESPACE XSharp.IO #region External methods /// + // The handle is opened synchronously (no FILE_FLAG_OVERLAPPED), so these calls do not return before + // the transfer is done. The OVERLAPPED is only there to carry the offset to read or write at. [DllImport("kernel32.dll", SetLastError := TRUE, EntryPoint := "ReadFile")]; - PRIVATE STATIC EXTERN METHOD ReadFile(hFile AS IntPtr, bytes AS BYTE[], numbytes AS INT, numbytesread OUT INT , mustbezero AS IntPtr) AS LOGIC + PRIVATE STATIC EXTERN METHOD ReadFile(hFile AS IntPtr, bytes AS BYTE[], numbytes AS INT, numbytesread OUT INT , lpOverlapped REF NativeOverlapped) AS LOGIC /// [DllImport("kernel32.dll", SetLastError := TRUE, EntryPoint := "WriteFile")]; - PRIVATE STATIC EXTERN METHOD WriteFile(hFile AS IntPtr, bytes AS BYTE[], numbytes AS INT, numbyteswritten OUT INT , lpOverlapped AS INT) AS LOGIC + PRIVATE STATIC EXTERN METHOD WriteFile(hFile AS IntPtr, bytes AS BYTE[], numbytes AS INT, numbyteswritten OUT INT , lpOverlapped REF NativeOverlapped) AS LOGIC /// [DllImport("kernel32.dll", SetLastError := TRUE, EntryPoint := "SetFilePointerEx")]; PRIVATE STATIC EXTERN METHOD SetFilePointerEx(handle AS IntPtr, distance AS INT64 , newAddress OUT INT64, origin AS SeekOrigin ) AS LOGIC From cb57f94110e43b56e89f0e472c2a4322969e8aff Mon Sep 17 00:00:00 2001 From: Hansjoerg Petriffer Date: Fri, 11 Sep 2026 16:11:13 +0200 Subject: [PATCH 2/3] [Runtime] Drop the Win32 API from the shared file stream The hand written Win32 layer in XsWin32FileStream existed because the FileStream of the time cached the file position and the file length, which is wrong for a file that other processes may change. That is no longer true: the .Net FileStream reads and writes at an explicit offset and does not cache the length of a file that others may write to, so it reports the truth for shared access by itself, on every platform. XsWin32FileStream is now a plain XsFileStream that is unbuffered and commits on Flush(). That removes 8 DllImports and every hand rolled Seek / Read / Write / Lock / Length implementation, and with them the whole class of bugs where our position and the position of the base class disagree. Carried over, because these lived only in the Win32 class: - Lock() and Unlock() set NetErr(TRUE) and FError(33) when a lock fails. SetErrorState() only sets NetErr for a sharing violation (32), so a lock violation had to be handled explicitly. Moved to XsFileStream, so the non-Windows path gets it too. - A shared stream commits to disk on Flush(). - A shared stream is unbuffered. Behaviour changes, both of them towards what the rest of the runtime already did: - SetLength() no longer moves the position to the new length unconditionally. It now follows the Stream contract and only clamps the position when it is past the new end of the file, which is what XsFileStream and the non-Windows path always did. The old behaviour was flagged in the source as a wart. - Shared access is no longer buffered on non-Windows platforms. That branch passed a buffer size of 0xFFFF, so a second process could not see writes and reads could return stale data. The class name is kept because it is public and is handed to user code through DBI_FILESTREAM. CreateWin32FileStream() is kept as an obsolete forwarder to the new CreateSharedFileStream(). Co-Authored-By: Claude Opus 5 --- src/Runtime/XSharp.Core/Types/FileStream.prg | 46 ++- .../XSharp.Core/Types/SharedFileStream.prg | 319 ++---------------- 2 files changed, 54 insertions(+), 311 deletions(-) diff --git a/src/Runtime/XSharp.Core/Types/FileStream.prg b/src/Runtime/XSharp.Core/Types/FileStream.prg index 2da1a42b08..e37cad37e9 100644 --- a/src/Runtime/XSharp.Core/Types/FileStream.prg +++ b/src/Runtime/XSharp.Core/Types/FileStream.prg @@ -66,15 +66,33 @@ BEGIN NAMESPACE XSharp.IO RETURN /// PUBLIC OVERRIDE METHOD Lock(position AS INT64, length AS INT64) AS VOID - STARTIO - SUPER:Lock(position, length) - ENDIO + TRY + XSharp.IO.File.ClearErrorState() + SUPER:Lock(position, length) + CATCH e AS Exception + SELF:__SetLockError(e) + THROW + END TRY RETURN /// PUBLIC OVERRIDE METHOD Unlock(position AS INT64, length AS INT64) AS VOID - STARTIO - SUPER:Unlock(position, length) - ENDIO + TRY + XSharp.IO.File.ClearErrorState() + SUPER:Unlock(position, length) + CATCH e AS Exception + SELF:__SetLockError(e) + THROW + END TRY + RETURN + + // A failed lock is what NetErr() reports in the xBase world. SetErrorState() only sets NetErr for + // a sharing violation (32), but a lock violation is 33, so it has to be set here. + PRIVATE METHOD __SetLockError(e AS Exception) AS VOID + XSharp.IO.File.SetErrorState(e) + IF RuntimeState.FileError == 0 + FError(33) // DOS lock violation + ENDIF + NetErr(TRUE) RETURN /// PUBLIC OVERRIDE METHOD Flush(lCommit AS LOGIC) AS VOID @@ -91,11 +109,10 @@ BEGIN NAMESPACE XSharp.IO /// STATIC METHOD CreateFileStream (path AS STRING, mode AS FileMode, faccess AS FileAccess, share AS FileShare, bufferSize AS LONG, options AS FileOptions) AS FileStream IF share != FileShare.None - IF RuntimeState.RunningOnWindows - RETURN CreateWin32FileStream(path, mode, faccess, share, bufferSize, options) - ELSE - RETURN XsFileStream{path, mode, faccess, share, 0xFFFF, options} - ENDIF + // Shared access, so the same file may change under us at any moment. There is no platform + // check here anymore: this used to be the branch that decided between a hand written Win32 + // stream on Windows and a buffered stream everywhere else. + RETURN CreateSharedFileStream(path, mode, faccess, share, bufferSize, options) ELSE IF UseBufferedFileStream RETURN XsBufferedFileStream{path, mode, faccess, share, bufferSize, options} @@ -104,8 +121,13 @@ BEGIN NAMESPACE XSharp.IO ENDIF ENDIF - INTERNAL STATIC METHOD CreateWin32FileStream(path AS STRING, mode AS FileMode, faccess AS FileAccess, share AS FileShare, bufferSize AS LONG, options AS FileOptions) AS FileStream + INTERNAL STATIC METHOD CreateSharedFileStream(path AS STRING, mode AS FileMode, faccess AS FileAccess, share AS FileShare, bufferSize AS LONG, options AS FileOptions) AS FileStream RETURN XsWin32FileStream{path, mode, faccess, share, bufferSize, options} + + /// + [Obsolete("Use CreateSharedFileStream(). The shared file stream no longer uses the Win32 API.")]; + INTERNAL STATIC METHOD CreateWin32FileStream(path AS STRING, mode AS FileMode, faccess AS FileAccess, share AS FileShare, bufferSize AS LONG, options AS FileOptions) AS FileStream + RETURN CreateSharedFileStream(path, mode, faccess, share, bufferSize, options) #endregion END CLASS diff --git a/src/Runtime/XSharp.Core/Types/SharedFileStream.prg b/src/Runtime/XSharp.Core/Types/SharedFileStream.prg index 9eb0992970..0f4c9733b6 100644 --- a/src/Runtime/XSharp.Core/Types/SharedFileStream.prg +++ b/src/Runtime/XSharp.Core/Types/SharedFileStream.prg @@ -1,6 +1,6 @@ // -// Copyright (c) XSharp B.V. All Rights Reserved. -// Licensed under the Apache License, Version 2.0. +// Copyright (c) XSharp B.V. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. // See License.txt in the project root for license information. // USING System @@ -8,315 +8,36 @@ USING System.IO USING System.Runtime USING System.Runtime.InteropServices USING System.Collections.Generic -USING System.Threading -USING Microsoft.Win32.SafeHandles BEGIN NAMESPACE XSharp.IO /// + /// + /// This class used to do all of its IO with the Win32 API (ReadFile, WriteFile, SetFilePointerEx, + /// LockFile) on the raw file handle of the base class, because the FileStream of that time cached the + /// file position and the file length, which is wrong for a file that other processes may change. + ///
+ /// It does not do that anymore. The .Net FileStream reads and writes at an explicit offset and does not + /// cache the length of a file that others may write to, so it reports the truth for shared access by + /// itself, on every platform. What is left here is the one thing that is special about a shared stream: + /// it must not buffer, and Flush() must commit to disk. + ///
+ /// The name is kept because this class is public and is handed to user code through DBI_FILESTREAM. + ///
CLASS XsWin32FileStream INHERIT XsFileStream - PRIVATE CONST ERROR_HANDLE_EOF := 38 AS DWORD - - PRIVATE hFile AS IntPtr - PRIVATE smallBuff AS BYTE[] - // The current position of this stream. We do not ask the OS for it, and we do not let the OS file - // pointer decide where we read and write: every ReadFile() / WriteFile() below passes this offset - // explicitly in an OVERLAPPED, the way the .Net FileStream does it itself. That makes this field the - // single source of truth, so nothing that touches the file handle behind our back can desync us, and - // it saves a syscall per access: no SetFilePointerEx() before the read, and none for FTell(), - // FEof() or the SafeReadAt() / SafeSetPos() pattern that the RDDs use on every record. - PRIVATE nPos AS INT64 INTERNAL CONSTRUCTOR(path AS STRING, mode AS FileMode, faccess AS FileAccess, share AS FileShare, bufferSize AS LONG, options AS FileOptions) - // All IO in this class is done with the Win32 API on the OS file pointer. The base class must - // therefore not buffer: its buffer is filled from the position that the base class maintains - // itself, which is not the position that we read from and write to. + // bufferSize 1 means unbuffered. A buffer would hand out bytes that another process has already + // changed, and would hold back bytes that another process is waiting for, so the bufferSize of + // the caller is deliberately ignored: for shared access there is no good buffer size but none. SUPER(path, mode, faccess, share, 1, options) - // SUPER:SafeFileHandle and not SELF:SafeFileHandle, because our override below needs hFile. - hFile := SUPER:SafeFileHandle:DangerousGetHandle() - smallBuff := BYTE[]{1} - // Read the start position from the OS once. It is not always 0: FileMode.Append opens the file - // positioned at the end. - IF ! SetFilePointerEx(hFile, 0, OUT nPos, SeekOrigin.Current) - nPos := 0 - ENDIF - RETURN - - /// - PUBLIC OVERRIDE PROPERTY SafeFileHandle AS SafeFileHandle - GET - // When the handle is exposed, the base class moves the OS file pointer to the position that - // IT maintains, and that is not our position. Move it back, otherwise our position and the - // OS file pointer drift apart and every following read uses the wrong offset. - VAR oHandle := SUPER:SafeFileHandle - IF SELF:hFile != IntPtr.Zero - LOCAL nNew AS INT64 - IF SetFilePointerEx(SELF:hFile, SELF:nPos, OUT nNew, SeekOrigin.Begin) - SELF:nPos := nNew - ENDIF - ENDIF - RETURN oHandle - END GET - END PROPERTY - /// - PUBLIC OVERRIDE METHOD Seek(offset AS INT64, origin AS SeekOrigin) AS INT64 - // Pure arithmetic: reads and writes carry their own offset, so seeking never needs a syscall. - // Only SeekOrigin.End has to ask for the file size. Seeking past the end of the file is legal, - // exactly like SetFilePointerEx() allowed. - LOCAL result AS INT64 - SWITCH origin - CASE SeekOrigin.Begin - result := offset - CASE SeekOrigin.Current - result := SELF:nPos + offset - OTHERWISE - result := SELF:Length + offset - END SWITCH - IF result >= 0 - SELF:nPos := result - RETURN result - ENDIF - FError(131) // ERROR_NEGATIVE_SEEK, what SetFilePointerEx() used to report here - THROW IOException{i"Error moving file pointer from {origin} to {offset}"} - - /// - PUBLIC OVERRIDE PROPERTY Position AS INT64 - // Not the position that the base class caches: that one is only updated by the base class' own - // IO and therefore stays at 0 forever. Until .Net 5 FileStream resynced it from the OS whenever - // the SafeFileHandle had been exposed. The .Net 6 FileStream rewrite removed that, which made - // every FTell(), FEof() and SafeReadAt() / SafeWriteAt() on a shared file use the wrong offset. - GET - RETURN SELF:nPos - END GET - SET - // The RDDs constantly save the position, read somewhere else and restore it afterwards. - // When the position does not really change there is nothing to do. - IF value != SELF:nPos - SELF:Seek(value, SeekOrigin.Begin) - ENDIF - END SET - END PROPERTY - - /// - PUBLIC OVERRIDE METHOD SetLength(length AS INT64 ) AS VOID - // warning: does not restore original file pos - // SetEndOfFile() truncates at the OS file pointer, so this is the one operation that still has - // to move it. Our reads and writes do not care where it ends up. - LOCAL nNew AS INT64 - IF ! SetFilePointerEx(SELF:hFile, length, OUT nNew, SeekOrigin.Begin) - VAR nErr := (DWORD) Marshal.GetLastWin32Error() - if (nErr == 0) - nErr := 30 // Dos error Read Fault - ENDIF - FError(nErr) - THROW IOException{i"Error moving file pointer from {SeekOrigin.Begin} to {length}"} - ENDIF - SELF:nPos := nNew - SetEndOfFile(hFile) RETURN - /// - PUBLIC OVERRIDE PROPERTY Length AS INT64 - GET - IF GetFileSizeEx(SELF:hFile, OUT VAR size) - RETURN size - ENDIF - VAR nErr := (DWORD) Marshal.GetLastWin32Error() - if (nErr == 0) - nErr := 30 // Dos error Read Fault - ENDIF - FError(nErr) - THROW IOException{"Could not retrieve file length"} - END GET - END PROPERTY - /// - PUBLIC OVERRIDE METHOD Read(bytes AS BYTE[] , offset AS INT, count AS INT) AS INT - LOCAL ret := FALSE AS LOGIC - LOCAL bytesRead := 0 AS INT - LOCAL ov := SELF:__Overlapped() AS NativeOverlapped - IF offset == 0 - ret := ReadFile(SELF:hFile, bytes, count, OUT bytesRead, REF ov) - ELSE - LOCAL data AS BYTE[] - data := BYTE[]{count} - ret := ReadFile(SELF:hFile, data, count, OUT bytesRead, REF ov) - IF ret .and. bytesRead > 0 - // Copy the bytes that were really read. Copying count bytes would overwrite bytes in - // the target buffer beyond the end of the file with the zeroes from our temp buffer, - // and would throw when bytes is only large enough for offset + bytesRead. - System.Array.Copy(data, 0, bytes, offset, bytesRead) - ENDIF - ENDIF - IF !ret - VAR nErr := (DWORD) Marshal.GetLastWin32Error() - IF nErr == ERROR_HANDLE_EOF - // Reading at or past the end of the file. With an explicit offset Windows reports that - // as a failure, where a read from the file pointer simply returned 0 bytes. It is not - // an error: the caller decides that 0 bytes means EOF, and FError() stays untouched. - RETURN 0 - ENDIF - if (nErr == 0) - nErr := 30 // Dos error Read Fault - ENDIF - FError(nErr) - // Stream:Read() must return a value between 0 and count. Returning -1 breaks every - // generic Stream consumer. The failure is reported through FError() instead. - RETURN 0 - ENDIF - SELF:nPos += bytesRead - RETURN bytesRead - - // The offset that the next read or write has to use, in the form the Win32 API wants it. - PRIVATE METHOD __Overlapped() AS NativeOverlapped - LOCAL ov AS NativeOverlapped - ov := NativeOverlapped{} - ov:OffsetLow := (INT) SELF:nPos - ov:OffsetHigh := (INT) (SELF:nPos >> 32) - RETURN ov /// - PUBLIC OVERRIDE METHOD ReadByte() AS INT - // FileStream overrides ReadByte() on .Net Core and reads at its own position, bypassing our - // Read() override, so it has to be overridden here as well. - IF SELF:Read(SELF:smallBuff, 0, 1) != 1 - RETURN -1 - ENDIF - RETURN SELF:smallBuff[0] - /// - PUBLIC OVERRIDE METHOD Write(bytes AS BYTE[] , offset AS INT , count AS INT) AS VOID - LOCAL ret := FALSE AS LOGIC - LOCAL bytesWritten := 0 AS INT - LOCAL ov := SELF:__Overlapped() AS NativeOverlapped - IF offset == 0 - ret := WriteFile(SELF:hFile, bytes, count, OUT bytesWritten, REF ov) - ELSE - LOCAL aCopy AS BYTE[] - aCopy := BYTE[]{count} - System.Array.Copy(bytes,offset, aCopy,0, count) - ret := WriteFile(SELF:hFile, aCopy, count, OUT bytesWritten, REF ov) - ENDIF - IF !ret - VAR nErr := (DWORD) Marshal.GetLastWin32Error() - if (nErr == 0) - nErr := 29 // Dos error Write Fault - ENDIF - FError(nErr) - THROW IOException{i"Write: File write failed offset {offset} count {count}"} - ENDIF - SELF:nPos += bytesWritten - IF bytesWritten != count - VAR nErr := (DWORD) Marshal.GetLastWin32Error() - if (nErr == 0) - nErr := 29 // Dos error Write Fault - ENDIF - FError(nErr) - THROW IOException{i"Write: Not all bytes written to file offset {offset} count {count} written {bytesWritten}"} - ENDIF - RETURN -#ifdef NET5_0_OR_GREATER - /// - PUBLIC OVERRIDE METHOD Read(buffer AS System.Span) AS INT - // FileStream overrides the Span overloads on .Net Core. Without these overrides they would - // read and write at the position that the base class maintains instead of the OS file pointer. - LOCAL bytes := BYTE[]{buffer:Length} AS BYTE[] - LOCAL bytesRead := SELF:Read(bytes, 0, buffer:Length) AS INT - IF bytesRead > 0 - System.MemoryExtensions.AsSpan(bytes, 0, bytesRead):CopyTo(buffer) - ENDIF - RETURN bytesRead - - /// - PUBLIC OVERRIDE METHOD Write(buffer AS System.ReadOnlySpan) AS VOID - LOCAL bytes := buffer:ToArray() AS BYTE[] - SELF:Write(bytes, 0, bytes:Length) - RETURN -#endif - - /// - PUBLIC OVERRIDE METHOD WriteByte(b AS BYTE ) AS VOID - SELF:smallBuff[0] := b - SELF:Write(SELF:smallBuff , 0 , 1) - /// - PUBLIC OVERRIDE METHOD Lock(position AS INT64, length AS INT64) AS VOID - - LOCAL ret := FALSE AS LOGIC - ret := LockFile(SELF:hFile, (INT)position, (INT)(position >> 32), (INT)(length), (INT)(length >> 32)) - IF !ret - NetErr(TRUE) - VAR nErr := (DWORD) Marshal.GetLastWin32Error() - if (nErr == 0) - nErr := 33 // DOS Lock violation - ENDIF - FError(nErr) - THROW IOException{i"Lock: File lock failed, pos: {position}, length: {length} "} - ENDIF - RETURN - /// - PUBLIC OVERRIDE METHOD Unlock( position AS INT64, length AS INT64) AS VOID - LOCAL ret := FALSE AS LOGIC - ret := UnlockFile(SELF:hFile, (INT)position, (INT)(position >> 32), (INT)(length), (INT)(length >> 32)) - IF !ret - NetErr(TRUE) - VAR nErr := (DWORD) Marshal.GetLastWin32Error() - if (nErr == 0) - nErr := 33 // DOS Lock violation - ENDIF - FError(nErr) - THROW IOException{i"UnLock: File Unlock failed, pos: {position}, length: {length} "} - ENDIF - RETURN - /// - PUBLIC OVERRIDE METHOD Flush(lCommit AS LOGIC) AS VOID - // Note that GetDangerousFileHandle() calls Flush before we have the file handle - IF lCommit .and. SELF:hFile != NULL - IF ! FlushFileBuffers(SELF:hFile) - XSharp.IO.File.SetErrorState(IOException{i"Flush: Error Flushing File Buffer "}) - ENDIF - ENDIF - RETURN - - /// + /// A shared file stream commits to disk, so that other processes see the change. PUBLIC OVERRIDE METHOD Flush() AS VOID - // Note that GetDangerousFileHandle() calls Flush before we have the file handle - IF SELF:hFile == NULL - SUPER:Flush() - RETURN - ENDIF - // Shared FileStream should default to Committing the changes SELF:Flush(TRUE) RETURN - - #region External methods - /// - // The handle is opened synchronously (no FILE_FLAG_OVERLAPPED), so these calls do not return before - // the transfer is done. The OVERLAPPED is only there to carry the offset to read or write at. - [DllImport("kernel32.dll", SetLastError := TRUE, EntryPoint := "ReadFile")]; - PRIVATE STATIC EXTERN METHOD ReadFile(hFile AS IntPtr, bytes AS BYTE[], numbytes AS INT, numbytesread OUT INT , lpOverlapped REF NativeOverlapped) AS LOGIC - /// - [DllImport("kernel32.dll", SetLastError := TRUE, EntryPoint := "WriteFile")]; - PRIVATE STATIC EXTERN METHOD WriteFile(hFile AS IntPtr, bytes AS BYTE[], numbytes AS INT, numbyteswritten OUT INT , lpOverlapped REF NativeOverlapped) AS LOGIC - /// - [DllImport("kernel32.dll", SetLastError := TRUE, EntryPoint := "SetFilePointerEx")]; - PRIVATE STATIC EXTERN METHOD SetFilePointerEx(handle AS IntPtr, distance AS INT64 , newAddress OUT INT64, origin AS SeekOrigin ) AS LOGIC - /// - [DllImport("kernel32.dll", SetLastError := TRUE,EntryPoint := "LockFile")]; - PRIVATE STATIC EXTERN METHOD LockFile(hFile AS IntPtr , dwFileOffsetLow AS INT , dwFileOffsetHigh AS INT , nNumberOfBytesToLockLow AS INT , nNumberOfBytesToLockHigh AS INT ) AS LOGIC - /// - [DllImport("kernel32.dll", SetLastError := TRUE,EntryPoint := "UnlockFile")]; - PRIVATE STATIC EXTERN METHOD UnlockFile(hFile AS IntPtr , dwFileOffsetLow AS INT , dwFileOffsetHigh AS INT , nNumberOfBytesToLockLow AS INT , nNumberOfBytesToLockHigh AS INT ) AS LOGIC - /// - [DllImport("kernel32.dll", SetLastError := TRUE,EntryPoint := "FlushFileBuffers")]; - PRIVATE STATIC EXTERN METHOD FlushFileBuffers(hFile AS IntPtr ) AS LOGIC - /// - [DllImport("kernel32.dll", SetLastError := TRUE,EntryPoint := "SetEndOfFile")]; - PRIVATE STATIC EXTERN METHOD SetEndOfFile(hFile AS IntPtr ) AS LOGIC - /// - [DllImport("kernel32.dll", SetLastError := TRUE,EntryPoint := "GetFileSize")]; - PRIVATE STATIC EXTERN METHOD GetFileSize(hFile AS IntPtr , highSize OUT INT) AS DWORD - /// - [DllImport("kernel32.dll", SetLastError := TRUE,EntryPoint := "GetFileSizeEx")]; - PRIVATE STATIC EXTERN METHOD GetFileSizeEx(hFile AS IntPtr , FileSize OUT INT64) AS LOGIC -#endregion - END CLASS - - + + END NAMESPACE From f55e90eff9af0e5c338a26e6cc1c87672a232ebd Mon Sep 17 00:00:00 2001 From: Hansjoerg Petriffer Date: Mon, 14 Sep 2026 14:34:38 +0200 Subject: [PATCH 3/3] [Runtime] Use the managed shared file stream only on .Net 6 and later The managed shared file stream is now selected with the standard .Net define NET6_0_OR_GREATER. Below .Net 6 nothing changes at all: both the class and CreateFileStream() are byte for byte what ships today, so .Net Framework is untouched. The Position bug only exists from .Net 6 on. Until .Net 5, FileStream re-verified the OS file position on every access once the SafeFileHandle had been exposed, which kept the Win32 implementation correct even though it never overrode Position. The .Net 6 FileStream rewrite removed that re-verification. From .Net 6 on, FileStream also reads and writes at an explicit offset and does not cache the length of a file that others may write to, so the hand written Win32 layer buys nothing there. - SharedFileStream.prg carries both implementations, selected by #ifdef NET6_0_OR_GREATER. The #else branch is identical to the shipped code. - CreateFileStream() keeps its original body for the pre .Net 6 path, including the platform check and the buffered non Windows fallback. Only the .Net 6 path is new, and it needs no platform check because it is plain managed code. - The managed shared stream overrides Lock() and Unlock() to set NetErr(TRUE), because SetErrorState() only sets NetErr for a sharing violation (32) and a lock violation is 33. The Win32 implementation does that itself. Deliberately on the shared stream only: putting it on XsFileStream also changed exclusive streams, which then reported NetErr on .Net 6+ but not on .Net Framework. Two differences between the two paths, both pre existing defects of the Win32 implementation that are now fixed on .Net 6 and later only: - Read() with a non zero offset copies count bytes instead of the number of bytes really read, so the caller buffer is zeroed past the end of the file. - SetLength() seeks to the new length unconditionally instead of only clamping the position when it is past the new end. No caller in the RDDs depends on either. Co-Authored-By: Claude Opus 5 --- src/Runtime/XSharp.Core/Types/FileStream.prg | 52 ++--- .../XSharp.Core/Types/SharedFileStream.prg | 216 ++++++++++++++++++ 2 files changed, 234 insertions(+), 34 deletions(-) diff --git a/src/Runtime/XSharp.Core/Types/FileStream.prg b/src/Runtime/XSharp.Core/Types/FileStream.prg index e37cad37e9..e91a7e984f 100644 --- a/src/Runtime/XSharp.Core/Types/FileStream.prg +++ b/src/Runtime/XSharp.Core/Types/FileStream.prg @@ -66,33 +66,15 @@ BEGIN NAMESPACE XSharp.IO RETURN /// PUBLIC OVERRIDE METHOD Lock(position AS INT64, length AS INT64) AS VOID - TRY - XSharp.IO.File.ClearErrorState() - SUPER:Lock(position, length) - CATCH e AS Exception - SELF:__SetLockError(e) - THROW - END TRY + STARTIO + SUPER:Lock(position, length) + ENDIO RETURN /// PUBLIC OVERRIDE METHOD Unlock(position AS INT64, length AS INT64) AS VOID - TRY - XSharp.IO.File.ClearErrorState() - SUPER:Unlock(position, length) - CATCH e AS Exception - SELF:__SetLockError(e) - THROW - END TRY - RETURN - - // A failed lock is what NetErr() reports in the xBase world. SetErrorState() only sets NetErr for - // a sharing violation (32), but a lock violation is 33, so it has to be set here. - PRIVATE METHOD __SetLockError(e AS Exception) AS VOID - XSharp.IO.File.SetErrorState(e) - IF RuntimeState.FileError == 0 - FError(33) // DOS lock violation - ENDIF - NetErr(TRUE) + STARTIO + SUPER:Unlock(position, length) + ENDIO RETURN /// PUBLIC OVERRIDE METHOD Flush(lCommit AS LOGIC) AS VOID @@ -109,10 +91,17 @@ BEGIN NAMESPACE XSharp.IO /// STATIC METHOD CreateFileStream (path AS STRING, mode AS FileMode, faccess AS FileAccess, share AS FileShare, bufferSize AS LONG, options AS FileOptions) AS FileStream IF share != FileShare.None - // Shared access, so the same file may change under us at any moment. There is no platform - // check here anymore: this used to be the branch that decided between a hand written Win32 - // stream on Windows and a buffered stream everywhere else. - RETURN CreateSharedFileStream(path, mode, faccess, share, bufferSize, options) +#ifdef NET6_0_OR_GREATER + // The shared stream is plain managed code here, so there is no platform check needed. + RETURN CreateWin32FileStream(path, mode, faccess, share, bufferSize, options) +#else + // Unchanged: the shared stream does its IO with the Win32 API, so Windows only. + IF RuntimeState.RunningOnWindows + RETURN CreateWin32FileStream(path, mode, faccess, share, bufferSize, options) + ELSE + RETURN XsFileStream{path, mode, faccess, share, 0xFFFF, options} + ENDIF +#endif ELSE IF UseBufferedFileStream RETURN XsBufferedFileStream{path, mode, faccess, share, bufferSize, options} @@ -121,13 +110,8 @@ BEGIN NAMESPACE XSharp.IO ENDIF ENDIF - INTERNAL STATIC METHOD CreateSharedFileStream(path AS STRING, mode AS FileMode, faccess AS FileAccess, share AS FileShare, bufferSize AS LONG, options AS FileOptions) AS FileStream - RETURN XsWin32FileStream{path, mode, faccess, share, bufferSize, options} - - /// - [Obsolete("Use CreateSharedFileStream(). The shared file stream no longer uses the Win32 API.")]; INTERNAL STATIC METHOD CreateWin32FileStream(path AS STRING, mode AS FileMode, faccess AS FileAccess, share AS FileShare, bufferSize AS LONG, options AS FileOptions) AS FileStream - RETURN CreateSharedFileStream(path, mode, faccess, share, bufferSize, options) + RETURN XsWin32FileStream{path, mode, faccess, share, bufferSize, options} #endregion END CLASS diff --git a/src/Runtime/XSharp.Core/Types/SharedFileStream.prg b/src/Runtime/XSharp.Core/Types/SharedFileStream.prg index 0f4c9733b6..1d1923f9b5 100644 --- a/src/Runtime/XSharp.Core/Types/SharedFileStream.prg +++ b/src/Runtime/XSharp.Core/Types/SharedFileStream.prg @@ -11,6 +11,11 @@ USING System.Collections.Generic BEGIN NAMESPACE XSharp.IO /// +#ifdef NET6_0_OR_GREATER + // .Net 6 and later. FileStream reads and writes at an explicit offset and does not cache the length + // of a file that others may write to, so it reports the truth for shared access by itself. The hand + // written Win32 layer below is not only unnecessary there, it is broken: it never overrode Position, + // and the .Net 6 rewrite removed the re-verification of the OS position that used to paper over that. /// /// This class used to do all of its IO with the Win32 API (ReadFile, WriteFile, SetFilePointerEx, /// LockFile) on the raw file handle of the base class, because the FileStream of that time cached the @@ -37,7 +42,218 @@ BEGIN NAMESPACE XSharp.IO SELF:Flush(TRUE) RETURN + /// + PUBLIC OVERRIDE METHOD Lock(position AS INT64, length AS INT64) AS VOID + TRY + SUPER:Lock(position, length) + CATCH e AS Exception + SELF:__SetLockError() + THROW e + END TRY + RETURN + + /// + PUBLIC OVERRIDE METHOD Unlock(position AS INT64, length AS INT64) AS VOID + TRY + SUPER:Unlock(position, length) + CATCH e AS Exception + SELF:__SetLockError() + THROW e + END TRY + RETURN + + // A failed lock on a shared file is what NetErr() reports in the xBase world. The base class has + // already recorded the exception through SetErrorState(), but that only sets NetErr for a sharing + // violation (32) and a lock violation is 33. The Win32 implementation used below .Net 6 sets NetErr + // itself, so it has to happen here as well - and only here, so that exclusive streams keep behaving + // exactly like they always did. + PRIVATE METHOD __SetLockError() AS VOID + IF RuntimeState.FileError == 0 + FError(33) // DOS lock violation + ENDIF + NetErr(TRUE) + RETURN + END CLASS +#else + // Before .Net 6, unchanged. FileStream of that time cached the file position and the file length, + // which is wrong for a file that other processes may change, so the IO is done with the Win32 API. + // Position is not overridden here on purpose: until .Net 5 FileStream re-verified the OS position on + // every access once the SafeFileHandle had been exposed, which keeps this correct. + CLASS XsWin32FileStream INHERIT XsFileStream + PRIVATE hFile AS IntPtr + PRIVATE smallBuff AS BYTE[] + INTERNAL CONSTRUCTOR(path AS STRING, mode AS FileMode, faccess AS FileAccess, share AS FileShare, bufferSize AS LONG, options AS FileOptions) + SUPER(path, mode, faccess, share, bufferSize, options) + hFile := SELF:SafeFileHandle:DangerousGetHandle() + smallBuff := BYTE[]{1} + RETURN + /// + PUBLIC OVERRIDE METHOD Seek(offset AS INT64, origin AS SeekOrigin) AS INT64 + LOCAL result AS INT64 + LOCAL lOk AS LOGIC + lOk := SetFilePointerEx(hFile, offset, OUT result, origin) + IF lOk + RETURN result + ENDIF + VAR nErr := (DWORD) Marshal.GetLastWin32Error() + if (nErr == 0) + nErr := 30 // Dos error Read Fault + ENDIF + FError(nErr) + THROW IOException{i"Error moving file pointer from {origin} to {offset}"} + + /// + PUBLIC OVERRIDE METHOD SetLength(length AS INT64 ) AS VOID + // warning: does not restore original file pos + SELF:Seek(length, SeekOrigin.Begin) + SetEndOfFile(hFile) + RETURN + /// + PUBLIC OVERRIDE PROPERTY Length AS INT64 + GET + IF GetFileSizeEx(SELF:hFile, OUT VAR size) + RETURN size + ENDIF + VAR nErr := (DWORD) Marshal.GetLastWin32Error() + if (nErr == 0) + nErr := 30 // Dos error Read Fault + ENDIF + FError(nErr) + THROW IOException{"Could not retrieve file length"} + END GET + END PROPERTY + /// + PUBLIC OVERRIDE METHOD Read(bytes AS BYTE[] , offset AS INT, count AS INT) AS INT + LOCAL ret := FALSE AS LOGIC + LOCAL bytesRead := 0 AS INT + IF offset == 0 + ret := ReadFile(SELF:hFile, bytes, count, OUT bytesRead, IntPtr.Zero) + ELSE + LOCAL data AS BYTE[] + data := BYTE[]{count} + ret := ReadFile(SELF:hFile, data, count, OUT bytesRead, IntPtr.Zero) + System.Array.Copy(data, 0, bytes, offset, count) + ENDIF + IF !ret + RETURN -1 + ENDIF + RETURN bytesRead + /// + PUBLIC OVERRIDE METHOD Write(bytes AS BYTE[] , offset AS INT , count AS INT) AS VOID + LOCAL ret := FALSE AS LOGIC + LOCAL bytesWritten := 0 AS INT + IF offset == 0 + ret := WriteFile(SELF:hFile, bytes, count, OUT bytesWritten, 0) + ELSE + LOCAL aCopy AS BYTE[] + aCopy := BYTE[]{count} + System.Array.Copy(bytes,offset, aCopy,0, count) + ret := WriteFile(SELF:hFile, aCopy, count, OUT bytesWritten, 0) + ENDIF + IF !ret + VAR nErr := (DWORD) Marshal.GetLastWin32Error() + if (nErr == 0) + nErr := 29 // Dos error Write Fault + ENDIF + FError(nErr) + THROW IOException{i"Write: File write failed offset {offset} count {count}"} + ENDIF + IF bytesWritten != count + VAR nErr := (DWORD) Marshal.GetLastWin32Error() + if (nErr == 0) + nErr := 29 // Dos error Write Fault + ENDIF + FError(nErr) + THROW IOException{i"Write: Not all bytes written to file offset {offset} count {count} written {bytesWritten}"} + ENDIF + RETURN + /// + PUBLIC OVERRIDE METHOD WriteByte(b AS BYTE ) AS VOID + SELF:smallBuff[0] := b + SELF:Write(SELF:smallBuff , 0 , 1) + /// + PUBLIC OVERRIDE METHOD Lock(position AS INT64, length AS INT64) AS VOID + + LOCAL ret := FALSE AS LOGIC + ret := LockFile(SELF:hFile, (INT)position, (INT)(position >> 32), (INT)(length), (INT)(length >> 32)) + IF !ret + NetErr(TRUE) + VAR nErr := (DWORD) Marshal.GetLastWin32Error() + if (nErr == 0) + nErr := 33 // DOS Lock violation + ENDIF + FError(nErr) + THROW IOException{i"Lock: File lock failed, pos: {position}, length: {length} "} + ENDIF + RETURN + /// + PUBLIC OVERRIDE METHOD Unlock( position AS INT64, length AS INT64) AS VOID + LOCAL ret := FALSE AS LOGIC + ret := UnlockFile(SELF:hFile, (INT)position, (INT)(position >> 32), (INT)(length), (INT)(length >> 32)) + IF !ret + NetErr(TRUE) + VAR nErr := (DWORD) Marshal.GetLastWin32Error() + if (nErr == 0) + nErr := 33 // DOS Lock violation + ENDIF + FError(nErr) + THROW IOException{i"UnLock: File Unlock failed, pos: {position}, length: {length} "} + ENDIF + RETURN + /// + PUBLIC OVERRIDE METHOD Flush(lCommit AS LOGIC) AS VOID + // Note that GetDangerousFileHandle() calls Flush before we have the file handle + IF lCommit .and. SELF:hFile != NULL + IF ! FlushFileBuffers(SELF:hFile) + XSharp.IO.File.SetErrorState(IOException{i"Flush: Error Flushing File Buffer "}) + ENDIF + ENDIF + RETURN + + /// + PUBLIC OVERRIDE METHOD Flush() AS VOID + // Note that GetDangerousFileHandle() calls Flush before we have the file handle + IF SELF:hFile == NULL + SUPER:Flush() + RETURN + ENDIF + // Shared FileStream should default to Committing the changes + SELF:Flush(TRUE) + RETURN + + #region External methods + /// + [DllImport("kernel32.dll", SetLastError := TRUE, EntryPoint := "ReadFile")]; + PRIVATE STATIC EXTERN METHOD ReadFile(hFile AS IntPtr, bytes AS BYTE[], numbytes AS INT, numbytesread OUT INT , mustbezero AS IntPtr) AS LOGIC + /// + [DllImport("kernel32.dll", SetLastError := TRUE, EntryPoint := "WriteFile")]; + PRIVATE STATIC EXTERN METHOD WriteFile(hFile AS IntPtr, bytes AS BYTE[], numbytes AS INT, numbyteswritten OUT INT , lpOverlapped AS INT) AS LOGIC + /// + [DllImport("kernel32.dll", SetLastError := TRUE, EntryPoint := "SetFilePointerEx")]; + PRIVATE STATIC EXTERN METHOD SetFilePointerEx(handle AS IntPtr, distance AS INT64 , newAddress OUT INT64, origin AS SeekOrigin ) AS LOGIC + /// + [DllImport("kernel32.dll", SetLastError := TRUE,EntryPoint := "LockFile")]; + PRIVATE STATIC EXTERN METHOD LockFile(hFile AS IntPtr , dwFileOffsetLow AS INT , dwFileOffsetHigh AS INT , nNumberOfBytesToLockLow AS INT , nNumberOfBytesToLockHigh AS INT ) AS LOGIC + /// + [DllImport("kernel32.dll", SetLastError := TRUE,EntryPoint := "UnlockFile")]; + PRIVATE STATIC EXTERN METHOD UnlockFile(hFile AS IntPtr , dwFileOffsetLow AS INT , dwFileOffsetHigh AS INT , nNumberOfBytesToLockLow AS INT , nNumberOfBytesToLockHigh AS INT ) AS LOGIC + /// + [DllImport("kernel32.dll", SetLastError := TRUE,EntryPoint := "FlushFileBuffers")]; + PRIVATE STATIC EXTERN METHOD FlushFileBuffers(hFile AS IntPtr ) AS LOGIC + /// + [DllImport("kernel32.dll", SetLastError := TRUE,EntryPoint := "SetEndOfFile")]; + PRIVATE STATIC EXTERN METHOD SetEndOfFile(hFile AS IntPtr ) AS LOGIC + /// + [DllImport("kernel32.dll", SetLastError := TRUE,EntryPoint := "GetFileSize")]; + PRIVATE STATIC EXTERN METHOD GetFileSize(hFile AS IntPtr , highSize OUT INT) AS DWORD + /// + [DllImport("kernel32.dll", SetLastError := TRUE,EntryPoint := "GetFileSizeEx")]; + PRIVATE STATIC EXTERN METHOD GetFileSizeEx(hFile AS IntPtr , FileSize OUT INT64) AS LOGIC +#endregion + + END CLASS +#endif END NAMESPACE