#!/usr/bin/python3 -su

## Copyright (C) 2017 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

#### meta start
#### project Whonix
#### category tor-control
#### description
## Processes files matching <code>/etc/onion-grater-merger.d/*.yml</code> and
## <code>/usr/local/etc/onion-grater-merger.d/*.yml</code>.
##
## Creates <code>/etc/onion-grater.d/30_autogenerated.yml</code>.
##
## Only known use so far is by Whonix.
#### meta end

import glob
import yaml
import os

# Override elements
no_merge_elements = ['suppress','match-hosts']
# Override elements with fixed value
fixed_value_elements = {'match-exe-paths':'*','match-users':'*'}

def merge_yml(final_obj, parsing_obj):
   # Inputs - 2 dictionaries/list from YML/YAML file
   # "parsing_obj" is parsed and merged into "final_obj" and returned back
   if isinstance(final_obj,dict) and isinstance(parsing_obj,dict):
      for k,v in parsing_obj.items():
         if k not in final_obj:
            final_obj[k] = v
         else:
            if(k in fixed_value_elements):
               final_obj[k] = fixed_value_elements[k]
            elif(k not in no_merge_elements):
               final_obj[k] = merge_yml(final_obj[k],v)
            else:
               pass
               # Override case
   else: # Non Dictionary case - LIST
      pattern_final_obj = {}
      if(final_obj != parsing_obj): #Only when the LIST are different
         # Specially handle GETINFO case which contains pattern and response
         for ele in final_obj:
            if(type(ele) is dict):
               if all (k in ele for k in ('pattern','response')):
                  pattern_final_obj = ele

         # If pattern is same, no need for this whole LIST
         # We will use the higher priority pattern,response
         for ele in parsing_obj:
            if(type(ele) is dict):
               if all (k in ele for k in ('pattern','response')):
                  if 'pattern' in pattern_final_obj:
                     if pattern_final_obj['pattern'] == ele['pattern']:
                        parsing_obj.remove(ele)

         # Add without duplicates
         final_obj += [ele for ele in parsing_obj if ele not in final_obj]
   return final_obj

def main():
   merged_file = '/etc/onion-grater.d/30_autogenerated.yml'

   if not os.path.exists('/etc/onion-grater.d'):
      os.mkdir('/etc/onion-grater.d')

   ## delete only if file exists ##
   if os.path.exists(merged_file):
      os.remove(merged_file)

   filter_files_set1 = glob.glob('/etc/onion-grater-merger.d/*.yml')
   filter_files_set1.sort(reverse = True)
   filter_files_set2 = glob.glob('/usr/local/etc/onion-grater-merger.d/*.yml')
   filter_files_set2.sort(reverse = True)
   filter_files = filter_files_set1 + filter_files_set2

   ## File list
   #print(filter_files)

   merged_filter_dict = {}

   for filter_file in filter_files:
      try:
         with open(filter_file, "rb") as fh:
            filters = yaml.safe_load(fh.read())
            for i in range(len(filters)):
                merged_filter_dict = merge_yml(merged_filter_dict,filters[i])
      except (yaml.parser.ParserError, yaml.scanner.ScannerError):
         print("Error in loading \n");

   merged_filter = [merged_filter_dict,]

   # Get the list of files name without path
   file_names = ''
   for i in range(len(filter_files)):
      file_names += ' ' + filter_files[i].split('/')[-1]

   name = 'merged_filter_files'
   for filter_ in merged_filter:
      if name not in filter_:
         filter_['name'] = "merged_filter_files: " + file_names

   ## Merged Filter
   #print(merged_filter)

   comment = """\
## Manual edits to this file will be lost!
##
## This file has been auto generated by /usr/lib/onion-grater-merger.
##
## onion-grater-merger parses,
##
## - /etc/onion-grater-merger.d/*.yml as well as
## - /usr/local/etc/onion-grater-merger.d/*.yml
##
## in lexical order and then generates /etc/onion-grater.d/30_autogenerated.yml.
"""

   with open(merged_file, 'a') as modified:
      modified.write(comment)
      modified.write("\n")
      modified.write("---")
      modified.write("\n")
      yaml.dump(merged_filter, modified, default_flow_style=False)



if __name__ == "__main__":
    main()
