File: net\System\Net\Cache\WinInetCache.cs
Project: ndp\fx\src\System.csproj (System)
/*++
Copyright (c) Microsoft Corporation
 
Module Name:
 
    _WinInetCache.cs
 
Abstract:
    The class implements low-level object model for
    communications with the caching part of WinInet DLL
 
 
Author:
 
    Alexei Vopilov    21-Dec-2002
 
Revision History:
 
--*/
namespace System.Net.Cache {
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using System.Collections;
using System.Text;
using System.Collections.Specialized;
using System.Threading;
using System.Globalization;
 
    //
    // WinInet OS Provider implementation (Caching part only)
    //
    // Contains methods marked with unsafe keyword
    //
    internal static class _WinInetCache {
 
        private const int  c_CharSz = 2;
 
 
        //
        // DATA Definitions
        //
 
        //  Cache Entry declarations
        [Flags]
        internal enum EntryType {
            NormalEntry     = 0x00000041, // ored with HTTP_1_1_CACHE_ENTRY
            StickyEntry     = 0x00000044, // ored with HTTP_1_1_CACHE_ENTRY
            Edited          = 0x00000008,
            TrackOffline    = 0x00000010,
            TrackOnline     = 0x00000020,
            Sparse          = 0x00010000,
            Cookie          = 0x00100000,
            UrlHistory      = 0x00200000,
//            FindDefaultFilter   = NormalEntry|StickyEntry|Cookie|UrlHistory|TrackOffline|TrackOnline
        }
        /*
            Some More IE private entry types
        HTTP_1_1_CACHE_ENTRY            0x00000040
        STATIC_CACHE_ENTRY              0x00000080
        MUST_REVALIDATE_CACHE_ENTRY     0x00000100
        COOKIE_ACCEPTED_CACHE_ENTRY     0x00001000
        COOKIE_LEASHED_CACHE_ENTRY      0x00002000
        COOKIE_DOWNGRADED_CACHE_ENTRY   0x00004000
        COOKIE_REJECTED_CACHE_ENTRY     0x00008000
        PENDING_DELETE_CACHE_ENTRY      0x00400000
        OTHER_USER_CACHE_ENTRY          0x00800000
        PRIVACY_IMPACTED_CACHE_ENTRY    0x02000000
        POST_RESPONSE_CACHE_ENTRY       0x04000000
        INSTALLED_CACHE_ENTRY           0x10000000
        POST_
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
*/
 
 
        //  Some supported Entry fields references
        [Flags]
        internal enum Entry_FC {
            None            = 0x0,
            Attribute       = 0x00000004,
            Hitrate         = 0x00000010,
            Modtime         = 0x00000040,
            Exptime         = 0x00000080,
            Acctime         = 0x00000100,
            Synctime        = 0x00000200,
            Headerinfo      = 0x00000400,
            ExemptDelta     = 0x00000800
        }
 
        //  Error status codes, some are mapped to native ones
        internal enum Status {
            Success                 = 0,
            InsufficientBuffer      = 122,
            FileNotFound            = 2,
            NoMoreItems             = 259,
            NotEnoughStorage        = 8,
            SharingViolation        = 32,
            InvalidParameter        = 87,
 
            // Below are extensions of native errors (no real errors exist)
            Warnings                = 0x1000000,
 
            FatalErrors             = Warnings + 0x1000,
            CorruptedHeaders        = (int)FatalErrors+1,
            InternalError           = (int)FatalErrors+2
        }
 
        [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)]
        internal struct FILETIME {
            public uint Low;
            public uint High;
 
            public static readonly FILETIME Zero = new FILETIME(0L);
 
            public FILETIME(long time) {
                unchecked {
                    Low  = (uint)time;
                    High = (uint)(time>>32);
                }
            }
 
            public long ToLong() {
                return ((long)High<<32) | Low;
            }
 
            public bool IsNull {
                get {return Low == 0 && High == 0;}
            }
        }
        //
        // It's an unmanamged layout of WinInet cache Entry Info
        // The pointer on this guy will represent the entry info for wininet
        // native calls
        //
        [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)]
        internal struct EntryBuffer {
            public static int MarshalSize = Marshal.SizeOf(typeof(EntryBuffer));
 
            public  int         StructSize;        // version of cache system MUST BE == sizeof(this)
            // We replace this with an offset from the struct start
            public  IntPtr      _OffsetSourceUrlName;
            // We replace this with an offset from the struct start
            public  IntPtr      _OffsetFileName;   // embedded pointer to the local file name.
            public  EntryType   EntryType;         // cache type bit mask.
            public  int         UseCount;          // current users count of the cache entry.
            public  int         HitRate;           // num of times the cache entry was retrieved.
            public  int         SizeLow;           // low DWORD of the file size.
            public  int         SizeHigh;          // high DWORD of the file size.
            public  FILETIME    LastModifiedTime;  // last modified time of the file in GMT format.
            public  FILETIME    ExpireTime;        // expire time of the file in GMT format
            public  FILETIME    LastAccessTime;    // last accessed time in GMT format
            public  FILETIME    LastSyncTime;      // last time the URL was synchronized with the source
            // We replace this with an offset from the struct start
            public  IntPtr      _OffsetHeaderInfo; // embedded pointer to the header info.
            public  int         HeaderInfoChars;   // size of the above header.
            // We replace this with an offset from the struct start
            public  IntPtr     _OffsetExtension;   // File extension used to retrive the urldata as a file.
 
            [StructLayout(LayoutKind.Explicit)]
            public struct Rsv {
                [FieldOffset(0)] public  int         ExemptDelta;       // Exemption delta from last access
                [FieldOffset(0)] public  int         Reserved;          // To keep the unmanaged layout
            }
            public Rsv U;
 
        }
 
        //
        // This class holds a manged version of native WinInet buffer
        //
        internal class Entry {
            public const int    DefaultBufferSize = 2048;
 
            public Status       Error;
            public string       Key;
            public string       Filename;           // filled by Create() or returned by LookupXXX()
            public string       FileExt;            // filled by Create() or returned by LookupXXX()
            public int          OptionalLength;     // should be always null
            public string       OriginalUrl;        // should be always null
            public string       MetaInfo;           // referenced to by Entry_FC.Headerinfo
            public int          MaxBufferBytes;     // contains the buffer size in bytes on input and
                                                    // copied bytes count in the buffer on output
            // The tail represents the entry info in its unmanaged layout;
            public EntryBuffer  Info;
 
            public Entry(string key, int maxHeadersSize) {
                Key = key;
                MaxBufferBytes = maxHeadersSize;
                if (maxHeadersSize != Int32.MaxValue && (Int32.MaxValue - (key.Length + EntryBuffer.MarshalSize + 1024)*2) > maxHeadersSize) {
                    //
                    // The buffer size is restricted mostly by headers, we reserve 1k more CHARS for additional
                    // metadata, otherwise user has to play with the maxHeadersSize parameter
                    //
                    //
                    MaxBufferBytes += (key.Length + EntryBuffer.MarshalSize + 1024)*2;
                }
                Info.EntryType = EntryType.NormalEntry;
            }
        }
 
        //
        // Method Definitions
        //
 
 
        //
        // Looks up an entry based on url string key.
        // Parses Headers and strings into entry mamabers.
        // Parses the raw output into entry.Info member.
        //
        unsafe internal static Status LookupInfo(Entry entry) {
 
            byte[] entryBuffer = new byte[Entry.DefaultBufferSize];
            int size      = entryBuffer.Length;
            byte[] buffer = entryBuffer;
 
            //We may need to adjust the buffer size (using 64 attempts although I would rather try to death)
            for (int k = 0; k < 64; ++k) {
                fixed (byte* entryPtr = buffer) {
 
                    bool found = UnsafeNclNativeMethods.UnsafeWinInetCache.GetUrlCacheEntryInfoW(entry.Key, entryPtr, ref size);
 
                    if (found) {
                        entryBuffer = buffer;
                        entry.MaxBufferBytes = size;
                        EntryFixup(entry, (EntryBuffer*) entryPtr, buffer);
                        entry.Error = Status.Success;
                        return entry.Error;
                    }
 
                    entry.Error = (Status)Marshal.GetLastWin32Error();
                    if (entry.Error == Status.InsufficientBuffer) {
                        if ((object) buffer == (object)entryBuffer) {
                            // did not reallocate yet.
                            if (size <= entry.MaxBufferBytes) {
                                buffer = new byte[size];
                                continue;
                            }
                        }
                    }
                    //some error has occured
                    break;
                }
            }
            return entry.Error;
        }
 
        //
        // Lookups an entry based on url string key.
        // If exists, locks the entry and hands out a managed handle representing a locked entry.
        //
        unsafe internal static SafeUnlockUrlCacheEntryFile LookupFile(Entry entry) {
 
            byte[] buffer       = new byte[Entry.DefaultBufferSize];
            int size            = buffer.Length;
            SafeUnlockUrlCacheEntryFile handle = null;
 
            try {
                while (true) {
                    fixed (byte* entryPtr = buffer) {
                        //We may need to adjust the buffer size
                        entry.Error = SafeUnlockUrlCacheEntryFile.GetAndLockFile(entry.Key, entryPtr, ref size, out handle);
 
                        if (entry.Error  == Status.Success) {
                            entry.MaxBufferBytes = size;
                            EntryFixup(entry, (EntryBuffer*) entryPtr, buffer);
                            //The method is available in TRAVE
                            return handle;
                        }
 
 
                        if (entry.Error == Status.InsufficientBuffer) {
                            if (size <= entry.MaxBufferBytes) {
                                buffer = new byte[size];
                                continue;
                            }
                        }
                        //some error has occured
                        break;
                    }
 
                }
            }
            catch(Exception e) {
                if (handle != null)
                    handle.Close();
 
                if (e is ThreadAbortException || e is StackOverflowException || e is OutOfMemoryException)
                    throw;
 
                if (entry.Error == Status.Success) {
                    entry.Error = Status.InternalError;
                }
            }
            return null;
        }
 
 
        //
        // Does the fixup of the returned buffer by converting internal pointer to offsets
        // it also does copying of non-string values from unmanaged buffer to Entry.Buffer members
        //
        unsafe private static Status EntryFixup(Entry entry, EntryBuffer* bufferPtr, byte[] buffer) {
            unchecked {
                bufferPtr->_OffsetExtension     = bufferPtr->_OffsetExtension == IntPtr.Zero? IntPtr.Zero: (IntPtr)((byte*)bufferPtr->_OffsetExtension - (byte*)bufferPtr);
                bufferPtr->_OffsetFileName      = bufferPtr->_OffsetFileName == IntPtr.Zero? IntPtr.Zero: (IntPtr)((byte*)bufferPtr->_OffsetFileName - (byte*)bufferPtr);
                bufferPtr->_OffsetHeaderInfo    = bufferPtr->_OffsetHeaderInfo == IntPtr.Zero? IntPtr.Zero: (IntPtr)((byte*)bufferPtr->_OffsetHeaderInfo - (byte*)bufferPtr);
                bufferPtr->_OffsetSourceUrlName = bufferPtr->_OffsetSourceUrlName == IntPtr.Zero? IntPtr.Zero: (IntPtr)((byte*)bufferPtr->_OffsetSourceUrlName - (byte*)bufferPtr);
 
                // Get a managed EntryBuffer copy out of byte[]
                entry.Info = *bufferPtr;
                entry.OriginalUrl   = GetEntryBufferString(bufferPtr, (int)(bufferPtr->_OffsetSourceUrlName));
                entry.Filename      = GetEntryBufferString(bufferPtr, (int)(bufferPtr->_OffsetFileName));
                entry.FileExt       = GetEntryBufferString(bufferPtr, (int)(bufferPtr->_OffsetExtension));
            }
            return GetEntryHeaders(entry, bufferPtr, buffer);
        }
 
        //
        // Returns a filename for a cache entry to write to.
        //
        internal static Status CreateFileName(Entry entry) {
 
            entry.Error = Status.Success;
            StringBuilder sb = new StringBuilder(UnsafeNclNativeMethods.UnsafeWinInetCache.MAX_PATH);
            if (UnsafeNclNativeMethods.UnsafeWinInetCache.CreateUrlCacheEntryW(entry.Key, entry.OptionalLength, entry.FileExt, sb, 0)) {
                entry.Filename = sb.ToString();
                return Status.Success;
            }
            entry.Error = (Status) Marshal.GetLastWin32Error();
            return entry.Error;
        }
 
 
        //
        // Associates a file with a cache entry.
        //
        internal static Status Commit(Entry entry) {
            string s = entry.MetaInfo;
            if (s == null) {
                s = string.Empty;
            }
            if ((s.Length + entry.Key.Length + entry.Filename.Length + (entry.OriginalUrl==null? 0: entry.OriginalUrl.Length)) > entry.MaxBufferBytes/c_CharSz) {
                entry.Error = Status.InsufficientBuffer;
                return entry.Error;
            }
 
            entry.Error = Status.Success;
            unsafe {
                fixed (char *ptr = s) {
 
                    byte* realBytesPtr = s.Length == 0? null: (byte*)ptr;
                    if (!UnsafeNclNativeMethods.UnsafeWinInetCache.CommitUrlCacheEntryW(
                                                                    entry.Key,
                                                                    entry.Filename,
                                                                    entry.Info.ExpireTime,
                                                                    entry.Info.LastModifiedTime,
                                                                    entry.Info.EntryType,
                                                                    realBytesPtr,
                                                                    s.Length,
                                                                    null,               // FileExt is reserved, must be null
                                                                    entry.OriginalUrl   // It's better to not play with redirections
                                                                                        // OrigianlUri should be nulled by the caller
                                                                    ))
                    {
                        entry.Error = (Status)Marshal.GetLastWin32Error();
                    }
                }
            }
 
            return entry.Error;
        }
 
        //
        // Updates a Cached Entry metadata according to attibutes flags.
        //
        internal static Status Update(Entry newEntry, Entry_FC attributes) {
            // Currently WinInet does not support headers update,
            // hence don't need space for them although we'll need recreate a cache entry
            // if headers update is requested
 
            byte[]  buffer = new byte[EntryBuffer.MarshalSize];
            newEntry.Error = Status.Success;
            unsafe {
                fixed (byte *bytePtr = buffer) {
                    EntryBuffer *ePtr = (EntryBuffer*) bytePtr;
                    *ePtr = newEntry.Info;
                    //set the version just in case
                    ePtr->StructSize =  EntryBuffer.MarshalSize;
 
                    if ((attributes & Entry_FC.Headerinfo) == 0) {
                        if (!UnsafeNclNativeMethods.UnsafeWinInetCache.SetUrlCacheEntryInfoW(newEntry.Key, bytePtr, attributes)) {
                            newEntry.Error = (Status)Marshal.GetLastWin32Error();
                        }
                    }
                    else {
                        // simulating headers update using Edited cache entry feature of WinInet
                        Entry oldEntry = new Entry(newEntry.Key, newEntry.MaxBufferBytes);
 
                        SafeUnlockUrlCacheEntryFile handle = null;
                        bool wasEdited = false;
                        try {
                            // lock the entry and get the filename out.
                            handle = LookupFile(oldEntry);
                            if (handle == null) {
                                //The same error would happen on update attributes, return it.
                                newEntry.Error = oldEntry.Error;
                                return newEntry.Error;
                            }
 
                            //Copy strings from old entry that are not present in the method parameters
                            newEntry.Filename       = oldEntry.Filename;
                            newEntry.OriginalUrl    = oldEntry.OriginalUrl;
                            newEntry.FileExt        = oldEntry.FileExt;
 
                            // We don't need to update this and some other attributes since will replace entire entry
                            attributes &= ~Entry_FC.Headerinfo;
 
                            //Copy attributes from an old entry that are not present in the method parameters
                            if ((attributes & Entry_FC.Exptime) == 0) {
                                newEntry.Info.ExpireTime = oldEntry.Info.ExpireTime;
                            }
 
                            if ((attributes & Entry_FC.Modtime) == 0) {
                                newEntry.Info.LastModifiedTime = oldEntry.Info.LastModifiedTime;
                            }
 
                            if ((attributes & Entry_FC.Attribute) == 0) {
                                newEntry.Info.EntryType = oldEntry.Info.EntryType;
                                newEntry.Info.U.ExemptDelta = oldEntry.Info.U.ExemptDelta;
                                if ((oldEntry.Info.EntryType & EntryType.StickyEntry) == EntryType.StickyEntry) {
                                    attributes |= (Entry_FC.Attribute | Entry_FC.ExemptDelta);
                                }
                            }
 
                            // Those attributes will be taken care of by Commit()
                            attributes &= ~(Entry_FC.Exptime|Entry_FC.Modtime);
 
                            wasEdited = (oldEntry.Info.EntryType & EntryType.Edited) != 0;
 
                            if (!wasEdited) {
                                // Prevent the file from being deleted on entry Remove (kinda hack)
                                oldEntry.Info.EntryType |= EntryType.Edited;
                                // Recursion!
                                if (Update(oldEntry, Entry_FC.Attribute) != Status.Success) {
                                    newEntry.Error = oldEntry.Error;
                                    return newEntry.Error;
                                }
                            }
                        }
                        finally {
                            if (handle != null) {
                                handle.Close();
                            }
                        }
 
                        // At this point we try to delete the exisintg item and create a new one with the same
                        // filename and the new headers.
                        //We wish to ignore any errors from Remove since are going to replace the entry.
                        Remove(oldEntry);
                        if (Commit(newEntry) != Status.Success) {
                            if (!wasEdited) {
                                //revert back the original entry type
                                oldEntry.Info.EntryType &= ~EntryType.Edited;
                                Update(oldEntry, Entry_FC.Attribute);
                                // Being already in error mode, cannot do much if Update fails.
                            }
                            return newEntry.Error;
                        }
 
                        // Now see what's left in attributes change request.
                        if (attributes != Entry_FC.None) {
                            Update(newEntry, attributes);
                        }
                        //At this point newEntry.Error should contain the resulting status
                        //and we replaced the entry in the cache with the same body
                        //but different headers. Some more attributes may have changed as well.
                    }
                }
            }
 
            return newEntry.Error;
        }
 
        //
        // Updates a Cached Entry metadata according to attibutes flags.
        //
        internal static Status Remove(Entry entry) {
            entry.Error = Status.Success;
            if (!UnsafeNclNativeMethods.UnsafeWinInetCache.DeleteUrlCacheEntryW(entry.Key)) {
                entry.Error = (Status)Marshal.GetLastWin32Error();
            }
            return entry.Error;
        }
 
 
        //
        // Gets the managed copy of a null terminated string resided in the buffer
        //
#if DEBUG
        /*
        // Consider removing.
        private static unsafe string GetEntryBufferString(byte[] buffer, int offset) {
            fixed (void* bufferPtr = buffer) {
                return GetEntryBufferString(bufferPtr, offset);
            }
        }
        */
#endif
        private static unsafe string GetEntryBufferString(void* bufferPtr, int offset) {
            if (offset == 0) {
                return null;
            }
            IntPtr pointer = new IntPtr((byte*)bufferPtr + offset);
            return Marshal.PtrToStringUni(pointer);
        }
 
        //
        // Gets the headers and optionally other meta data out of a cached entry
        //
        // Whenever an empty line found in the buffer, the resulting array of
        // collections will grow in size
        //
        private static unsafe Status GetEntryHeaders(Entry entry, EntryBuffer* bufferPtr, byte[] buffer) {
            entry.Error = Status.Success;
            entry.MetaInfo = null;
 
            //
            if (bufferPtr->_OffsetHeaderInfo == IntPtr.Zero || bufferPtr->HeaderInfoChars == 0 || (bufferPtr->EntryType & EntryType.UrlHistory) != 0) {
                return Status.Success;
            }
 
            int bufferCharLength = bufferPtr->HeaderInfoChars + ((int)(bufferPtr->_OffsetHeaderInfo))/c_CharSz;
            if (bufferCharLength*c_CharSz > entry.MaxBufferBytes) {
                // WinInet bug? They may report offset+HeaderInfoChars as a greater value than MaxBufferBytes as total buffer size.
                // Actually, the last one seems to be accurate based on the data we have provided for Commit.
                bufferCharLength = entry.MaxBufferBytes/c_CharSz;
            }
            //WinInet may put terminating nulls at the end of the buffer, remove them.
            while (((char*)bufferPtr)[bufferCharLength-1] == 0)
                {--bufferCharLength;}
            entry.MetaInfo = Encoding.Unicode.GetString(buffer, (int)bufferPtr->_OffsetHeaderInfo, (bufferCharLength-(int)bufferPtr->_OffsetHeaderInfo/2)*2);
            return entry.Error;
        }
/********************
            ArrayList result = new ArrayList();
            NameValueCollection collection = new NameValueCollection();
            int offset = (int)bufferPtr->_OffsetHeaderInfo/c_CharSz;
            char *charPtr = (char*)bufferPtr;
            {
                int i = offset+1;
                for (; i < entry.MaxBufferBytes/c_CharSz; ++i) {
                    if ((charPtr[i] == ':' || (charPtr[i] == '\n' && charPtr[(i-1)] == '\r'))) {
                        break;
                    }
                }
                if (i < entry.MaxBufferBytes/c_CharSz) {
                    //If this looks like a status line
                    if (charPtr[i] == '\n' && i > offset+1) {
                        string s = Encoding.Unicode.GetString(buffer, offset*2, (i-offset-1)*2);
                        offset = i+1;
                        collection[string.Empty] = s;
                    }
                }
            }
            int bufferCharLength = bufferPtr->HeaderInfoChars + ((int)(bufferPtr->_OffsetHeaderInfo))/c_CharSz;
            if (bufferCharLength*c_CharSz > entry.MaxBufferBytes) {
                // WinInet bug? They may report offset+HeaderInfoChars as a greater value than total buffer size.
                // Actually, the last one seems to be accurate based on the data we have provided for Commit.
                bufferCharLength = entry.MaxBufferBytes/c_CharSz;
            }
 
            while (true) {
                int totalLength = 0;
                DataParseStatus status = WebHeaderCollection.ParseHeaders(collection, false, buffer, bufferCharLength,
                                        ref offset,
                                        ref totalLength,
                                        entry.MaxBufferBytes/c_CharSz);
 
                if (status != DataParseStatus.Done) {
                    if (status == DataParseStatus.NeedMoreData) {
                        //WinInet puts terminating null at the end of the buffer, accept that as a "normal" case.
                        if ((offset+1 == bufferCharLength) && charPtr[offset] == 0) {
                             // accept as the last metainfo block
                             if (collection.Count != 0) {
                                 result.Add(collection);
                             }
                             break;
                        }
                    }
                    entry.Error = Status.CorruptedHeaders;
                    //throw new InvalidOperationException("Cannot convert Cache Entry metadata into a NameValueCollection instance");
                    break;
                }
 
                result.Add(collection);
                // do we have more meta data?
                if (offset >= bufferCharLength) {
                    break;
                }
                // continue parsing next collection
                collection = new NameValueCollection();
            }
            entry.MetaInfo = (result.Count == 0? null: (NameValueCollection[])result.ToArray(typeof(NameValueCollection)));
            return entry.Error;
        }
*********************/
#if DEBUG

        /*
        // Consider removing.
        //
        // For debugging will return a readbale representation of a cached entry info
        //
        private static string DebugEntryBuffer(byte[] buffer, int entryBufSize) {
 
            EntryBuffer Info;
            if (entryBufSize < EntryBuffer.MarshalSize) {
                throw new ArgumentOutOfRangeException("size");
            }
            unsafe {
                fixed(void* vptr = buffer) {
                    IntPtr ptr = new IntPtr(vptr);
                    Info = (EntryBuffer)Marshal.PtrToStructure(ptr,typeof(EntryBuffer));
                }
            }
 
            string allHeaders = null;
            //
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
*/
#endif  //TRAVE
 
    } //END WinInet class
}