File: System\Xml\Xsl\Runtime\XmlCollation.cs
Web Access
Project: src\src\libraries\System.Private.Xml\src\System.Private.Xml.csproj (System.Private.Xml)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
 
namespace System.Xml.Xsl.Runtime
{
    [EditorBrowsable(EditorBrowsableState.Never)]
    public sealed partial class XmlCollation
    {
        // lgid support for sort
        private const string deDE = "de-DE";
        private const string huHU = "hu-HU";
        private const string jaJP = "ja-JP";
        private const string kaGE = "ka-GE";
        private const string koKR = "ko-KR";
        private const string zhTW = "zh-TW";
        private const string zhCN = "zh-CN";
        private const string zhHK = "zh-HK";
        private const string zhSG = "zh-SG";
        private const string zhMO = "zh-MO";
        private const string zhTWbopo = "zh-TW_pronun";
        private const string deDEphon = "de-DE_phoneb";
        private const string huHUtech = "hu-HU_technl";
        private const string kaGEmode = "ka-GE_modern";
 
        // Invariant: compops == (options & Options.mask)
        private readonly CultureInfo? _cultInfo;
        private Options _options;
        private readonly CompareOptions _compops;
 
 
        /// <summary>
        /// Extends System.Globalization.CompareOptions with additional flags.
        /// </summary>
        private struct Options
        {
            public const int FlagUpperFirst = 0x1000;
            public const int FlagEmptyGreatest = 0x2000;
            public const int FlagDescendingOrder = 0x4000;
 
            private const int Mask = FlagUpperFirst | FlagEmptyGreatest | FlagDescendingOrder;
 
            private int _value;
 
            public Options(int value)
            {
                _value = value;
            }
 
            public bool GetFlag(int flag)
            {
                return (_value & flag) != 0;
            }
 
            public void SetFlag(int flag, bool value)
            {
                if (value)
                    _value |= flag;
                else
                    _value &= ~flag;
            }
 
            public bool UpperFirst
            {
                get { return GetFlag(FlagUpperFirst); }
                set { SetFlag(FlagUpperFirst, value); }
            }
 
            public bool EmptyGreatest
            {
                get { return GetFlag(FlagEmptyGreatest); }
            }
 
            public bool DescendingOrder
            {
                get { return GetFlag(FlagDescendingOrder); }
            }
 
            public bool IgnoreCase
            {
                get { return GetFlag((int)CompareOptions.IgnoreCase); }
            }
 
            public bool Ordinal
            {
                get { return GetFlag((int)CompareOptions.Ordinal); }
            }
 
            public CompareOptions CompareOptions
            {
                get
                {
                    return (CompareOptions)(_value & ~Mask);
                }
                set
                {
                    Debug.Assert(((int)value & Mask) == 0);
                    _value = (_value & Mask) | (int)value;
                }
            }
 
            public static implicit operator int(Options options)
            {
                return options._value;
            }
        }
 
 
        //-----------------------------------------------
        // Constructors
        //-----------------------------------------------
 
        /// <summary>
        /// Construct a collation that uses the specified culture and compare options.
        /// </summary>
        private XmlCollation(CultureInfo? cultureInfo, Options options)
        {
            _cultInfo = cultureInfo;
            _options = options;
            _compops = options.CompareOptions;
        }
 
 
        //-----------------------------------------------
        // Create
        //-----------------------------------------------
 
        /// <summary>
        /// Singleton collation that sorts according to Unicode code points.
        /// </summary>
        private static readonly XmlCollation s_cp = new XmlCollation(CultureInfo.InvariantCulture, new Options((int)CompareOptions.Ordinal));
 
        internal static XmlCollation CodePointCollation
        {
            get { return s_cp; }
        }
 
        internal static XmlCollation Create(string collationLiteral)
        {
            return Create(collationLiteral, throwOnError: true)!;
        }
 
        // This function is used in both parser and F&O library, so just strictly map valid literals to XmlCollation.
        // Set compare options one by one:
        //     0, false: no effect; 1, true: yes
        // Disregard unrecognized options.
        internal static XmlCollation? Create(string collationLiteral, bool throwOnError)
        {
            Debug.Assert(collationLiteral != null, "collation literal should not be null");
 
            if (collationLiteral == XmlReservedNs.NsCollCodePoint)
            {
                return CodePointCollation;
            }
 
            Uri? collationUri;
            CultureInfo? cultInfo = null;
            Options options = default;
 
            if (throwOnError)
            {
                collationUri = new Uri(collationLiteral);
            }
            else
            {
                if (!Uri.TryCreate(collationLiteral, UriKind.Absolute, out collationUri))
                {
                    return null;
                }
            }
            string authority = collationUri.GetComponents(UriComponents.Scheme | UriComponents.UserInfo | UriComponents.Host | UriComponents.Port, UriFormat.UriEscaped);
            if (authority == XmlReservedNs.NsCollationBase)
            {
                // Language
                // at least a '/' will be returned for Uri.LocalPath
                string lang = collationUri.LocalPath.Substring(1);
                if (lang.Length == 0)
                {
                    // Use default culture of current thread (cultinfo = null)
                }
                else
                {
                    // Create culture from RFC 1766 string
                    try
                    {
                        cultInfo = new CultureInfo(lang);
                    }
                    catch (ArgumentException)
                    {
                        if (!throwOnError) return null;
                        throw new XslTransformException(SR.Coll_UnsupportedLanguage, lang);
                    }
                }
            }
            else if (collationUri.IsBaseOf(new Uri(XmlReservedNs.NsCollCodePoint)))
            {
                // language with codepoint collation is not allowed
                options.CompareOptions = CompareOptions.Ordinal;
            }
            else
            {
                // Unrecognized collation
                if (!throwOnError) return null;
                throw new XslTransformException(SR.Coll_Unsupported, collationLiteral);
            }
 
            // Sort & Compare option
            // at least a '?' will be returned for Uri.Query if not empty
            string query = collationUri.Query;
            string? sort = null;
 
            if (query.Length != 0)
            {
                foreach (string option in query.Substring(1).Split('&'))
                {
                    string[] pair = option.Split('=');
 
                    if (pair.Length != 2)
                    {
                        if (!throwOnError) return null;
                        throw new XslTransformException(SR.Coll_BadOptFormat, option);
                    }
 
                    string optionName = pair[0].ToUpperInvariant();
                    string optionValue = pair[1].ToUpperInvariant();
 
                    if (optionName == "SORT")
                    {
                        sort = optionValue;
                    }
                    else
                    {
                        int flag;
 
                        switch (optionName)
                        {
                            case "IGNORECASE": flag = (int)CompareOptions.IgnoreCase; break;
                            case "IGNORENONSPACE": flag = (int)CompareOptions.IgnoreNonSpace; break;
                            case "IGNORESYMBOLS": flag = (int)CompareOptions.IgnoreSymbols; break;
                            case "IGNOREKANATYPE": flag = (int)CompareOptions.IgnoreKanaType; break;
                            case "IGNOREWIDTH": flag = (int)CompareOptions.IgnoreWidth; break;
                            case "UPPERFIRST": flag = Options.FlagUpperFirst; break;
                            case "EMPTYGREATEST": flag = Options.FlagEmptyGreatest; break;
                            case "DESCENDINGORDER": flag = Options.FlagDescendingOrder; break;
                            default:
                                if (!throwOnError) return null;
                                throw new XslTransformException(SR.Coll_UnsupportedOpt, pair[0]);
                        }
 
                        switch (optionValue)
                        {
                            case "0": case "FALSE": options.SetFlag(flag, false); break;
                            case "1": case "TRUE": options.SetFlag(flag, true); break;
                            default:
                                if (!throwOnError) return null;
                                throw new XslTransformException(SR.Coll_UnsupportedOptVal, pair[0], pair[1]);
                        }
                    }
                }
            }
 
            // upperfirst option is only meaningful when not ignore case
            if (options.UpperFirst && options.IgnoreCase)
                options.UpperFirst = false;
 
            // other CompareOptions are only meaningful if Ordinal comparison is not being used
            if (options.Ordinal)
            {
                options.CompareOptions = CompareOptions.Ordinal;
                options.UpperFirst = false;
            }
 
            // new cultureinfo based on alternate sorting option
            if (sort != null && cultInfo != null)
            {
                string cultName = cultInfo.Name;
                switch (sort)
                {
                    case "bopo":
                        if (cultName == zhTW)
                        {
                            cultInfo = new CultureInfo(zhTWbopo);
                        }
                        break;
                    case "strk":
                        if (cultName == zhCN || cultName == zhHK || cultName == zhSG || cultName == zhMO)
                        {
                            cultInfo = new CultureInfo(cultName);
                        }
                        break;
                    case "uni":
                        if (cultName == jaJP || cultName == koKR)
                        {
                            cultInfo = new CultureInfo(cultName);
                        }
                        break;
                    case "phn":
                        if (cultName == deDE)
                        {
                            cultInfo = new CultureInfo(deDEphon);
                        }
                        break;
                    case "tech":
                        if (cultName == huHU)
                        {
                            cultInfo = new CultureInfo(huHUtech);
                        }
                        break;
                    case "mod":
                        // ka-GE(Georgian - Georgia) Modern Sort: 0x00010437
                        if (cultName == kaGE)
                        {
                            cultInfo = new CultureInfo(kaGEmode);
                        }
                        break;
                    case "pron":
                    case "dict":
                    case "trad":
                        // es-ES(Spanish - Spain) Traditional: 0x0000040A
                        // They are removing 0x040a (Spanish Traditional sort) in NLS+.
                        // So if you create 0x040a, it's just like 0x0c0a (Spanish International sort).
                        // Thus I don't handle it differently.
                        break;
                    default:
                        if (!throwOnError) return null;
                        throw new XslTransformException(SR.Coll_UnsupportedSortOpt, sort);
                }
            }
            return new XmlCollation(cultInfo, options);
        }
 
 
        //-----------------------------------------------
        // Collection Support
        //-----------------------------------------------
 
        // Redefine Equals and GetHashCode methods, they are needed for UniqueList<XmlCollation>
        public override bool Equals([NotNullWhen(true)] object? obj)
        {
            if (this == obj)
            {
                return true;
            }
 
            XmlCollation? that = obj as XmlCollation;
            return that != null &&
                _options == that._options &&
                object.Equals(_cultInfo, that._cultInfo);
        }
 
        public override int GetHashCode()
        {
            int hashCode = _options;
            if (_cultInfo != null)
            {
                hashCode ^= _cultInfo.GetHashCode();
            }
            return hashCode;
        }
 
 
        //-----------------------------------------------
        // Serialization Support
        //-----------------------------------------------
 
        // Denotes the current thread locale
        private const string LOCALE_CURRENT = "<!-- LOCALE CURRENT -->";
 
        internal void GetObjectData(BinaryWriter writer)
        {
            // NOTE: For CultureInfo we serialize only LCID. It seems to suffice for our purposes.
            Debug.Assert(_cultInfo == null || _cultInfo.Equals(new CultureInfo(_cultInfo.Name)),
                "Cannot serialize CultureInfo correctly");
            writer.Write(_cultInfo != null ? _cultInfo.Name : LOCALE_CURRENT);
            writer.Write(_options);
        }
 
        internal XmlCollation(BinaryReader reader)
        {
            string cultName = reader.ReadString();
            _cultInfo = (cultName != LOCALE_CURRENT) ? new CultureInfo(cultName) : null;
            _options = new Options(reader.ReadInt32());
            _compops = _options.CompareOptions;
        }
 
        //-----------------------------------------------
        // Compare Properties
        //-----------------------------------------------
 
        internal bool UpperFirst
        {
            get { return _options.UpperFirst; }
        }
 
        internal bool EmptyGreatest
        {
            get { return _options.EmptyGreatest; }
        }
 
        internal bool DescendingOrder
        {
            get { return _options.DescendingOrder; }
        }
 
        internal CultureInfo Culture
        {
            get
            {
                // Use default thread culture if this.cultinfo = null
                if (_cultInfo == null)
                    return CultureInfo.CurrentCulture;
 
                return _cultInfo;
            }
        }
 
#if not_used
        /// <summary>
        /// Compare two strings with each other.  Return <0 if str1 sorts before str2, 0 if they're equal, and >0
        /// if str1 sorts after str2.
        /// </summary>
        internal int Compare(string str1, string str2) {
            CultureInfo cultinfo = Culture;
            int result;
 
            if (this.options.Ordinal) {
                result = string.CompareOrdinal(str1, str2);
                if (result < 0) result = -1;
                else if (result > 0) result = 1;
            }
            else if (UpperFirst) {
                // First compare case-insensitive, then break ties by considering case
                result = cultinfo.CompareInfo.Compare(str1, str2, this.compops | CompareOptions.IgnoreCase);
                if (result == 0)
                    result = -cultinfo.CompareInfo.Compare(str1, str2, this.compops);
            }
            else {
                result = cultinfo.CompareInfo.Compare(str1, str2, this.compops);
            }
 
            if (DescendingOrder)
                result = -result;
 
            return result;
        }
 
        /// <summary>
        /// Return the index of str1 in str2, or -1 if str1 is not a substring of str2.
        /// </summary>
        internal int IndexOf(string str1, string str2) {
            return Culture.CompareInfo.IndexOf(str1, str2, this.compops);
        }
 
        /// <summary>
        /// Return true if str1 ends with str2.
        /// </summary>
        internal bool IsSuffix(string str1, string str2) {
            if (this.options.Ordinal){
                if (str1.Length < str2.Length) {
                    return false;
                } else {
                    return String.CompareOrdinal(str1, str1.Length - str2.Length, str2, 0, str2.Length) == 0;
                }
            }
            return Culture.CompareInfo.IsSuffix (str1, str2, this.compops);
        }
 
        /// <summary>
        /// Return true if str1 starts with str2.
        /// </summary>
        internal bool IsPrefix(string str1, string str2) {
            if (this.options.Ordinal) {
                if (str1.Length < str2.Length) {
                    return false;
                } else {
                    return String.CompareOrdinal(str1, 0, str2, 0, str2.Length) == 0;
                }
            }
            return Culture.CompareInfo.IsPrefix (str1, str2, this.compops);
        }
#endif
    }
}