# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true module Contrast module Utils # Utility methods for working with tag ranges class TagUtil class << self # Determine if the given array of tags is covered by the other # # @param remaining_ranges [Array] the tags left that haven't been covered by # those given # @param ranges Array the tags that are covering the first def covered? remaining_ranges, ranges return true unless remaining_ranges&.any? tag = remaining_ranges[0] ranges.each do |range| # If we covered the tag before using all the ranges, break break if tag.length <= 0 relationship = tag.compare_range(range.start_idx, range.end_idx) case relationship # since the tags are ordered, if we're below, nope out when Contrast::Agent::Assess::Tag::BELOW, # if we ever get a low span, that means a low part # won't be covered. there's no need to continue Contrast::Agent::Assess::Tag::LOW_SPAN, # if we ever get a without, that means a low part won't # be covered. there's no need to continue Contrast::Agent::Assess::Tag::WITHOUT return false when Contrast::Agent::Assess::Tag::WITHIN # if we're within, then 0 out this tag since it is # completely covered tag.update_start(0) tag.update_end(0) when Contrast::Agent::Assess::Tag::HIGH_SPAN tag.update_start(range.end_idx) when Contrast::Agent::Assess::Tag::ABOVE # The tag's above where we are, it doesn't cover us but a later # one may end end return false unless tag.length <= 0 remaining_ranges.shift covered?(remaining_ranges, ranges) end # Given an array of tags, add all new tags to that array # # The addition is done such that the new entry(ies) # are inserted so that the range they cover is in order # Any overlapping ranges are merged before returning # # arr: the array of tags to which we are adding # add_arr: array of Tags or a single Tag to be added def ordered_merge arr, add_arr # [Contrast::Agent::Assess::Tag, ...] if add_arr.is_a?(Array) return arr unless add_arr.any? return add_arr unless arr&.any? add_arr.each { |new_element| single_ordered_merge(arr, new_element) } # Contrast::Agent::Assess::Tag else return arr unless add_arr return [add_arr] unless arr single_ordered_merge(arr, add_arr) end smallerize(arr) end # Given a collection of tags, merge any tags that are continuous # # If tags is a hash, it should be in the format label => [tags]. The array of tags will each be merged # If tags is an array in the format [tags], the array will be merged # # The original object is returned, although setters should not be necessary since tags is a collection in # either case # # @param tags [Hash{String => Array}, Array] # @return [Hash{String => Array}, Array] def merge_tags tags if tags.is_a?(Hash) tags.each_value { |value| smallerize(value) } else smallerize(tags) end end # Merge the given set of tags such that any overlap combines. For any tag which extends beyond the size of the # target_object, the end will be updated to the target_object's length. # # @param target_object [Object] the thing to which the tags apply # @param tags [Hash{String => Array}, Array] # @return [Hash{String => Array}, Array] def size_aware_merge target_object, tags max_size = target_object.to_s.length tags = merge_tags(tags) tags.each do |tag| tag.update_end(max_size) if tag.extends_beyond_string_size?(max_size) end end private # Add one new element to the given array. The addition is done such that the new entry is inserted so that the # range they cover is in order. Any overlapping ranges are merged before returning. # # @param arr [Array] # @param new_element []Contrast::Agent::Assess::Tag] # @return [Array] def single_ordered_merge arr, new_element idx = 0 arr.each do |existing| break unless existing.start_idx < new_element.start_idx if existing.overlaps?(new_element.start_idx, new_element.end_idx) existing.merge(new_element) return # rubocop:disable Lint/NonLocalExitFromIterator end idx += 1 end arr.insert(idx, new_element) end # Given an arry of tags, merge any that overlap. The tag that was higher up is removed from the list of tags. # ranges like [0-3][3-6]-6-9] become [0-9] # # @param tags [Array] # @return [Array] def smallerize tags smallered = [] curr = nil tags.sort_by! { |tag| [tag.start_idx, tag.end_idx] } tags.each do |tag| if curr && tag.start_idx <= curr.end_idx curr.update_end(tag.end_idx) if tag.end_idx > curr.end_idx else curr = tag smallered << curr end end tags.delete_if { |tag| !smallered.include?(tag) } end end end end end