File: RowBinding.cs
Web Access
Project: src\src\runtime\src\libraries\System.Data.OleDb\src\System.Data.OleDb.csproj (System.Data.OleDb)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Data.Common;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;

namespace System.Data.OleDb
{
    internal sealed class RowBinding : System.Data.ProviderBase.DbBuffer
    {
        private readonly int _bindingCount;
        private readonly int _headerLength;
        private readonly int _dataLength;
        private readonly int _emptyStringOffset;

        private UnsafeNativeMethods.IAccessor? _iaccessor;
        private IntPtr _accessorHandle;

        private readonly bool _needToReset;
        private bool _haveData;

        // tagDBBINDING[] starting 64bit aligned
        // all DBSTATUS values (32bit per value), starting 64bit aligned
        // all DBLENGTH values (32/64bit per value), starting 64bit alignedsa
        // all data values listed after that (variable length), each individual starting 64bit aligned
        // Int64 - zero for pointers to emptystring

        internal static RowBinding CreateBuffer(int bindingCount, int databuffersize, bool needToReset)
        {
            int headerLength = RowBinding.AlignDataSize(bindingCount * ODB.SizeOf_tagDBBINDING);
            int length = RowBinding.AlignDataSize(headerLength + databuffersize) + 8; // 8 bytes for a null terminated string
            return new RowBinding(bindingCount, headerLength, databuffersize, length, needToReset);
        }

        private RowBinding(int bindingCount, int headerLength, int dataLength, int length, bool needToReset) : base(length)
        {
            _bindingCount = bindingCount;
            _headerLength = headerLength;
            _dataLength = dataLength;
            _emptyStringOffset = length - 8; // 8 bytes for a null terminated string
            _needToReset = needToReset;

            Debug.Assert(0 < _bindingCount, "bad _bindingCount");
            Debug.Assert(0 < _headerLength, "bad _headerLength");
            Debug.Assert(0 < _dataLength, "bad _dataLength");
            Debug.Assert(_bindingCount * 3 * IntPtr.Size <= _dataLength, "_dataLength too small");
            Debug.Assert(_headerLength + _dataLength <= _emptyStringOffset, "bad string offset");
            Debug.Assert(_headerLength + _dataLength + 8 <= length, "bad length");
        }

        internal void StartDataBlock()
        {
            if (_haveData)
            {
                Debug.Fail("previous row not yet cleared");
                ResetValues();
            }
            _haveData = true;
        }

        internal int BindingCount()
        {
            return _bindingCount;
        }

        internal IntPtr DangerousGetAccessorHandle()
        {
            return _accessorHandle;
        }

        internal IntPtr DangerousGetDataPtr()
        {
            //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            // NOTE: You must have called DangerousAddRef before calling this
            //       method, or you run the risk of allowing Handle Recycling
            //       to occur!
            //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            return ADP.IntPtrOffset(DangerousGetHandle(), _headerLength);
        }

        internal IntPtr DangerousGetDataPtr(int valueOffset)
        {
            //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            // NOTE: You must have called DangerousAddRef before calling this
            //       method, or you run the risk of allowing Handle Recycling
            //       to occur!
            //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            return ADP.IntPtrOffset(DangerousGetHandle(), valueOffset);
        }

        internal OleDbHResult CreateAccessor(UnsafeNativeMethods.IAccessor iaccessor, int flags, ColumnBinding[] bindings)
        {
            int[] rowBindStatus = new int[BindingCount()];

            _iaccessor = iaccessor;
            OleDbHResult hr = iaccessor.CreateAccessor(flags, (IntPtr)rowBindStatus.Length, this, (IntPtr)_dataLength, out _accessorHandle, rowBindStatus);

            for (int k = 0; k < rowBindStatus.Length; ++k)
            {
                if (DBBindStatus.OK != (DBBindStatus)rowBindStatus[k])
                {
                    if (ODB.DBACCESSOR_PARAMETERDATA == flags)
                    {
                        throw ODB.BadStatus_ParamAcc(bindings[k].ColumnBindingOrdinal, (DBBindStatus)rowBindStatus[k]);
                    }
                    else if (ODB.DBACCESSOR_ROWDATA == flags)
                    {
                        throw ODB.BadStatusRowAccessor(bindings[k].ColumnBindingOrdinal, (DBBindStatus)rowBindStatus[k]);
                    }
                    else
                        Debug.Fail("unknown accessor buffer");
                }
            }
            return hr;
        }

        internal ColumnBinding[] SetBindings(OleDbDataReader? dataReader, Bindings bindings,
                                             int indexStart, int indexForAccessor,
                                             OleDbParameter[]? parameters, tagDBBINDING[] dbbindings, bool ifIRowsetElseIRow)
        {
            Debug.Assert(null != bindings, "null bindings");
            Debug.Assert(dbbindings.Length == BindingCount(), "count mismatch");

            bool mustRelease = false;

            RuntimeHelpers.PrepareConstrainedRegions();
            try
            {
                DangerousAddRef(ref mustRelease);

                IntPtr buffer = DangerousGetHandle();
                for (int i = 0; i < dbbindings.Length; ++i)
                {
                    IntPtr ptr = ADP.IntPtrOffset(buffer, (i * ODB.SizeOf_tagDBBINDING));
                    Marshal.StructureToPtr(dbbindings[i], ptr, false/*deleteold*/);
                }
            }
            finally
            {
                if (mustRelease)
                {
                    DangerousRelease();
                }
            }

            ColumnBinding[] columns = new ColumnBinding[dbbindings.Length];
            for (int indexWithinAccessor = 0; indexWithinAccessor < columns.Length; ++indexWithinAccessor)
            {
                int index = indexStart + indexWithinAccessor;
                OleDbParameter? parameter = parameters?[index];
                columns[indexWithinAccessor] = new ColumnBinding(
                    dataReader!, index, indexForAccessor, indexWithinAccessor,
                    parameter, this, bindings, dbbindings[indexWithinAccessor], _headerLength,
                    ifIRowsetElseIRow);
            }
            return columns;
        }

        internal static int AlignDataSize(int value)
        {
            // buffer data to start on 8-byte boundary
            return Math.Max(8, (value + 7) & ~0x7);
        }

        internal object GetVariantValue(int offset)
        {
            Debug.Assert(_needToReset, "data type requires resetting and _needToReset is false");
            Debug.Assert(0 == (ODB.SizeOf_Variant % 8), "unexpected VARIANT size mutiplier");
            Debug.Assert(0 == offset % 8, "invalid alignment");
            ValidateCheck(offset, 2 * ODB.SizeOf_Variant);

            object? value = null;
            bool mustRelease = false;
            RuntimeHelpers.PrepareConstrainedRegions();
            try
            {
                DangerousAddRef(ref mustRelease);

                IntPtr buffer = ADP.IntPtrOffset(DangerousGetHandle(), offset);
                value = Marshal.GetObjectForNativeVariant(buffer);
            }
            finally
            {
                if (mustRelease)
                {
                    DangerousRelease();
                }
            }

            return value ?? DBNull.Value;
        }

        // translate to native
        internal void SetVariantValue(int offset, object value)
        {
            // two contiguous VARIANT structures, second should be a binary copy of the first
            Debug.Assert(_needToReset, "data type requires resetting and _needToReset is false");
            Debug.Assert(0 == (ODB.SizeOf_Variant % 8), "unexpected VARIANT size mutiplier");
            Debug.Assert(0 == offset % 8, "invalid alignment");
            ValidateCheck(offset, 2 * ODB.SizeOf_Variant);

            bool mustRelease = false;
            RuntimeHelpers.PrepareConstrainedRegions();
            try
            {
                DangerousAddRef(ref mustRelease);

                IntPtr buffer = ADP.IntPtrOffset(DangerousGetHandle(), offset);

                RuntimeHelpers.PrepareConstrainedRegions();
                try
                {
                    // GetNativeVariantForObject must be in try block since it has no reliability contract
                    Marshal.GetNativeVariantForObject(value, buffer);
                }
                finally
                {
                    // safe to copy memory(dst,src,count), even if GetNativeVariantForObject failed
                    NativeOledbWrapper.MemoryCopy(ADP.IntPtrOffset(buffer, ODB.SizeOf_Variant), buffer, ODB.SizeOf_Variant);
                }
            }
            finally
            {
                if (mustRelease)
                {
                    DangerousRelease();
                }
            }
        }

        // value
        // cached value
        // cached zero value
        // translate to native
        internal void SetBstrValue(int offset, string value)
        {
            // two contiguous BSTR ptr, second should be a binary copy of the first
            Debug.Assert(_needToReset, "data type requires resetting and _needToReset is false");
            Debug.Assert(0 == offset % IntPtr.Size, "invalid alignment");
            ValidateCheck(offset, 2 * IntPtr.Size);

            IntPtr ptr;
            bool mustRelease = false;
            RuntimeHelpers.PrepareConstrainedRegions();
            try
            {
                DangerousAddRef(ref mustRelease);

                RuntimeHelpers.PrepareConstrainedRegions();
                try
                { }
                finally
                {
                    ptr = Interop.OleAut32.SysAllocStringLen(value, (uint)value.Length);

                    // safe to copy ptr, even if SysAllocStringLen failed
                    Marshal.WriteIntPtr(base.handle, offset, ptr);
                    Marshal.WriteIntPtr(base.handle, offset + IntPtr.Size, ptr);
                }
            }
            finally
            {
                if (mustRelease)
                {
                    DangerousRelease();
                }
            }
            if (IntPtr.Zero == ptr)
            {
                throw new OutOfMemoryException();
            }
        }

        // translate to native
        internal void SetByRefValue(int offset, IntPtr pinnedValue)
        {
            Debug.Assert(_needToReset, "data type requires resetting and _needToReset is false");
            Debug.Assert(0 == offset % IntPtr.Size, "invalid alignment");
            ValidateCheck(offset, 2 * IntPtr.Size);

            if (IntPtr.Zero == pinnedValue)
            { // empty array scenario
                pinnedValue = ADP.IntPtrOffset(base.handle, _emptyStringOffset);
            }
            bool mustRelease = false;
            RuntimeHelpers.PrepareConstrainedRegions();
            try
            {
                DangerousAddRef(ref mustRelease);

                RuntimeHelpers.PrepareConstrainedRegions();
                try
                { }
                finally
                {
                    Marshal.WriteIntPtr(base.handle, offset, pinnedValue);               // parameter input value
                    Marshal.WriteIntPtr(base.handle, offset + IntPtr.Size, pinnedValue); // original parameter value
                }
            }
            finally
            {
                if (mustRelease)
                {
                    DangerousRelease();
                }
            }
        }

        internal void CloseFromConnection()
        {
            _iaccessor = null;
            _accessorHandle = ODB.DB_INVALID_HACCESSOR;
        }

        internal new void Dispose()
        {
            ResetValues();

            UnsafeNativeMethods.IAccessor? iaccessor = _iaccessor;
            IntPtr accessorHandle = _accessorHandle;

            _iaccessor = null;
            _accessorHandle = ODB.DB_INVALID_HACCESSOR;

            if ((ODB.DB_INVALID_HACCESSOR != accessorHandle) && (null != iaccessor))
            {
                OleDbHResult hr;
                hr = iaccessor.ReleaseAccessor(accessorHandle, out _);
                if (hr < 0)
                { // ignore any error msgs
                    SafeNativeMethods.Wrapper.ClearErrorInfo();
                }
            }

            base.Dispose();
        }

        internal void ResetValues()
        {
            if (_needToReset && _haveData)
            {
                lock (this)
                { // prevent Dispose/ResetValues race condition

                    bool mustRelease = false;
                    RuntimeHelpers.PrepareConstrainedRegions();
                    try
                    {
                        DangerousAddRef(ref mustRelease);

                        ResetValues(DangerousGetHandle(), _iaccessor);
                    }
                    finally
                    {
                        if (mustRelease)
                        {
                            DangerousRelease();
                        }
                    }
                }
            }
            else
            {
                _haveData = false;
            }
#if DEBUG
            // verify types that need resetting are not forgotton, since the code
            // that sets this up is in dbbinding.cs, MaxLen { set; }
            if (!_needToReset)
            {
                Debug.Assert(0 <= _bindingCount && (_bindingCount * ODB.SizeOf_tagDBBINDING) < Length, "bad _bindingCount");
                for (int i = 0; i < _bindingCount; ++i)
                {
                    short wtype = ReadInt16((i * ODB.SizeOf_tagDBBINDING) + ODB.OffsetOf_tagDBBINDING_wType);
                    switch (wtype)
                    {
                        case (NativeDBType.BYREF | NativeDBType.BYTES):
                        case (NativeDBType.BYREF | NativeDBType.WSTR):
                        case NativeDBType.PROPVARIANT:
                        case NativeDBType.VARIANT:
                        case NativeDBType.BSTR:
                        case NativeDBType.HCHAPTER:
                            Debug.Fail("expected _needToReset");
                            break;
                    }
                }
            }
#endif
        }

        private unsafe void ResetValues(IntPtr buffer, object? iaccessor)
        {
            Debug.Assert(IntPtr.Zero != buffer && _needToReset && _haveData, "shouldn't be calling ResetValues");
            for (int i = 0; i < _bindingCount; ++i)
            {
                IntPtr ptr = ADP.IntPtrOffset(buffer, (i * ODB.SizeOf_tagDBBINDING));

                int valueOffset = _headerLength + Marshal.ReadIntPtr(ptr, ODB.OffsetOf_tagDBBINDING_obValue).ToInt32();
                short wtype = Marshal.ReadInt16(ptr, ODB.OffsetOf_tagDBBINDING_wType);

                switch (wtype)
                {
                    case (NativeDBType.BYREF | NativeDBType.BYTES):
                    case (NativeDBType.BYREF | NativeDBType.WSTR):
                        ValidateCheck(valueOffset, 2 * IntPtr.Size);
                        FreeCoTaskMem(buffer, valueOffset);
                        break;
                    case NativeDBType.VARIANT:
                    case NativeDBType.PROPVARIANT:
                        ValidateCheck(valueOffset, 2 * sizeof(ComVariant));
                        FreePropVariant(buffer, valueOffset);
                        break;
                    case NativeDBType.BSTR:
                        ValidateCheck(valueOffset, 2 * IntPtr.Size);
                        FreeBstr(buffer, valueOffset);
                        break;
                    case NativeDBType.HCHAPTER:
                        if (null != iaccessor)
                        {
                            // iaccessor will always be null when from ReleaseHandle
                            FreeChapter(buffer, valueOffset, iaccessor);
                        }
                        break;
#if DEBUG

                    case NativeDBType.EMPTY:
                    case NativeDBType.NULL:
                    case NativeDBType.I2:
                    case NativeDBType.I4:
                    case NativeDBType.R4:
                    case NativeDBType.R8:
                    case NativeDBType.CY:
                    case NativeDBType.DATE:
                    case NativeDBType.ERROR:
                    case NativeDBType.BOOL:
                    case NativeDBType.DECIMAL:
                    case NativeDBType.I1:
                    case NativeDBType.UI1:
                    case NativeDBType.UI2:
                    case NativeDBType.UI4:
                    case NativeDBType.I8:
                    case NativeDBType.UI8:
                    case NativeDBType.FILETIME:
                    case NativeDBType.GUID:
                    case NativeDBType.BYTES:
                    case NativeDBType.WSTR:
                    case NativeDBType.NUMERIC:
                    case NativeDBType.DBDATE:
                    case NativeDBType.DBTIME:
                    case NativeDBType.DBTIMESTAMP:
                        break; // known, do nothing
                    case NativeDBType.IDISPATCH:
                    case NativeDBType.IUNKNOWN:
                        break; // known, releasing RowHandle will handle lifetimes correctly
                    default:
                        Debug.Fail("investigate");
                        break;
#endif
                }
            }
            _haveData = false;
        }

        private static void FreeChapter(IntPtr buffer, int valueOffset, object iaccessor)
        {
            Debug.Assert(0 == valueOffset % 8, "unexpected unaligned ptr offset");

            UnsafeNativeMethods.IChapteredRowset chapteredRowset = (iaccessor as UnsafeNativeMethods.IChapteredRowset)!;
            IntPtr chapter = SafeNativeMethods.InterlockedExchangePointer(ADP.IntPtrOffset(buffer, valueOffset), IntPtr.Zero);
            if (ODB.DB_NULL_HCHAPTER != chapter)
            {
                chapteredRowset.ReleaseChapter(chapter, out _);
            }
        }

        private static void FreeBstr(IntPtr buffer, int valueOffset)
        {
            Debug.Assert(0 == valueOffset % 8, "unexpected unaligned ptr offset");

            // two contiguous BSTR ptrs that need to be freed
            // the second should only be freed if different from the first
            RuntimeHelpers.PrepareConstrainedRegions();
            try
            { }
            finally
            {
                IntPtr currentValue = Marshal.ReadIntPtr(buffer, valueOffset);
                IntPtr originalValue = Marshal.ReadIntPtr(buffer, valueOffset + IntPtr.Size);

                if ((IntPtr.Zero != currentValue) && (currentValue != originalValue))
                {
                    Interop.OleAut32.SysFreeString(currentValue);
                }
                if (IntPtr.Zero != originalValue)
                {
                    Interop.OleAut32.SysFreeString(originalValue);
                }

                // for debugability - delay clearing memory until after FreeBSTR
                Marshal.WriteIntPtr(buffer, valueOffset, IntPtr.Zero);
                Marshal.WriteIntPtr(buffer, valueOffset + IntPtr.Size, IntPtr.Zero);
            }
        }

        private static void FreeCoTaskMem(IntPtr buffer, int valueOffset)
        {
            Debug.Assert(0 == valueOffset % 8, "unexpected unaligned ptr offset");

            // two contiguous CoTaskMemAlloc ptrs that need to be freed
            // the first should only be freed if different from the first
            RuntimeHelpers.PrepareConstrainedRegions();
            try
            { }
            finally
            {
                IntPtr currentValue = Marshal.ReadIntPtr(buffer, valueOffset);
                IntPtr originalValue = Marshal.ReadIntPtr(buffer, valueOffset + IntPtr.Size);

                // originalValue is pinned managed memory or pointer to emptyStringOffset
                if ((IntPtr.Zero != currentValue) && (currentValue != originalValue))
                {
                    Interop.Ole32.CoTaskMemFree(currentValue);
                }

                // for debugability - delay clearing memory until after CoTaskMemFree
                Marshal.WriteIntPtr(buffer, valueOffset, IntPtr.Zero);
                Marshal.WriteIntPtr(buffer, valueOffset + IntPtr.Size, IntPtr.Zero);
            }
        }

        private static unsafe void FreePropVariant(IntPtr buffer, int valueOffset)
        {
            // two contiguous PROPVARIANT structures that need to be freed
            // the second should only be freed if different from the first
            Debug.Assert(0 == (sizeof(ComVariant) % 8), "unexpected PROPVARIANT size mutiplier");
            Debug.Assert(0 == valueOffset % 8, "unexpected unaligned ptr offset");

            IntPtr currentHandle = ADP.IntPtrOffset(buffer, valueOffset);
            IntPtr originalHandle = ADP.IntPtrOffset(buffer, valueOffset + sizeof(ComVariant));
            bool different = NativeOledbWrapper.MemoryCompare(currentHandle, originalHandle, sizeof(ComVariant));

            RuntimeHelpers.PrepareConstrainedRegions();
            try
            { }
            finally
            {
                // always clear the first structure
                Interop.Ole32.PropVariantClear(currentHandle);
                if (different)
                {
                    // second structure different from the first
                    Interop.Ole32.PropVariantClear(originalHandle);
                }
                else
                {
                    // second structure same as the first, just clear the field
                    SafeNativeMethods.ZeroMemory(originalHandle, sizeof(ComVariant));
                }
            }
        }

        internal IntPtr InterlockedExchangePointer(int offset)
        {
            ValidateCheck(offset, IntPtr.Size);
            Debug.Assert(0 == offset % IntPtr.Size, "invalid alignment");

            IntPtr value;
            bool mustRelease = false;
            RuntimeHelpers.PrepareConstrainedRegions();
            try
            {
                DangerousAddRef(ref mustRelease);

                IntPtr ptr = ADP.IntPtrOffset(DangerousGetHandle(), offset);
                value = SafeNativeMethods.InterlockedExchangePointer(ptr, IntPtr.Zero);
            }
            finally
            {
                if (mustRelease)
                {
                    DangerousRelease();
                }
            }
            return value;
        }

        protected override bool ReleaseHandle()
        {
            // NOTE: The SafeHandle class guarantees this will be called exactly once.
            _iaccessor = null;
            if (_needToReset && _haveData)
            {
                IntPtr buffer = base.handle;
                if (IntPtr.Zero != buffer)
                {
                    ResetValues(buffer, null);
                }
            }
            return base.ReleaseHandle();
        }
    }
}