# frozen_string_literal: true module Facter class QueryParser @log = Log.new(self) class << self # Searches for facts that could resolve a user query. # There are 4 types of facts: # root facts # e.g. networking # child facts # e.g. networking.dhcp # composite facts # e.g. networking.interfaces.en0.bindings.address # regex facts (legacy) # e.g. impaddress_end160 # # Because a root fact will always be resolved by a collection of child facts, # we can return one or more child facts for each parent. # # @param query_list [Array] The list of facts to search for # @param loaded_facts [Array] All of the fact definitions for the current operating system # # @return [Array] a list of searchable facts that resolve the user's query def parse(query_list, loaded_facts) matched_facts = [] @query_list = query_list return no_user_query(loaded_facts) unless query_list.any? query_list.each do |query| found_facts = search_for_facts(query, loaded_facts) matched_facts << found_facts end matched_facts.flatten(1) end def no_user_query(loaded_facts) searched_facts = [] loaded_facts.each do |loaded_fact| searched_facts << SearchedFact.new(loaded_fact.name, loaded_fact.klass, '', loaded_fact.type) end searched_facts end def search_for_facts(query, loaded_facts) resolvable_fact_list = [] query = query.to_s query_tokens = query.end_with?('.*') ? [query] : query.split('.') size = query_tokens.size # Try to match the most specific query_tokens to the least, returning the first match size.times do |i| query_token_range = 0..size - i - 1 query_fact = query_tokens[query_token_range].join('.') resolvable_fact_list = get_facts_matching_tokens(query_tokens, query_fact, loaded_facts) return resolvable_fact_list if resolvable_fact_list.any? end resolvable_fact_list << SearchedFact.new(query, nil, query, :nil) if resolvable_fact_list.empty? resolvable_fact_list end def get_facts_matching_tokens(query_tokens, query_fact, loaded_facts) resolvable_fact_list = [] loaded_facts.each do |loaded_fact| next unless found_fact?(loaded_fact.name, query_fact) searched_fact = construct_loaded_fact(query_tokens, loaded_fact) resolvable_fact_list << searched_fact end @log.debug "List of resolvable facts: #{resolvable_fact_list.inspect}" if resolvable_fact_list.any? resolvable_fact_list end def found_fact?(fact_name, query_fact) # This is the case where the fact_name contains a wildcard like # blockdevice_.*_model and we're querying for the legacy fact # specifically using 'blockdevice_sba_model' and we don't want the query # 'blockdevice.sba.model' to match fact_with_wildcard = fact_name.include?('.*') && !query_fact.include?('.') if fact_with_wildcard # fact_name contains wildcard, so we're intentially not escaping. query_fact.match("^#{fact_name}$") else processed_equery_fact = query_fact.gsub('\\', '\\\\\\\\') # Must escape metacharacters (like dots) to ensure the correct fact is found fact_name.match("^#{Regexp.escape(processed_equery_fact)}($|\\.)") end end def construct_loaded_fact(query_tokens, loaded_fact) user_query = @query_list.any? ? query_tokens.join('.') : '' fact_name = loaded_fact.name.to_s klass_name = loaded_fact.klass type = loaded_fact.type sf = SearchedFact.new(fact_name, klass_name, user_query, type) sf.file = loaded_fact.file sf end end end end