# Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/components/logger' module Contrast module Utils # Utilities for encoding and normalizing strings class StringUtils include Contrast::Components::Logger::InstanceMethods UTF8 = 'utf-8' HTTP_PREFIX = 'HTTP_' # Convenience method. We assume that we're working on Strings or tags # String representations of things. To that end, we'll to_s anything # that comes in before returning its length. # # But don't worry though, String.to_s just returns self. teehee def self.ret_length string string.nil? ? 0 : string.to_s.length end def self.present? str !str.nil? && !str.to_s.empty? end def self.protobuf_format data, truncate: true data = data&.to_s data = Contrast::Utils::StringUtils.force_utf8(data) data = Contrast::Utils::StringUtils.truncate(data) if truncate data end # Protobuf has a very strict typing. Nil is not a String and will throw # an exception if you try to set it. Use this to be safe. # Uses the object share to avoid creating several new strings per request def self.protobuf_safe_string string string.nil? ? Contrast::Utils::ObjectShare::EMPTY_STRING : string.to_s end # Truncate a string to 255 characters max length def self.truncate str, default = Contrast::Utils::ObjectShare::EMPTY_STRING return default if str.nil? str.to_s[0..255] end def self.force_utf8 str return Contrast::Utils::ObjectShare::EMPTY_STRING unless str str = str.to_s if str.encoding == Encoding::UTF_8 str = str.encode(UTF8, invalid: :replace, undef: :replace) unless str.valid_encoding? else str = str.encode(UTF8, str.encoding, invalid: :replace, undef: :replace) end str.to_s rescue StandardError => e # We were unable to switch the String to a UTF-8 format. # Return non-nil so as not to throw an exception later when trying # to do regexp or other compares on the String logger.trace('Unable to cast String to UTF-8 format', e, value: str) Contrast::Utils::ObjectShare::EMPTY_STRING end # Given a string return a normalized version of that string. # Keys are memoized so that the normalization process doesn't need # to happen every time. # # @param str [String] the String to normalize # @return [String] a copy of the given String, upper cased, trimmed, # dashes replaced with underscore, and HTTP trimmed def self.normalized_key str return unless str str = str.to_s @_normalized_keys ||= {} if @_normalized_keys.key?(str) @_normalized_keys[str] else upped = str.upcase stripped = upped.strip! || upped trimmed = stripped.tr!('-', '_') || stripped cut = trimmed.start_with?(HTTP_PREFIX) ? trimmed[5..-1] : trimmed @_normalized_keys[str] = cut end end end end end