lib/active_ldap/schema/syntaxes.rb



module ActiveLdap
  class Schema
    module Syntaxes
      class << self
        def [](id)
          syntax = Base::SYNTAXES[id]
          if syntax
            syntax.new
          else
            nil
          end
        end
      end

      class Base
        include GetTextSupport
        SYNTAXES = {}

        printable_character_source = "a-zA-Z\\d\"()+,\\-.\\/:? "
        PRINTABLE_CHARACTER = /[#{printable_character_source}]/ #
        UNPRINTABLE_CHARACTER = /[^#{printable_character_source}]/ #

        def binary?
          false
        end

        def type_cast(value)
          value
        end

        def valid?(value)
          validate(value).nil?
        end

        def validate(value)
          validate_normalized_value(normalize_value(value), value)
        end

        def normalize_value(value)
          value
        end
      end

      class BitString < Base
        SYNTAXES["1.3.6.1.4.1.1466.115.121.1.6"] = self

        def type_cast(value)
          return nil if value.nil?
          if /\A'([01]*)'B\z/ =~ value.to_s
            $1
          else
            value
          end
        end

        def normalize_value(value)
          if value.is_a?(String) and /\A[01]*\z/ =~ value
            "'#{value}'B"
          else
            value
          end
        end

        private
        def validate_normalized_value(value, original_value)
          if /\A'/ !~ value
            return _("%s doesn't have the first \"'\"") % original_value.inspect
          end

          if /'B\z/ !~ value
            return _("%s doesn't have the last \"'B\"") % original_value.inspect
          end

          if /([^01])/ =~ value[1..-3]
            return _("%s has invalid character '%s'") % [value.inspect, $1]
          end

          nil
        end
      end

      class Boolean < Base
        SYNTAXES["1.3.6.1.4.1.1466.115.121.1.7"] = self

        def type_cast(value)
          case value
          when "TRUE"
            true
          when "FALSE"
            false
          else
            value
          end
        end

        def normalize_value(value)
          case value
          when true, "1"
            "TRUE"
          when false, "0"
            "FALSE"
          else
            value
          end
        end

        private
        def validate_normalized_value(value, original_value)
          if %w(TRUE FALSE).include?(value)
            nil
          else
            _("%s should be TRUE or FALSE") % original_value.inspect
          end
        end
      end

      class CountryString < Base
        SYNTAXES["1.3.6.1.4.1.1466.115.121.1.11"] = self

        private
        def validate_normalized_value(value, original_value)
          if /\A#{PRINTABLE_CHARACTER}{2,2}\z/i =~ value
            nil
          else
            format = _("%s should be just 2 printable characters")
            format % original_value.inspect
          end
        end
      end

      class DistinguishedName < Base
        SYNTAXES["1.3.6.1.4.1.1466.115.121.1.12"] = self

        def type_cast(value)
          return nil if value.nil?
          DN.parse(value)
        rescue DistinguishedNameInvalid
          value
        end

        def normalize_value(value)
          if value.is_a?(DN)
            value.to_s
          else
            value
          end
        end

        private
        def validate_normalized_value(value, original_value)
          DN.parse(value)
          nil
        rescue DistinguishedNameInvalid
          $!.message
        end
      end

      class DirectoryString < Base
        SYNTAXES["1.3.6.1.4.1.1466.115.121.1.15"] = self

        private
        def validate_normalized_value(value, original_value)
          value.unpack("U*")
          nil
        rescue ArgumentError
          _("%s has invalid UTF-8 character") % original_value.inspect
        end
      end

      class GeneralizedTime < Base
        SYNTAXES["1.3.6.1.4.1.1466.115.121.1.24"] = self
        FORMAT = /\A
                  (\d{4,4})?
                  (\d{2,2})?
                  (\d{2,2})?
                  (\d{2,2})?
                  (\d{2,2})?
                  (\d{2,2})?
                  ([,.]\d+)?
                  ([+-]\d{4,4}|Z)?
                 \z/x

        def type_cast(value)
          return value if value.nil? or value.is_a?(Time)
          match_data = FORMAT.match(value)
          if match_data
            required_components = match_data.to_a[1, 5]
            return value if required_components.any?(&:nil?)
            year, month, day, hour, minute = required_components.collect(&:to_i)
            second = match_data[-3].to_i
            fraction = match_data[-2]
            fraction = fraction.to_f if fraction
            time_zone = match_data[-1]
            arguments = [
              value, year, month, day, hour, minute, second, fraction, time_zone,
              Time.now,
            ]
            if Time.method(:make_time).arity == 11
              arguments[2, 0] = nil
            end
            begin
              Time.send(:make_time, *arguments)
            rescue ArgumentError
              raise if year >= 1700
              out_of_range_messages = ["argument out of range",
                                       "time out of range"]
              raise unless out_of_range_messages.include?($!.message)
              Time.at(0)
            rescue RangeError
              raise if year >= 1700
              raise if $!.message != "bignum too big to convert into `long'"
              Time.at(0)
            end
          else
            value
          end
        end

        def normalize_value(value)
          if value.is_a?(Time)
            normalized_value = value.strftime("%Y%m%d%H%M%S")
            if value.gmt?
              normalized_value + "Z"
            else
              # for timezones with non-zero minutes, such as IST which is +0530,
              # divmod(3600) will give wrong value of 1800

              offset = value.gmtoff / 60 # in minutes
              normalized_value + ("%+03d%02d" % offset.divmod(60))
            end
          else
            value
          end
        end

        private
        def validate_normalized_value(value, original_value)
          match_data = FORMAT.match(value)
          if match_data
            date_data = match_data.to_a[1..-1]
            missing_components = []
            required_components = %w(year month day hour minute)
            required_components.each_with_index do |component, i|
              missing_components << component unless date_data[i]
            end
            if missing_components.empty?
              nil
            else
              params = [original_value.inspect, missing_components.join(", ")]
              _("%s has missing components: %s") % params
            end
          else
            _("%s is invalid time format") % original_value.inspect
          end
        end
      end

      class Integer < Base
        SYNTAXES["1.3.6.1.4.1.1466.115.121.1.27"] = self

        def type_cast(value)
          return value if value.nil?
          begin
            Integer(value)
          rescue ArgumentError
            value
          end
        end

        def normalize_value(value)
          if value.is_a?(::Integer)
            value.to_s
          else
            value
          end
        end

        private
        def validate_normalized_value(value, original_value)
          Integer(value)
          nil
        rescue ArgumentError
          _("%s is invalid integer format") % original_value.inspect
        end
      end

      class JPEG < Base
        SYNTAXES["1.3.6.1.4.1.1466.115.121.1.28"] = self

        def binary?
          true
        end

        private
        def validate_normalized_value(value, original_value)
          if value.unpack("n")[0] == 0xffd8
            nil
          else
            _("invalid JPEG format")
          end
        end
      end

      class NameAndOptionalUID < Base
        SYNTAXES["1.3.6.1.4.1.1466.115.121.1.34"] = self

        private
        def validate_normalized_value(value, original_value)
          separator_index = value.rindex("#")
          if separator_index
            dn = value[0, separator_index]
            bit_string = value[(separator_index + 1)..-1]
            bit_string_reason = BitString.new.validate(bit_string)
            dn_reason = DistinguishedName.new.validate(dn)
            if bit_string_reason
              if dn_reason
                value_reason = DistinguishedName.new.validate(value)
                return nil unless value_reason
                dn_reason
              else
                bit_string_reason
              end
            else
              dn_reason
            end
          else
            DistinguishedName.new.validate(value)
          end
        end
      end

      class NumericString < Base
        SYNTAXES["1.3.6.1.4.1.1466.115.121.1.36"] = self

        private
        def validate_normalized_value(value, original_value)
          if /\A\d+\z/ =~ value
            nil
          else
            _("%s is invalid numeric format") % original_value.inspect
          end
        end
      end

      class OID < Base
        SYNTAXES["1.3.6.1.4.1.1466.115.121.1.38"] = self

        private
        def validate_normalized_value(value, original_value)
          DN.parse("#{value}=dummy")
          nil
        rescue DistinguishedNameInvalid
          reason = $!.reason
          if reason
            _("%s is invalid OID format: %s") % [original_value.inspect, reason]
          else
            _("%s is invalid OID format") % original_value.inspect
          end
        end
      end

      class OtherMailbox < Base
        SYNTAXES["1.3.6.1.4.1.1466.115.121.1.39"] = self

        private
        def validate_normalized_value(value, original_value)
          type, mailbox = value.split('$', 2)

          if type.empty?
            return _("%s has no mailbox type") % original_value.inspect
          end

          if /(#{UNPRINTABLE_CHARACTER})/i =~ type
            format = _("%s has unprintable character in mailbox type: '%s'")
            return format % [original_value.inspect, $1]
          end

          if mailbox.blank?
            return _("%s has no mailbox") % original_value.inspect
          end

          nil
        end
      end

      class OctetString < Base
        SYNTAXES["1.3.6.1.4.1.1466.115.121.1.40"] = self

        def binary?
          true
        end

        private
        def validate_normalized_value(value, original_value)
          nil
        end
      end

      class PostalAddress < Base
        SYNTAXES["1.3.6.1.4.1.1466.115.121.1.41"] = self

        private
        def validate_normalized_value(value, original_value)
          if value.blank?
            return _("empty string")
          end

          begin
            value.unpack("U*")
          rescue ArgumentError
            return _("%s has invalid UTF-8 character") % original_value.inspect
          end

          nil
        end
      end

      class PrintableString < Base
        SYNTAXES["1.3.6.1.4.1.1466.115.121.1.44"] = self

        private
        def validate_normalized_value(value, original_value)
          if value.blank?
            return _("empty string")
          end

          if /(#{UNPRINTABLE_CHARACTER})/i =~ value
            format = _("%s has unprintable character: '%s'")
            return format % [original_value.inspect, $1]
          end

          nil
        end
      end

      class TelephoneNumber < PrintableString
        SYNTAXES["1.3.6.1.4.1.1466.115.121.1.50"] = self

        private
        def validate_normalized_value(value, original_value)
          return nil if value.blank?
          super
        end
      end

      class ObjectSecurityDescriptor < OctetString
        # @see http://tools.ietf.org/html/draft-armijo-ldap-syntax-00
        #   Object-Security-Descriptor: 1.2.840.113556.1.4.907
        #
        #   Encoded as an Octet-String (OID 1.3.6.1.4.1.1466.115.121.1.40)
        #
        # @see http://msdn.microsoft.com/en-us/library/cc223229.aspx
        #   String(NT-Sec-Desc) 1.2.840.113556.1.4.907
        SYNTAXES["1.2.840.113556.1.4.907"] = self
      end

      class UnicodePwd < OctetString
        # @see http://msdn.microsoft.com/en-us/library/cc220961.aspx
        #   cn: Unicode-Pwd
        #   ldapDisplayName: unicodePwd
        #   attributeId: 1.2.840.113556.1.4.90
        #   attributeSyntax: 2.5.5.10
        #   omSyntax: 4
        #   isSingleValued: TRUE
        #   schemaIdGuid: bf9679e1-0de6-11d0-a285-00aa003049e2
        #   systemOnly: FALSE
        #   searchFlags: 0
        #   systemFlags: FLAG_SCHEMA_BASE_OBJECT
        #   schemaFlagsEx: FLAG_ATTR_IS_CRITICAL
        #
        # @see http://msdn.microsoft.com/en-us/library/cc223177.aspx
        #   String(Octet) 2.5.5.10
        SYNTAXES["1.2.840.113556.1.4.90"] = self
      end
    end
  end
end