File: Handlers\ScriptResourceHandler.cs
Project: ndp\fx\src\xsp\system\Extensions\System.Web.Extensions.csproj (System.Web.Extensions)
//------------------------------------------------------------------------------
// <copyright file="ScriptResourceHandler.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
 
namespace System.Web.Handlers {
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.IO.Compression;
    using System.Reflection;
    using System.Resources;
    using System.Security;
    using System.Security.Cryptography;
    using System.Security.Policy;
    using System.Text;
    using System.Web;
    using System.Web.Configuration;
    using System.Web.Hosting;
    using System.Web.Resources;
    using System.Web.Security.Cryptography;
    using System.Web.UI;
    using System.Web.Util;
    using Debug = System.Diagnostics.Debug;
 
    public class ScriptResourceHandler : IHttpHandler {
 
        private const string _scriptResourceUrl = "~/ScriptResource.axd";
        private static readonly IDictionary _assemblyInfoCache = Hashtable.Synchronized(new Hashtable());
        private static readonly IDictionary _cultureCache = Hashtable.Synchronized(new Hashtable());
        private static readonly Object _getMethodLock = new Object();
        private static IScriptResourceHandler _scriptResourceHandler = new RuntimeScriptResourceHandler();
        private static string _scriptResourceAbsolutePath;
        // _bypassVirtualPathResolution set by unit tests to avoid resolving ~/ paths from unit tests.
        private static bool _bypassVirtualPathResolution = false;
        private static int _maximumResourceUrlLength = 2048;
 
        private static string ScriptResourceAbsolutePath {
            get {
                if (_scriptResourceAbsolutePath == null) {
                    _scriptResourceAbsolutePath = VirtualPathUtility.ToAbsolute(_scriptResourceUrl);
                }
                return _scriptResourceAbsolutePath;
            }
        }
 
        private static Exception Create404(Exception innerException) {
            return new HttpException(404, AtlasWeb.ScriptResourceHandler_InvalidRequest, innerException);
        }
 
        internal static CultureInfo DetermineNearestAvailableCulture(
            Assembly assembly,
            string scriptResourceName,
            CultureInfo culture) {
 
            if (String.IsNullOrEmpty(scriptResourceName)) return CultureInfo.InvariantCulture;
 
            Tuple<Assembly, string, CultureInfo> cacheKey = Tuple.Create(assembly, scriptResourceName, culture);
            CultureInfo cachedCulture = (CultureInfo)_cultureCache[cacheKey];
            if (cachedCulture == null) {
 
                string releaseResourceName =
                    scriptResourceName.EndsWith(".debug.js", StringComparison.OrdinalIgnoreCase) ?
                    scriptResourceName.Substring(0, scriptResourceName.Length - 9) + ".js" :
                    null;
 
                ScriptResourceInfo resourceInfo = ScriptResourceInfo.GetInstance(assembly, scriptResourceName);
                ScriptResourceInfo releaseResourceInfo = (releaseResourceName != null) ?
                    ScriptResourceInfo.GetInstance(assembly, releaseResourceName) : null;
 
                if (!String.IsNullOrEmpty(resourceInfo.ScriptResourceName) ||
                    ((releaseResourceInfo != null) && !String.IsNullOrEmpty(releaseResourceInfo.ScriptResourceName))) {
 
                    ResourceManager resourceManager =
                        ScriptResourceAttribute.GetResourceManager(resourceInfo.ScriptResourceName, assembly);
                    ResourceManager releaseResourceManager = (releaseResourceInfo != null) ?
                        ScriptResourceAttribute.GetResourceManager(releaseResourceInfo.ScriptResourceName, assembly) : null;
 
                    ResourceSet localizedSet = null;
                    ResourceSet releaseSet = null;
                    if (resourceManager != null) {
                        resourceManager.GetResourceSet(CultureInfo.InvariantCulture, true, true);
                        // Look for the explicitly localized version of the resources that is nearest the culture.
                        localizedSet = resourceManager.GetResourceSet(culture, true, false);
                    }
                    if (releaseResourceManager != null) {
                        releaseResourceManager.GetResourceSet(CultureInfo.InvariantCulture, true, true);
                        // Look for the explicitly localized version of the resources that is nearest the culture.
                        releaseSet = releaseResourceManager.GetResourceSet(culture, true, false);
                    }
                    if ((resourceManager != null) || (releaseResourceManager != null)) {
                        while ((localizedSet == null) && (releaseSet == null)) {
                            culture = culture.Parent;
                            if (culture.Equals(CultureInfo.InvariantCulture)) break;
                            localizedSet = resourceManager.GetResourceSet(culture, true, false);
                            releaseSet = (releaseResourceManager != null) ?
                                releaseResourceManager.GetResourceSet(culture, true, false) : null;
                        }
                    }
                    else {
                        culture = CultureInfo.InvariantCulture;
                    }
                }
                else {
                    culture = CultureInfo.InvariantCulture;
                }
                // Neutral assembly culture falls back on invariant
                CultureInfo neutralCulture = GetAssemblyNeutralCulture(assembly);
                if ((neutralCulture != null) && neutralCulture.Equals(culture)) {
                    culture = CultureInfo.InvariantCulture;
                }
                cachedCulture = culture;
                _cultureCache[cacheKey] = cachedCulture;
            }
            return cachedCulture;
        }
 
        private static void EnsureScriptResourceRequest(string path) {
            if (!IsScriptResourceRequest(path)) {
                Throw404();
            }
        }
 
        private static Assembly GetAssembly(string assemblyName) {
            Debug.Assert(!String.IsNullOrEmpty(assemblyName));
            string[] parts = assemblyName.Split(',');
 
            if ((parts.Length != 1) && (parts.Length != 4)) {
                Throw404();
            }
 
            AssemblyName realName = new AssemblyName();
            realName.Name = parts[0];
            if (parts.Length == 4) {
                realName.Version = new Version(parts[1]);
                string cultureString = parts[2];
                realName.CultureInfo = (cultureString.Length > 0) ?
                    new CultureInfo(cultureString) :
                    CultureInfo.InvariantCulture;
                realName.SetPublicKeyToken(HexParser.Parse(parts[3]));
            }
            Assembly assembly = null;
            try {
                assembly = Assembly.Load(realName);
            }
            catch (FileNotFoundException fnf) {
                Throw404(fnf);
            }
            catch (FileLoadException fl) {
                Throw404(fl);
            }
            catch (BadImageFormatException badImage) {
                Throw404(badImage);
            }
 
            return assembly;
        }
 
        private static Tuple<AssemblyName, String> GetAssemblyInfo(Assembly assembly) {
            Tuple<AssemblyName, String> assemblyInfo =
                (Tuple<AssemblyName, String>)_assemblyInfoCache[assembly];
            if (assemblyInfo == null) {
                assemblyInfo = GetAssemblyInfoInternal(assembly);
                _assemblyInfoCache[assembly] = assemblyInfo;
            }
            Debug.Assert(assemblyInfo != null, "Assembly info should not be null");
            return assemblyInfo;
        }
 
        private static Tuple<AssemblyName, String> GetAssemblyInfoInternal(Assembly assembly) {
            AssemblyName assemblyName = new AssemblyName(assembly.FullName);
            string hash = Convert.ToBase64String(assembly.ManifestModule.ModuleVersionId.ToByteArray());
            return new Tuple<AssemblyName, String>(assemblyName, hash);
        }
 
        private static CultureInfo GetAssemblyNeutralCulture(Assembly assembly) {
            CultureInfo neutralCulture = (CultureInfo)_cultureCache[assembly];
            if (neutralCulture == null) {
                object[] nrlas = assembly.GetCustomAttributes(typeof(NeutralResourcesLanguageAttribute), false);
                if ((nrlas != null) && (nrlas.Length != 0)) {
                    neutralCulture = CultureInfo.GetCultureInfo(
                        ((NeutralResourcesLanguageAttribute)nrlas[0]).CultureName);
                    _cultureCache[assembly] = neutralCulture;
                }
            }
            return neutralCulture;
        }
 
        internal static string GetEmptyPageUrl(string title) {
            return GetScriptResourceHandler().GetEmptyPageUrl(title);
        }
 
        private static IScriptResourceHandler GetScriptResourceHandler() {
            if (_scriptResourceHandler == null) {
                _scriptResourceHandler = new RuntimeScriptResourceHandler();
            }
            return _scriptResourceHandler;
        }
 
        internal static string GetScriptResourceUrl(
            Assembly assembly,
            string resourceName,
            CultureInfo culture,
            bool zip) {
 
            return GetScriptResourceHandler()
                .GetScriptResourceUrl(assembly, resourceName, culture, zip);
        }
 
        internal static string GetScriptResourceUrl(
            List<Tuple<Assembly, List<Tuple<string, CultureInfo>>>> assemblyResourceLists,
            bool zip) {
 
            return GetScriptResourceHandler().GetScriptResourceUrl(assemblyResourceLists, zip);
        }
 
        protected virtual bool IsReusable {
            get {
                return true;
            }
        }
 
        internal delegate string VirtualFileReader(string virtualPath, out Encoding encoding);
 
        private static bool IsCompressionEnabled(HttpContext context) {
            return ScriptingScriptResourceHandlerSection.ApplicationSettings.EnableCompression &&
                ((context == null) ||
                !context.Request.Browser.IsBrowser("IE") ||
                (context.Request.Browser.MajorVersion > 6));
        }
 
        internal static bool IsScriptResourceRequest(string path) {
            return !String.IsNullOrEmpty(path) &&
                String.Equals(path, ScriptResourceAbsolutePath, StringComparison.OrdinalIgnoreCase);
        }
 
        private static void OutputEmptyPage(HttpResponseBase response, string title) {
            PrepareResponseCache(response);
            response.Write(@"<!DOCTYPE html PUBLIC ""-//W3C//DTD XHTML 1.0 Transitional//EN"" ""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"">
<html xmlns=""http://www.w3.org/1999/xhtml""><head><script type=""text/javascript"">parent.Sys.Application._onIFrameLoad();</script><title>" +
                           HttpUtility.HtmlEncode(title) +
                           @"</title></head><body></body></html>");
        }
 
        private static void PrepareResponseCache(HttpResponseBase response) {
            HttpCachePolicyBase cachePolicy = response.Cache;
            DateTime now = DateTime.Now;
            cachePolicy.SetCacheability(HttpCacheability.Public);
            cachePolicy.VaryByParams["d"] = true;
            cachePolicy.SetOmitVaryStar(true);
            cachePolicy.SetExpires(now + TimeSpan.FromDays(365));
            cachePolicy.SetValidUntilExpires(true);
            cachePolicy.SetLastModified(now);
        }
 
        private static void PrepareResponseNoCache(HttpResponseBase response) {
            HttpCachePolicyBase cachePolicy = response.Cache;
            DateTime now = DateTime.Now;
            cachePolicy.SetCacheability(HttpCacheability.Public);
            cachePolicy.SetExpires(now + TimeSpan.FromDays(365));
            cachePolicy.SetValidUntilExpires(true);
            cachePolicy.SetLastModified(now);
            cachePolicy.SetNoServerCaching();
        }
 
        [SecuritySafeCritical]
        protected virtual void ProcessRequest(HttpContext context) {
            ProcessRequest(new HttpContextWrapper(context));
        }
 
        internal static void ProcessRequest(HttpContextBase context, VirtualFileReader fileReader = null, Action<string, Exception> logAction = null, bool validatePath = true) {
            string decryptedString = null;
            bool resourceIdentifierPresent = false;
            try {
                HttpResponseBase response = context.Response;
                response.Clear();
 
                if (validatePath) {
                    // Checking that the handler is not being called from a different path.
                    EnsureScriptResourceRequest(context.Request.Path);
                }
 
                string encryptedData = context.Request.QueryString["d"];
                if (String.IsNullOrEmpty(encryptedData)) {
                    Throw404();
                }
 
                resourceIdentifierPresent = true;
                try {
                    decryptedString = Page.DecryptString(encryptedData, Purpose.ScriptResourceHandler_ScriptResourceUrl);
                }
                catch (CryptographicException ex) {
                    Throw404(ex);
                }
 
                fileReader = fileReader ?? new VirtualFileReader(delegate(string virtualPath, out Encoding encoding) {
                    VirtualPathProvider vpp = HostingEnvironment.VirtualPathProvider;
                    if (!vpp.FileExists(virtualPath)) {
                        Throw404();
                    }
                    VirtualFile file = vpp.GetFile(virtualPath);
                    if (!AppSettings.ScriptResourceAllowNonJsFiles && !file.Name.EndsWith(".js", StringComparison.OrdinalIgnoreCase)) {
                        // MSRC 10405: Disallow all extensions other than *.js
                        Throw404();
                    }
                    using (Stream stream = file.Open()) {
                        using (StreamReader reader = new StreamReader(stream, true)) {
                            encoding = reader.CurrentEncoding;
                            return reader.ReadToEnd();
                        }
                    }
                });
                ProcessRequestInternal(response, decryptedString, fileReader);
            } catch(Exception e) {
                if (resourceIdentifierPresent) {
                    logAction = logAction ?? AssemblyResourceLoader.LogWebResourceFailure;
                    logAction(decryptedString, e);
                }
                // MSRC 10405: There's no reason for this to return anything other than a 404 if something
                // goes wrong. We shouldn't propagate the inner exception inside the YSOD, as it might
                // contain sensitive cryptographic information.
                Throw404();
            }
        }
 
        private static void ProcessRequestInternal(
            HttpResponseBase response,
            string decryptedString,
            VirtualFileReader fileReader) {
 
            if (String.IsNullOrEmpty(decryptedString)) {
                Throw404();
            }
            bool zip;
            bool singleAssemblyReference;
            // See GetScriptResourceUrl comment below for first character meanings.
            switch (decryptedString[0]) {
                case 'Z':
                case 'z':
                    singleAssemblyReference = true;
                    zip = true;
                    break;
                case 'U':
                case 'u':
                    singleAssemblyReference = true;
                    zip = false;
                    break;
                case 'Q':
                case 'q':
                    singleAssemblyReference = false;
                    zip = true;
                    break;
                case 'R':
                case 'r':
                    singleAssemblyReference = false;
                    zip = false;
                    break;
                case 'T':
                    OutputEmptyPage(response, decryptedString.Substring(1));
                    return;
                default:
                    Throw404();
                    return;
            }
 
            decryptedString = decryptedString.Substring(1);
            if (String.IsNullOrEmpty(decryptedString)) {
                Throw404();
            }
            string[] decryptedData = decryptedString.Split('|');
 
            if (singleAssemblyReference) {
                // expected: <assembly>|<resource>|<culture>[|#|<hash>]
                if (decryptedData.Length != 3 && decryptedData.Length != 5) {
                    // The decrypted data must have 3 parts plus an optional 2 part hash code separated by pipes.
                    Throw404();
                }
            }
            else {
                // expected: <assembly1>|<resource1a>,<culture1a>,<resource1b>,<culture1b>,...|<assembly2>|<resource2a>,<culture2a>,<resource2b>,<culture2b>,...|#|<hash>
                if (decryptedData.Length % 2 != 0) {
                    // The decrypted data must have an even number of parts separated by pipes.
                    Throw404();
                }
            }
 
            StringBuilder script = new StringBuilder();
 
            string firstContentType = null;
 
            if (singleAssemblyReference) {
                // single assembly reference, format is
                // <assembly>|<resource>|<culture>
                string assemblyName = decryptedData[0];
                string resourceName = decryptedData[1];
                string cultureName = decryptedData[2];
 
                Assembly assembly = GetAssembly(assemblyName);
                if (assembly == null) {
                    Throw404();
                }
 
                script.Append(ScriptResourceAttribute.GetScriptFromWebResourceInternal(
                    assembly,
                    resourceName,
                    String.IsNullOrEmpty(cultureName) ? CultureInfo.InvariantCulture : new CultureInfo(cultureName),
                    zip,
                    out firstContentType
                ));
            }
            else {
                // composite script reference, format is:
                // <assembly1>|<resource1a>,<culture1a>,<resource1b>,<culture1b>,...|<assembly2>|<resource2a>,<culture2a>,<resource2b>,<culture2b>,...
                // Assembly is empty for path based scripts, and their resource/culture list is <path1>,<path2>,...
                
                // If an assembly starts with "#", the segment is ignored (expected that this includes a hash to ensure
                // url uniqueness when resources are changed). Also, for forward compatibility '#' segments may contain
                // other data. 
 
                bool needsNewline = false;
 
                for (int i = 0; i < decryptedData.Length; i += 2) {
                    string assemblyName = decryptedData[i];
                    bool hasAssembly = !String.IsNullOrEmpty(assemblyName);
                    if (hasAssembly && assemblyName[0] == '#') {
                        // hash segments are ignored, it contains a hash code for url uniqueness
                        continue;
                    }
                    Debug.Assert(!String.IsNullOrEmpty(decryptedData[i + 1]));
                    string[] resourcesAndCultures = decryptedData[i + 1].Split(',');
 
                    if (resourcesAndCultures.Length == 0) {
                        Throw404();
                    }
 
                    Assembly assembly = hasAssembly ? GetAssembly(assemblyName) : null;
 
                    if (assembly == null) {
                        // The scripts are path-based
                        if (firstContentType == null) {
                            firstContentType = "text/javascript";
                        }
                        for (int j = 0; j < resourcesAndCultures.Length; j++) {
                            Encoding encoding;
                            // DevDiv Bugs 197242
                            // path will either be absolute, as in "/app/foo/bar.js" or app relative, as in "~/foo/bar.js"
                            // ToAbsolute() ensures it is in the form /app/foo/bar.js
                            // This conversion was not done when the url was created to conserve url length.
                            string path = _bypassVirtualPathResolution ?
                                resourcesAndCultures[j] :
                                VirtualPathUtility.ToAbsolute(resourcesAndCultures[j]);
                            string fileContents = fileReader(path, out encoding);
 
                            if (needsNewline) {
                                // Output an additional newline between resources but not for the last one
                                script.Append('\n');
                            }
                            needsNewline = true;
 
                            script.Append(fileContents);
                        }
                    }
                    else {
                        Debug.Assert(resourcesAndCultures.Length % 2 == 0, "The list of resource names and cultures must have an even number of parts separated by commas.");
 
                        for (int j = 0; j < resourcesAndCultures.Length; j += 2) {
                            try {
                                string contentType;
                                string resourceName = resourcesAndCultures[j];
                                string cultureName = resourcesAndCultures[j + 1];
 
                                if (needsNewline) {
                                    // Output an additional newline between resources but not for the last one
                                    script.Append('\n');
                                }
                                needsNewline = true;
 
                                script.Append(ScriptResourceAttribute.GetScriptFromWebResourceInternal(
                                    assembly,
                                    resourceName,
                                    String.IsNullOrEmpty(cultureName) ? CultureInfo.InvariantCulture : new CultureInfo(cultureName),
                                    zip,
                                    out contentType
                                ));
 
                                if (firstContentType == null) {
                                    firstContentType = contentType;
                                }
                            }
                            catch (MissingManifestResourceException ex) {
                                throw Create404(ex);
                            }
                            catch (HttpException ex) {
                                throw Create404(ex);
                            }
                        }
                    }
                }
            }
 
            if (ScriptingScriptResourceHandlerSection.ApplicationSettings.EnableCaching) {
                PrepareResponseCache(response);
            }
            else {
                PrepareResponseNoCache(response);
            }
 
            response.ContentType = firstContentType;
 
            if (zip) {
                using (MemoryStream zipped = new MemoryStream()) {
                    using (Stream outputStream = new GZipStream(zipped, CompressionMode.Compress)) {
                        // The choice of an encoding matters little here.
                        // Input streams being of potentially different encodings, UTF-8 is the better
                        // choice as it's the natural encoding for JavaScript.
                        using (StreamWriter writer = new StreamWriter(outputStream, Encoding.UTF8)) {
                            writer.Write(script.ToString());
                        }
                    }
                    byte[] zippedBytes = zipped.ToArray();
                    response.AddHeader("Content-encoding", "gzip");
                    response.OutputStream.Write(zippedBytes, 0, zippedBytes.Length);
                }
            }
            else {
                // Bug DevDiv #175061, we don't want to force any encoding here and let the default
                // encoding apply no matter what the incoming scripts might have been encoded with.
                response.Write(script.ToString());
            }
        }
 
        internal static void SetScriptResourceHandler(IScriptResourceHandler scriptResourceHandler) {
            _scriptResourceHandler = scriptResourceHandler;
        }
 
        private static void Throw404() {
            throw Create404(null);
        }
 
        private static void Throw404(Exception innerException) {
            throw Create404(innerException);
        }
 
        #region IHttpHandler implementation
        void IHttpHandler.ProcessRequest(HttpContext context) {
            ProcessRequest(context);
        }
 
        bool IHttpHandler.IsReusable {
            get {
                return IsReusable;
            }
        }
        #endregion
 
        private class RuntimeScriptResourceHandler : IScriptResourceHandler {
 
            // Keys in the URL cache will be IList objects, so use our custom list comparer.
            private static readonly IDictionary _urlCache = Hashtable.Synchronized(new Hashtable(ListEqualityComparer.Instance));
            private static readonly IDictionary _cultureCache = Hashtable.Synchronized(new Hashtable());
            private static string _absoluteScriptResourceUrl;
 
            string IScriptResourceHandler.GetScriptResourceUrl(
                Assembly assembly, string resourceName, CultureInfo culture, bool zip) {
 
                return ((IScriptResourceHandler)this).GetScriptResourceUrl(
                    new List<Tuple<Assembly, List<Tuple<string, CultureInfo>>>>() {
                        new Tuple<Assembly, List<Tuple<string, CultureInfo>>>(
                            assembly,
                            new List<Tuple<string,CultureInfo>>() {
                                new Tuple<string, CultureInfo>(resourceName, culture)
                            }
                        )
                    }, zip);
            }
 
            string IScriptResourceHandler.GetScriptResourceUrl(
                List<Tuple<Assembly, List<Tuple<string, CultureInfo>>>> assemblyResourceLists,
                bool zip) {
 
                if (!IsCompressionEnabled(HttpContext.Current)) {
                    zip = false;
                }
 
                bool allAssemblyResources = true;
                foreach (Tuple<Assembly, List<Tuple<string, CultureInfo>>> assemblyData in assemblyResourceLists) {
                    if (assemblyData.Item1 == null) {
                        allAssemblyResources = false;
                        break;
                    }
                }
                
                // If all the scripts are assembly resources, we can cache the generated ScriptResource URL, since
                // the appdomain will reset if any of the assemblies are changed.  We cannot cache the URL if any
                // scripts are path-based, since the cache entry will not be removed if a path-based script is changed.
                if (allAssemblyResources) {
                    List<object> cacheKeys = new List<object>();
                    
                    foreach (Tuple<Assembly, List<Tuple<string, CultureInfo>>> assemblyData in assemblyResourceLists) {
                        cacheKeys.Add(assemblyData.Item1);
                        foreach (Tuple<string, CultureInfo> resourceAndCulture in assemblyData.Item2) {
                            cacheKeys.Add(resourceAndCulture.Item1);
                            cacheKeys.Add(resourceAndCulture.Item2);
                        }
                    }
 
                    cacheKeys.Add(zip);
 
                    string url = (string)_urlCache[cacheKeys];
 
                    if (url == null) {
                        url = GetScriptResourceUrlImpl(assemblyResourceLists, zip);
                        _urlCache[cacheKeys] = url;
                    }
 
                    return url;
                }
                else {
                    return GetScriptResourceUrlImpl(assemblyResourceLists, zip);
                }
            }
 
            [SecuritySafeCritical]
            private static string GetScriptResourceUrlImpl(
                List<Tuple<Assembly, List<Tuple<string, CultureInfo>>>> assemblyResourceLists,
                bool zip) {
 
                EnsureAbsoluteScriptResourceUrl();
 
                // If there's only a single assembly resource, format is
                //      [Z|U|z|u]<assembly>|<resource>|<culture>
                // If there are multiple resources, or a single resource that is path based, format is
                //      [Q|R|q|r]<assembly1>|<resource1a>,<culture1a>,<resource1b>,<culture1b>...|<assembly2>|<resource2a>,<culture2a>,<resource2b>,<culture2b>...
                // A path based reference has no assembly (empty).
                // (the Q/R indicators used in place of Z/U give the handler indiciation that the url is a composite
                // reference, and allows for System.Web.Extensions SP1 to maintain compatibility with RTM, should a
                // single resource be encrypted with SP1 and decrypted with RTM).
 
                bool singleAssemblyResource = false;
                if (assemblyResourceLists.Count == 1) {
                    // only one assembly to pull from...
                    var reference = assemblyResourceLists[0];
                    if ((reference.Item1 != null) && (reference.Item2.Count == 1)) {
                        // resource is assembly not path, and there's only one resource within it to load
                        singleAssemblyResource = true;
                    }
                }
 
                // Next character of the encoded string is:
                // Format: S = Single Assembly Reference, C = Composite Reference or Single Path Reference
                // Zip: compress or not (true or false)
                // First    Format  Zip?
                // =====================
                // Z        S       T   
                // U        S       F   
                // Q        C       T   
                // R        C       F   
 
                string indicator;
                if (singleAssemblyResource) {
                    indicator = (zip ? "Z" : "U");
                }
                else {
                    indicator = (zip ? "Q" : "R");
                }
 
                StringBuilder url = new StringBuilder(indicator);
 
                HashCodeCombiner hashCombiner = new HashCodeCombiner();
 
                bool firstAssembly = true;
                foreach (Tuple<Assembly, List<Tuple<string, CultureInfo>>> assemblyData in assemblyResourceLists) {
 
                    if (!firstAssembly) {
                        url.Append('|');
                    }
                    else {
                        firstAssembly = false;
                    }
                    if (assemblyData.Item1 != null) {
                        Tuple<AssemblyName, String> assemblyInfo = GetAssemblyInfo(assemblyData.Item1);
 
                        AssemblyName assemblyName = (AssemblyName)assemblyInfo.Item1;
                        string assemblyHash = (String)assemblyInfo.Item2;
                        hashCombiner.AddObject(assemblyHash);
 
                        if (assemblyData.Item1.GlobalAssemblyCache) {
                            // If the assembly is in the GAC, we need to store a full name to load the assembly later
                            // Pack the necessary values into a more compact format than FullName
                            url.Append(assemblyName.Name);
                            url.Append(',');
                            url.Append(assemblyName.Version);
                            url.Append(',');
                            if (assemblyName.CultureInfo != null) {
                                url.Append(assemblyName.CultureInfo);
                            }
                            url.Append(',');
                            url.Append(HexParser.ToString(assemblyName.GetPublicKeyToken()));
                        }
                        else {
                            // Otherwise, we can just use a partial name
                            url.Append(assemblyName.Name);
                        }
                    }
                    url.Append('|');
 
                    bool firstResource = true;
                    foreach (Tuple<string, CultureInfo> resourceAndCulture in assemblyData.Item2) {
 
                        if (!firstResource) {
                            url.Append(',');
                        }
 
                        if (assemblyData.Item1 != null) {
                            url.Append(resourceAndCulture.Item1);
                            Tuple<Assembly, string, CultureInfo> cacheKey = Tuple.Create(
                                assemblyData.Item1,
                                resourceAndCulture.Item1,
                                resourceAndCulture.Item2
                            );
                            string cultureName = (string)_cultureCache[cacheKey];
                            if (cultureName == null) {
                                // Check if the resources exist
                                ScriptResourceInfo resourceInfo =
                                    ScriptResourceInfo.GetInstance(assemblyData.Item1, resourceAndCulture.Item1);
                                if (resourceInfo == ScriptResourceInfo.Empty) {
                                    ThrowUnknownResource(resourceAndCulture.Item1);
                                }
                                Stream scriptStream = assemblyData.Item1.GetManifestResourceStream(resourceInfo.ScriptName);
                                if (scriptStream == null) {
                                    ThrowUnknownResource(resourceAndCulture.Item1);
                                }
                                cultureName = DetermineNearestAvailableCulture(
                                    assemblyData.Item1, resourceAndCulture.Item1, resourceAndCulture.Item2).Name;
                                _cultureCache[cacheKey] = cultureName;
                            }
                            url.Append(singleAssemblyResource ? "|" : ",");
                            url.Append(cultureName);
                        }
                        else {
                            Debug.Assert(!singleAssemblyResource, "This should never happen since this is a path reference.");
 
                            if (!_bypassVirtualPathResolution) {
                                VirtualPathProvider vpp = HostingEnvironment.VirtualPathProvider;
                                if (!vpp.FileExists(resourceAndCulture.Item1)) {
                                    ThrowUnknownResource(resourceAndCulture.Item1);
                                }
                                string hash = vpp.GetFileHash(resourceAndCulture.Item1, new string[] { resourceAndCulture.Item1 });
                                hashCombiner.AddObject(hash);
                            }
                            url.Append(resourceAndCulture.Item1);
                        }
                        firstResource = false;
                    }
                }
 
                // DevDiv Bugs 186624: The hash code needs to be part of the encrypted blob for composite scripts
                // because we cache the composite script on the server using a VaryByParam["d"]. Otherwise, if a
                // path based script in the composite changes, only the 't' parameter would change, which would
                // cause a new request to the server, but it would be served via cache since 'd' would be the same.
                // This isn't a problem for assembly based resources since changing them also restarts the app and
                // clears the cache. We do not vary by 't' because that makes it possible to flood the server cache
                // with cache entries, since anything could be used for 't'. Putting the hash in 'd' ensures a different
                // url and different cache entry when a script changes, but without the possibility of flooding
                // the server cache.
 
                // However, we continue to use the 't' parameter for single assembly references for compatibility.
 
                string resourceUrl;
                if (singleAssemblyResource) {
                    resourceUrl = _absoluteScriptResourceUrl +
                            Page.EncryptString(url.ToString(), Purpose.ScriptResourceHandler_ScriptResourceUrl) +
                            "&t=" + hashCombiner.CombinedHashString;
                }
                else {
                    // note that CombinedHashString is hex, it will never include a '|' that would confuse the handler.
                    url.Append("|#|");
                    url.Append(hashCombiner.CombinedHashString);
                    resourceUrl = _absoluteScriptResourceUrl +
                            Page.EncryptString(url.ToString(), Purpose.ScriptResourceHandler_ScriptResourceUrl);
                }
 
                if (resourceUrl.Length > _maximumResourceUrlLength) {
                    throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, AtlasWeb.ScriptResourceHandler_ResourceUrlTooLong, _maximumResourceUrlLength));
                }
                return resourceUrl;
            }
 
            private static void EnsureAbsoluteScriptResourceUrl() {
                if (_absoluteScriptResourceUrl == null) {
                    _absoluteScriptResourceUrl = _bypassVirtualPathResolution ?
                        _scriptResourceUrl + "?d=" :
                        VirtualPathUtility.ToAbsolute(_scriptResourceUrl) + "?d=";
                }
            }
 
            string IScriptResourceHandler.GetEmptyPageUrl(string title) {
                EnsureAbsoluteScriptResourceUrl();
                return _absoluteScriptResourceUrl +
                        Page.EncryptString('T' + title, Purpose.ScriptResourceHandler_ScriptResourceUrl);
            }
 
            private static void ThrowUnknownResource(string resourceName) {
                throw new HttpException(String.Format(CultureInfo.CurrentCulture,
                    AtlasWeb.ScriptResourceHandler_UnknownResource, resourceName));
            }
 
        }
    }
}