File: System\Net\Mime\ContentType.cs
Web Access
Project: src\src\libraries\System.Net.Mail\src\System.Net.Mail.csproj (System.Net.Mail)
// 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.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Net.Mail;
using System.Text;
 
namespace System.Net.Mime
{
    // Typed Content-Type header
    //
    // We parse the type during construction and set.
    // null and string.empty will throw for construction,set and mediatype/subtype
    // constructors set isPersisted to false.  isPersisted needs to be tracked separately
    // than isChanged because isChanged only determines if the cached value should be used.
    // isPersisted tracks if the object has been persisted. However, obviously if isChanged is true
    // the object isn't  persisted.
    // If any subcomponents are changed, isChanged is set to true and isPersisted is false
    // ToString caches the value until a isChanged is true, then it recomputes the full value.
 
    public class ContentType
    {
        private readonly TrackingStringDictionary _parameters = new TrackingStringDictionary();
 
        private string _mediaType;
        private string _subType;
        private bool _isChanged;
        private string _type;
        private bool _isPersisted;
 
        /// <summary>
        /// Default content type - can be used if the Content-Type header
        /// is not defined in the message headers.
        /// </summary>
        internal const string Default = "application/octet-stream";
 
        public ContentType() : this(Default)
        {
        }
 
        /// <summary>
        /// ctor.
        /// </summary>
        /// <param name="contentType">Unparsed value of the Content-Type header.</param>
        public ContentType(string contentType)
        {
            ArgumentException.ThrowIfNullOrEmpty(contentType);
 
            _isChanged = true;
            _type = contentType;
            ParseValue();
        }
 
        public string? Boundary
        {
            get { return Parameters["boundary"]; }
            set
            {
                if (string.IsNullOrEmpty(value))
                {
                    Parameters.Remove("boundary");
                }
                else
                {
                    Parameters["boundary"] = value;
                }
            }
        }
 
        public string? CharSet
        {
            get { return Parameters["charset"]; }
            set
            {
                if (string.IsNullOrEmpty(value))
                {
                    Parameters.Remove("charset");
                }
                else
                {
                    Parameters["charset"] = value;
                }
            }
        }
 
        /// <summary>
        /// Gets the media type.
        /// </summary>
        public string MediaType
        {
            get { return _mediaType + "/" + _subType; }
            set
            {
                ArgumentException.ThrowIfNullOrEmpty(value);
 
                int offset = 0;
                _mediaType = MailBnfHelper.ReadToken(value, ref offset);
                if (_mediaType.Length == 0 || offset >= value.Length || value[offset++] != '/')
                    throw new FormatException(SR.MediaTypeInvalid);
 
                _subType = MailBnfHelper.ReadToken(value, ref offset);
                if (_subType.Length == 0 || offset < value.Length)
                {
                    throw new FormatException(SR.MediaTypeInvalid);
                }
 
                _isChanged = true;
                _isPersisted = false;
            }
        }
 
        [AllowNull]
        public string Name
        {
            get
            {
                string? value = Parameters["name"];
                Encoding? nameEncoding = MimeBasePart.DecodeEncoding(value);
                if (nameEncoding != null)
                {
                    value = MimeBasePart.DecodeHeaderValue(value);
                }
                return value!;
            }
            set
            {
                if (value == null || value == string.Empty)
                {
                    Parameters.Remove("name");
                }
                else
                {
                    Parameters["name"] = value;
                }
            }
        }
 
        public StringDictionary Parameters => _parameters;
 
        internal void Set(string contentType, HeaderCollection headers)
        {
            _type = contentType;
            ParseValue();
            headers.InternalSet(MailHeaderInfo.GetString(MailHeaderID.ContentType)!, ToString());
            _isPersisted = true;
        }
 
        internal void PersistIfNeeded(HeaderCollection headers, bool forcePersist)
        {
            if (IsChanged || !_isPersisted || forcePersist)
            {
                headers.InternalSet(MailHeaderInfo.GetString(MailHeaderID.ContentType)!, ToString());
                _isPersisted = true;
            }
        }
 
        internal bool IsChanged => _isChanged || _parameters != null && _parameters.IsChanged;
 
        public override string ToString()
        {
            if (_type == null || IsChanged)
            {
                _type = Encode(false); // Legacy wire-safe format
                _isChanged = false;
                _parameters.IsChanged = false;
                _isPersisted = false;
            }
            return _type;
        }
 
        internal string Encode(bool allowUnicode)
        {
            var builder = new StringBuilder();
 
            builder.Append(_mediaType); // Must not have unicode, already validated
            builder.Append('/');
            builder.Append(_subType);  // Must not have unicode, already validated
 
            // Validate and encode unicode where required
            foreach (string key in Parameters.Keys)
            {
                builder.Append("; ");
                EncodeToBuffer(key, builder, allowUnicode);
                builder.Append('=');
                EncodeToBuffer(_parameters[key]!, builder, allowUnicode);
            }
 
            return builder.ToString();
        }
 
        private static void EncodeToBuffer(string value, StringBuilder builder, bool allowUnicode)
        {
            Encoding? encoding = MimeBasePart.DecodeEncoding(value);
            if (encoding != null) // Manually encoded elsewhere, pass through
            {
                builder.Append('\"').Append(value).Append('"');
            }
            else if ((allowUnicode && !MailBnfHelper.HasCROrLF(value)) // Unicode without CL or LF's
                || MimeBasePart.IsAscii(value, false)) // Ascii
            {
                MailBnfHelper.GetTokenOrQuotedString(value, builder, allowUnicode);
            }
            else
            {
                // MIME Encoding required
                encoding = Encoding.GetEncoding(MimeBasePart.DefaultCharSet);
                builder.Append('"').Append(MimeBasePart.EncodeHeaderValue(value, encoding, MimeBasePart.ShouldUseBase64Encoding(encoding))).Append('"');
            }
        }
 
        public override bool Equals([NotNullWhen(true)] object? rparam) =>
            rparam == null ? false : string.Equals(ToString(), rparam.ToString(), StringComparison.OrdinalIgnoreCase);
 
        public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(ToString());
 
        // Helper methods.
        [MemberNotNull(nameof(_mediaType))]
        [MemberNotNull(nameof(_subType))]
        private void ParseValue()
        {
            try
            {
                int offset = 0;
 
                _mediaType = MailBnfHelper.ReadToken(_type, ref offset);
                if (string.IsNullOrEmpty(_mediaType) || offset >= _type.Length || _type[offset++] != '/')
                {
                    throw new FormatException(SR.ContentTypeInvalid);
                }
 
                _subType = MailBnfHelper.ReadToken(_type, ref offset);
                if (string.IsNullOrEmpty(_subType))
                {
                    throw new FormatException(SR.ContentTypeInvalid);
                }
 
                while (MailBnfHelper.SkipCFWS(_type, ref offset))
                {
                    if (_type[offset++] != ';')
                    {
                        throw new FormatException(SR.ContentTypeInvalid);
                    }
 
                    if (!MailBnfHelper.SkipCFWS(_type, ref offset))
                    {
                        break;
                    }
 
                    string? paramAttribute = MailBnfHelper.ReadParameterAttribute(_type, ref offset);
 
                    if (string.IsNullOrEmpty(paramAttribute))
                    {
                        throw new FormatException(SR.ContentTypeInvalid);
                    }
 
                    string? paramValue;
                    if (offset >= _type.Length || _type[offset++] != '=')
                    {
                        throw new FormatException(SR.ContentTypeInvalid);
                    }
 
                    if (!MailBnfHelper.SkipCFWS(_type, ref offset))
                    {
                        throw new FormatException(SR.ContentTypeInvalid);
                    }
 
                    paramValue = _type[offset] == '"' ?
                        MailBnfHelper.ReadQuotedString(_type, ref offset, null) :
                        MailBnfHelper.ReadToken(_type, ref offset);
 
                    if (paramValue == null)
                    {
                        throw new FormatException(SR.ContentTypeInvalid);
                    }
 
                    _parameters.Add(paramAttribute, paramValue);
                }
 
                _parameters.IsChanged = false;
            }
            catch (FormatException fe) when (fe.Message != SR.ContentTypeInvalid)
            {
                throw new FormatException(SR.ContentTypeInvalid);
            }
        }
    }
}