# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true module Contrast module Utils module Assess # This module will include all methods for some internal validations in the Tagged property module # and some other module methods from the same place, so we can ease the main module # This module includes simple methods for the tags like # adding tags, getting tags, deleting tags and similar module TaggedUtils # Is the given tag present? # Used in testing, so found by `be_tagged`, if you're grepping for it # # @param label [Symbol] the tag to check for # @return [Boolean] def tagged? label tracked? && tags.key?(label) end # Similar to #tracked?, but limited to a given range. # # @param start [Integer] the inclusive start index to check. # @param finish [Integer] the exclusive end index to check. # @return [Boolean] def any_tags_between? start, finish return false unless tracked? tags.each_value do |tag_array| return true if tag_array.any? { |tag| tag.overlaps?(start, finish) } end false end # Given a tag name and range object, add a new tag to this # collection. If the given range touches an existing tag, # we'll combine the two, adjusting the existing one and # dropping this new one. # # @param label [String] the name of the tag # @param range [Range] the Range that the tag covers, inclusive to # exclusive def add_tag label, range length = range.end - range.begin tag = Contrast::Agent::Assess::Tag.new(label, length, range.begin) existing = fetch_tag(label) tags[label] = Contrast::Utils::TagUtil.ordered_merge(existing, tag) end def set_tags label, tag_ranges tags[label] = tag_ranges end # Returns a list of all current tags. # # @return [Hash] def get_tags # rubocop:disable Naming/AccessorMethodName return Contrast::Utils::ObjectShare::EMPTY_HASH unless tracked? tags end # We'll use this as a helper method to retrieve tags from the hash. # Because the hash auto-populates an empty array when we try to # access a tag in it, we cannot use the [] method without side # effect. To get around this, we'll use a fetch work around. # # @param label [Symbol] the label to look up # @return [Array] all the tags with # that label def fetch_tag label get_tags.fetch(label, nil) if tracked? end # Remove all tags with a given label def delete_tags label tags.delete(label) if tracked? end # Reset the tag hash def clear_tags tags.clear if tracked? end # Returns a list of all current tag labels, most likely to be used for # a splat operation def tag_keys return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tracked? tags.keys end # Calls merge to combine touching or overlapping tags # Deletes empty tags def cleanup_tags return unless tracked? Contrast::Utils::TagUtil.merge_tags(tags) tags.delete_if { |_, value| value.empty? } end # Find all of the ranges that span a given index. This is used # in propagation when we need to shift tags about. For instance, in # the append method when we need to see if any tag at the end needs # to be expanded out to the size of the new String. # # Note: Tags do not know their key, so this is only the range covered # # @param idx [Integer] the index to check for tags # @return [Array] a set of all the Tags # covering the given index. This is only the ranges, not the names. def tags_at idx return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tracked? at = [] tags.each_value do |tag_array| tag_array.each do |tag| if tag.covers?(idx) at << tag elsif tag.above?(idx) break end end end at end # given a range, select all tags in that range the selected tags are # shifted such that the start index of the new tag (0) aligns with # the given start index in the range # # current tags: 5-15 # range : 5-10 # result : 0-05 # # Note that we disable Lint/DuplicateBranch in this branch in order # list out all tag range cases in the proper order to make this # easier to understand # # @param range [Range] the span to check, inclusive to exclusive # @return [Hash{String => Contrast::Agent::Assess::Tag}] the hash of # key to tags def tags_at_range range return Contrast::Utils::ObjectShare::EMPTY_HASH unless tracked? at = Hash.new { |h, k| h[k] = [] } tags.each_pair do |key, value| add = nil value.each do |tag| within_range = resize_to_range(tag, range) if within_range add ||= [] add << within_range end end next unless add&.any? at[key] = add end at end end end end end